Merge branch '2master' of ssh://git.cy55.de/home/git/loops into 2master
This commit is contained in:
commit
9d9cfd6fc7
136 changed files with 4198 additions and 791 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
|
dist/
|
||||||
*.project
|
*.project
|
||||||
*.pydevproject
|
*.pydevproject
|
||||||
*.sublime-project
|
*.sublime-project
|
||||||
|
|
11
MANIFEST.in
Normal file
11
MANIFEST.in
Normal 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
7
README.md
Normal 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.
|
18
README.txt
18
README.txt
|
@ -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
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
|
35
__init__.py
35
__init__.py
|
@ -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$
|
|
||||||
"""
|
|
||||||
|
|
4
base.py
4
base.py
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -20,13 +20,14 @@
|
||||||
Common base class for loops browser view classes.
|
Common base class for loops browser view classes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from cgi import parse_qs, parse_qsl
|
from cgi import 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
|
||||||
from urllib import urlencode
|
from urllib import urlencode
|
||||||
|
from urlparse import parse_qs
|
||||||
from zope import component
|
from zope import component
|
||||||
from zope.app.form.browser.interfaces import ITerms
|
from zope.app.form.browser.interfaces import ITerms
|
||||||
from zope.app.i18n.interfaces import ITranslationDomain
|
from zope.app.i18n.interfaces import ITranslationDomain
|
||||||
|
@ -62,17 +63,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 +142,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 +202,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 +220,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 +275,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 +418,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 +470,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 +826,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 +902,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 +1028,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 +1087,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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -238,18 +238,21 @@ fieldset.box td {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #444;
|
color: #444;
|
||||||
padding-top: 0.4em;
|
padding-top: 0.4em;
|
||||||
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-4 h1, .content-3 h2, .content-2 h3, .content-1 h4, h4 {
|
.content-4 h1, .content-3 h2, .content-2 h3, .content-1 h4, h4 {
|
||||||
font-size: 130%;
|
font-size: 130%;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
padding-top: 0.3em;
|
padding-top: 0.3em;
|
||||||
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-5 h1, .content-4 h2, .content-3 h3, content-2 h4, h5 {
|
.content-5 h1, .content-4 h2, .content-3 h3, content-2 h4, h5 {
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
/* border: none; */
|
/* border: none; */
|
||||||
padding-top: 0.2em;
|
padding-top: 0.2em;
|
||||||
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
.box {
|
||||||
|
|
|
@ -47,6 +47,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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
@ -214,8 +218,18 @@ class ResourceView(BaseView):
|
||||||
data = context.data
|
data = context.data
|
||||||
if useAttachment:
|
if useAttachment:
|
||||||
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 ''
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -69,9 +69,9 @@
|
||||||
metal:define-macro="footer">
|
metal:define-macro="footer">
|
||||||
<metal:footer define-slot="footer">
|
<metal:footer define-slot="footer">
|
||||||
© Copyright <span tal:replace="view/currentYear" />,
|
© 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> ·
|
<b><a href="http://www.wissen-statt-suchen.de">loops</a></b> ·
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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."
|
||||||
|
|
14
common.py
14
common.py
|
@ -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',
|
||||||
|
|
|
@ -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']
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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)
|
||||||
node(u'participants', u'Participants', u'home', 'page',
|
|
||||||
body=u'Participants\n============', target=u'concepts/person',
|
# resources
|
||||||
viewName=u'listchildren')
|
resource(u'homepage', u'Welcome', u'textdocument',
|
||||||
node(u'topics', u'Topics', u'home', 'page', body=u'Topics\n======',
|
contentType='text/restructured')
|
||||||
target=u'concepts/topic', viewName=u'listchildren')
|
resource(u'impressum', u'Legal Information', u'textdocument',
|
||||||
|
contentType='text/restructured')
|
||||||
|
|
||||||
|
#nodes
|
||||||
|
node(u'home', u'Home', '', 'menu')
|
||||||
|
node(u'welcome', u'Welcome', u'home', u'text',
|
||||||
|
target=u'resources/homepage')
|
||||||
|
node(u'participants', u'Participants', u'home', 'page',
|
||||||
|
target=u'concepts/participants')
|
||||||
|
node(u'topics', u'Topics', u'home', 'page', target=u'concepts/topics')
|
||||||
node(u'glossary', u'Glossary', u'home', 'page', target=u'concepts/glossary')
|
node(u'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')
|
||||||
|
|
9
data/loops_std_update_de.dmp
Normal file
9
data/loops_std_update_de.dmp
Normal 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')
|
|
@ -27,7 +27,7 @@ configuration):
|
||||||
>>> concepts, resources, views = t.setup()
|
>>> concepts, resources, views = t.setup()
|
||||||
|
|
||||||
>>> len(concepts) + len(resources)
|
>>> len(concepts) + len(resources)
|
||||||
36
|
38
|
||||||
|
|
||||||
>>> loopsRoot = site['loops']
|
>>> loopsRoot = site['loops']
|
||||||
|
|
||||||
|
@ -47,11 +47,11 @@ Type- and text-based queries
|
||||||
>>> from loops.expert import query
|
>>> from loops.expert import query
|
||||||
>>> qu = query.Title('ty*')
|
>>> qu = query.Title('ty*')
|
||||||
>>> list(qu.apply())
|
>>> list(qu.apply())
|
||||||
[0, 2, 65]
|
[0, 2, 70]
|
||||||
|
|
||||||
>>> qu = query.Type('loops:*')
|
>>> qu = query.Type('loops:*')
|
||||||
>>> len(list(qu.apply()))
|
>>> len(list(qu.apply()))
|
||||||
36
|
38
|
||||||
|
|
||||||
>>> qu = query.Type('loops:concept:predicate')
|
>>> qu = query.Type('loops:concept:predicate')
|
||||||
>>> len(list(qu.apply()))
|
>>> len(list(qu.apply()))
|
||||||
|
|
|
@ -91,4 +91,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
167
expert/browser/export.py
Normal 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 '???'
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
132
expert/field.py
132
expert/field.py
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
|
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -22,6 +22,7 @@ Field definitions for reports.
|
||||||
|
|
||||||
from zope.app.form.browser.interfaces import ITerms
|
from zope.app.form.browser.interfaces import ITerms
|
||||||
from zope import component
|
from zope import component
|
||||||
|
from zope.i18n import translate
|
||||||
from zope.i18n.locales import locales
|
from zope.i18n.locales import locales
|
||||||
from zope.schema.interfaces import IVocabularyFactory, IContextSourceBinder
|
from zope.schema.interfaces import IVocabularyFactory, IContextSourceBinder
|
||||||
|
|
||||||
|
@ -29,18 +30,32 @@ from cybertools.composer.report.field import Field as BaseField
|
||||||
from cybertools.composer.report.field import TableCellStyle
|
from cybertools.composer.report.field import TableCellStyle
|
||||||
from cybertools.composer.report.result import ResultSet
|
from cybertools.composer.report.result import ResultSet
|
||||||
from cybertools.stateful.interfaces import IStateful, IStatesDefinition
|
from cybertools.stateful.interfaces import IStateful, IStatesDefinition
|
||||||
from cybertools.util.date import timeStamp2Date
|
from cybertools.util.date import timeStamp2Date, timeStamp2ISO
|
||||||
|
from cybertools.util.format import formatDate
|
||||||
from loops.common import baseObject
|
from loops.common import baseObject
|
||||||
from loops.expert.report import ReportInstance
|
from loops.expert.report import ReportInstance
|
||||||
|
from loops.organize.work.browser import WorkItemDetails
|
||||||
from loops import util
|
from loops import util
|
||||||
|
|
||||||
|
|
||||||
class Field(BaseField):
|
class Field(BaseField):
|
||||||
|
|
||||||
|
def getContext(self, row):
|
||||||
|
return row.context
|
||||||
|
|
||||||
def getSelectValue(self, row):
|
def getSelectValue(self, row):
|
||||||
return self.getValue(row)
|
return self.getValue(row)
|
||||||
|
|
||||||
|
|
||||||
|
class StringField(Field):
|
||||||
|
|
||||||
|
def getSelectValue(self, row):
|
||||||
|
return self.getValue(row).strip()
|
||||||
|
|
||||||
|
def getSortValue(self, row):
|
||||||
|
return self.getValue(row).strip()
|
||||||
|
|
||||||
|
|
||||||
class TextField(Field):
|
class TextField(Field):
|
||||||
|
|
||||||
format = 'text/restructured'
|
format = 'text/restructured'
|
||||||
|
@ -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)):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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"}'
|
'{"items": [{"id": "...", "name": "Zope", "label": "Zope (Thema)"}, {"id": "...", "name": "Zope 2", "label": "Zope 2 (Thema)"}, {"id": "...", "name": "Zope 3", "label": "Zope 3 (Thema)"}], "identifier": "id"}'
|
||||||
|
|
||||||
Preset Concept Types on Search Forms
|
Preset Concept Types on Search Forms
|
||||||
------------------------------------
|
------------------------------------
|
||||||
|
@ -219,13 +219,13 @@ and thus include the customer type in the preset search types.
|
||||||
|
|
||||||
>>> searchView.conceptsForType('loops:concept:customer')
|
>>> searchView.conceptsForType('loops:concept:customer')
|
||||||
[{'token': 'none', 'title': u'not selected'},
|
[{'token': 'none', 'title': u'not selected'},
|
||||||
{'token': '74', 'title': u'Customer 1'},
|
{'token': '...', 'title': u'Customer 1'},
|
||||||
{'token': '76', 'title': u'Customer 2'},
|
{'token': '...', 'title': u'Customer 2'},
|
||||||
{'token': '78', 'title': u'Customer 3'}]
|
{'token': '...', 'title': u'Customer 3'}]
|
||||||
|
|
||||||
Let's use this new search option for querying:
|
Let's use this new search option for querying:
|
||||||
|
|
||||||
>>> form = {'search.4.text_selected': u'74'}
|
>>> form = {'search.4.text_selected': u'75'}
|
||||||
>>> resultsView = SearchResults(page, TestRequest(form=form))
|
>>> resultsView = SearchResults(page, TestRequest(form=form))
|
||||||
>>> results = list(resultsView.results)
|
>>> results = list(resultsView.results)
|
||||||
>>> results[0].title
|
>>> results[0].title
|
||||||
|
|
6
external/README.txt
vendored
6
external/README.txt
vendored
|
@ -17,7 +17,7 @@ Let's set up a loops site with basic and example concepts and resources.
|
||||||
>>> concepts, resources, views = t.setup()
|
>>> concepts, resources, views = t.setup()
|
||||||
>>> loopsRoot = site['loops']
|
>>> loopsRoot = site['loops']
|
||||||
>>> len(concepts), len(resources), len(views)
|
>>> len(concepts), len(resources), len(views)
|
||||||
(33, 3, 1)
|
(35, 3, 1)
|
||||||
|
|
||||||
|
|
||||||
Importing loops Objects
|
Importing loops Objects
|
||||||
|
@ -44,7 +44,7 @@ Creating the corresponding objects
|
||||||
>>> loader = Loader(loopsRoot)
|
>>> loader = Loader(loopsRoot)
|
||||||
>>> loader.load(elements)
|
>>> loader.load(elements)
|
||||||
>>> len(concepts), len(resources), len(views)
|
>>> len(concepts), len(resources), len(views)
|
||||||
(34, 3, 1)
|
(36, 3, 1)
|
||||||
|
|
||||||
>>> from loops.common import adapted
|
>>> from loops.common import adapted
|
||||||
>>> adMyquery = adapted(concepts['myquery'])
|
>>> adMyquery = adapted(concepts['myquery'])
|
||||||
|
@ -131,7 +131,7 @@ Extracting elements
|
||||||
>>> extractor = Extractor(loopsRoot, os.path.join(dataDirectory, 'export'))
|
>>> extractor = Extractor(loopsRoot, os.path.join(dataDirectory, 'export'))
|
||||||
>>> elements = list(extractor.extract())
|
>>> elements = list(extractor.extract())
|
||||||
>>> len(elements)
|
>>> len(elements)
|
||||||
66
|
69
|
||||||
|
|
||||||
Writing object information to the external storage
|
Writing object information to the external storage
|
||||||
--------------------------------------------------
|
--------------------------------------------------
|
||||||
|
|
6
external/pyfunc.py
vendored
6
external/pyfunc.py
vendored
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -66,7 +70,7 @@ class ExternalCollectionAdapter(AdapterBase):
|
||||||
|
|
||||||
newResources = None
|
newResources = None
|
||||||
updateMessage = None
|
updateMessage = None
|
||||||
|
|
||||||
def getExclude(self):
|
def getExclude(self):
|
||||||
return getattr(self.context, '_exclude', None) or []
|
return getattr(self.context, '_exclude', None) or []
|
||||||
def setExclude(self, value):
|
def setExclude(self, value):
|
||||||
|
@ -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):
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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."
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# Copyright (c) 2011 Helmut Merz helmutm@cy55.de
|
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -18,8 +18,6 @@
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Integrator interfaces.
|
Integrator interfaces.
|
||||||
|
|
||||||
$Id$
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from zope.interface import Interface, Attribute
|
from zope.interface import Interface, Attribute
|
||||||
|
@ -133,3 +131,5 @@ class IOfficeFile(IExternalFile):
|
||||||
It provides access to the document content and properties.
|
It provides access to the document content and properties.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
documentPropertiesAccessible = Attribute(
|
||||||
|
'Are document properties accessible?')
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
|
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -26,7 +26,7 @@ from lxml import etree
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from time import strptime
|
from time import strptime
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile, BadZipfile
|
||||||
from zope.cachedescriptors.property import Lazy
|
from zope.cachedescriptors.property import Lazy
|
||||||
from zope import component
|
from zope import component
|
||||||
from zope.component import adapts
|
from zope.component import adapts
|
||||||
|
@ -52,12 +52,22 @@ class OfficeFile(ExternalFileAdapter):
|
||||||
|
|
||||||
implements(IOfficeFile)
|
implements(IOfficeFile)
|
||||||
|
|
||||||
|
_adapterAttributes = (ExternalFileAdapter._adapterAttributes +
|
||||||
|
('documentPropertiesAccessible',))
|
||||||
|
|
||||||
propertyMap = {u'Revision:': 'version'}
|
propertyMap = {u'Revision:': 'version'}
|
||||||
propFileName = 'docProps/custom.xml'
|
propFileName = 'docProps/custom.xml'
|
||||||
corePropFileName = 'docProps/core.xml'
|
corePropFileName = 'docProps/core.xml'
|
||||||
fileExtensions = ('.docm', '.docx', 'dotm', 'dotx', 'pptx', 'potx', 'ppsx',
|
fileExtensions = ('.docm', '.docx', 'dotm', 'dotx', 'pptx', 'potx', 'ppsx',
|
||||||
'.xlsm', '.xlsx', '.xltm', '.xltx')
|
'.xlsm', '.xlsx', '.xltm', '.xltx')
|
||||||
|
|
||||||
|
def getDocumentPropertiesAccessible(self):
|
||||||
|
return getattr(self.context, '_documentPropertiesAccessible', True)
|
||||||
|
def setDocumentPropertiesAccessible(self, value):
|
||||||
|
self.context._documentPropertiesAccessible = value
|
||||||
|
documentPropertiesAccessible = property(
|
||||||
|
getDocumentPropertiesAccessible, setDocumentPropertiesAccessible)
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def logger(self):
|
def logger(self):
|
||||||
return getLogger('loops.integrator.office.base.OfficeFile')
|
return getLogger('loops.integrator.office.base.OfficeFile')
|
||||||
|
@ -79,14 +89,19 @@ class OfficeFile(ExternalFileAdapter):
|
||||||
def docPropertyDom(self):
|
def docPropertyDom(self):
|
||||||
fn = self.docFilename
|
fn = self.docFilename
|
||||||
result = dict(core=[], custom=[])
|
result = dict(core=[], custom=[])
|
||||||
|
if not os.path.exists(fn):
|
||||||
|
# may happen before file has been created
|
||||||
|
return result
|
||||||
root, ext = os.path.splitext(fn)
|
root, ext = os.path.splitext(fn)
|
||||||
if not ext.lower() in self.fileExtensions:
|
if not ext.lower() in self.fileExtensions:
|
||||||
return result
|
return result
|
||||||
try:
|
try:
|
||||||
zf = ZipFile(fn, 'r')
|
zf = ZipFile(fn, 'r')
|
||||||
except IOError, e:
|
self.documentPropertiesAccessible = True
|
||||||
|
except (IOError, BadZipfile), e:
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
self.logger.warn(e)
|
self.logger.warn(e)
|
||||||
|
self.documentPropertiesAccessible = False
|
||||||
return result
|
return result
|
||||||
if self.corePropFileName not in zf.namelist():
|
if self.corePropFileName not in zf.namelist():
|
||||||
self.logger.warn('Core properties not found in file %s.' %
|
self.logger.warn('Core properties not found in file %s.' %
|
||||||
|
@ -123,6 +138,8 @@ class OfficeFile(ExternalFileAdapter):
|
||||||
attributes = {}
|
attributes = {}
|
||||||
# get dc:description from core.xml
|
# get dc:description from core.xml
|
||||||
desc = self.getCoreProperty('description')
|
desc = self.getCoreProperty('description')
|
||||||
|
if not self.documentPropertiesAccessible:
|
||||||
|
return
|
||||||
if desc is not None:
|
if desc is not None:
|
||||||
attributes['comments'] = desc
|
attributes['comments'] = desc
|
||||||
dom = self.docPropertyDom['custom']
|
dom = self.docPropertyDom['custom']
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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."
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
|
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -30,8 +30,13 @@ from cybertools.typology.interfaces import IType
|
||||||
from loops.browser.action import DialogAction
|
from loops.browser.action import DialogAction
|
||||||
from loops.browser.common import BaseView
|
from loops.browser.common import BaseView
|
||||||
from loops.browser.concept import ConceptView
|
from loops.browser.concept import ConceptView
|
||||||
|
from loops.common import adapted
|
||||||
from loops.knowledge.interfaces import IPerson, ITask
|
from loops.knowledge.interfaces import IPerson, ITask
|
||||||
from loops.organize.party import getPersonForUser
|
from loops.organize.party import getPersonForUser
|
||||||
|
from loops.organize.personal import favorite
|
||||||
|
from loops.organize.personal.interfaces import IFavorites
|
||||||
|
from loops.security.common import checkPermission
|
||||||
|
from loops import util
|
||||||
from loops.util import _
|
from loops.util import _
|
||||||
|
|
||||||
|
|
||||||
|
@ -69,6 +74,63 @@ actions.register('createQualification', 'portlet', DialogAction,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InstitutionMixin(object):
|
||||||
|
|
||||||
|
knowledge_macros = knowledge_macros
|
||||||
|
|
||||||
|
adminMaySelectAllInstitutions = True
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def institutionType(self):
|
||||||
|
return self.conceptManager['institution']
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def institutions(self):
|
||||||
|
if self.adminMaySelectAllInstitutions:
|
||||||
|
if checkPermission('loops.ManageWorkspaces', self.context):
|
||||||
|
return self.getAllInstitutions()
|
||||||
|
result = []
|
||||||
|
p = getPersonForUser(self.context, self.request)
|
||||||
|
if p is None:
|
||||||
|
return result
|
||||||
|
for parent in p.getParents(
|
||||||
|
[self.memberPredicate, self.masterPredicate]):
|
||||||
|
if parent.conceptType == self.institutionType:
|
||||||
|
result.append(dict(
|
||||||
|
object=adapted(parent),
|
||||||
|
title=parent.title,
|
||||||
|
uid=util.getUidForObject(parent)))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def getAllInstitutions(self):
|
||||||
|
insts = self.institutionType.getChildren([self.typePredicate])
|
||||||
|
return [dict(object=adapted(inst),
|
||||||
|
title=inst.title,
|
||||||
|
uid=util.getUidForObject(inst)) for inst in insts]
|
||||||
|
|
||||||
|
def setInstitution(self, uid):
|
||||||
|
inst = util.getObjectForUid(uid)
|
||||||
|
person = getPersonForUser(self.context, self.request)
|
||||||
|
favorite.setInstitution(person, inst)
|
||||||
|
self.institution = inst
|
||||||
|
return True
|
||||||
|
|
||||||
|
def getSavedInstitution(self):
|
||||||
|
person = getPersonForUser(self.context, self.request)
|
||||||
|
favorites = IFavorites(self.loopsRoot.getRecordManager()['favorites'])
|
||||||
|
for inst in favorites.list(person, type='institution'):
|
||||||
|
return adapted(util.getObjectForUid(inst))
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def institution(self):
|
||||||
|
saved = self.getSavedInstitution()
|
||||||
|
for inst in self.institutions:
|
||||||
|
if inst['object'] == saved:
|
||||||
|
return inst['object']
|
||||||
|
if self.institutions:
|
||||||
|
return self.institutions[0]['object']
|
||||||
|
|
||||||
|
|
||||||
class MyKnowledge(ConceptView):
|
class MyKnowledge(ConceptView):
|
||||||
|
|
||||||
template = template
|
template = template
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
type(u'competence', u'Kompetenz', viewName=u'',
|
type(u'competence', u'Qualifikation', viewName=u'',
|
||||||
typeInterface=u'loops.knowledge.qualification.interfaces.ICompetence',
|
typeInterface=u'loops.knowledge.qualification.interfaces.ICompetence',
|
||||||
options=u'action.portlet:create_subtype,edit_concept')
|
options=u'action.portlet:create_subtype,edit_concept')
|
||||||
type(u'person', u'Person', viewName=u'',
|
type(u'person', u'Person', viewName=u'',
|
||||||
typeInterface=u'loops.knowledge.interfaces.IPerson',
|
typeInterface=u'loops.knowledge.interfaces.IPerson',
|
||||||
options=u'action.portlet:createQualification,editPerson')
|
options=u'action.portlet:createQualification,editPerson')
|
||||||
|
type(u'report', u'Report', viewName=u'',
|
||||||
|
typeInterface='loops.expert.report.IReport')
|
||||||
type(u'task', u'Aufgabe', viewName=u'',
|
type(u'task', u'Aufgabe', viewName=u'',
|
||||||
typeInterface=u'loops.knowledge.interfaces.ITask',
|
typeInterface=u'loops.knowledge.interfaces.ITask',
|
||||||
options=u'action.portlet:createTask,editTask')
|
options=u'action.portlet:createTask,editTask')
|
||||||
|
@ -26,6 +28,10 @@ concept(u'requires', u'requires', u'predicate')
|
||||||
concept(u'issubtype', u'is Subtype', u'predicate', options=u'hide_children',
|
concept(u'issubtype', u'is Subtype', u'predicate', options=u'hide_children',
|
||||||
predicateInterface='loops.interfaces.IIsSubtype')
|
predicateInterface='loops.interfaces.IIsSubtype')
|
||||||
|
|
||||||
|
# reports
|
||||||
|
concept(u'qualification_overview', u'Qualification Overview', u'report',
|
||||||
|
reportType=u'qualification_overview')
|
||||||
|
|
||||||
# structure
|
# structure
|
||||||
child(u'general', u'competence', u'standard')
|
child(u'general', u'competence', u'standard')
|
||||||
child(u'general', u'depends', u'standard')
|
child(u'general', u'depends', u'standard')
|
||||||
|
@ -38,6 +44,7 @@ child(u'general', u'topic', u'standard')
|
||||||
#child(u'general', u'training', u'standard')
|
#child(u'general', u'training', u'standard')
|
||||||
|
|
||||||
child(u'system', u'issubtype', u'standard')
|
child(u'system', u'issubtype', u'standard')
|
||||||
|
child(u'system', u'report', u'standard')
|
||||||
|
|
||||||
child(u'competence', u'competence', u'issubtype')
|
child(u'competence', u'competence', u'issubtype')
|
||||||
#child(u'competence', u'training', u'issubtype', usePredicate=u'provides')
|
#child(u'competence', u'training', u'issubtype', usePredicate=u'provides')
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
type(u'competence', u'Kompetenz', viewName=u'',
|
type(u'competence', u'Qualifikation', viewName=u'',
|
||||||
typeInterface=u'loops.knowledge.qualification.interfaces.ICompetence',
|
typeInterface=u'loops.knowledge.qualification.interfaces.ICompetence',
|
||||||
options=u'action.portlet:create_subtype,edit_concept')
|
options=u'action.portlet:create_subtype,edit_concept')
|
||||||
|
type(u'ipskill', u'Kompetenz', viewName=u'',
|
||||||
|
options=u'action.portlet:edit_concept')
|
||||||
|
type(u'ipskillsrequired', u'Soll-Profil', viewName=u'',
|
||||||
|
options=u'action.portlet:edit_concept')
|
||||||
|
type(u'jobposition', u'Stelle', viewName=u'',
|
||||||
|
options=u'action.portlet:edit_concept')
|
||||||
|
type(u'report', u'Report', viewName=u'',
|
||||||
|
typeInterface='loops.expert.report.IReport')
|
||||||
# type(u'person', u'Person', viewName=u'',
|
# type(u'person', u'Person', viewName=u'',
|
||||||
# typeInterface=u'loops.knowledge.interfaces.IPerson',
|
# typeInterface=u'loops.knowledge.interfaces.IPerson',
|
||||||
# options=u'action.portlet:editPerson')
|
# options=u'action.portlet:editPerson')
|
||||||
|
@ -26,9 +34,16 @@ concept(u'requires', u'requires', u'predicate')
|
||||||
concept(u'issubtype', u'is Subtype', u'predicate', options=u'hide_children',
|
concept(u'issubtype', u'is Subtype', u'predicate', options=u'hide_children',
|
||||||
predicateInterface='loops.interfaces.IIsSubtype')
|
predicateInterface='loops.interfaces.IIsSubtype')
|
||||||
|
|
||||||
|
# reports
|
||||||
|
concept(u'qualification_overview', u'Qualification Overview', u'report',
|
||||||
|
reportType=u'qualification_overview')
|
||||||
|
|
||||||
# structure
|
# structure
|
||||||
child(u'general', u'competence', u'standard')
|
child(u'general', u'competence', u'standard')
|
||||||
child(u'general', u'depends', u'standard')
|
child(u'general', u'depends', u'standard')
|
||||||
|
child(u'general', u'ipskill', u'standard')
|
||||||
|
child(u'general', u'ipskillsrequired', u'standard')
|
||||||
|
child(u'general', u'jobposition', u'standard')
|
||||||
child(u'general', u'knows', u'standard')
|
child(u'general', u'knows', u'standard')
|
||||||
#child(u'general', u'person', u'standard')
|
#child(u'general', u'person', u'standard')
|
||||||
child(u'general', u'provides', u'standard')
|
child(u'general', u'provides', u'standard')
|
||||||
|
@ -38,6 +53,7 @@ child(u'general', u'requires', u'standard')
|
||||||
#child(u'general', u'training', u'standard')
|
#child(u'general', u'training', u'standard')
|
||||||
|
|
||||||
child(u'system', u'issubtype', u'standard')
|
child(u'system', u'issubtype', u'standard')
|
||||||
|
child(u'system', u'report', u'standard')
|
||||||
|
|
||||||
child(u'competence', u'competence', u'issubtype')
|
child(u'competence', u'competence', u'issubtype')
|
||||||
#child(u'competence', u'training', u'issubtype', usePredicate=u'provides')
|
#child(u'competence', u'training', u'issubtype', usePredicate=u'provides')
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
<!-- $Id$ -->
|
|
||||||
|
|
||||||
<configure
|
<configure
|
||||||
xmlns:zope="http://namespaces.zope.org/zope"
|
xmlns:zope="http://namespaces.zope.org/zope"
|
||||||
xmlns="http://namespaces.zope.org/browser"
|
xmlns="http://namespaces.zope.org/browser"
|
||||||
|
@ -28,14 +26,14 @@
|
||||||
name="create_glossaryitem.html"
|
name="create_glossaryitem.html"
|
||||||
for="loops.interfaces.INode"
|
for="loops.interfaces.INode"
|
||||||
class="loops.knowledge.glossary.browser.CreateGlossaryItemForm"
|
class="loops.knowledge.glossary.browser.CreateGlossaryItemForm"
|
||||||
permission="zope.ManageContent"
|
permission="zope.View"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<page
|
<page
|
||||||
name="edit_glossaryitem.html"
|
name="edit_glossaryitem.html"
|
||||||
for="loops.interfaces.INode"
|
for="loops.interfaces.INode"
|
||||||
class="loops.knowledge.glossary.browser.EditGlossaryItemForm"
|
class="loops.knowledge.glossary.browser.EditGlossaryItemForm"
|
||||||
permission="zope.ManageContent"
|
permission="zope.View"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<zope:adapter
|
<zope:adapter
|
||||||
|
@ -43,7 +41,7 @@
|
||||||
for="loops.browser.node.NodeView
|
for="loops.browser.node.NodeView
|
||||||
zope.publisher.interfaces.browser.IBrowserRequest"
|
zope.publisher.interfaces.browser.IBrowserRequest"
|
||||||
factory="loops.knowledge.glossary.browser.CreateGlossaryItem"
|
factory="loops.knowledge.glossary.browser.CreateGlossaryItem"
|
||||||
permission="zope.ManageContent"
|
permission="zope.View"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<zope:adapter
|
<zope:adapter
|
||||||
|
@ -51,7 +49,7 @@
|
||||||
for="loops.browser.node.NodeView
|
for="loops.browser.node.NodeView
|
||||||
zope.publisher.interfaces.browser.IBrowserRequest"
|
zope.publisher.interfaces.browser.IBrowserRequest"
|
||||||
factory="loops.knowledge.glossary.browser.EditGlossaryItem"
|
factory="loops.knowledge.glossary.browser.EditGlossaryItem"
|
||||||
permission="zope.ManageContent"
|
permission="zope.View"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</configure>
|
</configure>
|
||||||
|
|
|
@ -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"> </a></div>
|
<div><a name="top"> </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> </div>
|
<div> </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:[]">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
|
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -18,19 +18,25 @@
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Definition of view classes and other browser related stuff for the
|
Definition of view classes and other browser related stuff for the
|
||||||
loops.knowledge package.
|
loops.knowledge.qualification package.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from zope import interface, component
|
from zope import interface, component
|
||||||
from zope.app.pagetemplate import ViewPageTemplateFile
|
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||||
from zope.cachedescriptors.property import Lazy
|
from zope.cachedescriptors.property import Lazy
|
||||||
|
|
||||||
|
from loops.browser.concept import ConceptView
|
||||||
|
from loops.expert.browser.export import ResultsConceptCSVExport
|
||||||
from loops.expert.browser.report import ResultsConceptView
|
from loops.expert.browser.report import ResultsConceptView
|
||||||
from loops.knowledge.browser import template, knowledge_macros
|
from loops.organize.party import getPersonForUser
|
||||||
from loops.knowledge.qualification.base import QualificationRecord
|
from loops.util import _
|
||||||
|
|
||||||
|
|
||||||
class PersonQualificationView(ResultsConceptView):
|
class Qualifications(ResultsConceptView):
|
||||||
|
|
||||||
pass
|
# obsolete because we can directly use ResultsConceptView
|
||||||
|
|
||||||
|
#reportName = 'qualification_overview'
|
||||||
|
|
||||||
|
pass # report assigned to query via hasReport relation
|
||||||
|
|
||||||
|
|
|
@ -15,4 +15,50 @@
|
||||||
|
|
||||||
<!-- views -->
|
<!-- views -->
|
||||||
|
|
||||||
|
<zope:adapter
|
||||||
|
name="qualifications.html"
|
||||||
|
for="loops.interfaces.IConcept
|
||||||
|
zope.publisher.interfaces.browser.IBrowserRequest"
|
||||||
|
provides="zope.interface.Interface"
|
||||||
|
factory="loops.knowledge.qualification.browser.Qualifications"
|
||||||
|
permission="zope.View" />
|
||||||
|
|
||||||
|
<!-- reports -->
|
||||||
|
|
||||||
|
<zope:adapter
|
||||||
|
name="qualification_overview"
|
||||||
|
factory="loops.knowledge.qualification.report.QualificationOverview"
|
||||||
|
provides="loops.expert.report.IReportInstance"
|
||||||
|
trusted="True" />
|
||||||
|
<zope:class class="loops.knowledge.qualification.report.QualificationOverview">
|
||||||
|
<require permission="zope.View"
|
||||||
|
interface="loops.expert.report.IReportInstance" />
|
||||||
|
<require permission="zope.ManageContent"
|
||||||
|
set_schema="loops.expert.report.IReportInstance" />
|
||||||
|
</zope:class>
|
||||||
|
|
||||||
|
<zope:adapter
|
||||||
|
name="qualifications"
|
||||||
|
factory="loops.knowledge.qualification.report.Qualifications"
|
||||||
|
provides="loops.expert.report.IReportInstance"
|
||||||
|
trusted="True" />
|
||||||
|
<zope:class class="loops.knowledge.qualification.report.Qualifications">
|
||||||
|
<require permission="zope.View"
|
||||||
|
interface="loops.expert.report.IReportInstance" />
|
||||||
|
<require permission="zope.ManageContent"
|
||||||
|
set_schema="loops.expert.report.IReportInstance" />
|
||||||
|
</zope:class>
|
||||||
|
|
||||||
|
<zope:adapter
|
||||||
|
name="person_qualifications"
|
||||||
|
factory="loops.knowledge.qualification.report.PersonQualifications"
|
||||||
|
provides="loops.expert.report.IReportInstance"
|
||||||
|
trusted="True" />
|
||||||
|
<zope:class class="loops.knowledge.qualification.report.PersonQualifications">
|
||||||
|
<require permission="zope.View"
|
||||||
|
interface="loops.expert.report.IReportInstance" />
|
||||||
|
<require permission="zope.ManageContent"
|
||||||
|
set_schema="loops.expert.report.IReportInstance" />
|
||||||
|
</zope:class>
|
||||||
|
|
||||||
</configure>
|
</configure>
|
||||||
|
|
133
knowledge/qualification/report.py
Normal file
133
knowledge/qualification/report.py
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Qualification management report definitions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.cachedescriptors.property import Lazy
|
||||||
|
|
||||||
|
from cybertools.composer.report.base import LeafQueryCriteria, CompoundQueryCriteria
|
||||||
|
from cybertools.util.jeep import Jeep
|
||||||
|
from loops.expert.report import ReportInstance
|
||||||
|
from loops.organize.work.report import WorkRow
|
||||||
|
from loops.organize.work.report import deadline, day, task, party, state
|
||||||
|
from loops.organize.work.report import dayStart, dayEnd
|
||||||
|
from loops.organize.work.report import workTitle, workDescription
|
||||||
|
from loops.organize.work.report import partyState
|
||||||
|
from loops import util
|
||||||
|
|
||||||
|
|
||||||
|
class QualificationOverview(ReportInstance):
|
||||||
|
|
||||||
|
type = 'qualification_overview'
|
||||||
|
label = u'Qualification Overview'
|
||||||
|
|
||||||
|
rowFactory = WorkRow
|
||||||
|
|
||||||
|
fields = Jeep((task, party, workTitle, dayStart, dayEnd, state,
|
||||||
|
partyState,)) # +deadline?
|
||||||
|
|
||||||
|
defaultOutputFields = Jeep(list(fields)[:-1])
|
||||||
|
defaultSortCriteria = (party, task,)
|
||||||
|
|
||||||
|
def getOptions(self, option):
|
||||||
|
return self.view.options(option)
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def states(self):
|
||||||
|
return self.getOptions('report_select_state' or ('planned',))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def queryCriteria(self):
|
||||||
|
crit = self.context.queryCriteria or []
|
||||||
|
f = self.fields.partyState
|
||||||
|
crit.append(
|
||||||
|
LeafQueryCriteria(f.name, f.operator, 'active', f))
|
||||||
|
return CompoundQueryCriteria(crit)
|
||||||
|
|
||||||
|
def selectObjects(self, parts):
|
||||||
|
result = []
|
||||||
|
workItems = self.recordManager['work']
|
||||||
|
pred = self.conceptManager['querytarget']
|
||||||
|
types = self.view.context.getChildren([pred])
|
||||||
|
for t in types:
|
||||||
|
for c in t.getChildren([self.view.typePredicate]):
|
||||||
|
uid = util.getUidForObject(c)
|
||||||
|
for wi in workItems.query(taskId=uid, state=self.states):
|
||||||
|
result.append(wi)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class Qualifications(QualificationOverview):
|
||||||
|
|
||||||
|
type = 'qualifications'
|
||||||
|
label = u'Qualifications'
|
||||||
|
|
||||||
|
taskTypeNames = ('competence',)
|
||||||
|
|
||||||
|
def getOptions(self, option):
|
||||||
|
return self.view.typeOptions(option)
|
||||||
|
|
||||||
|
def selectObjects(self, parts):
|
||||||
|
result = []
|
||||||
|
workItems = self.recordManager['work']
|
||||||
|
target = self.view.context
|
||||||
|
tasks = [target] + self.getAllSubtasks(target)
|
||||||
|
for t in tasks:
|
||||||
|
uid = util.getUidForObject(t)
|
||||||
|
for wi in workItems.query(taskId=uid, state=self.states):
|
||||||
|
result.append(wi)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def getAllSubtasks(self, concept):
|
||||||
|
result = []
|
||||||
|
for c in concept.getChildren([self.view.defaultPredicate]):
|
||||||
|
if c.conceptType in self.taskTypes:
|
||||||
|
result.append(c)
|
||||||
|
result.extend(self.getAllSubtasks(c))
|
||||||
|
return result
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def taskTypes(self):
|
||||||
|
return [c for c in [self.conceptManager.get(name)
|
||||||
|
for name in self.taskTypeNames]
|
||||||
|
if c is not None]
|
||||||
|
|
||||||
|
|
||||||
|
class PersonQualifications(QualificationOverview):
|
||||||
|
|
||||||
|
type = 'person_qualifications'
|
||||||
|
label = u'Qualifications for Person'
|
||||||
|
|
||||||
|
defaultSortCriteria = (task,)
|
||||||
|
|
||||||
|
def getOptions(self, option):
|
||||||
|
return self.view.typeOptions(option)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def queryCriteria(self):
|
||||||
|
crit = self.context.queryCriteria or []
|
||||||
|
return CompoundQueryCriteria(crit)
|
||||||
|
|
||||||
|
def selectObjects(self, parts):
|
||||||
|
workItems = self.recordManager['work']
|
||||||
|
person = self.view.context
|
||||||
|
uid = util.getUidForObject(person)
|
||||||
|
return workItems.query(userName=uid, state=self.states)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
|
# Copyright (c) 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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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>:
|
||||||
|
<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"> </td></tr>
|
tal:define="personId request/person|nothing"
|
||||||
|
tal:condition="personId"
|
||||||
|
tal:attributes="value personId" />
|
||||||
|
<tal:group repeat="group item/groups">
|
||||||
|
<tr>
|
||||||
|
<td> </td>
|
||||||
|
<td tal:repeat="opt item/answerOptions"> </td></tr>
|
||||||
<tr class="vpad">
|
<tr class="vpad">
|
||||||
<td tal:define="infoText python:item.getInfoText(qugroup)">
|
<td tal:define="infoText group/infoText">
|
||||||
<b tal:content="qugroup/title" />
|
<b i18n:translate=""
|
||||||
|
tal:content="group/title" />
|
||||||
<div class="infotext"
|
<div class="infotext"
|
||||||
tal:condition="infoText">
|
tal:condition="infoText">
|
||||||
<span tal:content="structure infoText" />
|
<span tal:content="structure infoText" />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center"
|
<td tal:repeat="opt python:[opt for opt in item.answerOptions
|
||||||
i18n:translate="">No answer</td>
|
if opt.get('colspan') != '0']"
|
||||||
<td colspan="2"
|
i18n:translate=""
|
||||||
i18n:translate="">Fully applies</td>
|
i18n:attributes="title"
|
||||||
<td colspan="2"
|
tal:attributes="title opt/description|string:;
|
||||||
style="text-align: right"
|
class python:opt.get('cssclass') or 'center';
|
||||||
i18n:translate="">Does not apply</td>
|
colspan python:opt.get('colspan')"
|
||||||
|
tal:content="opt/label|string:" />
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="vpad"
|
<tal:question repeat="question group/questions">
|
||||||
tal:repeat="question qugroup/questions">
|
<tal:question define="qutype python:
|
||||||
<td tal:content="question/text" />
|
question.questionType or 'value_selection'">
|
||||||
<td style="white-space: nowrap; text-align: center"
|
<metal:question use-macro="item/template/macros/?qutype" />
|
||||||
tal:repeat="value python:item.getValues(question)">
|
</tal:question>
|
||||||
<input type="radio"
|
</tal:question>
|
||||||
i18n:attributes="title"
|
</tal:group>
|
||||||
tal:condition="value/radio"
|
|
||||||
tal:attributes="
|
|
||||||
name string:question_${question/uid};
|
|
||||||
value value/value;
|
|
||||||
checked value/checked;
|
|
||||||
title string:survey_value_${value/value}" />
|
|
||||||
<span tal:condition="not:value/radio"
|
|
||||||
title="Obligatory question, must be answered"
|
|
||||||
i18n:attributes="title">***
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tal:qugroup>
|
|
||||||
</table>
|
</table>
|
||||||
<input type="submit" name="submit" value="Evaluate Questionnaire"
|
<input type="submit" name="submit" value="Evaluate Questionnaire"
|
||||||
i18n:attributes="value" />
|
i18n:attributes="value" />
|
||||||
|
<input type="submit" name="save" value="Save Data"
|
||||||
|
i18n:attributes="value" />
|
||||||
<input type="button" name="reset_responses" value="Reset Responses Entered"
|
<input type="button" name="reset_responses" value="Reset Responses Entered"
|
||||||
i18n:attributes="value"
|
i18n:attributes="value; onclick"
|
||||||
onclick="setRadioButtons('none'); return false" />
|
onclick="if (confirm('Do you really want to reset all response data?')) setRadioButtons('none'); return false" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</metal:block>
|
||||||
|
|
||||||
|
|
||||||
|
<metal:block define-macro="quest_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> </td><td> </td></tr>
|
||||||
|
<tal:question repeat="question group/questions">
|
||||||
|
<tr tal:attributes="class python:item.getCssClass(question)">
|
||||||
|
<td tal:content="question/text" />
|
||||||
|
<td tal:define="value python:item.getPrefsValue(question)">
|
||||||
|
<input type="radio"
|
||||||
|
tal:attributes="name string:group_${repeat/group/index};
|
||||||
|
value question/uid;
|
||||||
|
checked value" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tal:question>
|
||||||
|
</tal:group>
|
||||||
|
</table>
|
||||||
|
<input type="submit" name="submit" value="Evaluate Questionnaire"
|
||||||
|
i18n:attributes="value" />
|
||||||
|
<input type="submit" name="save" value="Save Data"
|
||||||
|
i18n:attributes="value" />
|
||||||
|
<input type="button" name="reset_responses" value="Reset Responses Entered"
|
||||||
|
i18n:attributes="value; onclick"
|
||||||
|
onclick="if (confirm('Do you really want to reset all response data?')) setRadioButtons('none'); return false" />
|
||||||
|
</form>
|
||||||
|
</metal:block>
|
||||||
|
|
||||||
|
|
||||||
|
<metal:block define-macro="value_selection">
|
||||||
|
<tr tal:attributes="class python:item.getCssClass(question)">
|
||||||
|
<td tal:content="question/text" />
|
||||||
|
<td style="white-space: nowrap; text-align: center"
|
||||||
|
tal:repeat="value python:item.getValues(question)">
|
||||||
|
<input type="radio"
|
||||||
|
i18n:attributes="title"
|
||||||
|
tal:attributes="name string:question_${question/uid};
|
||||||
|
value value/value;
|
||||||
|
checked value/checked;
|
||||||
|
title value/title" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</metal:block>
|
||||||
|
|
||||||
|
|
||||||
|
<metal:block define-macro="text">
|
||||||
|
<tr tal:attributes="class python:item.getCssClass(question)">
|
||||||
|
<td>
|
||||||
|
<div tal:content="question/text" />
|
||||||
|
<textarea style="width: 90%; margin-left: 20px"
|
||||||
|
tal:content="python:item.getTextValue(question)"
|
||||||
|
tal:attributes="name string:question_${question/uid}">
|
||||||
|
</textarea>
|
||||||
|
</td>
|
||||||
|
<td tal:repeat="opt item/answerOptions" />
|
||||||
|
</tr>
|
||||||
|
</metal:block>
|
||||||
|
|
||||||
|
|
||||||
|
<metal:block define-macro="report_standard">
|
||||||
|
<h3 i18n:translate="">Feedback</h3>
|
||||||
|
<div tal:define="header item/adapted/feedbackHeader"
|
||||||
|
tal:condition="header"
|
||||||
|
tal:content="structure python:
|
||||||
|
item.renderText(header, 'text/restructured')" />
|
||||||
|
<table class="listing">
|
||||||
|
<tr>
|
||||||
|
<th i18n:translate="">Category</th>
|
||||||
|
<th tal:repeat="col item/feedbackColumns"
|
||||||
|
i18n:translate=""
|
||||||
|
tal:attributes="class python:
|
||||||
|
col['name'] != 'text' and 'center' or None"
|
||||||
|
tal:content="col/label" />
|
||||||
|
</tr>
|
||||||
|
<tr style="vertical-align: top"
|
||||||
|
tal:repeat="fbitem feedback">
|
||||||
|
<td style="vertical-align: top"
|
||||||
|
tal:content="fbitem/category" />
|
||||||
|
<tal:cols repeat="col item/feedbackColumns">
|
||||||
|
<td style="vertical-align: top"
|
||||||
|
tal:define="name col/name"
|
||||||
|
tal:attributes="class python:name != 'text' and 'center' or None"
|
||||||
|
tal:content="fbitem/?name|string:" />
|
||||||
|
</tal:cols>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p tal:define="teamData item/teamData"
|
||||||
|
tal:condition="teamData">
|
||||||
|
<b><span i18n:translate="">Team Size</span>:
|
||||||
|
<span tal:content="python:len(teamData)" /></b><br />
|
||||||
|
</p>
|
||||||
|
<div class="button" id="show_questionnaire">
|
||||||
|
<a i18n:translate=""
|
||||||
|
tal:attributes="href string:${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> </td>
|
||||||
|
<td> </td>
|
||||||
|
<!--<td> </td>-->
|
||||||
|
</tr>
|
||||||
|
<tr class="vpad">
|
||||||
|
<td><b tal:content="group/title" /></td>
|
||||||
|
<td i18n:translate="">Average</td>
|
||||||
|
<!--<td i18n:translate="">Deviation</td>-->
|
||||||
|
</tr>
|
||||||
|
<tr tal:repeat="question group/questions">
|
||||||
|
<tal:question
|
||||||
|
define="qutype python:
|
||||||
|
question.questionType or 'value_selection';
|
||||||
|
data python:
|
||||||
|
item.getTeamResultsForQuestion(question, item.adapted)">
|
||||||
|
<td>
|
||||||
|
<div tal:content="question/text" />
|
||||||
|
<div style="width: 90%; margin-left: 20px"
|
||||||
|
tal:condition="python:qutype == 'text'"
|
||||||
|
tal:content="structure data/texts" />
|
||||||
|
</td>
|
||||||
|
<td class="center">
|
||||||
|
<span tal:condition="python:qutype == 'value_selection'"
|
||||||
|
tal:content="data/average" /></td>
|
||||||
|
<!--<td class="center">
|
||||||
|
<span tal:condition="python:qutype == 'value_selection'"
|
||||||
|
tal:content="data/stddev" /></td>-->
|
||||||
|
</tal:question>
|
||||||
|
</tr>
|
||||||
|
</tal:group>
|
||||||
|
</table>
|
||||||
|
<p tal:define="teamData item/teamData"
|
||||||
|
tal:condition="teamData">
|
||||||
|
<b><span i18n:translate="">Team Size</span>:
|
||||||
|
<span tal:content="python:len(teamData)" /></b><br />
|
||||||
|
</p>
|
||||||
|
<div class="button" id="show_questionnaire">
|
||||||
|
<a i18n:translate=""
|
||||||
|
tal:attributes="href string:${view/requestUrl}${item/urlParamString}">
|
||||||
|
Back to Questionnaire</a></div>
|
||||||
|
</div>
|
||||||
</metal:block>
|
</metal:block>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -228,6 +228,23 @@ We need a principal for testing the login stuff:
|
||||||
>>> pwcView.update()
|
>>> pwcView.update()
|
||||||
False
|
False
|
||||||
|
|
||||||
|
Reset Password
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Invalidates the user account by generating a new password. A mail ist sent to
|
||||||
|
the email address of the person with a link for re-activating the account
|
||||||
|
and enter a new password.
|
||||||
|
|
||||||
|
>>> data = {'loginName': u'dummy',
|
||||||
|
... 'action': 'update'}
|
||||||
|
|
||||||
|
>>> request = TestRequest(form=data)
|
||||||
|
|
||||||
|
>>> from loops.organize.browser.member import PasswordReset
|
||||||
|
>>> pwrView = PasswordReset(menu, request)
|
||||||
|
>>> pwrView.update()
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
Pure Person-based Authentication
|
Pure Person-based Authentication
|
||||||
================================
|
================================
|
||||||
|
@ -410,7 +427,7 @@ Send Email to Members
|
||||||
>>> form.subject
|
>>> form.subject
|
||||||
u"loops Notification from '$site'"
|
u"loops Notification from '$site'"
|
||||||
>>> form.mailBody
|
>>> form.mailBody
|
||||||
u'\n\nEvent #1\nhttp://127.0.0.1/loops/views/menu/.113\n\n'
|
u'\n\nEvent #1\nhttp://127.0.0.1/loops/views/menu/.118\n\n'
|
||||||
|
|
||||||
|
|
||||||
Show Presence of Other Users
|
Show Presence of Other Users
|
||||||
|
|
|
@ -45,6 +45,12 @@
|
||||||
class="loops.organize.browser.member.PasswordChange"
|
class="loops.organize.browser.member.PasswordChange"
|
||||||
permission="zope.View" />
|
permission="zope.View" />
|
||||||
|
|
||||||
|
<browser:page
|
||||||
|
for="loops.interfaces.INode"
|
||||||
|
name="reset_password.html"
|
||||||
|
class="loops.organize.browser.member.PasswordReset"
|
||||||
|
permission="zope.View" />
|
||||||
|
|
||||||
<zope:adapter
|
<zope:adapter
|
||||||
name="task.html"
|
name="task.html"
|
||||||
for="loops.interfaces.IConcept
|
for="loops.interfaces.IConcept
|
||||||
|
@ -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>
|
||||||
|
|
|
@ -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.')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -244,7 +245,7 @@ class SecureMemberRegistration(BaseMemberRegistration, CreateForm):
|
||||||
regMan = IMemberRegistrationManager(self.context.getLoopsRoot())
|
regMan = IMemberRegistrationManager(self.context.getLoopsRoot())
|
||||||
pw = generateName()
|
pw = generateName()
|
||||||
email = form.get('email')
|
email = form.get('email')
|
||||||
try:
|
try:
|
||||||
result = regMan.register(login, pw,
|
result = regMan.register(login, pw,
|
||||||
form.get('lastName'), form.get('firstName'),
|
form.get('lastName'), form.get('firstName'),
|
||||||
email=email,)
|
email=email,)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -45,6 +45,9 @@ to assign comments to this document.
|
||||||
>>> home = views['home']
|
>>> home = views['home']
|
||||||
>>> home.target = resources['d001.txt']
|
>>> home.target = resources['d001.txt']
|
||||||
|
|
||||||
|
>>> from loops.organize.comment.base import commentStates
|
||||||
|
>>> component.provideUtility(commentStates(), name='organize.commentStates')
|
||||||
|
|
||||||
Creating comments
|
Creating comments
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
@ -75,6 +78,12 @@ Viewing comments
|
||||||
('My comment', u'... ...', u'john')
|
('My comment', u'... ...', u'john')
|
||||||
|
|
||||||
|
|
||||||
|
Reporting
|
||||||
|
=========
|
||||||
|
|
||||||
|
>>> from loops.organize.comment.report import CommentsOverview
|
||||||
|
|
||||||
|
|
||||||
Fin de partie
|
Fin de partie
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -18,24 +18,55 @@
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Base classes for comments/discussions.
|
Base classes for comments/discussions.
|
||||||
|
|
||||||
$Id$
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from zope.component import adapts
|
from zope.component import adapts
|
||||||
from zope.interface import implements
|
from zope.interface import implementer, implements
|
||||||
|
from zope.traversing.api import getParent
|
||||||
|
|
||||||
|
from cybertools.stateful.definition import StatesDefinition
|
||||||
|
from cybertools.stateful.definition import State, Transition
|
||||||
|
from cybertools.stateful.interfaces import IStatesDefinition
|
||||||
from cybertools.tracking.btree import Track
|
from cybertools.tracking.btree import Track
|
||||||
from cybertools.tracking.interfaces import ITrackingStorage
|
from cybertools.tracking.interfaces import ITrackingStorage
|
||||||
from cybertools.tracking.comment.interfaces import IComment
|
from loops.organize.comment.interfaces import IComment
|
||||||
|
from loops.organize.stateful.base import Stateful
|
||||||
from loops import util
|
from loops import util
|
||||||
|
|
||||||
|
|
||||||
class Comment(Track):
|
@implementer(IStatesDefinition)
|
||||||
|
def commentStates():
|
||||||
|
return StatesDefinition('commentStates',
|
||||||
|
State('new', 'new', ('accept', 'reject'), color='red'),
|
||||||
|
State('public', 'public', ('retract', 'reject'), color='green'),
|
||||||
|
State('rejected', 'rejected', ('accept',), color='grey'),
|
||||||
|
Transition('accept', 'accept', 'public'),
|
||||||
|
Transition('reject', 'reject', 'rejected'),
|
||||||
|
Transition('retract', 'retract', 'new'),
|
||||||
|
initialState='new')
|
||||||
|
|
||||||
|
|
||||||
|
class Comment(Stateful, Track):
|
||||||
|
|
||||||
implements(IComment)
|
implements(IComment)
|
||||||
|
|
||||||
|
metadata_attributes = Track.metadata_attributes + ('state',)
|
||||||
|
index_attributes = metadata_attributes
|
||||||
typeName = 'Comment'
|
typeName = 'Comment'
|
||||||
|
typeInterface = IComment
|
||||||
|
statesDefinition = 'organize.commentStates'
|
||||||
|
|
||||||
contentType = 'text/restructured'
|
contentType = 'text/restructured'
|
||||||
|
|
||||||
|
def __init__(self, taskId, runId, userName, data):
|
||||||
|
super(Comment, self).__init__(taskId, runId, userName, data)
|
||||||
|
self.state = self.getState() # make initial state persistent
|
||||||
|
|
||||||
|
@property
|
||||||
|
def title(self):
|
||||||
|
return self.data['subject']
|
||||||
|
|
||||||
|
def doTransition(self, action):
|
||||||
|
super(Comment, self).doTransition(action)
|
||||||
|
getParent(self).indexTrack(None, self, 'state')
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# Copyright (c) 2012 Helmut Merz helmutm@cy55.de
|
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -23,15 +23,17 @@ Definition of view classes and other browser related stuff for comments.
|
||||||
from zope import interface, component
|
from zope import interface, component
|
||||||
from zope.app.pagetemplate import ViewPageTemplateFile
|
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||||
from zope.cachedescriptors.property import Lazy
|
from zope.cachedescriptors.property import Lazy
|
||||||
|
from zope.security import checkPermission
|
||||||
|
|
||||||
from cybertools.browser.action import actions
|
from cybertools.browser.action import actions
|
||||||
from cybertools.tracking.btree import TrackingStorage
|
from cybertools.tracking.btree import TrackingStorage
|
||||||
from loops.browser.action import DialogAction
|
from loops.browser.action import Action, DialogAction
|
||||||
from loops.browser.common import BaseView
|
from loops.browser.common import BaseView
|
||||||
from loops.browser.form import ObjectForm, EditObject
|
from loops.browser.form import ObjectForm, EditObject
|
||||||
from loops.browser.node import NodeView
|
from loops.browser.node import NodeView
|
||||||
from loops.organize.comment.base import Comment
|
from loops.organize.comment.base import Comment
|
||||||
from loops.organize.party import getPersonForUser
|
from loops.organize.party import getPersonForUser
|
||||||
|
from loops.organize.stateful.browser import StateAction
|
||||||
from loops.organize.tracking.report import TrackDetails
|
from loops.organize.tracking.report import TrackDetails
|
||||||
from loops.security.common import canAccessObject
|
from loops.security.common import canAccessObject
|
||||||
from loops.setup import addObject
|
from loops.setup import addObject
|
||||||
|
@ -50,10 +52,17 @@ class CommentsView(NodeView):
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def allowed(self):
|
def allowed(self):
|
||||||
if self.isAnonymous:
|
if self.virtualTargetObject is None:
|
||||||
return False
|
return False
|
||||||
return (self.virtualTargetObject is not None and
|
opts = (self.globalOptions('organize.allowComments') or
|
||||||
self.globalOptions('organize.allowComments'))
|
self.typeOptions('organize.allowComments'))
|
||||||
|
if not opts:
|
||||||
|
return False
|
||||||
|
if opts is True:
|
||||||
|
opts = []
|
||||||
|
if self.isAnonymous and not 'all' in opts:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def addUrl(self):
|
def addUrl(self):
|
||||||
|
@ -76,9 +85,47 @@ class CommentsView(NodeView):
|
||||||
result.append(CommentDetails(self, tr))
|
result.append(CommentDetails(self, tr))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def getActionsFor(self, comment):
|
||||||
|
if not self.globalOptions('organize.showCommentState'):
|
||||||
|
return []
|
||||||
|
if not checkPermission('loops.ViewRestricted', self.context):
|
||||||
|
return []
|
||||||
|
trackUid = util.getUidForObject(comment.track)
|
||||||
|
url = '%s/.%s/change_state.html' % (
|
||||||
|
self.page.virtualTargetUrl, trackUid)
|
||||||
|
onClick = ("objectDialog('change_state', "
|
||||||
|
"'%s?dialog=change_state"
|
||||||
|
"&target_uid=%s'); return false;" % (url, trackUid))
|
||||||
|
stateAct = StateAction(self,
|
||||||
|
definition='organize.commentStates',
|
||||||
|
stateful=comment.track,
|
||||||
|
url=url,
|
||||||
|
onClick=onClick)
|
||||||
|
actions = [stateAct]
|
||||||
|
if not checkPermission('loops.EditRestricted', self.context):
|
||||||
|
return actions
|
||||||
|
baseUrl = self.page.virtualTargetUrl
|
||||||
|
url = '%s/delete_object?uid=%s' % (baseUrl, trackUid)
|
||||||
|
onClick = _("return confirm('Do you really want to delete this object?')")
|
||||||
|
delAct = Action(self,
|
||||||
|
url=url,
|
||||||
|
description=_('Delete Comment'),
|
||||||
|
icon='cybertools.icons/delete.png',
|
||||||
|
cssClass='icon-action',
|
||||||
|
onClick=onClick)
|
||||||
|
actions.append(delAct)
|
||||||
|
return actions
|
||||||
|
|
||||||
|
|
||||||
class CommentDetails(TrackDetails):
|
class CommentDetails(TrackDetails):
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def poster(self):
|
||||||
|
name = self.track.data.get('name')
|
||||||
|
if name:
|
||||||
|
return name
|
||||||
|
return self.user['title']
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def subject(self):
|
def subject(self):
|
||||||
return self.track.data['subject']
|
return self.track.data['subject']
|
||||||
|
@ -108,6 +155,8 @@ class CreateComment(EditObject):
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def personId(self):
|
def personId(self):
|
||||||
|
if self.view.isAnonymous:
|
||||||
|
return self.request.form.get('email')
|
||||||
p = getPersonForUser(self.context, self.request)
|
p = getPersonForUser(self.context, self.request)
|
||||||
if p is not None:
|
if p is not None:
|
||||||
return util.getUidForObject(p)
|
return util.getUidForObject(p)
|
||||||
|
@ -129,8 +178,11 @@ class CreateComment(EditObject):
|
||||||
if ts is None:
|
if ts is None:
|
||||||
ts = addObject(rm, TrackingStorage, 'comments', trackFactory=Comment)
|
ts = addObject(rm, TrackingStorage, 'comments', trackFactory=Comment)
|
||||||
uid = util.getUidForObject(self.object)
|
uid = util.getUidForObject(self.object)
|
||||||
ts.saveUserTrack(uid, 0, self.personId, dict(
|
data = dict(subject=subject, text=text)
|
||||||
subject=subject, text=text))
|
for k in ('name', 'email'):
|
||||||
|
if k in form:
|
||||||
|
data[k] = form[k]
|
||||||
|
ts.saveUserTrack(uid, 0, self.personId, data)
|
||||||
url = self.view.virtualTargetUrl + '?version=this'
|
url = self.view.virtualTargetUrl + '?version=this'
|
||||||
self.request.response.redirect(url)
|
self.request.response.redirect(url)
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -14,10 +14,17 @@
|
||||||
<tal:comment tal:repeat="comment items">
|
<tal:comment tal:repeat="comment items">
|
||||||
<br />
|
<br />
|
||||||
<div class="comment">
|
<div class="comment">
|
||||||
|
<div class="object-actions"
|
||||||
|
tal:define="actions python:comments.getActionsFor(comment)"
|
||||||
|
tal:condition="actions">
|
||||||
|
<tal:actions repeat="action actions">
|
||||||
|
<metal:action use-macro="action/macro" />
|
||||||
|
</tal:actions>
|
||||||
|
</div>
|
||||||
<h3>
|
<h3>
|
||||||
<span tal:content="comment/subject">Subject</span></h3>
|
<span tal:content="comment/subject">Subject</span></h3>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<span tal:replace="comment/user/title">John</span>,
|
<span tal:replace="comment/poster">John</span>,
|
||||||
<span tal:replace="comment/timeStamp">2007-03-30</span>
|
<span tal:replace="comment/timeStamp">2007-03-30</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="content"
|
<p class="content"
|
||||||
|
@ -44,6 +51,18 @@
|
||||||
<input type="hidden" name="contentType" value="text/restructured" />
|
<input type="hidden" name="contentType" value="text/restructured" />
|
||||||
<div class="heading" i18n:translate="">Add Comment</div>
|
<div class="heading" i18n:translate="">Add Comment</div>
|
||||||
<div>
|
<div>
|
||||||
|
<tal:anonymous condition="view/isAnonymous">
|
||||||
|
<label i18n:translate=""
|
||||||
|
for="comment_name">Name</label>
|
||||||
|
<div><input type="text" name="name" id="comment_name"
|
||||||
|
dojoType="dijit.form.ValidationTextBox" required="true"
|
||||||
|
style="width: 60em" /></div>
|
||||||
|
<label i18n:translate=""
|
||||||
|
for="comment_email">Email Address</label>
|
||||||
|
<div><input type="text" name="email" id="comment_email"
|
||||||
|
dojoType="dijit.form.ValidationTextBox" required="true"
|
||||||
|
style="width: 60em" /></div>
|
||||||
|
</tal:anonymous>
|
||||||
<label i18n:translate=""
|
<label i18n:translate=""
|
||||||
for="comment_subject">Subject</label>
|
for="comment_subject">Subject</label>
|
||||||
<div><input type="text" name="subject" id="comment_subject"
|
<div><input type="text" name="subject" id="comment_subject"
|
||||||
|
|
|
@ -12,6 +12,10 @@
|
||||||
set_schema="cybertools.tracking.comment.interfaces.IComment" />
|
set_schema="cybertools.tracking.comment.interfaces.IComment" />
|
||||||
</zope:class>
|
</zope:class>
|
||||||
|
|
||||||
|
<zope:utility
|
||||||
|
factory="loops.organize.comment.base.commentStates"
|
||||||
|
name="organize.commentStates" />
|
||||||
|
|
||||||
<!-- views -->
|
<!-- views -->
|
||||||
|
|
||||||
<browser:page
|
<browser:page
|
||||||
|
@ -33,4 +37,26 @@
|
||||||
factory="loops.organize.comment.browser.CreateComment"
|
factory="loops.organize.comment.browser.CreateComment"
|
||||||
permission="zope.View" />
|
permission="zope.View" />
|
||||||
|
|
||||||
|
<!-- reporting -->
|
||||||
|
|
||||||
|
<zope:adapter
|
||||||
|
name="list_comments.html"
|
||||||
|
for="loops.interfaces.IConcept
|
||||||
|
zope.publisher.interfaces.browser.IBrowserRequest"
|
||||||
|
provides="zope.interface.Interface"
|
||||||
|
factory="loops.organize.comment.report.CommentsOverview"
|
||||||
|
permission="zope.View" />
|
||||||
|
|
||||||
|
<zope:adapter
|
||||||
|
name="comments_overview"
|
||||||
|
factory="loops.organize.comment.report.CommentsReportInstance"
|
||||||
|
provides="loops.expert.report.IReportInstance"
|
||||||
|
trusted="True" />
|
||||||
|
<zope:class class="loops.organize.comment.report.CommentsReportInstance">
|
||||||
|
<require permission="zope.View"
|
||||||
|
interface="loops.expert.report.IReportInstance" />
|
||||||
|
<require permission="zope.ManageContent"
|
||||||
|
set_schema="loops.expert.report.IReportInstance" />
|
||||||
|
</zope:class>
|
||||||
|
|
||||||
</configure>
|
</configure>
|
||||||
|
|
26
organize/comment/interfaces.py
Normal file
26
organize/comment/interfaces.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Interface definitions for comments - discussions - forums.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.interface import Interface, Attribute
|
||||||
|
from zope import schema
|
||||||
|
|
||||||
|
from cybertools.tracking.comment.interfaces import IComment
|
6
organize/comment/loops_comment_de.dmp
Normal file
6
organize/comment/loops_comment_de.dmp
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
type(u'report', u'Report', options=u'',
|
||||||
|
typeInterface='loops.expert.report.IReport', viewName=u'')
|
||||||
|
concept(u'comments_overview', u'\xdcbersicht Kommentare', u'report',
|
||||||
|
reportType=u'comments_overview')
|
||||||
|
concept(u'comments', u'Kommentare', u'query', options=u'',
|
||||||
|
viewName=u'list_comments.html')
|
75
organize/comment/report.py
Normal file
75
organize/comment/report.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Report views and definitions for comments listings and similar stuff.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from cybertools.util.jeep import Jeep
|
||||||
|
from loops.expert.browser.report import ReportConceptView
|
||||||
|
from loops.expert.field import Field, StateField, TargetField
|
||||||
|
from loops.expert.field import TrackDateField
|
||||||
|
from loops.expert.report import ReportInstance, TrackRow
|
||||||
|
|
||||||
|
|
||||||
|
class CommentsOverview(ReportConceptView):
|
||||||
|
|
||||||
|
reportName = 'comments_overview'
|
||||||
|
|
||||||
|
|
||||||
|
timeStamp = TrackDateField('timeStamp', u'Timestamp',
|
||||||
|
description=u'The date and time the comment was posted.',
|
||||||
|
part='dateTime', descending=True,
|
||||||
|
executionSteps=['sort', 'output'])
|
||||||
|
target = TargetField('taskId', u'Target',
|
||||||
|
description=u'The resource or concept the comment was posted at.',
|
||||||
|
executionSteps=['output'])
|
||||||
|
name = Field('name', u'Name',
|
||||||
|
description=u'The name addres of the poster.',
|
||||||
|
executionSteps=['output'])
|
||||||
|
email = Field('email', u'E-Mail Address',
|
||||||
|
description=u'The email addres of the poster.',
|
||||||
|
executionSteps=['output'])
|
||||||
|
subject = Field('subject', u'Subject',
|
||||||
|
description=u'The subject of the comment.',
|
||||||
|
executionSteps=['output'])
|
||||||
|
state = StateField('state', u'State',
|
||||||
|
description=u'The state of the comment.',
|
||||||
|
cssClass='center',
|
||||||
|
statesDefinition='organize.commentStates',
|
||||||
|
executionSteps=['query', 'sort', 'output'])
|
||||||
|
|
||||||
|
|
||||||
|
class CommentsRow(TrackRow):
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CommentsReportInstance(ReportInstance):
|
||||||
|
|
||||||
|
type = "comments_overview"
|
||||||
|
label = u'Comments Overview'
|
||||||
|
|
||||||
|
rowFactory = CommentsRow
|
||||||
|
|
||||||
|
fields = Jeep((timeStamp, target, name, email, subject, state))
|
||||||
|
defaultOutputFields = fields
|
||||||
|
defaultSortCriteria = (state, timeStamp)
|
||||||
|
|
||||||
|
def selectObjects(self, parts):
|
||||||
|
return self.recordManager['comments'].values()
|
4
organize/data/organize_work_reports_de.dmp
Normal file
4
organize/data/organize_work_reports_de.dmp
Normal 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')
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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']
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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"> <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"> <a
|
||||||
i18n:attributes="title">X</a> </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> </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>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
||||||
|
@ -110,7 +126,7 @@ def updateSortInfo(person, task, data):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
if data:
|
if data:
|
||||||
Favorites(favorites).add(task, person,
|
Favorites(favorites).add(task, person,
|
||||||
dict(type='sort', sortInfo=data))
|
dict(type='sort', sortInfo=data))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue