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
|
||||
*.pyo
|
||||
dist/
|
||||
*.project
|
||||
*.pydevproject
|
||||
*.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)
|
||||
>>> request = TestRequest(form={'title': u'Test Note',
|
||||
... 'form.type': u'.loops/concepts/note'})
|
||||
... 'form.type': u'.loops/concepts/note',
|
||||
... 'contentType': u'text/restructured',
|
||||
... 'linkUrl': u'http://'})
|
||||
>>> view = NodeView(m112, request)
|
||||
>>> cont = CreateObject(view, request)
|
||||
>>> cont.update()
|
||||
|
@ -802,7 +804,7 @@ The new technique uses the ``fields`` and ``data`` attributes...
|
|||
linkText textline False None
|
||||
|
||||
>>> view.data
|
||||
{'linkUrl': u'http://', 'contentType': 'text/restructured', 'data': u'',
|
||||
{'linkUrl': u'http://', 'contentType': u'text/restructured', 'data': u'',
|
||||
'linkText': u'', 'title': u'Test Note'}
|
||||
|
||||
The object is changed via a FormController adapter created for
|
||||
|
@ -913,6 +915,12 @@ relates ISO country codes with the full name of the country.
|
|||
>>> sorted(adapted(concepts['countries']).data.items())
|
||||
[('at', ['Austria']), ('de', ['Germany'])]
|
||||
|
||||
>>> countries.dataAsRecords()
|
||||
[{'value': 'Austria', 'key': 'at'}, {'value': 'Germany', 'key': 'de'}]
|
||||
|
||||
>>> countries.getRowsByValue('value', 'Germany')
|
||||
[{'value': 'Germany', 'key': 'de'}]
|
||||
|
||||
|
||||
Caching
|
||||
=======
|
||||
|
@ -932,6 +940,12 @@ Security
|
|||
>>> from loops.security.browser import admin, audit
|
||||
|
||||
|
||||
Paster Shell Utilities - Repair Scripts
|
||||
=======================================
|
||||
|
||||
>>> from loops.repair.base import removeRecords
|
||||
|
||||
|
||||
Import/Export
|
||||
=============
|
||||
|
||||
|
|
35
__init__.py
35
__init__.py
|
@ -1,22 +1,17 @@
|
|||
#
|
||||
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# package loops
|
||||
|
||||
"""
|
||||
# intid monkey patch for avoiding ForbiddenAttribute error
|
||||
|
||||
from zope import component
|
||||
from zope.intid.interfaces import IIntIds
|
||||
from zope import intid
|
||||
from zope.security.proxy import removeSecurityProxy
|
||||
|
||||
def queryId(self, ob, default=None):
|
||||
try:
|
||||
return self.getId(removeSecurityProxy(ob))
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
intid.IntIds.queryId = queryId
|
||||
|
||||
$Id$
|
||||
"""
|
||||
|
|
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
|
||||
#
|
||||
|
@ -19,7 +17,7 @@
|
|||
#
|
||||
|
||||
"""
|
||||
The loops container class.
|
||||
Implementation of loops root object.
|
||||
"""
|
||||
|
||||
from zope.app.container.btree import BTreeContainer
|
||||
|
|
|
@ -92,6 +92,8 @@ class DialogAction(Action):
|
|||
urlParams['fixed_type'] = 'yes'
|
||||
if self.viewTitle:
|
||||
urlParams['view_title'] = self.viewTitle
|
||||
#for k, v in self.page.sortInfo.items():
|
||||
# urlParams['sortinfo_' + k] = v['fparam']
|
||||
urlParams.update(self.addParams)
|
||||
if self.target is not None:
|
||||
url = self.page.getUrlForTarget(self.target)
|
||||
|
|
|
@ -20,13 +20,14 @@
|
|||
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
|
||||
from datetime import datetime
|
||||
from datetime import date, datetime
|
||||
from logging import getLogger
|
||||
import re
|
||||
from time import strptime
|
||||
from urllib import urlencode
|
||||
from urlparse import parse_qs
|
||||
from zope import component
|
||||
from zope.app.form.browser.interfaces import ITerms
|
||||
from zope.app.i18n.interfaces import ITranslationDomain
|
||||
|
@ -62,17 +63,21 @@ from cybertools.stateful.interfaces import IStateful
|
|||
from cybertools.text import mimetypes
|
||||
from cybertools.typology.interfaces import IType, ITypeManager
|
||||
from cybertools.util.date import toLocalTime
|
||||
from cybertools.util.format import formatDate
|
||||
from cybertools.util.jeep import Jeep
|
||||
from loops.browser.util import normalizeForUrl
|
||||
from loops.common import adapted, baseObject
|
||||
from loops.config.base import DummyOptions
|
||||
from loops.i18n.browser import I18NView
|
||||
from loops.interfaces import IResource, IView, INode, ITypeConcept
|
||||
from loops.organize.personal import favorite
|
||||
from loops.organize.party import getPersonForUser
|
||||
from loops.organize.tracking import access
|
||||
from loops.organize.util import getRolesForPrincipal
|
||||
from loops.resource import Resource
|
||||
from loops.security.common import checkPermission
|
||||
from loops.security.common import canAccessObject, canListObject, canWriteObject
|
||||
from loops.security.common import canEditRestricted
|
||||
from loops.type import ITypeConcept, LoopsTypeInfo
|
||||
from loops import util
|
||||
from loops.util import _, saveRequest
|
||||
|
@ -137,7 +142,58 @@ class EditForm(form.EditForm):
|
|||
return parentUrl + '/contents.html'
|
||||
|
||||
|
||||
class BaseView(GenericView, I18NView):
|
||||
class SortableMixin(object):
|
||||
|
||||
@Lazy
|
||||
def sortInfo(self):
|
||||
result = {}
|
||||
for k, v in self.request.form.items():
|
||||
if k.startswith('sortinfo_'):
|
||||
tableName = k[len('sortinfo_'):]
|
||||
if ',' in v:
|
||||
fn, dir = v.split(',')
|
||||
else:
|
||||
fn = v
|
||||
dir = 'asc'
|
||||
result[tableName] = dict(
|
||||
colName=fn, ascending=(dir=='asc'), fparam=v)
|
||||
result = favorite.updateSortInfo(getPersonForUser(
|
||||
self.context, self.request), self.target, result)
|
||||
return result
|
||||
|
||||
def isSortableColumn(self, tableName, colName):
|
||||
return False # overwrite in subclass
|
||||
|
||||
def getSortUrl(self, tableName, colName):
|
||||
url = str(self.request.URL)
|
||||
paramChar = '?' in url and '&' or '?'
|
||||
si = self.sortInfo.get(tableName)
|
||||
if si is not None and si.get('colName') == colName:
|
||||
dir = si['ascending'] and 'desc' or 'asc'
|
||||
else:
|
||||
dir = 'asc'
|
||||
return '%s%ssortinfo_%s=%s,%s' % (url, paramChar, tableName, colName, dir)
|
||||
|
||||
def getSortParams(self, tableName):
|
||||
url = str(self.request.URL)
|
||||
paramChar = '?' in url and '&' or '?'
|
||||
si = self.sortInfo.get(tableName)
|
||||
if si is not None:
|
||||
colName = si['colName']
|
||||
dir = si['ascending'] and 'asc' or 'desc'
|
||||
return '%ssortinfo_%s=%s,%s' % (paramChar, tableName, colName, dir)
|
||||
return ''
|
||||
|
||||
def getSortImage(self, tableName, colName):
|
||||
si = self.sortInfo.get(tableName)
|
||||
if si is not None and si.get('colName') == colName:
|
||||
if si['ascending']:
|
||||
return '/@@/cybertools.icons/arrowdown.gif'
|
||||
else:
|
||||
return '/@@/cybertools.icons/arrowup.gif'
|
||||
|
||||
|
||||
class BaseView(GenericView, I18NView, SortableMixin):
|
||||
|
||||
actions = {}
|
||||
portlet_actions = []
|
||||
|
@ -146,6 +202,7 @@ class BaseView(GenericView, I18NView):
|
|||
icon = None
|
||||
modeName = 'view'
|
||||
isToplevel = False
|
||||
isVisible = True
|
||||
|
||||
def __init__(self, context, request):
|
||||
context = baseObject(context)
|
||||
|
@ -163,6 +220,10 @@ class BaseView(GenericView, I18NView):
|
|||
pass
|
||||
saveRequest(request)
|
||||
|
||||
def todayFormatted(self):
|
||||
return formatDate(date.today(), 'date', 'short',
|
||||
self.languageInfo.language)
|
||||
|
||||
def checkPermissions(self):
|
||||
return canAccessObject(self.context)
|
||||
|
||||
|
@ -214,6 +275,16 @@ class BaseView(GenericView, I18NView):
|
|||
result.append(view)
|
||||
return result
|
||||
|
||||
@Lazy
|
||||
def urlParamString(self):
|
||||
return self.getUrlParamString()
|
||||
|
||||
def getUrlParamString(self):
|
||||
qs = self.request.get('QUERY_STRING')
|
||||
if qs:
|
||||
return '?' + qs
|
||||
return ''
|
||||
|
||||
@Lazy
|
||||
def principalId(self):
|
||||
principal = self.request.principal
|
||||
|
@ -347,6 +418,10 @@ class BaseView(GenericView, I18NView):
|
|||
def isPartOfPredicate(self):
|
||||
return self.conceptManager.get('ispartof')
|
||||
|
||||
@Lazy
|
||||
def queryTargetPredicate(self):
|
||||
return self.conceptManager.get('querytarget')
|
||||
|
||||
@Lazy
|
||||
def memberPredicate(self):
|
||||
return self.conceptManager.get('ismember')
|
||||
|
@ -395,6 +470,10 @@ class BaseView(GenericView, I18NView):
|
|||
def description(self):
|
||||
return self.adapted.description
|
||||
|
||||
@Lazy
|
||||
def tabTitle(self):
|
||||
return u'Info'
|
||||
|
||||
@Lazy
|
||||
def additionalInfos(self):
|
||||
return []
|
||||
|
@ -747,6 +826,8 @@ class BaseView(GenericView, I18NView):
|
|||
return result
|
||||
|
||||
def checkState(self):
|
||||
if checkPermission('loops.ManageSite', self.context):
|
||||
return True
|
||||
if not self.allStates:
|
||||
return True
|
||||
for stf in self.allStates:
|
||||
|
@ -821,6 +902,10 @@ class BaseView(GenericView, I18NView):
|
|||
def canAccessRestricted(self):
|
||||
return checkPermission('loops.ViewRestricted', self.context)
|
||||
|
||||
@Lazy
|
||||
def canEditRestricted(self):
|
||||
return canEditRestricted(self.context)
|
||||
|
||||
def openEditWindow(self, viewName='edit.html'):
|
||||
if self.editable:
|
||||
if checkPermission('loops.ManageSite', self.context):
|
||||
|
@ -943,6 +1028,12 @@ class BaseView(GenericView, I18NView):
|
|||
jsCall = 'dojo.require("dojox.image.Lightbox");'
|
||||
self.controller.macros.register('js-execute', jsCall, jsCall=jsCall)
|
||||
|
||||
def registerDojoComboBox(self):
|
||||
self.registerDojo()
|
||||
jsCall = ('dojo.require("dijit.form.ComboBox");')
|
||||
self.controller.macros.register('js-execute',
|
||||
'dojo.require.ComboBox', jsCall=jsCall)
|
||||
|
||||
def registerDojoFormAll(self):
|
||||
self.registerDojo()
|
||||
self.registerDojoEditor()
|
||||
|
@ -996,6 +1087,7 @@ class LoggedIn(object):
|
|||
params = parse_qsl(qs)
|
||||
params = [(k, v) for k, v in params if k != 'loops.messages.top:record']
|
||||
params.append(('loops.messages.top:record', message.encode('UTF-8')))
|
||||
url = url.encode('utf-8')
|
||||
return '%s?%s' % (url, urlencode(params))
|
||||
|
||||
# vocabulary stuff
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2016 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -254,18 +254,35 @@ class ConceptView(BaseView):
|
|||
result.append(view)
|
||||
return result
|
||||
|
||||
def viewModes(self):
|
||||
modes = Jeep()
|
||||
current = self.request.form.get('loops.viewName')
|
||||
parts = (self.options('view_tabs') or
|
||||
self.typeOptions('view_tabs') or [])
|
||||
if not parts:
|
||||
return modes
|
||||
activeMode = None
|
||||
for p in parts:
|
||||
view = component.queryMultiAdapter(
|
||||
(self.adapted, self.request), name=p)
|
||||
if view is None:
|
||||
view = component.queryMultiAdapter(
|
||||
(self.context, self.request), name=p)
|
||||
if view is None:
|
||||
continue
|
||||
active = (activeMode is None and p == current)
|
||||
if active:
|
||||
activeMode = p
|
||||
url = '%s?loops.viewName=%s' % (self.targetUrl, p)
|
||||
modes.append(ViewMode(p, view.tabTitle, url, active))
|
||||
if activeMode is None:
|
||||
modes[0].active = True
|
||||
return modes
|
||||
|
||||
@Lazy
|
||||
def adapted(self):
|
||||
return adapted(self.context, self.languageInfo)
|
||||
|
||||
@Lazy
|
||||
def title(self):
|
||||
return self.adapted.title or getName(self.context)
|
||||
|
||||
@Lazy
|
||||
def description(self):
|
||||
return self.adapted.description
|
||||
|
||||
@Lazy
|
||||
def targetUrl(self):
|
||||
return self.nodeView.getUrlForTarget(self.context)
|
||||
|
@ -282,8 +299,17 @@ class ConceptView(BaseView):
|
|||
def breadcrumbsTitle(self):
|
||||
return self.title
|
||||
|
||||
@Lazy
|
||||
def showInBreadcrumbs(self):
|
||||
return (self.options('show_in_breadcrumbs') or
|
||||
self.typeOptions('show_in_breadcrumbs'))
|
||||
|
||||
@Lazy
|
||||
def breadcrumbsParent(self):
|
||||
for p in self.context.getParents([self.defaultPredicate]):
|
||||
view = self.nodeView.getViewForTarget(p)
|
||||
if view.showInBreadcrumbs:
|
||||
return view
|
||||
return None
|
||||
|
||||
def getData(self, omit=('title', 'description')):
|
||||
|
@ -389,7 +415,8 @@ class ConceptView(BaseView):
|
|||
children = getChildren
|
||||
|
||||
def childrenAlphaGroups(self, predicates=None):
|
||||
result = Jeep()
|
||||
#result = Jeep()
|
||||
result = {}
|
||||
rels = self.getChildren(predicates=predicates or [self.defaultPredicate],
|
||||
topLevelOnly=False, sort=False)
|
||||
rels = sorted(rels, key=lambda r: r.title.lower())
|
||||
|
@ -449,7 +476,7 @@ class ConceptView(BaseView):
|
|||
if r.order != pos:
|
||||
r.order = pos
|
||||
|
||||
def getResources(self):
|
||||
def getResources(self, relView=None, sort='default'):
|
||||
form = self.request.form
|
||||
#if form.get('loops.viewName') == 'index.html' and self.editable:
|
||||
if self.editable:
|
||||
|
@ -458,13 +485,17 @@ class ConceptView(BaseView):
|
|||
tokens = form.get('resources_tokens')
|
||||
if tokens:
|
||||
self.reorderResources(tokens)
|
||||
if relView is None:
|
||||
from loops.browser.resource import ResourceRelationView
|
||||
relView = ResourceRelationView
|
||||
from loops.organize.personal.browser.filter import FilterView
|
||||
fv = FilterView(self.context, self.request)
|
||||
rels = self.context.getResourceRelations()
|
||||
rels = self.context.getResourceRelations(sort=sort)
|
||||
for r in rels:
|
||||
if fv.check(r.first):
|
||||
yield ResourceRelationView(r, self.request, contextIsSecond=True)
|
||||
view = relView(r, self.request, contextIsSecond=True)
|
||||
if view.checkState():
|
||||
yield view
|
||||
|
||||
def resources(self):
|
||||
return self.getResources()
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
<h1 tal:define="tabview item/tabview|nothing"
|
||||
tal:attributes="ondblclick item/openEditWindow">
|
||||
<a tal:omit-tag="python: level > 1"
|
||||
tal:attributes="href request/URL"
|
||||
tal:attributes="href string:${view/requestUrl}${item/urlParamString}"
|
||||
tal:content="item/title">Title</a>
|
||||
<a title="Show tabular view"
|
||||
i18n:attributes="title"
|
||||
|
@ -367,4 +367,21 @@
|
|||
</metal:actions>
|
||||
|
||||
|
||||
<metal:sortable define-macro="sortable_column_header"
|
||||
tal:define="tableName tableName|nothing">
|
||||
<a title="tooltip_sort_column"
|
||||
tal:define="colName col/name"
|
||||
tal:omit-tag="python:not item.isSortableColumn(tableName, colName)"
|
||||
tal:attributes="href python:item.getSortUrl(tableName, colName)"
|
||||
i18n:attributes="title">
|
||||
<span tal:content="col/title"
|
||||
tal:attributes="class col/cssClass|nothing"
|
||||
i18n:translate="" />
|
||||
<img tal:define="src python:item.getSortImage(tableName, colName)"
|
||||
tal:condition="src"
|
||||
tal:attributes="src src" />
|
||||
</a>
|
||||
</metal:sortable>
|
||||
|
||||
|
||||
</html>
|
||||
|
|
|
@ -125,7 +125,7 @@
|
|||
|
||||
<containerViews
|
||||
for="loops.interfaces.ILoops"
|
||||
index="zope.View"
|
||||
index="zope.ManageSite"
|
||||
contents="loops.ManageSite"
|
||||
add="loops.ManageSite" />
|
||||
|
||||
|
@ -365,7 +365,7 @@
|
|||
|
||||
<containerViews
|
||||
for="loops.interfaces.IViewManager"
|
||||
index="zope.View"
|
||||
index="zope.ManageSite"
|
||||
add="loops.ManageSite" />
|
||||
|
||||
<menuItem
|
||||
|
@ -571,6 +571,14 @@
|
|||
factory="loops.browser.concept.TabbedPage"
|
||||
permission="zope.View" />
|
||||
|
||||
<!-- delete object action -->
|
||||
|
||||
<page
|
||||
name="delete_object"
|
||||
for="loops.interfaces.INode"
|
||||
class="loops.browser.form.DeleteObject"
|
||||
permission="zope.ManageContent" />
|
||||
|
||||
<!-- dialogs/forms (end-user views) -->
|
||||
|
||||
<page
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2012 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2017 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -20,12 +20,13 @@
|
|||
Classes for form presentation and processing.
|
||||
"""
|
||||
|
||||
from urllib import urlencode, unquote_plus
|
||||
from zope.app.container.contained import ObjectRemovedEvent
|
||||
from zope import component, interface, schema
|
||||
from zope.component import adapts
|
||||
from zope.event import notify
|
||||
from zope.interface import Interface
|
||||
from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent
|
||||
|
||||
from zope.app.container.interfaces import INameChooser
|
||||
from zope.app.container.contained import ObjectAddedEvent
|
||||
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||
|
@ -35,7 +36,7 @@ from zope.publisher.browser import FileUpload
|
|||
from zope.publisher.interfaces import BadRequest
|
||||
from zope.security.interfaces import ForbiddenAttribute, Unauthorized
|
||||
from zope.security.proxy import isinstance, removeSecurityProxy
|
||||
from zope.traversing.api import getName
|
||||
from zope.traversing.api import getName, getParent
|
||||
|
||||
from cybertools.ajax import innerHtml
|
||||
from cybertools.browser.form import FormController
|
||||
|
@ -68,6 +69,25 @@ from loops.util import _
|
|||
from loops.versioning.interfaces import IVersionable
|
||||
|
||||
|
||||
# delete object
|
||||
|
||||
class DeleteObject(NodeView):
|
||||
|
||||
isTopLevel = True
|
||||
|
||||
def __call__(self):
|
||||
# todo: check permission; check security code
|
||||
form = self.request.form
|
||||
obj = util.getObjectForUid(form['uid'])
|
||||
container = getParent(obj)
|
||||
notify(ObjectRemovedEvent(obj))
|
||||
del container[getName(obj)]
|
||||
message = 'The object requested has been deleted.'
|
||||
params = [('loops.message', message.encode('UTF-8'))]
|
||||
nextUrl = '%s?%s' % (self.request.URL[-1], urlencode(params))
|
||||
return self.request.response.redirect(nextUrl)
|
||||
|
||||
|
||||
# forms
|
||||
|
||||
class ObjectForm(NodeView):
|
||||
|
@ -162,7 +182,8 @@ class ObjectForm(NodeView):
|
|||
field = self.schema.fields.get(k)
|
||||
if field:
|
||||
fi = field.getFieldInstance(self.instance)
|
||||
data[k] = fi.marshall(fi.unmarshall(form[k]))
|
||||
input = unquote_plus(form[k])
|
||||
data[k] = fi.marshall(fi.unmarshall(input))
|
||||
#data[k] = toUnicode(form[k])
|
||||
return data
|
||||
|
||||
|
@ -196,15 +217,37 @@ class ObjectForm(NodeView):
|
|||
def typeManager(self):
|
||||
return ITypeManager(self.target)
|
||||
|
||||
@Lazy
|
||||
def targetType(self):
|
||||
return self.target.getType()
|
||||
|
||||
@Lazy
|
||||
def presetTypesForAssignment(self):
|
||||
types = list(self.typeManager.listTypes(include=('assign',)))
|
||||
types = []
|
||||
tn = getName(self.targetType)
|
||||
for t in self.typeManager.listTypes(include=('assign',)):
|
||||
# check if type is appropriate for the object to be created
|
||||
opt = IOptions(adapted(t.context))('qualifier_assign_to')
|
||||
#print '***', t.context.__name__, opt, tn
|
||||
if not opt or tn in opt:
|
||||
types.append(t)
|
||||
assigned = [r.context.conceptType for r in self.assignments]
|
||||
types = [t for t in types if t.typeProvider not in assigned]
|
||||
return [dict(title=t.title, token=t.tokenForSearch) for t in types]
|
||||
|
||||
def conceptsForType(self, token):
|
||||
result = ConceptQuery(self).query(type=token)
|
||||
# check typeOption: include only matching instances
|
||||
include = []
|
||||
type = self.conceptManager[token.split(':')[-1]]
|
||||
#print '###', token, repr(type)
|
||||
opt = IOptions(adapted(type))('qualifier_assign_check_parents')
|
||||
if opt:
|
||||
for p in self.target.getAllParents([self.defaultPredicate]):
|
||||
for c in p.object.getChildren([self.defaultPredicate]):
|
||||
include.append(c)
|
||||
if include:
|
||||
result = [c for c in result if c in include]
|
||||
fv = FilterView(self.context, self.request)
|
||||
result = fv.apply(result)
|
||||
result.sort(key=lambda x: x.title)
|
||||
|
@ -288,8 +331,11 @@ class CreateObjectForm(ObjectForm):
|
|||
|
||||
@Lazy
|
||||
def defaultTypeToken(self):
|
||||
return (self.controller.params.get('form.create.defaultTypeToken')
|
||||
or '.loops/concepts/textdocument')
|
||||
setting = self.controller.params.get('form.create.defaultTypeToken')
|
||||
if setting:
|
||||
return setting
|
||||
opt = self.globalOptions('form.create.default_type_token')
|
||||
return opt and opt[0] or '.loops/concepts/textdocument'
|
||||
|
||||
@Lazy
|
||||
def typeToken(self):
|
||||
|
@ -310,6 +356,10 @@ class CreateObjectForm(ObjectForm):
|
|||
if typeToken:
|
||||
return self.loopsRoot.loopsTraverse(typeToken)
|
||||
|
||||
@Lazy
|
||||
def targetType(self):
|
||||
return self.typeConcept
|
||||
|
||||
@Lazy
|
||||
def adapted(self):
|
||||
ad = self.typeInterface(Resource())
|
||||
|
@ -423,6 +473,7 @@ class CreateConceptForm(CreateObjectForm):
|
|||
return c
|
||||
ad = ti(c)
|
||||
ad.__is_dummy__ = True
|
||||
ad.__type__ = adapted(self.typeConcept)
|
||||
return ad
|
||||
|
||||
@Lazy
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
|
||||
<metal:info define-macro="object_info"
|
||||
tal:define="item nocall:view/item">
|
||||
tal:define="item nocall:view/targetItem">
|
||||
<table class="object_info" width="400">
|
||||
<tr>
|
||||
<td colspan="2"><h2 i18n:translate="">Object Information</h2><br /></td>
|
||||
|
@ -52,7 +52,7 @@
|
|||
|
||||
|
||||
<metal:info define-macro="meta_info"
|
||||
tal:define="item nocall:view/item">
|
||||
tal:define="item nocall:view/targetItem">
|
||||
<table class="object_info" width="400">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
|
|
|
@ -238,18 +238,21 @@ fieldset.box td {
|
|||
font-weight: bold;
|
||||
color: #444;
|
||||
padding-top: 0.4em;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.content-4 h1, .content-3 h2, .content-2 h3, .content-1 h4, h4 {
|
||||
font-size: 130%;
|
||||
font-weight: normal;
|
||||
padding-top: 0.3em;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.content-5 h1, .content-4 h2, .content-3 h3, content-2 h4, h5 {
|
||||
font-size: 120%;
|
||||
/* border: none; */
|
||||
padding-top: 0.2em;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.box {
|
||||
|
|
|
@ -47,6 +47,35 @@ function showIfIn(node, conditions) {
|
|||
})
|
||||
}
|
||||
|
||||
function setIfIn(node, conditions) {
|
||||
dojo.forEach(conditions, function(cond) {
|
||||
if (node.value == cond[0]) {
|
||||
target = dijit.byId(cond[1]);
|
||||
target.setValue(cond[2]);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function setIf(node, cond, acts) {
|
||||
if (node.value == cond) {
|
||||
dojo.forEach(acts, function(act) {
|
||||
target = dijit.byId(act[0]);
|
||||
target.setValue(act[1]);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function setIfN(node, conds, acts) {
|
||||
dojo.forEach(conds, function(cond) {
|
||||
if (node.value == cond) {
|
||||
dojo.forEach(acts, function(act) {
|
||||
target = dijit.byId(act[0]);
|
||||
target.setValue(act[1]);
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function destroyWidgets(node) {
|
||||
dojo.forEach(dojo.query('[widgetId]', node), function(n) {
|
||||
w = dijit.byNode(n);
|
||||
|
@ -103,7 +132,7 @@ function submitReplacing(targetId, formId, url) {
|
|||
mimetype: "text/html",
|
||||
load: function(response, ioArgs) {
|
||||
replaceNode(response, targetId);
|
||||
return resonse;
|
||||
return response;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -115,7 +144,7 @@ function xhrSubmitPopup(formId, url) {
|
|||
mimetype: "text/html",
|
||||
load: function(response, ioArgs) {
|
||||
window.close();
|
||||
return resonse;
|
||||
return response;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2016 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2017 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -86,10 +86,14 @@ class NodeView(BaseView):
|
|||
super(NodeView, self).__init__(context, request)
|
||||
self.viewAnnotations.setdefault('nodeView', self)
|
||||
self.viewAnnotations.setdefault('node', self.context)
|
||||
viewConfig = getViewConfiguration(context, request)
|
||||
self.setSkin(viewConfig.get('skinName'))
|
||||
self.setSkin(self.viewConfig.get('skinName'))
|
||||
|
||||
def __call__(self, *args, **kw):
|
||||
if self.nodeType == 'raw':
|
||||
vn = self.context.viewName
|
||||
if vn:
|
||||
self.request.response.setHeader('content-type', vn)
|
||||
return self.context.body
|
||||
tv = self.viewAnnotations.get('targetView')
|
||||
if tv is not None:
|
||||
if tv.isToplevel:
|
||||
|
@ -98,6 +102,29 @@ class NodeView(BaseView):
|
|||
self.controller.setMainPage()
|
||||
return super(NodeView, self).__call__(*args, **kw)
|
||||
|
||||
@Lazy
|
||||
def viewConfig(self):
|
||||
return getViewConfiguration(self.context, self.request)
|
||||
|
||||
@Lazy
|
||||
def viewConfigOptions(self):
|
||||
result = {}
|
||||
for opt in self.viewConfig.get('options') or []:
|
||||
if ':' in opt:
|
||||
k, v = opt.split(':', 1)
|
||||
result[k] = v.split(',')
|
||||
else:
|
||||
result[opt] = True
|
||||
return result
|
||||
|
||||
@Lazy
|
||||
def copyright(self):
|
||||
cr = self.viewConfigOptions.get('copyright')
|
||||
if cr:
|
||||
return cr[0]
|
||||
cr = self.globalOptions('copyright')
|
||||
return cr and cr[0] or 'cyberconcepts.org team'
|
||||
|
||||
@Lazy
|
||||
def macro(self):
|
||||
return self.template.macros['content']
|
||||
|
@ -115,7 +142,9 @@ class NodeView(BaseView):
|
|||
parts.extend(getParts(n))
|
||||
return parts
|
||||
|
||||
def update(self):
|
||||
def update(self, topLevel=True):
|
||||
if topLevel and self.view != self:
|
||||
return self.view.update(False)
|
||||
result = super(NodeView, self).update()
|
||||
self.recordAccess()
|
||||
return result
|
||||
|
@ -129,7 +158,7 @@ class NodeView(BaseView):
|
|||
return []
|
||||
menu = self.menu
|
||||
data = [dict(label=menu.title, url=menu.url)]
|
||||
menuItem = self.nearestMenuItem
|
||||
menuItem = self.getNearestMenuItem(all=True)
|
||||
if menuItem != menu.context:
|
||||
data.append(dict(label=menuItem.title,
|
||||
url=absoluteURL(menuItem, self.request)))
|
||||
|
@ -140,6 +169,9 @@ class NodeView(BaseView):
|
|||
url=absoluteURL(p, self.request)))
|
||||
if self.virtualTarget:
|
||||
data.extend(self.virtualTarget.breadcrumbs())
|
||||
if data and not '?' in data[-1]['url']:
|
||||
if self.urlParamString:
|
||||
data[-1]['url'] += self.urlParamString
|
||||
return data
|
||||
|
||||
def viewModes(self):
|
||||
|
@ -366,6 +398,10 @@ class NodeView(BaseView):
|
|||
def editable(self):
|
||||
return canWrite(self.context, 'body')
|
||||
|
||||
def hasTopPage(self, name):
|
||||
page = self.topMenu.context.get(name)
|
||||
return page is not None
|
||||
|
||||
# menu stuff
|
||||
|
||||
@Lazy
|
||||
|
@ -411,8 +447,9 @@ class NodeView(BaseView):
|
|||
|
||||
@Lazy
|
||||
def menuItems(self):
|
||||
return [NodeView(child, self.request)
|
||||
items = [NodeView(child, self.request).view
|
||||
for child in self.context.getMenuItems()]
|
||||
return [item for item in items if item.isVisible]
|
||||
|
||||
@Lazy
|
||||
def parents(self):
|
||||
|
@ -420,10 +457,13 @@ class NodeView(BaseView):
|
|||
|
||||
@Lazy
|
||||
def nearestMenuItem(self):
|
||||
return self.getNearestMenuItem()
|
||||
|
||||
def getNearestMenuItem(self, all=False):
|
||||
menu = self.menuObject
|
||||
menuItem = None
|
||||
for p in [self.context] + self.parents:
|
||||
if not p.isMenuItem():
|
||||
if not all and not p.isMenuItem():
|
||||
menuItem = None
|
||||
elif menuItem is None:
|
||||
menuItem = p
|
||||
|
@ -469,7 +509,7 @@ class NodeView(BaseView):
|
|||
def targetView(self, name='index.html', methodName='show'):
|
||||
if name == 'index.html': # only when called for default view
|
||||
tv = self.viewAnnotations.get('targetView')
|
||||
if tv is not None:
|
||||
if tv is not None and callable(tv):
|
||||
return tv()
|
||||
if '?' in name:
|
||||
name, params = name.split('?', 1)
|
||||
|
@ -567,12 +607,21 @@ class NodeView(BaseView):
|
|||
""" Return URL of given target view given as .XXX URL.
|
||||
"""
|
||||
if isinstance(target, BaseView):
|
||||
miu = self.getMenuItemUrlForTarget(target.context)
|
||||
if miu is not None:
|
||||
return miu
|
||||
return self.makeTargetUrl(self.url, target.uniqueId, target.title)
|
||||
else:
|
||||
target = baseObject(target)
|
||||
return self.makeTargetUrl(self.url, util.getUidForObject(target),
|
||||
target.title)
|
||||
|
||||
def getMenuItemUrlForTarget(self, tobj):
|
||||
for node in tobj.getClients():
|
||||
if node.nodeType == 'page' and node.getMenu() == self.menuObject:
|
||||
return absoluteURL(node, self.request)
|
||||
|
||||
|
||||
def getActions(self, category='object', page=None, target=None):
|
||||
actions = []
|
||||
#self.registerDojo()
|
||||
|
@ -976,7 +1025,8 @@ class NodeTraverser(ItemTraverser):
|
|||
if context.nodeType == 'menu':
|
||||
setViewConfiguration(context, request)
|
||||
if name == '.loops':
|
||||
return self.context.getLoopsRoot()
|
||||
name = self.getTargetUid(request)
|
||||
#return self.context.getLoopsRoot()
|
||||
if name.startswith('.'):
|
||||
name = self.cleanUpTraversalStack(request, name)[1:]
|
||||
target = self.getTarget(name)
|
||||
|
@ -1008,17 +1058,34 @@ class NodeTraverser(ItemTraverser):
|
|||
raise
|
||||
return obj
|
||||
|
||||
def getTargetUid(self, request):
|
||||
parent = self.context.getLoopsRoot()
|
||||
stack = request._traversal_stack
|
||||
for i in range(2):
|
||||
name = stack.pop()
|
||||
obj = parent.get(name)
|
||||
if not obj:
|
||||
return name
|
||||
parent = obj
|
||||
return '.' + util.getUidForObject(obj)
|
||||
|
||||
def cleanUpTraversalStack(self, request, name):
|
||||
traversalStack = request._traversal_stack
|
||||
while traversalStack and traversalStack[0].startswith('.'):
|
||||
#traversalStack = request._traversal_stack
|
||||
#while traversalStack and traversalStack[0].startswith('.'):
|
||||
# skip obsolete target references in the url
|
||||
name = traversalStack.pop(0)
|
||||
# name = traversalStack.pop(0)
|
||||
traversedNames = request._traversed_names
|
||||
if traversedNames:
|
||||
lastTraversed = traversedNames[-1]
|
||||
if lastTraversed.startswith('.') and lastTraversed != name:
|
||||
for n in list(traversedNames):
|
||||
if n.startswith('.'):
|
||||
# remove obsolete target refs
|
||||
traversedNames.remove(n)
|
||||
#if traversedNames:
|
||||
# lastTraversed = traversedNames[-1]
|
||||
# if lastTraversed.startswith('.') and lastTraversed != name:
|
||||
# let <base .../> tag show the current object
|
||||
traversedNames[-1] = name
|
||||
# traversedNames[-1] = name
|
||||
# let <base .../> tag show the current object
|
||||
traversedNames.append(name)
|
||||
return name
|
||||
|
||||
def getTarget(self, name):
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
item nocall:target"
|
||||
tal:attributes="class string:content-$level;
|
||||
id id;
|
||||
ondblclick python: target.openEditWindow('configure.html')">
|
||||
ondblclick python:target.openEditWindow('configure.html')">
|
||||
<metal:body use-macro="item/macro">
|
||||
The body
|
||||
</metal:body>
|
||||
|
@ -41,17 +41,22 @@
|
|||
|
||||
|
||||
<metal:body define-macro="conceptbody">
|
||||
<tal:body define="body item/body;">
|
||||
<tal:body define="body item/body;
|
||||
itemNum view/itemNum;
|
||||
id string:$itemNum.body">
|
||||
<div class="content-1" id="1"
|
||||
tal:attributes="class string:content-$level;
|
||||
id string:${view/itemNum}.body;
|
||||
ondblclick python: item.openEditWindow('configure.html')">
|
||||
ondblclick python:item.openEditWindow('configure.html')">
|
||||
<span tal:content="structure body">Node Body</span>
|
||||
</div>
|
||||
<tal:concepts define="item nocall:item/targetObjectView;
|
||||
<div tal:define="item nocall:item/targetObjectView;
|
||||
macro item/macro">
|
||||
<div tal:attributes="class string:content-$level;
|
||||
id id;">
|
||||
<div metal:use-macro="macro" />
|
||||
</tal:concepts>
|
||||
</div>
|
||||
</div>
|
||||
</tal:body>
|
||||
</metal:body>
|
||||
|
||||
|
@ -328,11 +333,12 @@
|
|||
<metal:login define-macro="login">
|
||||
<div>
|
||||
<a href="login.html"
|
||||
tal:attributes="href string:${view/topMenu/url}/login.html"
|
||||
i18n:translate="">Log in</a></div>
|
||||
<div tal:define="register python:view.globalOptions('provideLogin')"
|
||||
tal:condition="register">
|
||||
<a tal:condition="python:register != True"
|
||||
tal:attributes="href python:register[0]"
|
||||
tal:condition="python:register and register != True">
|
||||
<a tal:define="reg python:register[0]"
|
||||
tal:attributes="href string:${view/topMenu/url}/$reg"
|
||||
i18n:translate="">Register new member</a></div>
|
||||
</metal:login>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -20,6 +20,7 @@
|
|||
View class for resource objects.
|
||||
"""
|
||||
|
||||
import os.path
|
||||
import urllib
|
||||
from zope.cachedescriptors.property import Lazy
|
||||
from zope import component
|
||||
|
@ -47,7 +48,7 @@ from loops.browser.common import EditForm, BaseView
|
|||
from loops.browser.concept import BaseRelationView, ConceptRelationView
|
||||
from loops.browser.concept import ConceptConfigureView
|
||||
from loops.browser.node import NodeView, node_macros
|
||||
from loops.common import adapted, NameChooser, normalizeName
|
||||
from loops.common import adapted, baseObject, NameChooser, normalizeName
|
||||
from loops.interfaces import IBaseResource, IDocument, ITextDocument
|
||||
from loops.interfaces import IMediaAsset as legacy_IMediaAsset
|
||||
from loops.interfaces import ITypeConcept
|
||||
|
@ -196,6 +197,9 @@ class ResourceView(BaseView):
|
|||
context = self.context
|
||||
ct = context.contentType
|
||||
response = self.request.response
|
||||
if self.typeOptions('x_robots_tag_header', None) is not None:
|
||||
tagVal = ', '.join(self.typeOptions('x_robots_tag_header'))
|
||||
response.setHeader('X-Robots-Tag', tagVal)
|
||||
self.recordAccess('show', target=self.uniqueId)
|
||||
if ct.startswith('image/'):
|
||||
#response.setHeader('Cache-Control', 'public,max-age=86400')
|
||||
|
@ -216,6 +220,16 @@ class ResourceView(BaseView):
|
|||
if filename is None:
|
||||
filename = (adapted(self.context).localFilename or
|
||||
getName(self.context))
|
||||
if self.typeOptions('use_title_for_download_filename'):
|
||||
base, ext = os.path.splitext(filename)
|
||||
filename = context.title
|
||||
vr = IVersionable(baseObject(context))
|
||||
if len(vr.versions) > 0:
|
||||
filename = vr.generateName(filename, ext, vr.versionId)
|
||||
else:
|
||||
if not filename.endswith(ext):
|
||||
filename += ext
|
||||
filename = filename.encode('UTF-8')
|
||||
if self.typeOptions('no_normalize_download_filename'):
|
||||
filename = '"%s"' % filename
|
||||
else:
|
||||
|
@ -262,11 +276,17 @@ class ResourceView(BaseView):
|
|||
#return util.toUnicode(wp.render(self.request))
|
||||
return super(ResourceView, self).renderText(text, contentType)
|
||||
|
||||
showMore = True
|
||||
|
||||
def renderShortText(self):
|
||||
return self.renderDescription() or self.createShortText(self.render())
|
||||
|
||||
def createShortText(self, text=None):
|
||||
return extractFirstPart(text or self.render())
|
||||
text = (text or self.render()).strip()
|
||||
shortText = extractFirstPart(text)
|
||||
if shortText == text:
|
||||
self.showMore = False
|
||||
return shortText
|
||||
|
||||
def download(self):
|
||||
""" Force download, e.g. of a PDF file """
|
||||
|
@ -471,4 +491,3 @@ class NoteView(DocumentView):
|
|||
def linkUrl(self):
|
||||
ad = self.typeAdapter
|
||||
return ad and ad.linkUrl or ''
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<div metal:use-macro="views/node_macros/object_actions" />
|
||||
</tal:actions>
|
||||
<h1><a tal:omit-tag="python: level > 1"
|
||||
tal:attributes="href request/URL"
|
||||
tal:attributes="href view/requestUrl"
|
||||
tal:content="item/title">Title</a></h1>
|
||||
<tal:desc define="description description|item/renderedDescription"
|
||||
condition="description">
|
||||
|
@ -51,7 +51,7 @@
|
|||
<div tal:attributes="ondblclick python: item.openEditWindow('edit.html')">
|
||||
<div metal:use-macro="views/node_macros/object_actions" />
|
||||
<h1><a tal:omit-tag="python: level > 1"
|
||||
tal:attributes="href request/URL"
|
||||
tal:attributes="href view/requestUrl"
|
||||
tal:content="item/title">Title</a></h1><br />
|
||||
<img tal:attributes="src
|
||||
string:${view/url}/.${view/targetId}/view?version=this" />
|
||||
|
@ -96,6 +96,7 @@
|
|||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<metal:custom define-slot="custom_info" />
|
||||
<metal:fields use-macro="view/comment_macros/comments" />
|
||||
</div>
|
||||
</metal:block>
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
$Id$
|
||||
"""
|
||||
# package loops.browser.skin
|
||||
|
||||
from cybertools.browser.liquid import Liquid
|
||||
from cybertools.browser.blue import Blue
|
||||
|
|
|
@ -69,8 +69,8 @@
|
|||
metal:define-macro="footer">
|
||||
<metal:footer define-slot="footer">
|
||||
© Copyright <span tal:replace="view/currentYear" />,
|
||||
cyberconcepts IT-Consulting Dr. Helmut Merz
|
||||
(<a href="#"
|
||||
<span tal:replace="view/topMenu/copyright" />
|
||||
(<a i18n:translate=""
|
||||
tal:attributes="href string:${view/topMenu/url}/impressum">Impressum</a>)
|
||||
<br />
|
||||
Powered by
|
||||
|
|
|
@ -20,6 +20,7 @@ body {
|
|||
|
||||
#portlets {
|
||||
margin-top: 1em;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
ul.view-modes {
|
||||
|
@ -108,6 +109,14 @@ thead th {
|
|||
background: none;
|
||||
}
|
||||
|
||||
/* printing */
|
||||
|
||||
@media print {
|
||||
.noprint {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* class-specific */
|
||||
|
||||
.breadcrumbs td {
|
||||
|
@ -253,10 +262,26 @@ table.records th, table.records td {
|
|||
border: 1px solid lightgrey;
|
||||
}
|
||||
|
||||
table.report {
|
||||
position: relative;
|
||||
z-index: 99;
|
||||
background: white;
|
||||
}
|
||||
|
||||
table.report th {
|
||||
border-bottom: 1px solid #bbbbbb;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
table.report td {
|
||||
border-bottom: 1px dotted #dddddd;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.report-meta table {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
dl.docutils dt {
|
||||
font-weight: bold;
|
||||
margin-top: 0.3em;
|
||||
|
|
|
@ -11,8 +11,18 @@ body {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#content {
|
||||
/* width: 100%; */
|
||||
width: 80%;
|
||||
width: auto;
|
||||
color: Black;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
method="post" name="listing" action="."
|
||||
tal:define="target nocall:view/target"
|
||||
tal:condition="python: target or items"
|
||||
tal:attributes="action request/URL">
|
||||
tal:attributes="action view/requestUrl">
|
||||
<input type="hidden" name="action" value="assign"
|
||||
tal:attributes="value action" />
|
||||
<table class="listing" summary="Currently assigned"
|
||||
|
@ -82,7 +82,7 @@
|
|||
<fieldset>
|
||||
<legend i18n:translate="">Create Target</legend>
|
||||
<form method="post" name="listing" action="."
|
||||
tal:attributes="action request/URL">
|
||||
tal:attributes="action view/requestUrl">
|
||||
<input type="hidden" name="action" value="create" />
|
||||
<div class="row">
|
||||
<span i18n:translate="">Name</span>
|
||||
|
@ -113,7 +113,7 @@
|
|||
|
||||
<metal:search define-macro="search">
|
||||
<form method="post" name="listing" action="."
|
||||
tal:attributes="action request/URL">
|
||||
tal:attributes="action view/requestUrl">
|
||||
<input type="hidden" name="action" value="search" />
|
||||
<div class="row"
|
||||
tal:define="searchTerm request/searchTerm | nothing;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2007 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -18,8 +18,6 @@
|
|||
|
||||
"""
|
||||
Adapters and others classes for analyzing resources.
|
||||
|
||||
$Id$
|
||||
"""
|
||||
|
||||
from itertools import tee
|
||||
|
@ -41,6 +39,7 @@ from loops.resource import Resource
|
|||
from loops.setup import addAndConfigureObject
|
||||
from loops.type import TypeInterfaceSourceList
|
||||
|
||||
logger = getLogger('Classifier')
|
||||
|
||||
TypeInterfaceSourceList.typeInterfaces += (IClassifier,)
|
||||
|
||||
|
@ -102,6 +101,7 @@ class Classifier(AdapterBase):
|
|||
if resource not in resources:
|
||||
concept.assignResource(resource, predicate)
|
||||
message = u'Assigning: %s %s %s'
|
||||
self.log(message % (resource.title, predicate.title, concept.title), 5)
|
||||
else:
|
||||
message = u'Already assigned: %s %s %s'
|
||||
self.log(message % (resource.title, predicate.title, concept.title), 4)
|
||||
|
@ -109,8 +109,7 @@ class Classifier(AdapterBase):
|
|||
def log(self, message, level=5):
|
||||
if level >= self.logLevel:
|
||||
#print 'Classifier %s:' % getName(self.context), message
|
||||
getLogger('Classifier').info(
|
||||
u'%s: %s' % (getName(self.context), message))
|
||||
logger.info(u'%s: %s' % (getName(self.context), message))
|
||||
|
||||
|
||||
class Extractor(object):
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2007 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -18,17 +18,20 @@
|
|||
|
||||
"""
|
||||
View class(es) for resource classifiers.
|
||||
|
||||
$Id$
|
||||
"""
|
||||
|
||||
from logging import getLogger
|
||||
import transaction
|
||||
from zope import interface, component
|
||||
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||
from zope.cachedescriptors.property import Lazy
|
||||
from zope.traversing.api import getName
|
||||
|
||||
from loops.browser.concept import ConceptView
|
||||
from loops.common import adapted
|
||||
|
||||
logger = getLogger('ClassifierView')
|
||||
|
||||
|
||||
class ClassifierView(ConceptView):
|
||||
|
||||
|
@ -42,12 +45,18 @@ class ClassifierView(ConceptView):
|
|||
if 'update' in self.request.form:
|
||||
cta = adapted(self.context)
|
||||
if cta is not None:
|
||||
for r in collectResources(self.context):
|
||||
for idx, r in enumerate(collectResources(self.context)):
|
||||
if idx % 1000 == 0:
|
||||
logger.info('Committing, resource # %s' % idx)
|
||||
transaction.commit()
|
||||
cta.process(r)
|
||||
logger.info('Finished processing')
|
||||
transaction.commit()
|
||||
return True
|
||||
|
||||
|
||||
def collectResources(concept, checkedConcepts=None, result=None):
|
||||
logger.info('Start collecting resources for %s' % getName(concept))
|
||||
if result is None:
|
||||
result = []
|
||||
if checkedConcepts is None:
|
||||
|
@ -59,4 +68,5 @@ def collectResources(concept, checkedConcepts=None, result=None):
|
|||
if c not in checkedConcepts:
|
||||
checkedConcepts.append(c)
|
||||
collectResources(c, checkedConcepts, result)
|
||||
logger.info('Collected %s resources' % len(result))
|
||||
return result
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
import unittest, doctest
|
||||
from zope.interface.verify import verifyClass
|
||||
#from loops.versioning import versionable
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
"Basic tests for the classifier sub-package."
|
||||
|
|
14
common.py
14
common.py
|
@ -235,17 +235,19 @@ class NameChooser(BaseNameChooser):
|
|||
return name
|
||||
|
||||
def generateNameFromTitle(self, obj):
|
||||
title = obj.title
|
||||
if len(title) > 15:
|
||||
words = title.split()
|
||||
if len(words) > 1:
|
||||
title = '_'.join((words[0], words[-1]))
|
||||
return self.normalizeName(title)
|
||||
return generateNameFromTitle(obj.title)
|
||||
|
||||
def normalizeName(self, baseName):
|
||||
return normalizeName(baseName)
|
||||
|
||||
|
||||
def generateNameFromTitle(title):
|
||||
if len(title) > 15:
|
||||
words = title.split()
|
||||
if len(words) > 1:
|
||||
title = '_'.join((words[0], words[-1]))
|
||||
return normalizeName(title)
|
||||
|
||||
def normalizeName(baseName):
|
||||
specialCharacters = {
|
||||
'\xc4': 'Ae', '\xe4': 'ae', '\xd6': 'Oe', '\xf6': 'oe',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2017 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -34,6 +34,7 @@ from loops.browser.concept import ConceptRelationView as \
|
|||
BaseConceptRelationView
|
||||
from loops.browser.resource import ResourceView as BaseResourceView
|
||||
from loops.common import adapted, baseObject
|
||||
from loops.util import _
|
||||
|
||||
|
||||
standard_template = standard.standard_template
|
||||
|
@ -54,42 +55,6 @@ class Base(object):
|
|||
def sectionType(self):
|
||||
return self.conceptManager['section']
|
||||
|
||||
@Lazy
|
||||
def isPartOfPredicate(self):
|
||||
return self.conceptManager['ispartof']
|
||||
|
||||
@Lazy
|
||||
def showNavigation(self):
|
||||
return self.typeOptions.show_navigation
|
||||
|
||||
@Lazy
|
||||
def breadcrumbsParent(self):
|
||||
for p in self.context.getParents([self.isPartOfPredicate]):
|
||||
return self.nodeView.getViewForTarget(p)
|
||||
|
||||
@Lazy
|
||||
def neighbours(self):
|
||||
pred = succ = None
|
||||
parent = self.breadcrumbsParent
|
||||
if parent is not None:
|
||||
myself = None
|
||||
children = list(parent.context.getChildren([self.isPartOfPredicate]))
|
||||
for idx, c in enumerate(children):
|
||||
if c == self.context:
|
||||
if idx > 0:
|
||||
pred = self.nodeView.getViewForTarget(children[idx-1])
|
||||
if idx < len(children) - 1:
|
||||
succ = self.nodeView.getViewForTarget(children[idx+1])
|
||||
return pred, succ
|
||||
|
||||
@Lazy
|
||||
def predecessor(self):
|
||||
return self.neighbours[0]
|
||||
|
||||
@Lazy
|
||||
def successor(self):
|
||||
return self.neighbours[1]
|
||||
|
||||
@Lazy
|
||||
def tabview(self):
|
||||
if self.editable:
|
||||
|
@ -107,6 +72,7 @@ class Base(object):
|
|||
@Lazy
|
||||
def textResources(self):
|
||||
self.images = [[]]
|
||||
self.otherResources = []
|
||||
result = []
|
||||
idx = 0
|
||||
for rv in self.getResources():
|
||||
|
@ -115,7 +81,7 @@ class Base(object):
|
|||
idx += 1
|
||||
result.append(rv)
|
||||
self.images.append([])
|
||||
else:
|
||||
elif rv.context.contentType.startswith('image/'):
|
||||
self.registerDojoLightbox()
|
||||
url = self.nodeView.getUrlForTarget(rv.context)
|
||||
src = '%s/mediaasset.html?v=small' % url
|
||||
|
@ -123,6 +89,8 @@ class Base(object):
|
|||
img = dict(src=src, fullImageUrl=fullSrc, title=rv.title,
|
||||
description=rv.description, url=url, object=rv)
|
||||
self.images[idx].append(img)
|
||||
else:
|
||||
self.otherResources.append(rv)
|
||||
return result
|
||||
|
||||
def getDocumentTypeForResource(self, r):
|
||||
|
@ -178,9 +146,47 @@ class SectionView(Base, ConceptView):
|
|||
def macro(self):
|
||||
return book_template.macros['section']
|
||||
|
||||
@Lazy
|
||||
def isPartOfPredicate(self):
|
||||
return self.conceptManager['ispartof']
|
||||
|
||||
@Lazy
|
||||
def breadcrumbsParent(self):
|
||||
for p in self.context.getParents([self.isPartOfPredicate]):
|
||||
return self.nodeView.getViewForTarget(p)
|
||||
|
||||
@Lazy
|
||||
def showNavigation(self):
|
||||
return self.typeOptions.show_navigation
|
||||
|
||||
@Lazy
|
||||
def neighbours(self):
|
||||
pred = succ = None
|
||||
parent = self.breadcrumbsParent
|
||||
if parent is not None:
|
||||
myself = None
|
||||
children = list(parent.context.getChildren([self.isPartOfPredicate]))
|
||||
for idx, c in enumerate(children):
|
||||
if c == self.context:
|
||||
if idx > 0:
|
||||
pred = self.nodeView.getViewForTarget(children[idx-1])
|
||||
if idx < len(children) - 1:
|
||||
succ = self.nodeView.getViewForTarget(children[idx+1])
|
||||
return pred, succ
|
||||
|
||||
@Lazy
|
||||
def predecessor(self):
|
||||
return self.neighbours[0]
|
||||
|
||||
@Lazy
|
||||
def successor(self):
|
||||
return self.neighbours[1]
|
||||
|
||||
|
||||
class TopicView(Base, ConceptView):
|
||||
|
||||
tabTitle = _(u'title_bookTopicView')
|
||||
|
||||
@Lazy
|
||||
def macro(self):
|
||||
return book_template.macros['topic']
|
||||
|
|
|
@ -130,7 +130,8 @@
|
|||
|
||||
<metal:topic define-macro="topic"
|
||||
tal:define="children children|python:list(item.children());
|
||||
textResources textResources|item/textResources">
|
||||
textResources textResources|item/textResources;
|
||||
resources item/otherResources">
|
||||
<metal:info use-macro="view/concept_macros/concepttitle" />
|
||||
<h2 i18n:translate=""
|
||||
tal:condition="children">Children</h2>
|
||||
|
@ -145,14 +146,25 @@
|
|||
<a tal:attributes="href python:view.getUrlForTarget(related.context)"
|
||||
tal:content="related/title" />
|
||||
</h3>
|
||||
<div>
|
||||
<div tal:replace="structure related/renderShortText" />
|
||||
<div tal:define="shortText related/renderShortText">
|
||||
<div tal:replace="structure shortText" />
|
||||
<p>
|
||||
<a i18n:translate=""
|
||||
tal:condition="related/showMore"
|
||||
tal:attributes="href python:view.getUrlForTarget(related.context)">
|
||||
more...</a></p>
|
||||
<div tal:repeat="image python:
|
||||
item.images[repeat['related'].index() + 1]">
|
||||
<a dojoType="dojox.image.Lightbox" group="mediasset"
|
||||
i18n:attributes="title"
|
||||
tal:attributes="href image/fullImageUrl;
|
||||
title image/title">
|
||||
<img tal:attributes="src image/src;
|
||||
alt image/title" /></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<metal:info use-macro="view/concept_macros/conceptresources" />
|
||||
</metal:topic>
|
||||
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ from cybertools.typology.interfaces import IType, ITypeManager
|
|||
from cybertools.util.jeep import Jeep
|
||||
|
||||
from loops.base import ParentInfo
|
||||
from loops.common import adapted, AdapterBase
|
||||
from loops.common import adapted, baseObject, AdapterBase
|
||||
from loops.i18n.common import I18NValue
|
||||
from loops.interfaces import IConcept, IConceptRelation, IConceptView
|
||||
from loops.interfaces import IResource
|
||||
|
@ -490,14 +490,14 @@ class IndexAttributes(object):
|
|||
title = u''
|
||||
if isinstance(title, I18NValue):
|
||||
title = ' '.join(title.values())
|
||||
return ' '.join((getName(context), title)).strip()
|
||||
return ' '.join((getName(baseObject(context)), title)).strip()
|
||||
|
||||
def date(self):
|
||||
if self.adaptedIndexAttributes is not None:
|
||||
return self.adaptedIndexAttributes.date()
|
||||
|
||||
def creators(self):
|
||||
cr = IZopeDublinCore(self.context).creators or []
|
||||
cr = IZopeDublinCore(baseObject(self.context)).creators or []
|
||||
pau = component.getUtility(IAuthentication)
|
||||
creators = []
|
||||
for c in cr:
|
||||
|
@ -514,7 +514,7 @@ class IndexAttributes(object):
|
|||
def identifier(self):
|
||||
id = getattr(self.adapted, 'identifier', None)
|
||||
if id is None:
|
||||
return getName(self.context)
|
||||
return getName(baseObject(self.context))
|
||||
return id
|
||||
|
||||
def keywords(self):
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<configure
|
||||
xmlns="http://namespaces.zope.org/zope"
|
||||
xmlns:i18n="http://namespaces.zope.org/i18n"
|
||||
xmlns:browser="http://namespaces.zope.org/browser"
|
||||
i18n_domain="loops">
|
||||
|
||||
<i18n:registerTranslations directory="locales" />
|
||||
|
@ -478,6 +479,19 @@
|
|||
component="loops.view.NodeTypeSourceList"
|
||||
name="loops.nodeTypeSource" />
|
||||
|
||||
<!-- Markdown support -->
|
||||
|
||||
<utility
|
||||
component="loops.util.MarkdownSourceFactory"
|
||||
name="loops.util.markdown"
|
||||
/>
|
||||
|
||||
<browser:view
|
||||
name=""
|
||||
for="loops.util.IMarkdownSource"
|
||||
class="loops.util.MarkdownToHTMLRenderer"
|
||||
permission="zope.Public" />
|
||||
|
||||
|
||||
<include package=".browser" />
|
||||
<include package=".classifier" />
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
# types
|
||||
type(u'query', u'Abfrage', options=u'',
|
||||
typeInterface='loops.expert.concept.IQueryConcept', viewName=u'')
|
||||
type(u'datatable', u'Datentabelle', options=u'action.portlet:edit_concept',
|
||||
typeInterface='loops.table.IDataTable', viewName=u'')
|
||||
type(u'task', u'Aufgabe', options=u'',
|
||||
typeInterface='loops.knowledge.interfaces.ITask', viewName=u'')
|
||||
type(u'domain', u'Bereich', options=u'', typeInterface=u'', viewName=u'')
|
||||
type(u'classifier', u'Classifier', options=u'',
|
||||
typeInterface='loops.classifier.interfaces.IClassifier', viewName=u'classifier.html')
|
||||
typeInterface='loops.classifier.interfaces.IClassifier',
|
||||
viewName=u'classifier.html')
|
||||
type(u'documenttype', u'Dokumentenart', options=u'', typeInterface=u'', viewName=u'')
|
||||
type(u'extcollection', u'External Collection', options=u'',
|
||||
typeInterface='loops.integrator.interfaces.IExternalCollection',
|
||||
|
@ -14,20 +17,23 @@ type(u'folder', u'Ordner', options=u'', typeInterface=u'', viewName=u'')
|
|||
type(u'glossaryitem', u'Glossareintrag', options=u'',
|
||||
typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'glossaryitem.html')
|
||||
type(u'media_asset', u'Media Asset',
|
||||
options=u'storage:varsubdir\nstorage_parameters:extfiles/sites_zzz\nasset_transform.minithumb: size(105)\nasset_transform.small: size(230)\nasset_transform.medium: size(480)', typeInterface='loops.media.interfaces.IMediaAsset', viewName=u'image_medium.html')
|
||||
options=u'storage:varsubdir\nstorage_parameters:extfiles/sites_zzz\nasset_transform.minithumb: size(105)\nasset_transform.small: size(230)\nasset_transform.medium: size(480)', typeInterface='loops.media.interfaces.IMediaAsset',
|
||||
viewName=u'image_medium.html')
|
||||
type(u'note', u'Notiz', options=u'', typeInterface='loops.interfaces.INote',
|
||||
viewName='note.html')
|
||||
type(u'person', u'Person', options=u'',
|
||||
typeInterface='loops.knowledge.interfaces.IPerson', viewName=u'')
|
||||
type(u'predicate', u'Prädikat', options=u'',
|
||||
typeInterface=u'loops.interfaces.IPredicate', viewName=u'')
|
||||
type(u'event', u'Termin', options=u'', typeInterface='loops.organize.interfaces.ITask',
|
||||
type(u'event', u'Termin', options=u'',
|
||||
typeInterface='loops.organize.interfaces.ITask',
|
||||
viewName=u'task.html')
|
||||
type(u'textdocument', u'Text', options=u'', typeInterface='loops.interfaces.ITextDocument', viewName=u'')
|
||||
type(u'topic', u'Thema', options=u'action.portlet:createTopic,editTopic',
|
||||
type(u'textdocument', u'Text', options=u'',
|
||||
typeInterface='loops.interfaces.ITextDocument', viewName=u'')
|
||||
type(u'topic', u'Thema', options=u'action.portlet:editTopic,createTopic',
|
||||
typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'')
|
||||
type(u'type', u'Typ', options=u'', typeInterface='loops.interfaces.ITypeConcept',
|
||||
viewName=u'')
|
||||
type(u'type', u'Typ', options=u'',
|
||||
typeInterface='loops.interfaces.ITypeConcept', viewName=u'')
|
||||
|
||||
# domains
|
||||
concept(u'general', u'Allgemein', u'domain')
|
||||
|
@ -74,16 +80,20 @@ child(u'system', u'media_asset', u'standard')
|
|||
child(u'system', u'personal_info', u'standard')
|
||||
child(u'topic', u'topic', u'issubtype', 1)
|
||||
|
||||
resource(u'homepage', u'Willkommen', u'textdocument', contentType='text/restructured')
|
||||
resource(u'impressum', u'Impressum', u'textdocument', contentType='text/restructured')
|
||||
# resources
|
||||
resource(u'homepage', u'Willkommen', u'textdocument',
|
||||
contentType='text/restructured')
|
||||
resource(u'impressum', u'Impressum', u'textdocument',
|
||||
contentType='text/restructured')
|
||||
|
||||
#nodes
|
||||
node(u'home', u'Startseite', '', 'menu')
|
||||
node(u'willkommen', u'Willkommen', u'home', u'text')
|
||||
node(u'willkommen', u'Willkommen', u'home/willkommen', u'text',
|
||||
node(u'willkommen', u'Willkommen', u'home', u'text',
|
||||
target=u'resources/homepage')
|
||||
node(u'participants', u'Teilnehmer', u'home', 'page', target=u'concepts/participants')
|
||||
node(u'participants', u'Teilnehmer', u'home', 'page',
|
||||
target=u'concepts/participants')
|
||||
node(u'topics', u'Themen', u'home', 'page', target=u'concepts/topics')
|
||||
node(u'glossary', u'Glossar', u'home', 'page', target=u'concepts/glossary')
|
||||
node(u'search', u'Suche', u'home', 'page', target=u'concepts/search')
|
||||
node(u'impressum', u'Impressum', u'home', u'info', target=u'resources/impressum')
|
||||
node(u'impressum', u'Impressum', u'home', u'info',
|
||||
target=u'resources/impressum')
|
||||
|
|
|
@ -1,60 +1,99 @@
|
|||
# types
|
||||
type(u'query', u'Query', options=u'',
|
||||
typeInterface='loops.expert.concept.IQueryConcept', viewName=u'')
|
||||
type(u'datatable', u'Data Table', options=u'action.portlet:edit_concept',
|
||||
typeInterface='loops.table.IDataTable', viewName=u'')
|
||||
type(u'task', u'Task', options=u'',
|
||||
typeInterface='loops.knowledge.interfaces.ITask', viewName=u'')
|
||||
type(u'domain', u'Domain', options=u'', typeInterface=u'', viewName=u'')
|
||||
type(u'classifier', u'Classifier', options=u'',
|
||||
typeInterface='loops.classifier.interfaces.IClassifier', viewName=u'classifier.html')
|
||||
typeInterface='loops.classifier.interfaces.IClassifier',
|
||||
viewName=u'classifier.html')
|
||||
type(u'documenttype', u'Document Type', options=u'', typeInterface=u'', viewName=u'')
|
||||
type(u'extcollection', u'External Collection', options=u'',
|
||||
typeInterface='loops.integrator.interfaces.IExternalCollection',
|
||||
viewName=u'collection.html')
|
||||
type(u'folder', u'Ordner', options=u'', typeInterface=u'', viewName=u'')
|
||||
type(u'glossaryitem', u'Glossary Item', options=u'',
|
||||
typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'glossaryitem.html')
|
||||
type(u'media_asset', u'Media Asset',
|
||||
options=u'storage:varsubdir\nstorage_parameters:extfiles/sites_zzz\nasset_transform.minithumb: size(105)\nasset_transform.small: size(230)\nasset_transform.medium: size(480)', typeInterface='loops.media.interfaces.IMediaAsset', viewName=u'image_medium.html')
|
||||
options=u'storage:varsubdir\nstorage_parameters:extfiles/sites_zzz\nasset_transform.minithumb: size(105)\nasset_transform.small: size(230)\nasset_transform.medium: size(480)', typeInterface='loops.media.interfaces.IMediaAsset',
|
||||
viewName=u'image_medium.html')
|
||||
type(u'note', u'Note', options=u'', typeInterface='loops.interfaces.INote',
|
||||
viewName='note.html')
|
||||
type(u'person', u'Person', options=u'',
|
||||
typeInterface='loops.knowledge.interfaces.IPerson', viewName=u'')
|
||||
type(u'predicate', u'Predicate', options=u'',
|
||||
typeInterface=u'loops.interfaces.IPredicate', viewName=u'')
|
||||
type(u'event', u'Event', options=u'', typeInterface='loops.organize.interfaces.ITask',
|
||||
type(u'event', u'Event', options=u'',
|
||||
typeInterface='loops.organize.interfaces.ITask',
|
||||
viewName=u'task.html')
|
||||
type(u'textdocument', u'Text', options=u'', typeInterface='loops.interfaces.ITextDocument', viewName=u'')
|
||||
type(u'topic', u'Topy', options=u'', typeInterface='loops.knowledge.interfaces.ITopic',
|
||||
viewName=u'')
|
||||
type(u'type', u'Type', options=u'', typeInterface='loops.interfaces.ITypeConcept',
|
||||
viewName=u'')
|
||||
type(u'textdocument', u'Text', options=u'',
|
||||
typeInterface='loops.interfaces.ITextDocument', viewName=u'')
|
||||
type(u'topic', u'Topic', options=u'action.portlet:editTopic,createTopic',
|
||||
typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'')
|
||||
type(u'type', u'Type', options=u'',
|
||||
typeInterface='loops.interfaces.ITypeConcept', viewName=u'')
|
||||
|
||||
#domains
|
||||
concept(u'general', u'General', u'domain')
|
||||
concept(u'system', u'System', u'domain')
|
||||
|
||||
# predicates
|
||||
concept(u'depends', u'depends', u'predicate')
|
||||
concept(u'follows', u'follows', u'predicate')
|
||||
concept(u'general', u'General', u'domain')
|
||||
concept(u'glossary', u'Glossary', u'query', options=u'', viewName=u'glossary.html')
|
||||
concept(u'hasType', u'has Type', u'predicate')
|
||||
concept(u'ispartof', u'is Part of', u'predicate')
|
||||
concept(u'issubtype', u'is Subtype', u'predicate')
|
||||
concept(u'knows', u'knows', u'predicate')
|
||||
concept(u'ownedby', u'owned by', u'predicate')
|
||||
concept(u'personal_info', u'Personal Information', u'query', options=u'',
|
||||
viewName=u'personal_info.html')
|
||||
concept(u'provides', u'provides', u'predicate')
|
||||
concept(u'querytarget', u'is Query Target', u'predicate')
|
||||
concept(u'requires', u'requires', u'predicate')
|
||||
concept(u'search', u'Search', u'query', options=u'', viewName=u'search')
|
||||
concept(u'standard', u'subobject', u'predicate')
|
||||
concept(u'system', u'System', u'domain')
|
||||
|
||||
#queries
|
||||
concept(u'events', u'Events', u'query', options=u'delta:2',
|
||||
viewName=u'list_events.html')
|
||||
concept(u'glossary', u'Glossary', u'query', options=u'', viewName=u'glossary.html')
|
||||
concept(u'personal_info', u'Personal Information', u'query', options=u'',
|
||||
viewName=u'personal_info.html')
|
||||
concept(u'participants', u'Participants', u'query', options=u'',
|
||||
viewName=u'list_children.html')
|
||||
concept(u'recenct_changes', u'Recent Changes', u'query',
|
||||
options=u'types:concept:*,resource:*',
|
||||
viewName=u'recent_changes.html')
|
||||
concept(u'search', u'Search', u'query', options=u'', viewName=u'search')
|
||||
concept(u'topics', u'Topics', u'query', options=u'action.portlet:createTopic',
|
||||
viewName=u'list_children.html')
|
||||
|
||||
# child assignments
|
||||
child(u'general', u'documenttype', u'standard')
|
||||
child(u'general', u'event', u'standard')
|
||||
child(u'general', u'events', u'standard')
|
||||
child(u'general', u'participants', u'standard')
|
||||
child(u'general', u'topics', u'standard')
|
||||
child(u'system', u'classifier', u'standard')
|
||||
child(u'system', u'extcollection', u'standard')
|
||||
child(u'system', u'issubtype', u'standard')
|
||||
child(u'system', u'media_asset', u'standard')
|
||||
child(u'system', u'personal_info', u'standard')
|
||||
node(u'home', u'Homepage', '', 'menu', body=u'Welcome\n=======)
|
||||
child(u'topic', u'topic', u'issubtype', 1)
|
||||
|
||||
# resources
|
||||
resource(u'homepage', u'Welcome', u'textdocument',
|
||||
contentType='text/restructured')
|
||||
resource(u'impressum', u'Legal Information', u'textdocument',
|
||||
contentType='text/restructured')
|
||||
|
||||
#nodes
|
||||
node(u'home', u'Home', '', 'menu')
|
||||
node(u'welcome', u'Welcome', u'home', u'text',
|
||||
target=u'resources/homepage')
|
||||
node(u'participants', u'Participants', u'home', 'page',
|
||||
body=u'Participants\n============', target=u'concepts/person',
|
||||
viewName=u'listchildren')
|
||||
node(u'topics', u'Topics', u'home', 'page', body=u'Topics\n======',
|
||||
target=u'concepts/topic', viewName=u'listchildren')
|
||||
target=u'concepts/participants')
|
||||
node(u'topics', u'Topics', u'home', 'page', target=u'concepts/topics')
|
||||
node(u'glossary', u'Glossary', u'home', 'page', target=u'concepts/glossary')
|
||||
node(u'search', u'Search', u'home', 'page', target=u'concepts/search')
|
||||
node(u'impressum', u'Legal Information', u'home', u'info',
|
||||
target=u'resources/impressum')
|
||||
|
|
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()
|
||||
|
||||
>>> len(concepts) + len(resources)
|
||||
36
|
||||
38
|
||||
|
||||
>>> loopsRoot = site['loops']
|
||||
|
||||
|
@ -47,11 +47,11 @@ Type- and text-based queries
|
|||
>>> from loops.expert import query
|
||||
>>> qu = query.Title('ty*')
|
||||
>>> list(qu.apply())
|
||||
[0, 2, 65]
|
||||
[0, 2, 70]
|
||||
|
||||
>>> qu = query.Type('loops:*')
|
||||
>>> len(list(qu.apply()))
|
||||
36
|
||||
38
|
||||
|
||||
>>> qu = query.Type('loops:concept:predicate')
|
||||
>>> len(list(qu.apply()))
|
||||
|
|
|
@ -91,4 +91,26 @@
|
|||
factory="loops.expert.browser.report.ResultsConceptView"
|
||||
permission="zope.View" />
|
||||
|
||||
<zope:adapter
|
||||
name="concept_report_embedded.html"
|
||||
for="loops.interfaces.IConcept
|
||||
zope.publisher.interfaces.browser.IBrowserRequest"
|
||||
provides="zope.interface.Interface"
|
||||
factory="loops.expert.browser.report.EmbeddedReportConceptView"
|
||||
permission="zope.View" />
|
||||
|
||||
<zope:adapter
|
||||
name="concept_results_embedded.html"
|
||||
for="loops.interfaces.IConcept
|
||||
zope.publisher.interfaces.browser.IBrowserRequest"
|
||||
provides="zope.interface.Interface"
|
||||
factory="loops.expert.browser.report.EmbeddedResultsConceptView"
|
||||
permission="zope.View" />
|
||||
|
||||
<browser:page
|
||||
name="concept_results.csv"
|
||||
for="loops.organize.interfaces.IConceptSchema"
|
||||
class="loops.expert.browser.export.ResultsConceptCSVExport"
|
||||
permission="zope.View" />
|
||||
|
||||
</configure>
|
||||
|
|
167
expert/browser/export.py
Normal file
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 tal:define="report item/reportInstance;
|
||||
reportView nocall:item"
|
||||
reportView nocall:item;
|
||||
renderer item/resultsRenderer"
|
||||
tal:attributes="class string:content-$level;">
|
||||
<div metal:use-macro="item/report_macros/header" />
|
||||
<div metal:use-macro="item/resultsRenderer" />
|
||||
<tal:renderer condition="renderer">
|
||||
<div metal:use-macro="renderer" />
|
||||
</tal:renderer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -23,29 +26,65 @@
|
|||
</div>
|
||||
|
||||
|
||||
<div metal:define-macro="embedded_report">
|
||||
<div tal:define="report item/reportInstance;
|
||||
reportView nocall:item"
|
||||
tal:attributes="class string:content-$level;">
|
||||
<div metal:use-macro="item/report_macros/header" />
|
||||
<div metal:use-macro="item/resultsRenderer" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div metal:define-macro="header">
|
||||
<metal:block use-macro="view/concept_macros/concepttitle" />
|
||||
<form method="get" name="report_data" class="report-meta">
|
||||
<input type="hidden" name="show_results" value="True" />
|
||||
<tal:hidden define="params item/dynamicParams"
|
||||
tal:condition="nothing">
|
||||
<tal:hidden define="params item/dynamicParams">
|
||||
<input type="hidden"
|
||||
tal:repeat="name params"
|
||||
tal:condition="nothing"
|
||||
tal:attributes="name name;
|
||||
value params/?name" /></tal:hidden>
|
||||
value params/?name" />
|
||||
<input type="hidden"
|
||||
tal:define="viewName request/loops.viewName|nothing"
|
||||
tal:condition="viewName"
|
||||
tal:attributes="name string:loops.viewName;
|
||||
value viewName" />
|
||||
<input type="hidden"
|
||||
tal:define="sortinfo request/sortinfo_results|nothing"
|
||||
tal:condition="sortinfo"
|
||||
tal:attributes="name string:sortinfo_results;
|
||||
value sortinfo" />
|
||||
<input type="hidden" name="report_name"
|
||||
tal:define="reportName item/reportName"
|
||||
tal:condition="reportName"
|
||||
tal:attributes="value reportName" />
|
||||
</tal:hidden>
|
||||
<div metal:use-macro="item/report_macros/params" />
|
||||
<div metal:define-macro="buttons">
|
||||
<input type="submit" name="report_execute" value="Execute Report"
|
||||
onclick="this.form.action = ''"
|
||||
tal:attributes="value item/reportExecuteTitle|string:Execute Report"
|
||||
tal:condition="item/queryFields"
|
||||
i18n:attributes="value" />
|
||||
<input type="submit"
|
||||
<input type="submit" name="report_download"
|
||||
tal:condition="item/reportDownload"
|
||||
tal:attributes="name string:${item/reportDownload}:method;
|
||||
value item/reportDownloadTitle"
|
||||
tal:attributes="value item/reportDownloadTitle;
|
||||
onclick string:
|
||||
this.form.action = '${item/reportDownload}'"
|
||||
i18n:attributes="value" />
|
||||
</div>
|
||||
<br />
|
||||
</form>
|
||||
<tal:ignore condition="nothing">
|
||||
<tal:list condition="renderer">
|
||||
<div metal:use-macro="renderer" />
|
||||
</tal:list>
|
||||
<tal:list condition="not:renderer">
|
||||
<div metal:use-macro="view/concept_macros/conceptchildren" />
|
||||
</tal:list>
|
||||
</tal:ignore>
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -113,7 +152,14 @@
|
|||
|
||||
|
||||
<metal:field define-macro="selection">
|
||||
<metal:use use-macro="item/report_macros/textline" />
|
||||
<select tal:attributes="name name">
|
||||
<option />
|
||||
<option tal:repeat="opt python:field.getVocabularyItems(
|
||||
context=item.adapted, request=request)"
|
||||
tal:attributes="value opt/token;
|
||||
selected python:value == opt['token']"
|
||||
tal:content="opt/title" />
|
||||
</select>
|
||||
</metal:field>
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2011 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2016 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -20,6 +20,7 @@
|
|||
View classes for reporting.
|
||||
"""
|
||||
|
||||
from logging import getLogger
|
||||
from urllib import urlencode
|
||||
from zope import interface, component
|
||||
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||
|
@ -46,6 +47,10 @@ class ReportView(ConceptView):
|
|||
""" A view for defining (editing) a report.
|
||||
"""
|
||||
|
||||
resultsRenderer = None # to be defined by subclass
|
||||
reportDownload = None
|
||||
reportName = None
|
||||
|
||||
@Lazy
|
||||
def report_macros(self):
|
||||
return self.controller.mergeTemplateMacros('report', report_template)
|
||||
|
@ -55,10 +60,33 @@ class ReportView(ConceptView):
|
|||
def macro(self):
|
||||
return self.report_macros['main']
|
||||
|
||||
@Lazy
|
||||
def tabTitle(self):
|
||||
return self.report.title
|
||||
|
||||
@Lazy
|
||||
def dynamicParams(self):
|
||||
return self.request.form
|
||||
|
||||
@Lazy
|
||||
def report(self):
|
||||
return self.adapted
|
||||
|
||||
@Lazy
|
||||
def reportInstance(self):
|
||||
instance = component.getAdapter(self.report, IReportInstance,
|
||||
name=self.report.reportType)
|
||||
instance.view = self
|
||||
return instance
|
||||
|
||||
@Lazy
|
||||
def queryFields(self):
|
||||
ri = self.reportInstance
|
||||
qf = ri.getAllQueryFields()
|
||||
if ri.userSettings:
|
||||
return [f for f in qf if f in ri.userSettings]
|
||||
return qf
|
||||
|
||||
|
||||
class ResultsView(NodeView):
|
||||
|
||||
|
@ -107,13 +135,6 @@ class ResultsView(NodeView):
|
|||
def report(self):
|
||||
return adapted(self.virtualTargetObject)
|
||||
|
||||
@Lazy
|
||||
def reportInstance(self):
|
||||
instance = component.getAdapter(self.report, IReportInstance,
|
||||
name=self.report.reportType)
|
||||
instance.view = self
|
||||
return instance
|
||||
|
||||
#@Lazy
|
||||
def results(self):
|
||||
return self.reportInstance.getResults(self.params)
|
||||
|
@ -139,6 +160,8 @@ class ResultsConceptView(ConceptView):
|
|||
""" View on a concept using the results of a report.
|
||||
"""
|
||||
|
||||
logger = getLogger ('ResultsConceptView')
|
||||
|
||||
reportName = None # define in subclass if applicable
|
||||
reportDownload = None
|
||||
reportType = None # set for using special report instance adapter
|
||||
|
@ -169,6 +192,9 @@ class ResultsConceptView(ConceptView):
|
|||
|
||||
@Lazy
|
||||
def reportName(self):
|
||||
rn = self.request.form.get('report_name')
|
||||
if rn is not None:
|
||||
return rn
|
||||
return (self.getOptions('report_name') or [None])[0]
|
||||
|
||||
@Lazy
|
||||
|
@ -179,7 +205,10 @@ class ResultsConceptView(ConceptView):
|
|||
@Lazy
|
||||
def report(self):
|
||||
if self.reportName:
|
||||
return adapted(self.conceptManager[self.reportName])
|
||||
report = adapted(self.conceptManager.get(self.reportName))
|
||||
if report is None:
|
||||
self.logger.warn("Report '%s' not found." % self.reportName)
|
||||
return report
|
||||
reports = self.context.getParents([self.hasReportPredicate])
|
||||
if not reports:
|
||||
type = self.context.conceptType
|
||||
|
@ -193,6 +222,13 @@ class ResultsConceptView(ConceptView):
|
|||
ri = component.getAdapter(self.report, IReportInstance,
|
||||
name=reportType)
|
||||
ri.view = self
|
||||
if not ri.sortCriteria:
|
||||
si = self.sortInfo.get('results')
|
||||
if si is not None:
|
||||
fnames = (si['colName'],)
|
||||
ri.sortCriteria = [f for f in ri.getSortFields()
|
||||
if f.name in fnames]
|
||||
ri.sortDescending = not si['ascending']
|
||||
return ri
|
||||
|
||||
def results(self):
|
||||
|
@ -207,6 +243,35 @@ class ResultsConceptView(ConceptView):
|
|||
def getColumnRenderer(self, col):
|
||||
return self.result_macros[col.renderer]
|
||||
|
||||
@Lazy
|
||||
def downloadLink(self, format='csv'):
|
||||
opt = self.options('download_' + format)
|
||||
if not opt:
|
||||
opt = self.typeOptions('download_' + format)
|
||||
if opt:
|
||||
return '/'.join((self.nodeView.virtualTargetUrl, opt[0]))
|
||||
|
||||
@Lazy
|
||||
def reportDownload(self):
|
||||
return self.downloadLink
|
||||
|
||||
def isSortableColumn(self, tableName, colName):
|
||||
if tableName == 'results':
|
||||
if colName in [f.name for f in self.reportInstance.getSortFields()]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class EmbeddedResultsConceptView(ResultsConceptView):
|
||||
|
||||
@Lazy
|
||||
def macro(self):
|
||||
return self.result_macros['embedded_content']
|
||||
|
||||
@Lazy
|
||||
def title(self):
|
||||
return self.report.title
|
||||
|
||||
|
||||
class ReportConceptView(ResultsConceptView, ReportView):
|
||||
""" View on a concept using a report.
|
||||
|
@ -229,6 +294,17 @@ class ReportConceptView(ResultsConceptView, ReportView):
|
|||
return qf
|
||||
|
||||
|
||||
class EmbeddedReportConceptView(ReportConceptView):
|
||||
|
||||
@Lazy
|
||||
def macro(self):
|
||||
return self.report_macros['embedded_report']
|
||||
|
||||
@Lazy
|
||||
def title(self):
|
||||
return self.report.title
|
||||
|
||||
|
||||
class ReportParamsView(ReportConceptView):
|
||||
""" Report view allowing to enter parameters before executing the report.
|
||||
"""
|
||||
|
|
|
@ -25,16 +25,49 @@
|
|||
</div>
|
||||
|
||||
|
||||
<div metal:define-macro="results">
|
||||
<div metal:define-macro="embedded_content"
|
||||
tal:define="report item/reportInstance;
|
||||
reportView nocall:item">
|
||||
<div tal:attributes="class string:content-$level;">
|
||||
<metal:block use-macro="view/concept_macros/concepttitle_only" />
|
||||
</div>
|
||||
<div metal:use-macro="item/resultsRenderer" />
|
||||
</div>
|
||||
|
||||
|
||||
<div metal:define-macro="results"
|
||||
tal:define="tableName string:results">
|
||||
<br />
|
||||
<tal:download condition="nothing">
|
||||
<div class="button">
|
||||
<a i18n:translate=""
|
||||
tal:define="dl string:${item/downloadLink}${item/urlParamString};
|
||||
params python:item.getSortParams(tableName)"
|
||||
tal:attributes="href dl">Download Data</a>
|
||||
</div>
|
||||
<br />
|
||||
</tal:download>
|
||||
<table class="report"
|
||||
tal:define="results reportView/results">
|
||||
<tr>
|
||||
<th tal:repeat="col results/displayedColumns"
|
||||
tal:content="col/title"
|
||||
<th style="white-space: nowrap"
|
||||
tal:repeat="col results/displayedColumns">
|
||||
<a title="tooltip_sort_column"
|
||||
tal:define="colName col/name"
|
||||
tal:omit-tag="python:not item.isSortableColumn(tableName, colName)"
|
||||
tal:attributes="href python:item.getSortUrl(tableName, colName)"
|
||||
i18n:attributes="title">
|
||||
<span tal:content="col/title"
|
||||
tal:attributes="class col/cssClass"
|
||||
i18n:translate="" />
|
||||
<img tal:define="src python:item.getSortImage(tableName, colName)"
|
||||
tal:condition="src"
|
||||
tal:attributes="src src" />
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
<tr tal:repeat="row results">
|
||||
<tr tal:repeat="row results"
|
||||
tal:attributes="class python:(repeat['row'].index() % 2) and 'even' or 'odd'">
|
||||
<td tal:repeat="col results/displayedColumns"
|
||||
tal:attributes="class col/cssClass">
|
||||
<metal:column use-macro="python:
|
||||
|
@ -78,6 +111,17 @@
|
|||
</metal:state>
|
||||
|
||||
|
||||
<metal:state define-macro="workitem_state">
|
||||
<tal:column define="value python:col.getDisplayValue(row)"
|
||||
condition="value">
|
||||
<tal:action repeat="action value/actions">
|
||||
<metal:action tal:condition="action"
|
||||
use-macro="action/macro" />
|
||||
</tal:action>
|
||||
</tal:column>
|
||||
</metal:state>
|
||||
|
||||
|
||||
<metal:target define-macro="target">
|
||||
<tal:column define="value python:col.getDisplayValue(row)">
|
||||
<a tal:omit-tag="python:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -93,7 +93,8 @@ class Search(ConceptView):
|
|||
|
||||
@Lazy
|
||||
def showActions(self):
|
||||
return checkPermission('loops.ManageSite', self.context)
|
||||
perm = (self.globalOptions('delete_permission') or ['loops.ManageSite'])[0]
|
||||
return checkPermission(perm, self.context)
|
||||
#return canWriteObject(self.context)
|
||||
|
||||
@property
|
||||
|
@ -168,7 +169,7 @@ class Search(ConceptView):
|
|||
title = request.get('name')
|
||||
if title == '*':
|
||||
title = None
|
||||
types = request.get('searchType')
|
||||
#types = request.get('searchType')
|
||||
data = []
|
||||
types = self.getTypes()
|
||||
if title or types:
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
<!-- $Id$ -->
|
||||
|
||||
<configure
|
||||
xmlns="http://namespaces.zope.org/zope"
|
||||
xmlns:browser="http://namespaces.zope.org/browser"
|
||||
|
|
126
expert/field.py
126
expert/field.py
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -22,6 +22,7 @@ Field definitions for reports.
|
|||
|
||||
from zope.app.form.browser.interfaces import ITerms
|
||||
from zope import component
|
||||
from zope.i18n import translate
|
||||
from zope.i18n.locales import locales
|
||||
from zope.schema.interfaces import IVocabularyFactory, IContextSourceBinder
|
||||
|
||||
|
@ -29,18 +30,32 @@ from cybertools.composer.report.field import Field as BaseField
|
|||
from cybertools.composer.report.field import TableCellStyle
|
||||
from cybertools.composer.report.result import ResultSet
|
||||
from cybertools.stateful.interfaces import IStateful, IStatesDefinition
|
||||
from cybertools.util.date import timeStamp2Date
|
||||
from cybertools.util.date import timeStamp2Date, timeStamp2ISO
|
||||
from cybertools.util.format import formatDate
|
||||
from loops.common import baseObject
|
||||
from loops.expert.report import ReportInstance
|
||||
from loops.organize.work.browser import WorkItemDetails
|
||||
from loops import util
|
||||
|
||||
|
||||
class Field(BaseField):
|
||||
|
||||
def getContext(self, row):
|
||||
return row.context
|
||||
|
||||
def getSelectValue(self, row):
|
||||
return self.getValue(row)
|
||||
|
||||
|
||||
class StringField(Field):
|
||||
|
||||
def getSelectValue(self, row):
|
||||
return self.getValue(row).strip()
|
||||
|
||||
def getSortValue(self, row):
|
||||
return self.getValue(row).strip()
|
||||
|
||||
|
||||
class TextField(Field):
|
||||
|
||||
format = 'text/restructured'
|
||||
|
@ -104,11 +119,16 @@ class IntegerField(Field):
|
|||
|
||||
class DateField(Field):
|
||||
|
||||
fieldType='date',
|
||||
fieldType='date'
|
||||
format = ('date', 'short')
|
||||
renderer = cssClass = 'center'
|
||||
dbtype = 'date'
|
||||
|
||||
def getValue(self, row):
|
||||
if getattr(row.parent.context.view, 'reportMode', None) == 'export':
|
||||
return self.getDisplayValue(row)
|
||||
super(DateField, self).getValue(row)
|
||||
|
||||
def getDisplayValue(self, row):
|
||||
value = self.getRawValue(row)
|
||||
if not value:
|
||||
|
@ -127,16 +147,17 @@ class DateField(Field):
|
|||
|
||||
class StateField(Field):
|
||||
|
||||
statesDefinition = 'workItemStates'
|
||||
statesDefinition = None
|
||||
renderer = 'state'
|
||||
|
||||
def getDisplayValue(self, row):
|
||||
if IStateful.providedBy(row.context):
|
||||
stf = row.context
|
||||
elif row.context is None:
|
||||
context = self.getContext(row)
|
||||
if IStateful.providedBy(context):
|
||||
stf = context
|
||||
elif context is None:
|
||||
return None
|
||||
else:
|
||||
stf = component.getAdapter(baseObject(row.context), IStateful,
|
||||
stf = component.getAdapter(context, IStateful,
|
||||
name=self.statesDefinition)
|
||||
stateObject = stf.getStateObject()
|
||||
icon = stateObject.icon or 'led%s.png' % stateObject.color
|
||||
|
@ -147,9 +168,32 @@ class StateField(Field):
|
|||
return util._(text)
|
||||
|
||||
|
||||
class WorkItemStateField(Field):
|
||||
|
||||
statesDefinition = 'workItemStates'
|
||||
renderer = 'workitem_state'
|
||||
|
||||
def getValue(self, row):
|
||||
view = row.parent.context.view
|
||||
if getattr(view, 'reportMode', None) == 'export':
|
||||
stateObject = row.context.getStateObject()
|
||||
lang = view.languageInfo.language
|
||||
return translate(util._(stateObject.title), target_language=lang)
|
||||
return super(WorkItemStateField, self).getValue(row)
|
||||
|
||||
|
||||
def getDisplayValue(self, row):
|
||||
if row.context is None:
|
||||
return None
|
||||
details = WorkItemDetails(row.parent.context.view, row.context)
|
||||
return dict(actions=details.actions())
|
||||
|
||||
|
||||
class VocabularyField(Field):
|
||||
|
||||
vocabulary = None
|
||||
sourceList = None
|
||||
fieldType = 'selection'
|
||||
|
||||
def getDisplayValue(self, row):
|
||||
value = self.getRawValue(row)
|
||||
|
@ -160,8 +204,10 @@ class VocabularyField(Field):
|
|||
if str(item['token']) == str(value):
|
||||
return item['title']
|
||||
|
||||
def getVocabularyItems(self, row):
|
||||
def getVocabularyItems(self, row=None, context=None, request=None):
|
||||
if context is None:
|
||||
context = row.context
|
||||
if request is None:
|
||||
request = row.parent.context.view.request
|
||||
voc = self.vocabulary
|
||||
if isinstance(voc, basestring):
|
||||
|
@ -171,7 +217,10 @@ class VocabularyField(Field):
|
|||
voc = voc.splitlines()
|
||||
return [dict(token=t, title=t) for t in voc if t.strip()]
|
||||
elif IContextSourceBinder.providedBy(voc):
|
||||
if row is not None:
|
||||
source = voc(row.parent.context)
|
||||
else:
|
||||
source = voc(context)
|
||||
terms = component.queryMultiAdapter((source, request), ITerms)
|
||||
if terms is not None:
|
||||
termsList = [terms.getTerm(value) for value in source]
|
||||
|
@ -233,6 +282,14 @@ class RelationField(Field):
|
|||
|
||||
class TargetField(RelationField):
|
||||
|
||||
def getSortValue(self, row):
|
||||
value = self.getRawValue(row)
|
||||
if value is not None:
|
||||
value = util.getObjectForUid(value)
|
||||
if value is not None:
|
||||
if value.title is not None:
|
||||
return value.title.split()
|
||||
|
||||
def getValue(self, row):
|
||||
value = self.getRawValue(row)
|
||||
if value is None:
|
||||
|
@ -247,6 +304,57 @@ class MultiLineField(Field):
|
|||
def getValue(self, row):
|
||||
return self.getRawValue(row)
|
||||
|
||||
|
||||
# track fields
|
||||
|
||||
class TrackDateField(Field):
|
||||
|
||||
fieldType = 'date'
|
||||
part = 'date'
|
||||
format = 'short'
|
||||
descending = False
|
||||
cssClass = 'right'
|
||||
|
||||
def getValue(self, row):
|
||||
reportMode = getattr(row.parent.context.view, 'reportMode', None)
|
||||
if reportMode == 'export':
|
||||
return self.getDisplayValue(row)
|
||||
value = self.getRawValue(row)
|
||||
if not value:
|
||||
return None
|
||||
return timeStamp2Date(value)
|
||||
|
||||
def getDisplayValue(self, row):
|
||||
value = self.getRawValue(row)
|
||||
if value:
|
||||
value = timeStamp2Date(value)
|
||||
view = row.parent.context.view
|
||||
return formatDate(value, self.part, self.format,
|
||||
view.languageInfo.language)
|
||||
return u''
|
||||
|
||||
def getSelectValue(self, row):
|
||||
value = self.getRawValue(row)
|
||||
if not value:
|
||||
return ''
|
||||
return timeStamp2ISO(value)[:10]
|
||||
|
||||
def getSortValue(self, row):
|
||||
value = self.getRawValue(row)
|
||||
if value and self.descending:
|
||||
return -value
|
||||
return value or None
|
||||
|
||||
|
||||
class TrackDateTimeField(TrackDateField):
|
||||
|
||||
part = 'dateTime'
|
||||
|
||||
|
||||
class TrackTimeField(TrackDateField):
|
||||
|
||||
part = 'time'
|
||||
|
||||
def getDisplayValues(self, row):
|
||||
value = self.getValue(row)
|
||||
if not isinstance(value, (list, tuple)):
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2017 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -35,6 +35,7 @@ from cybertools.composer.report.interfaces import IReportParams
|
|||
from cybertools.composer.report.result import ResultSet, Row
|
||||
from cybertools.util.jeep import Jeep
|
||||
from loops.common import AdapterBase
|
||||
from loops.expert.concept import IQueryConcept, QueryConcept
|
||||
from loops.interfaces import ILoopsAdapter
|
||||
from loops.type import TypeInterfaceSourceList
|
||||
from loops import util
|
||||
|
@ -43,7 +44,7 @@ from loops.util import _
|
|||
|
||||
# interfaces
|
||||
|
||||
class IReport(ILoopsAdapter, IReportParams):
|
||||
class IReport(ILoopsAdapter, IReportParams, IQueryConcept):
|
||||
""" The report adapter for the persistent object (concept) that stores
|
||||
the report in the concept map.
|
||||
"""
|
||||
|
@ -66,7 +67,7 @@ class IReportInstance(IBaseReport):
|
|||
|
||||
# report concept adapter and instances
|
||||
|
||||
class Report(AdapterBase):
|
||||
class Report(QueryConcept):
|
||||
|
||||
implements(IReport)
|
||||
|
||||
|
@ -88,6 +89,7 @@ class ReportInstance(BaseReport):
|
|||
#headerRowFactory = Row
|
||||
|
||||
view = None # set upon creation
|
||||
#headerRowFactory = Row
|
||||
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
|
@ -120,7 +122,9 @@ class ReportInstance(BaseReport):
|
|||
result = list(self.selectObjects(parts)) # may modify parts
|
||||
qc = CompoundQueryCriteria(parts)
|
||||
return ResultSet(self, result, rowFactory=self.rowFactory,
|
||||
sortCriteria=self.getSortCriteria(), queryCriteria=qc,
|
||||
sortCriteria=self.getSortCriteria(),
|
||||
sortDescending=self.sortDescending,
|
||||
queryCriteria=qc,
|
||||
limits=limits)
|
||||
|
||||
def selectObjects(self, parts):
|
||||
|
@ -173,3 +177,15 @@ class DefaultConceptReportInstance(ReportInstance):
|
|||
|
||||
label = u'Default Concept Report'
|
||||
|
||||
|
||||
# specialized rows
|
||||
|
||||
class TrackRow(Row):
|
||||
|
||||
@staticmethod
|
||||
def getContextAttr(obj, attr):
|
||||
if attr in obj.context.metadata_attributes:
|
||||
return getattr(obj.context, attr)
|
||||
return obj.context.data.get(attr)
|
||||
|
||||
|
||||
|
|
|
@ -66,13 +66,13 @@ zcml in real life:
|
|||
|
||||
>>> t = searchView.typesForSearch()
|
||||
>>> len(t)
|
||||
15
|
||||
16
|
||||
>>> t.getTermByToken('loops:resource:*').title
|
||||
'Any Resource'
|
||||
|
||||
>>> t = searchView.conceptTypesForSearch()
|
||||
>>> len(t)
|
||||
12
|
||||
13
|
||||
>>> t.getTermByToken('loops:concept:*').title
|
||||
'Any Concept'
|
||||
|
||||
|
@ -91,7 +91,7 @@ a controller attribute for the search view.
|
|||
|
||||
>>> searchView.submitReplacing('1.results', '1.search.form', pageView)
|
||||
'submitReplacing("1.results", "1.search.form",
|
||||
"http://127.0.0.1/loops/views/page/.target96/@@searchresults.html");...'
|
||||
"http://127.0.0.1/loops/views/page/.target.../@@searchresults.html");...'
|
||||
|
||||
Basic (text/title) search
|
||||
-------------------------
|
||||
|
@ -177,7 +177,7 @@ of the concepts' titles:
|
|||
>>> request = TestRequest(form=form)
|
||||
>>> view = Search(page, request)
|
||||
>>> view.listConcepts()
|
||||
'{"items": [{"id": "101", "name": "Zope", "label": "Zope (Thema)"}, {"id": "103", "name": "Zope 2", "label": "Zope 2 (Thema)"}, {"id": "105", "name": "Zope 3", "label": "Zope 3 (Thema)"}], "identifier": "id"}'
|
||||
'{"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
|
||||
------------------------------------
|
||||
|
@ -219,13 +219,13 @@ and thus include the customer type in the preset search types.
|
|||
|
||||
>>> searchView.conceptsForType('loops:concept:customer')
|
||||
[{'token': 'none', 'title': u'not selected'},
|
||||
{'token': '74', 'title': u'Customer 1'},
|
||||
{'token': '76', 'title': u'Customer 2'},
|
||||
{'token': '78', 'title': u'Customer 3'}]
|
||||
{'token': '...', 'title': u'Customer 1'},
|
||||
{'token': '...', 'title': u'Customer 2'},
|
||||
{'token': '...', 'title': u'Customer 3'}]
|
||||
|
||||
Let's use this new search option for querying:
|
||||
|
||||
>>> form = {'search.4.text_selected': u'74'}
|
||||
>>> form = {'search.4.text_selected': u'75'}
|
||||
>>> resultsView = SearchResults(page, TestRequest(form=form))
|
||||
>>> results = list(resultsView.results)
|
||||
>>> results[0].title
|
||||
|
|
6
external/README.txt
vendored
6
external/README.txt
vendored
|
@ -17,7 +17,7 @@ Let's set up a loops site with basic and example concepts and resources.
|
|||
>>> concepts, resources, views = t.setup()
|
||||
>>> loopsRoot = site['loops']
|
||||
>>> len(concepts), len(resources), len(views)
|
||||
(33, 3, 1)
|
||||
(35, 3, 1)
|
||||
|
||||
|
||||
Importing loops Objects
|
||||
|
@ -44,7 +44,7 @@ Creating the corresponding objects
|
|||
>>> loader = Loader(loopsRoot)
|
||||
>>> loader.load(elements)
|
||||
>>> len(concepts), len(resources), len(views)
|
||||
(34, 3, 1)
|
||||
(36, 3, 1)
|
||||
|
||||
>>> from loops.common import adapted
|
||||
>>> adMyquery = adapted(concepts['myquery'])
|
||||
|
@ -131,7 +131,7 @@ Extracting elements
|
|||
>>> extractor = Extractor(loopsRoot, os.path.join(dataDirectory, 'export'))
|
||||
>>> elements = list(extractor.extract())
|
||||
>>> len(elements)
|
||||
66
|
||||
69
|
||||
|
||||
Writing object information to the external storage
|
||||
--------------------------------------------------
|
||||
|
|
6
external/pyfunc.py
vendored
6
external/pyfunc.py
vendored
|
@ -44,11 +44,15 @@ class PyReader(object):
|
|||
|
||||
class InputProcessor(dict):
|
||||
|
||||
_constants = dict(True=True, False=False)
|
||||
|
||||
def __init__(self):
|
||||
self.elements = []
|
||||
self['__builtins__'] = {} # security!
|
||||
self['__builtins__'] = dict() # security!
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key in self._constants:
|
||||
return self._constants[key]
|
||||
def factory(*args, **kw):
|
||||
element = elementTypes[key](*args, **kw)
|
||||
if key in toplevelElements:
|
||||
|
|
|
@ -98,8 +98,9 @@ class I18NView(object):
|
|||
return adapted(self.context, self.languageInfo)
|
||||
|
||||
def checkLanguage(self):
|
||||
session = ISession(self.request)[packageId]
|
||||
lang = session.get('language') or self.languageInfo.language
|
||||
#session = ISession(self.request)[packageId]
|
||||
#lang = session.get('language') or self.languageInfo.language
|
||||
lang = self.languageInfo.language
|
||||
if lang:
|
||||
self.setLanguage(lang)
|
||||
|
||||
|
|
|
@ -44,5 +44,7 @@ class ExternalCollectionView(ConceptView):
|
|||
cta.update()
|
||||
if cta.updateMessage is not None:
|
||||
self.request.form['message'] = cta.updateMessage
|
||||
if 'no_show_page' in self.request.form:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
|
|
@ -24,8 +24,10 @@ file system.
|
|||
from datetime import datetime
|
||||
from logging import getLogger
|
||||
import os, re, stat
|
||||
import transaction
|
||||
|
||||
from zope.app.container.interfaces import INameChooser
|
||||
from zope.app.container.contained import ObjectRemovedEvent
|
||||
from zope.cachedescriptors.property import Lazy
|
||||
from zope import component
|
||||
from zope.component import adapts
|
||||
|
@ -51,6 +53,8 @@ from loops.versioning.interfaces import IVersionable
|
|||
|
||||
TypeInterfaceSourceList.typeInterfaces += (IExternalCollection,)
|
||||
|
||||
logger = getLogger('loops.integrator.collection')
|
||||
|
||||
|
||||
class ExternalCollectionAdapter(AdapterBase):
|
||||
""" A concept adapter for accessing an external collection.
|
||||
|
@ -83,10 +87,11 @@ class ExternalCollectionAdapter(AdapterBase):
|
|||
print '###', vaddr, vobj, vid
|
||||
versions.add(vaddr)
|
||||
new = []
|
||||
oldFound = []
|
||||
oldFound = set([])
|
||||
provider = component.getUtility(IExternalCollectionProvider,
|
||||
name=self.providerName or '')
|
||||
#print '*** old', old, versions, self.lastUpdated
|
||||
changeCount = 0
|
||||
for addr, mdate in provider.collect(self):
|
||||
#print '***', addr, mdate
|
||||
if addr in versions:
|
||||
|
@ -94,8 +99,9 @@ class ExternalCollectionAdapter(AdapterBase):
|
|||
if addr in old:
|
||||
# may be it would be better to return a file's hash
|
||||
# for checking for changes...
|
||||
oldFound.append(addr)
|
||||
oldFound.add(addr)
|
||||
if self.lastUpdated is None or (mdate and mdate > self.lastUpdated):
|
||||
changeCount +=1
|
||||
obj = old[addr]
|
||||
# update settings and regenerate scale variant for media asset
|
||||
adobj = adapted(obj)
|
||||
|
@ -110,29 +116,41 @@ class ExternalCollectionAdapter(AdapterBase):
|
|||
self.updateMessage = message
|
||||
# force reindexing
|
||||
notify(ObjectModifiedEvent(obj))
|
||||
if changeCount % 10 == 0:
|
||||
logger.info('Updated: %i.' % changeCount)
|
||||
transaction.commit()
|
||||
else:
|
||||
new.append(addr)
|
||||
logger.info('%i objects updated.' % changeCount)
|
||||
transaction.commit()
|
||||
if new:
|
||||
self.newResources = provider.createExtFileObjects(self, new)
|
||||
for r in self.newResources:
|
||||
self.context.assignResource(r)
|
||||
logger.info('%i objects created.' % len(new))
|
||||
transaction.commit()
|
||||
for addr in old:
|
||||
if str(addr) not in oldFound:
|
||||
# not part of the collection any more
|
||||
# TODO: only remove from collection but keep object?
|
||||
self.remove(old[addr])
|
||||
transaction.commit()
|
||||
for r in self.context.getResources():
|
||||
adobj = adapted(r)
|
||||
if self.metaInfo != adobj.metaInfo and (
|
||||
not adobj.metaInfo or self.overwriteMetaInfo):
|
||||
adobj.metaInfo = self.metaInfo
|
||||
self.lastUpdated = datetime.today()
|
||||
logger.info('External collection updated.')
|
||||
transaction.commit()
|
||||
|
||||
def clear(self):
|
||||
for obj in self.context.getResources():
|
||||
self.remove(obj)
|
||||
|
||||
def remove(self, obj):
|
||||
logger.info('Removing object: %s.' % getName(obj))
|
||||
notify(ObjectRemovedEvent(obj))
|
||||
del self.resourceManager[getName(obj)]
|
||||
|
||||
@Lazy
|
||||
|
@ -187,7 +205,7 @@ class DirectoryCollectionProvider(object):
|
|||
for k, v in self.extFileTypeMapping.items())
|
||||
container = client.context.getLoopsRoot().getResourceManager()
|
||||
directory = self.getDirectory(client)
|
||||
for addr in addresses:
|
||||
for idx, addr in enumerate(addresses):
|
||||
name = self.generateName(container, addr)
|
||||
title = self.generateTitle(addr)
|
||||
contentType = guess_content_type(addr,
|
||||
|
@ -200,8 +218,7 @@ class DirectoryCollectionProvider(object):
|
|||
if extFileType is None:
|
||||
extFileType = extFileTypes['image/*']
|
||||
if extFileType is None:
|
||||
getLogger('loops.integrator.collection.DirectoryCollectionProvider'
|
||||
).warn('No external file type found for %r, '
|
||||
logger.warn('No external file type found for %r, '
|
||||
'content type: %r' % (name, contentType))
|
||||
obj = addAndConfigureObject(
|
||||
container, Resource, name,
|
||||
|
@ -219,6 +236,9 @@ class DirectoryCollectionProvider(object):
|
|||
message = client.updateMessage or u''
|
||||
message += u'<br />'.join(adobj.processingErrors)
|
||||
client.updateMessage = message
|
||||
if idx and idx % 10 == 0:
|
||||
logger.info('Created: %i.' % idx)
|
||||
transaction.commit()
|
||||
yield obj
|
||||
|
||||
def getDirectory(self, client):
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
|
||||
<metal:block define-macro="render_collection"
|
||||
tal:define="dummy item/update">
|
||||
tal:condition="item/update">
|
||||
|
||||
<metal:block use-macro="view/concept_macros/conceptdata">
|
||||
<metal:fill tal:condition="item/editable"
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
import unittest, doctest
|
||||
from zope.interface.verify import verifyClass
|
||||
#from loops.versioning import versionable
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
"Basic tests for the loops.integrator.content package."
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2011 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -18,8 +18,6 @@
|
|||
|
||||
"""
|
||||
Integrator interfaces.
|
||||
|
||||
$Id$
|
||||
"""
|
||||
|
||||
from zope.interface import Interface, Attribute
|
||||
|
@ -133,3 +131,5 @@ class IOfficeFile(IExternalFile):
|
|||
It provides access to the document content and properties.
|
||||
"""
|
||||
|
||||
documentPropertiesAccessible = Attribute(
|
||||
'Are document properties accessible?')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -26,7 +26,7 @@ from lxml import etree
|
|||
import os
|
||||
import shutil
|
||||
from time import strptime
|
||||
from zipfile import ZipFile
|
||||
from zipfile import ZipFile, BadZipfile
|
||||
from zope.cachedescriptors.property import Lazy
|
||||
from zope import component
|
||||
from zope.component import adapts
|
||||
|
@ -52,12 +52,22 @@ class OfficeFile(ExternalFileAdapter):
|
|||
|
||||
implements(IOfficeFile)
|
||||
|
||||
_adapterAttributes = (ExternalFileAdapter._adapterAttributes +
|
||||
('documentPropertiesAccessible',))
|
||||
|
||||
propertyMap = {u'Revision:': 'version'}
|
||||
propFileName = 'docProps/custom.xml'
|
||||
corePropFileName = 'docProps/core.xml'
|
||||
fileExtensions = ('.docm', '.docx', 'dotm', 'dotx', 'pptx', 'potx', 'ppsx',
|
||||
'.xlsm', '.xlsx', '.xltm', '.xltx')
|
||||
|
||||
def getDocumentPropertiesAccessible(self):
|
||||
return getattr(self.context, '_documentPropertiesAccessible', True)
|
||||
def setDocumentPropertiesAccessible(self, value):
|
||||
self.context._documentPropertiesAccessible = value
|
||||
documentPropertiesAccessible = property(
|
||||
getDocumentPropertiesAccessible, setDocumentPropertiesAccessible)
|
||||
|
||||
@Lazy
|
||||
def logger(self):
|
||||
return getLogger('loops.integrator.office.base.OfficeFile')
|
||||
|
@ -79,14 +89,19 @@ class OfficeFile(ExternalFileAdapter):
|
|||
def docPropertyDom(self):
|
||||
fn = self.docFilename
|
||||
result = dict(core=[], custom=[])
|
||||
if not os.path.exists(fn):
|
||||
# may happen before file has been created
|
||||
return result
|
||||
root, ext = os.path.splitext(fn)
|
||||
if not ext.lower() in self.fileExtensions:
|
||||
return result
|
||||
try:
|
||||
zf = ZipFile(fn, 'r')
|
||||
except IOError, e:
|
||||
self.documentPropertiesAccessible = True
|
||||
except (IOError, BadZipfile), e:
|
||||
from logging import getLogger
|
||||
self.logger.warn(e)
|
||||
self.documentPropertiesAccessible = False
|
||||
return result
|
||||
if self.corePropFileName not in zf.namelist():
|
||||
self.logger.warn('Core properties not found in file %s.' %
|
||||
|
@ -123,6 +138,8 @@ class OfficeFile(ExternalFileAdapter):
|
|||
attributes = {}
|
||||
# get dc:description from core.xml
|
||||
desc = self.getCoreProperty('description')
|
||||
if not self.documentPropertiesAccessible:
|
||||
return
|
||||
if desc is not None:
|
||||
attributes['comments'] = desc
|
||||
dom = self.docPropertyDom['custom']
|
||||
|
|
|
@ -39,6 +39,7 @@ class ExternalSourceInfo(object):
|
|||
adapts(ILoopsObject)
|
||||
|
||||
def __init__(self, context):
|
||||
#import pdb; pdb.set_trace()
|
||||
self.context = self.__parent__ = context
|
||||
|
||||
def getSourceInfo(self):
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
import unittest, doctest
|
||||
from zope.interface.verify import verifyClass
|
||||
#from loops.versioning import versionable
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
"Basic tests for the integrator sub-package."
|
||||
|
|
|
@ -402,7 +402,7 @@ class IDocumentSchema(IResourceSchema):
|
|||
contentType = schema.Choice(
|
||||
title=_(u'Content Type'),
|
||||
description=_(u'Content type (format) of the data field'),
|
||||
values=('text/restructured', 'text/structured', 'text/html',
|
||||
values=('text/markdown', 'text/restructured', 'text/structured', 'text/html',
|
||||
'text/plain', 'text/xml', 'text/css'),
|
||||
default='text/restructured',
|
||||
required=True)
|
||||
|
@ -968,5 +968,3 @@ class IViewConfiguratorSchema(Interface):
|
|||
value_type=schema.TextLine(),
|
||||
default=[],
|
||||
required=False)
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -30,8 +30,13 @@ from cybertools.typology.interfaces import IType
|
|||
from loops.browser.action import DialogAction
|
||||
from loops.browser.common import BaseView
|
||||
from loops.browser.concept import ConceptView
|
||||
from loops.common import adapted
|
||||
from loops.knowledge.interfaces import IPerson, ITask
|
||||
from loops.organize.party import getPersonForUser
|
||||
from loops.organize.personal import favorite
|
||||
from loops.organize.personal.interfaces import IFavorites
|
||||
from loops.security.common import checkPermission
|
||||
from loops import util
|
||||
from loops.util import _
|
||||
|
||||
|
||||
|
@ -69,6 +74,63 @@ actions.register('createQualification', 'portlet', DialogAction,
|
|||
)
|
||||
|
||||
|
||||
class InstitutionMixin(object):
|
||||
|
||||
knowledge_macros = knowledge_macros
|
||||
|
||||
adminMaySelectAllInstitutions = True
|
||||
|
||||
@Lazy
|
||||
def institutionType(self):
|
||||
return self.conceptManager['institution']
|
||||
|
||||
@Lazy
|
||||
def institutions(self):
|
||||
if self.adminMaySelectAllInstitutions:
|
||||
if checkPermission('loops.ManageWorkspaces', self.context):
|
||||
return self.getAllInstitutions()
|
||||
result = []
|
||||
p = getPersonForUser(self.context, self.request)
|
||||
if p is None:
|
||||
return result
|
||||
for parent in p.getParents(
|
||||
[self.memberPredicate, self.masterPredicate]):
|
||||
if parent.conceptType == self.institutionType:
|
||||
result.append(dict(
|
||||
object=adapted(parent),
|
||||
title=parent.title,
|
||||
uid=util.getUidForObject(parent)))
|
||||
return result
|
||||
|
||||
def getAllInstitutions(self):
|
||||
insts = self.institutionType.getChildren([self.typePredicate])
|
||||
return [dict(object=adapted(inst),
|
||||
title=inst.title,
|
||||
uid=util.getUidForObject(inst)) for inst in insts]
|
||||
|
||||
def setInstitution(self, uid):
|
||||
inst = util.getObjectForUid(uid)
|
||||
person = getPersonForUser(self.context, self.request)
|
||||
favorite.setInstitution(person, inst)
|
||||
self.institution = inst
|
||||
return True
|
||||
|
||||
def getSavedInstitution(self):
|
||||
person = getPersonForUser(self.context, self.request)
|
||||
favorites = IFavorites(self.loopsRoot.getRecordManager()['favorites'])
|
||||
for inst in favorites.list(person, type='institution'):
|
||||
return adapted(util.getObjectForUid(inst))
|
||||
|
||||
@Lazy
|
||||
def institution(self):
|
||||
saved = self.getSavedInstitution()
|
||||
for inst in self.institutions:
|
||||
if inst['object'] == saved:
|
||||
return inst['object']
|
||||
if self.institutions:
|
||||
return self.institutions[0]['object']
|
||||
|
||||
|
||||
class MyKnowledge(ConceptView):
|
||||
|
||||
template = template
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
type(u'competence', u'Kompetenz', viewName=u'',
|
||||
type(u'competence', u'Qualifikation', viewName=u'',
|
||||
typeInterface=u'loops.knowledge.qualification.interfaces.ICompetence',
|
||||
options=u'action.portlet:create_subtype,edit_concept')
|
||||
type(u'person', u'Person', viewName=u'',
|
||||
typeInterface=u'loops.knowledge.interfaces.IPerson',
|
||||
options=u'action.portlet:createQualification,editPerson')
|
||||
type(u'report', u'Report', viewName=u'',
|
||||
typeInterface='loops.expert.report.IReport')
|
||||
type(u'task', u'Aufgabe', viewName=u'',
|
||||
typeInterface=u'loops.knowledge.interfaces.ITask',
|
||||
options=u'action.portlet:createTask,editTask')
|
||||
|
@ -26,6 +28,10 @@ concept(u'requires', u'requires', u'predicate')
|
|||
concept(u'issubtype', u'is Subtype', u'predicate', options=u'hide_children',
|
||||
predicateInterface='loops.interfaces.IIsSubtype')
|
||||
|
||||
# reports
|
||||
concept(u'qualification_overview', u'Qualification Overview', u'report',
|
||||
reportType=u'qualification_overview')
|
||||
|
||||
# structure
|
||||
child(u'general', u'competence', u'standard')
|
||||
child(u'general', u'depends', u'standard')
|
||||
|
@ -38,6 +44,7 @@ child(u'general', u'topic', u'standard')
|
|||
#child(u'general', u'training', u'standard')
|
||||
|
||||
child(u'system', u'issubtype', u'standard')
|
||||
child(u'system', u'report', u'standard')
|
||||
|
||||
child(u'competence', u'competence', u'issubtype')
|
||||
#child(u'competence', u'training', u'issubtype', usePredicate=u'provides')
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
type(u'competence', u'Kompetenz', viewName=u'',
|
||||
type(u'competence', u'Qualifikation', viewName=u'',
|
||||
typeInterface=u'loops.knowledge.qualification.interfaces.ICompetence',
|
||||
options=u'action.portlet:create_subtype,edit_concept')
|
||||
type(u'ipskill', u'Kompetenz', viewName=u'',
|
||||
options=u'action.portlet:edit_concept')
|
||||
type(u'ipskillsrequired', u'Soll-Profil', viewName=u'',
|
||||
options=u'action.portlet:edit_concept')
|
||||
type(u'jobposition', u'Stelle', viewName=u'',
|
||||
options=u'action.portlet:edit_concept')
|
||||
type(u'report', u'Report', viewName=u'',
|
||||
typeInterface='loops.expert.report.IReport')
|
||||
# type(u'person', u'Person', viewName=u'',
|
||||
# typeInterface=u'loops.knowledge.interfaces.IPerson',
|
||||
# options=u'action.portlet:editPerson')
|
||||
|
@ -26,9 +34,16 @@ concept(u'requires', u'requires', u'predicate')
|
|||
concept(u'issubtype', u'is Subtype', u'predicate', options=u'hide_children',
|
||||
predicateInterface='loops.interfaces.IIsSubtype')
|
||||
|
||||
# reports
|
||||
concept(u'qualification_overview', u'Qualification Overview', u'report',
|
||||
reportType=u'qualification_overview')
|
||||
|
||||
# structure
|
||||
child(u'general', u'competence', u'standard')
|
||||
child(u'general', u'depends', u'standard')
|
||||
child(u'general', u'ipskill', u'standard')
|
||||
child(u'general', u'ipskillsrequired', u'standard')
|
||||
child(u'general', u'jobposition', u'standard')
|
||||
child(u'general', u'knows', u'standard')
|
||||
#child(u'general', u'person', u'standard')
|
||||
child(u'general', u'provides', u'standard')
|
||||
|
@ -38,6 +53,7 @@ child(u'general', u'requires', u'standard')
|
|||
#child(u'general', u'training', u'standard')
|
||||
|
||||
child(u'system', u'issubtype', u'standard')
|
||||
child(u'system', u'report', u'standard')
|
||||
|
||||
child(u'competence', u'competence', u'issubtype')
|
||||
#child(u'competence', u'training', u'issubtype', usePredicate=u'provides')
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
<!-- $Id$ -->
|
||||
|
||||
<configure
|
||||
xmlns:zope="http://namespaces.zope.org/zope"
|
||||
xmlns="http://namespaces.zope.org/browser"
|
||||
|
@ -28,14 +26,14 @@
|
|||
name="create_glossaryitem.html"
|
||||
for="loops.interfaces.INode"
|
||||
class="loops.knowledge.glossary.browser.CreateGlossaryItemForm"
|
||||
permission="zope.ManageContent"
|
||||
permission="zope.View"
|
||||
/>
|
||||
|
||||
<page
|
||||
name="edit_glossaryitem.html"
|
||||
for="loops.interfaces.INode"
|
||||
class="loops.knowledge.glossary.browser.EditGlossaryItemForm"
|
||||
permission="zope.ManageContent"
|
||||
permission="zope.View"
|
||||
/>
|
||||
|
||||
<zope:adapter
|
||||
|
@ -43,7 +41,7 @@
|
|||
for="loops.browser.node.NodeView
|
||||
zope.publisher.interfaces.browser.IBrowserRequest"
|
||||
factory="loops.knowledge.glossary.browser.CreateGlossaryItem"
|
||||
permission="zope.ManageContent"
|
||||
permission="zope.View"
|
||||
/>
|
||||
|
||||
<zope:adapter
|
||||
|
@ -51,7 +49,7 @@
|
|||
for="loops.browser.node.NodeView
|
||||
zope.publisher.interfaces.browser.IBrowserRequest"
|
||||
factory="loops.knowledge.glossary.browser.EditGlossaryItem"
|
||||
permission="zope.ManageContent"
|
||||
permission="zope.View"
|
||||
/>
|
||||
|
||||
</configure>
|
||||
|
|
|
@ -4,20 +4,28 @@
|
|||
tal:define="data item/childrenAlphaGroups">
|
||||
<metal:title use-macro="item/conceptMacros/concepttitle" />
|
||||
<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)]"
|
||||
class="navlink">
|
||||
<a href="#"
|
||||
tal:omit-tag="python: letter not in data.keys()"
|
||||
tal:attributes="href string:${request/URL/-1}#$letter"
|
||||
tal:attributes="href string:${view/requestUrl/-1}#$letter"
|
||||
tal:content="letter">A</a>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span tal:repeat="letter python:sorted(data.keys())"
|
||||
class="navlink">
|
||||
<a href="#"
|
||||
tal:attributes="href string:${view/requestUrl/-1}#$letter"
|
||||
tal:content="letter">A</a>
|
||||
</span>
|
||||
</div>
|
||||
<div> </div>
|
||||
<div tal:repeat="letter data/keys">
|
||||
<div tal:repeat="letter python:sorted(data.keys())">
|
||||
<div class="subtitle"><a name="A" href="#top"
|
||||
tal:attributes="name letter;
|
||||
href string:${request/URL/-1}#top"
|
||||
href string:${view/requestUrl/-1}#top"
|
||||
tal:content="letter">A</a>
|
||||
</div>
|
||||
<div tal:repeat="related data/?letter|python:[]">
|
||||
|
|
|
@ -1,6 +1,27 @@
|
|||
<html i18n:domain="loops">
|
||||
|
||||
|
||||
<metal:institution define-macro="select_institution">
|
||||
<form method="post">
|
||||
<div style="font-size: 120%; padding: 10px 0 10px 0">
|
||||
<span i18n:translate="">Organisation/Team</span>:
|
||||
<b tal:content="item/institution/title" />
|
||||
<img tal:condition="python:len(item.institutions) > 1"
|
||||
src="/@@/cybertools.icons/application_edit.png"
|
||||
onclick="dojo.byId('select_institution').style.display = 'inline'" />
|
||||
<select name="select_institution" id="select_institution"
|
||||
style="display: none"
|
||||
onchange="submit()">
|
||||
<option tal:repeat="inst item/institutions"
|
||||
tal:content="inst/title"
|
||||
tal:attributes="value inst/uid;
|
||||
selected python:inst['object'] == item.institution" />
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</metal:institution>
|
||||
|
||||
|
||||
<metal:providers define-macro="requirement_providers">
|
||||
<metal:block use-macro="view/concept_macros/conceptdata" />
|
||||
<div>
|
||||
|
@ -32,6 +53,9 @@
|
|||
|
||||
<metal:candidates define-macro="requirement_candidates">
|
||||
<metal:block use-macro="view/concept_macros/conceptdata" />
|
||||
<div class="candidates"
|
||||
tal:define="candidates item/adapted/getCandidates"
|
||||
tal:condition="candidates">
|
||||
<h3 i18n:translate="">Candidates for Task</h3>
|
||||
<table class="listing">
|
||||
<tr>
|
||||
|
@ -54,6 +78,7 @@
|
|||
</tal:knowledge></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</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
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -18,19 +18,25 @@
|
|||
|
||||
"""
|
||||
Definition of view classes and other browser related stuff for the
|
||||
loops.knowledge package.
|
||||
loops.knowledge.qualification package.
|
||||
"""
|
||||
|
||||
from zope import interface, component
|
||||
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||
from zope.cachedescriptors.property import Lazy
|
||||
|
||||
from loops.browser.concept import ConceptView
|
||||
from loops.expert.browser.export import ResultsConceptCSVExport
|
||||
from loops.expert.browser.report import ResultsConceptView
|
||||
from loops.knowledge.browser import template, knowledge_macros
|
||||
from loops.knowledge.qualification.base import QualificationRecord
|
||||
from loops.organize.party import getPersonForUser
|
||||
from loops.util import _
|
||||
|
||||
|
||||
class PersonQualificationView(ResultsConceptView):
|
||||
class Qualifications(ResultsConceptView):
|
||||
|
||||
pass
|
||||
# obsolete because we can directly use ResultsConceptView
|
||||
|
||||
#reportName = 'qualification_overview'
|
||||
|
||||
pass # report assigned to query via hasReport relation
|
||||
|
||||
|
|
|
@ -15,4 +15,50 @@
|
|||
|
||||
<!-- views -->
|
||||
|
||||
<zope:adapter
|
||||
name="qualifications.html"
|
||||
for="loops.interfaces.IConcept
|
||||
zope.publisher.interfaces.browser.IBrowserRequest"
|
||||
provides="zope.interface.Interface"
|
||||
factory="loops.knowledge.qualification.browser.Qualifications"
|
||||
permission="zope.View" />
|
||||
|
||||
<!-- reports -->
|
||||
|
||||
<zope:adapter
|
||||
name="qualification_overview"
|
||||
factory="loops.knowledge.qualification.report.QualificationOverview"
|
||||
provides="loops.expert.report.IReportInstance"
|
||||
trusted="True" />
|
||||
<zope:class class="loops.knowledge.qualification.report.QualificationOverview">
|
||||
<require permission="zope.View"
|
||||
interface="loops.expert.report.IReportInstance" />
|
||||
<require permission="zope.ManageContent"
|
||||
set_schema="loops.expert.report.IReportInstance" />
|
||||
</zope:class>
|
||||
|
||||
<zope:adapter
|
||||
name="qualifications"
|
||||
factory="loops.knowledge.qualification.report.Qualifications"
|
||||
provides="loops.expert.report.IReportInstance"
|
||||
trusted="True" />
|
||||
<zope:class class="loops.knowledge.qualification.report.Qualifications">
|
||||
<require permission="zope.View"
|
||||
interface="loops.expert.report.IReportInstance" />
|
||||
<require permission="zope.ManageContent"
|
||||
set_schema="loops.expert.report.IReportInstance" />
|
||||
</zope:class>
|
||||
|
||||
<zope:adapter
|
||||
name="person_qualifications"
|
||||
factory="loops.knowledge.qualification.report.PersonQualifications"
|
||||
provides="loops.expert.report.IReportInstance"
|
||||
trusted="True" />
|
||||
<zope:class class="loops.knowledge.qualification.report.PersonQualifications">
|
||||
<require permission="zope.View"
|
||||
interface="loops.expert.report.IReportInstance" />
|
||||
<require permission="zope.ManageContent"
|
||||
set_schema="loops.expert.report.IReportInstance" />
|
||||
</zope:class>
|
||||
|
||||
</configure>
|
||||
|
|
133
knowledge/qualification/report.py
Normal file
133
knowledge/qualification/report.py
Normal file
|
@ -0,0 +1,133 @@
|
|||
#
|
||||
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
|
||||
"""
|
||||
Qualification management report definitions.
|
||||
"""
|
||||
|
||||
from zope.cachedescriptors.property import Lazy
|
||||
|
||||
from cybertools.composer.report.base import LeafQueryCriteria, CompoundQueryCriteria
|
||||
from cybertools.util.jeep import Jeep
|
||||
from loops.expert.report import ReportInstance
|
||||
from loops.organize.work.report import WorkRow
|
||||
from loops.organize.work.report import deadline, day, task, party, state
|
||||
from loops.organize.work.report import dayStart, dayEnd
|
||||
from loops.organize.work.report import workTitle, workDescription
|
||||
from loops.organize.work.report import partyState
|
||||
from loops import util
|
||||
|
||||
|
||||
class QualificationOverview(ReportInstance):
|
||||
|
||||
type = 'qualification_overview'
|
||||
label = u'Qualification Overview'
|
||||
|
||||
rowFactory = WorkRow
|
||||
|
||||
fields = Jeep((task, party, workTitle, dayStart, dayEnd, state,
|
||||
partyState,)) # +deadline?
|
||||
|
||||
defaultOutputFields = Jeep(list(fields)[:-1])
|
||||
defaultSortCriteria = (party, task,)
|
||||
|
||||
def getOptions(self, option):
|
||||
return self.view.options(option)
|
||||
|
||||
@Lazy
|
||||
def states(self):
|
||||
return self.getOptions('report_select_state' or ('planned',))
|
||||
|
||||
@property
|
||||
def queryCriteria(self):
|
||||
crit = self.context.queryCriteria or []
|
||||
f = self.fields.partyState
|
||||
crit.append(
|
||||
LeafQueryCriteria(f.name, f.operator, 'active', f))
|
||||
return CompoundQueryCriteria(crit)
|
||||
|
||||
def selectObjects(self, parts):
|
||||
result = []
|
||||
workItems = self.recordManager['work']
|
||||
pred = self.conceptManager['querytarget']
|
||||
types = self.view.context.getChildren([pred])
|
||||
for t in types:
|
||||
for c in t.getChildren([self.view.typePredicate]):
|
||||
uid = util.getUidForObject(c)
|
||||
for wi in workItems.query(taskId=uid, state=self.states):
|
||||
result.append(wi)
|
||||
return result
|
||||
|
||||
|
||||
class Qualifications(QualificationOverview):
|
||||
|
||||
type = 'qualifications'
|
||||
label = u'Qualifications'
|
||||
|
||||
taskTypeNames = ('competence',)
|
||||
|
||||
def getOptions(self, option):
|
||||
return self.view.typeOptions(option)
|
||||
|
||||
def selectObjects(self, parts):
|
||||
result = []
|
||||
workItems = self.recordManager['work']
|
||||
target = self.view.context
|
||||
tasks = [target] + self.getAllSubtasks(target)
|
||||
for t in tasks:
|
||||
uid = util.getUidForObject(t)
|
||||
for wi in workItems.query(taskId=uid, state=self.states):
|
||||
result.append(wi)
|
||||
return result
|
||||
|
||||
def getAllSubtasks(self, concept):
|
||||
result = []
|
||||
for c in concept.getChildren([self.view.defaultPredicate]):
|
||||
if c.conceptType in self.taskTypes:
|
||||
result.append(c)
|
||||
result.extend(self.getAllSubtasks(c))
|
||||
return result
|
||||
|
||||
@Lazy
|
||||
def taskTypes(self):
|
||||
return [c for c in [self.conceptManager.get(name)
|
||||
for name in self.taskTypeNames]
|
||||
if c is not None]
|
||||
|
||||
|
||||
class PersonQualifications(QualificationOverview):
|
||||
|
||||
type = 'person_qualifications'
|
||||
label = u'Qualifications for Person'
|
||||
|
||||
defaultSortCriteria = (task,)
|
||||
|
||||
def getOptions(self, option):
|
||||
return self.view.typeOptions(option)
|
||||
|
||||
@property
|
||||
def queryCriteria(self):
|
||||
crit = self.context.queryCriteria or []
|
||||
return CompoundQueryCriteria(crit)
|
||||
|
||||
def selectObjects(self, parts):
|
||||
workItems = self.recordManager['work']
|
||||
person = self.view.context
|
||||
uid = util.getUidForObject(person)
|
||||
return workItems.query(userName=uid, state=self.states)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2016 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -41,16 +41,34 @@ class Questionnaire(AdapterBase, Questionnaire):
|
|||
|
||||
_contextAttributes = list(IQuestionnaire)
|
||||
_adapterAttributes = AdapterBase._adapterAttributes + (
|
||||
'teamBasedEvaluation',
|
||||
'questionGroups', 'questions', 'responses',)
|
||||
_noexportAttributes = _adapterAttributes
|
||||
|
||||
def getTeamBasedEvaluation(self):
|
||||
return (self.questionnaireType == 'team' or
|
||||
getattr(self.context, '_teamBasedEvaluation', False))
|
||||
def setTeamBasedEvaluation(self, value):
|
||||
if not value and getattr(self.context, '_teamBasedEvaluation', False):
|
||||
self.context._teamBasedEvaluation = False
|
||||
teamBasedEvaluation = property(getTeamBasedEvaluation, setTeamBasedEvaluation)
|
||||
|
||||
@property
|
||||
def questionGroups(self):
|
||||
return self.getQuestionGroups()
|
||||
|
||||
def getAllQuestionGroups(self, personId=None):
|
||||
return [adapted(c) for c in self.context.getChildren()]
|
||||
|
||||
def getQuestionGroups(self, personId=None):
|
||||
return self.getAllQuestionGroups()
|
||||
|
||||
@property
|
||||
def questions(self):
|
||||
for qug in self.questionGroups:
|
||||
return self.getQuestions()
|
||||
|
||||
def getQuestions(self, personId=None):
|
||||
for qug in self.getQuestionGroups(personId):
|
||||
for qu in qug.questions:
|
||||
#qu.questionnaire = self
|
||||
yield qu
|
||||
|
@ -65,12 +83,18 @@ class QuestionGroup(AdapterBase, QuestionGroup):
|
|||
'questionnaire', 'questions', 'feedbackItems')
|
||||
_noexportAttributes = _adapterAttributes
|
||||
|
||||
@property
|
||||
def questionnaire(self):
|
||||
def getQuestionnaires(self):
|
||||
result = []
|
||||
for p in self.context.getParents():
|
||||
ap = adapted(p)
|
||||
if IQuestionnaire.providedBy(ap):
|
||||
return ap
|
||||
result.append(ap)
|
||||
return result
|
||||
|
||||
@property
|
||||
def questionnaire(self):
|
||||
for qu in self.getQuestionnaires():
|
||||
return qu
|
||||
|
||||
@property
|
||||
def subobjects(self):
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2016 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -23,6 +23,7 @@ surveys and self-assessments.
|
|||
|
||||
import csv
|
||||
from cStringIO import StringIO
|
||||
import math
|
||||
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||
from zope.cachedescriptors.property import Lazy
|
||||
from zope.i18n import translate
|
||||
|
@ -31,64 +32,318 @@ from cybertools.knowledge.survey.questionnaire import Response
|
|||
from cybertools.util.date import formatTimeStamp
|
||||
from loops.browser.concept import ConceptView
|
||||
from loops.browser.node import NodeView
|
||||
from loops.common import adapted
|
||||
from loops.common import adapted, baseObject
|
||||
from loops.knowledge.browser import InstitutionMixin
|
||||
from loops.knowledge.survey.response import Responses
|
||||
from loops.organize.party import getPersonForUser
|
||||
from loops.security.common import checkPermission
|
||||
from loops.util import getObjectForUid
|
||||
from loops.util import _
|
||||
|
||||
|
||||
template = ViewPageTemplateFile('view_macros.pt')
|
||||
|
||||
class SurveyView(ConceptView):
|
||||
class SurveyView(InstitutionMixin, ConceptView):
|
||||
|
||||
data = None
|
||||
errors = None
|
||||
errors = message = None
|
||||
batchSize = 12
|
||||
teamData = None
|
||||
|
||||
template = template
|
||||
|
||||
#adminMaySelectAllInstitutions = False
|
||||
|
||||
@Lazy
|
||||
def macro(self):
|
||||
self.registerDojo()
|
||||
return template.macros['survey']
|
||||
|
||||
@Lazy
|
||||
def title(self):
|
||||
title = self.context.title
|
||||
if self.personId:
|
||||
person = adapted(getObjectForUid(self.personId))
|
||||
if person is not None:
|
||||
return '%s: %s' % (title, person.title)
|
||||
return title
|
||||
|
||||
@Lazy
|
||||
def tabview(self):
|
||||
if self.editable:
|
||||
return 'index.html'
|
||||
|
||||
def results(self):
|
||||
def getUrlParamString(self):
|
||||
qs = super(SurveyView, self).getUrlParamString()
|
||||
if qs.startswith('?report='):
|
||||
return ''
|
||||
return qs
|
||||
|
||||
@Lazy
|
||||
def personId(self):
|
||||
return self.request.form.get('person')
|
||||
|
||||
@Lazy
|
||||
def report(self):
|
||||
return self.request.form.get('report')
|
||||
|
||||
@Lazy
|
||||
def questionnaireType(self):
|
||||
return self.adapted.questionnaireType
|
||||
|
||||
def teamReports(self):
|
||||
if self.adapted.teamBasedEvaluation:
|
||||
if checkPermission('loops.ViewRestricted', self.context):
|
||||
return [dict(name='standard', label='label_survey_report_standard'),
|
||||
dict(name='questions',
|
||||
label='label_survey_report_questions')]
|
||||
|
||||
def update(self):
|
||||
instUid = self.request.form.get('select_institution')
|
||||
if instUid:
|
||||
return self.setInstitution(instUid)
|
||||
|
||||
@Lazy
|
||||
def groups(self):
|
||||
result = []
|
||||
response = None
|
||||
if self.questionnaireType == 'pref_selection':
|
||||
groups = [g.questions for g in
|
||||
self.adapted.getQuestionGroups(self.personId)]
|
||||
questions = []
|
||||
for idxg, g in enumerate(groups):
|
||||
qus = []
|
||||
for idxq, qu in enumerate(g):
|
||||
questions.append((idxg + 3 * idxq, idxg, qu))
|
||||
questions.sort()
|
||||
questions = [item[2] for item in questions]
|
||||
size = len(questions)
|
||||
for idx in range(0, size, 3):
|
||||
result.append(dict(title=u'Question', infoText=None,
|
||||
questions=questions[idx:idx+3]))
|
||||
return [g for g in result if len(g['questions']) == 3]
|
||||
if self.adapted.noGrouping:
|
||||
questions = list(self.adapted.getQuestions(self.personId))
|
||||
questions.sort(key=lambda x: x.title)
|
||||
size = len(questions)
|
||||
bs = self.batchSize
|
||||
for idx in range(0, size, bs):
|
||||
result.append(dict(title=u'Question', infoText=None,
|
||||
questions=questions[idx:idx+bs]))
|
||||
else:
|
||||
for group in self.adapted.getQuestionGroups(self.personId):
|
||||
result.append(dict(title=group.title,
|
||||
infoText=self.getInfoText(group),
|
||||
questions=group.questions))
|
||||
return result
|
||||
|
||||
@Lazy
|
||||
def answerOptions(self):
|
||||
opts = self.adapted.answerOptions
|
||||
if not opts:
|
||||
opts = [
|
||||
dict(value='none', label=u'No answer',
|
||||
description=u'survey_value_none'),
|
||||
dict(value=3, label=u'Fully applies',
|
||||
description=u'survey_value_3'),
|
||||
dict(value=2, label=u'', description=u'survey_value_2'),
|
||||
dict(value=1, label=u'', description=u'survey_value_1'),
|
||||
dict(value=0, label=u'Does not apply',
|
||||
description=u'survey_value_0'),]
|
||||
return opts
|
||||
|
||||
@Lazy
|
||||
def showFeedbackText(self):
|
||||
sft = self.adapted.showFeedbackText
|
||||
return sft is None and True or sft
|
||||
|
||||
@Lazy
|
||||
def feedbackColumns(self):
|
||||
cols = self.adapted.feedbackColumns
|
||||
if not cols:
|
||||
cols = [
|
||||
dict(name='text', label=u'Response'),
|
||||
dict(name='score', label=u'Score')]
|
||||
if self.report == 'standard':
|
||||
cols = [c for c in cols if c['name'] in self.teamColumns]
|
||||
return cols
|
||||
|
||||
teamColumns = ['category', 'average', 'stddev', 'teamRank', 'text']
|
||||
|
||||
@Lazy
|
||||
def showTeamResults(self):
|
||||
for c in self.feedbackColumns:
|
||||
if c['name'] in ('average', 'teamRank'):
|
||||
return True
|
||||
return False
|
||||
|
||||
def getTeamData(self, respManager):
|
||||
result = []
|
||||
pred = [self.conceptManager.get('ismember'),
|
||||
self.conceptManager.get('ismaster')]
|
||||
if None in pred:
|
||||
return result
|
||||
inst = self.institution
|
||||
instUid = self.getUidForObject(inst)
|
||||
if inst:
|
||||
for c in inst.getChildren(pred):
|
||||
uid = self.getUidForObject(c)
|
||||
data = respManager.load(uid, instUid)
|
||||
if data:
|
||||
resp = Response(self.adapted, self.personId)
|
||||
for qu in self.adapted.getQuestions(self.personId):
|
||||
if qu.questionType in (None, 'value_selection'):
|
||||
if qu.uid in data:
|
||||
value = data[qu.uid]
|
||||
if isinstance(value, int) or value.isdigit():
|
||||
resp.values[qu] = int(value)
|
||||
else:
|
||||
resp.texts[qu] = data.get(qu.uid) or u''
|
||||
qgAvailable = True
|
||||
for qg in self.adapted.getQuestionGroups(self.personId):
|
||||
if qg.uid in data:
|
||||
resp.values[qg] = data[qg.uid]
|
||||
else:
|
||||
qgAvailable = False
|
||||
if not qgAvailable:
|
||||
values = resp.getGroupedResult()
|
||||
for v in values:
|
||||
resp.values[v['group']] = v['score']
|
||||
result.append(resp)
|
||||
return result
|
||||
|
||||
def results(self):
|
||||
if self.report:
|
||||
return self.teamResults(self.report)
|
||||
form = self.request.form
|
||||
if 'submit' in form:
|
||||
self.data = {}
|
||||
response = Response(self.adapted, None)
|
||||
action = None
|
||||
for k in ('submit', 'save'):
|
||||
if k in form:
|
||||
action = k
|
||||
break
|
||||
if action is None:
|
||||
return []
|
||||
respManager = Responses(self.context)
|
||||
respManager.personId = (self.request.form.get('person') or
|
||||
respManager.getPersonId())
|
||||
if self.adapted.teamBasedEvaluation and self.institution:
|
||||
respManager.institutionId = self.getUidForObject(
|
||||
baseObject(self.institution))
|
||||
if self.adapted.questionnaireType == 'person':
|
||||
respManager.referrerId = respManager.getPersonId()
|
||||
if self.adapted.questionnaireType == 'pref_selection':
|
||||
return self.prefsResults(respManager, form, action)
|
||||
data = {}
|
||||
response = Response(self.adapted, self.personId)
|
||||
for key, value in form.items():
|
||||
if key.startswith('question_'):
|
||||
if value != 'none':
|
||||
uid = key[len('question_'):]
|
||||
question = adapted(self.getObjectForUid(uid))
|
||||
if value != 'none':
|
||||
if value.isdigit():
|
||||
value = int(value)
|
||||
self.data[uid] = value
|
||||
data[uid] = value
|
||||
response.values[question] = value
|
||||
Responses(self.context).save(self.data)
|
||||
values = response.getGroupedResult()
|
||||
for v in values:
|
||||
data[self.getUidForObject(v['group'])] = v['score']
|
||||
self.data = data
|
||||
self.errors = self.check(response)
|
||||
if action == 'submit' and not self.errors:
|
||||
data['state'] = 'active'
|
||||
else:
|
||||
data['state'] = 'draft'
|
||||
respManager.save(data)
|
||||
if action == 'save':
|
||||
self.message = u'Your data have been saved.'
|
||||
return []
|
||||
if self.errors:
|
||||
return []
|
||||
if response is not None:
|
||||
result = response.getGroupedResult()
|
||||
return [dict(category=r[0].title, text=r[1].text,
|
||||
score=int(round(r[2] * 100)))
|
||||
for r in result]
|
||||
result = [dict(category=r['group'].title, text=r['feedback'].text,
|
||||
score=int(round(r['score'] * 100)), rank=r['rank'])
|
||||
for r in values]
|
||||
if self.showTeamResults:
|
||||
self.teamData = self.getTeamData(respManager)
|
||||
groups = [r['group'] for r in values]
|
||||
teamValues = response.getTeamResult(groups, self.teamData)
|
||||
for idx, r in enumerate(teamValues):
|
||||
result[idx]['average'] = int(round(r['average'] * 100))
|
||||
result[idx]['teamRank'] = r['rank']
|
||||
return result
|
||||
|
||||
def teamResults(self, report):
|
||||
result = []
|
||||
respManager = Responses(self.context)
|
||||
self.teamData = self.getTeamData(respManager)
|
||||
response = Response(self.adapted, None)
|
||||
groups = self.adapted.getQuestionGroups(self.personId)
|
||||
teamValues = response.getTeamResult(groups, self.teamData)
|
||||
for idx, r in enumerate(teamValues):
|
||||
group = r['group']
|
||||
item = dict(category=group.title,
|
||||
average=int(round(r['average'] * 100)),
|
||||
teamRank=r['rank'])
|
||||
if group.feedbackItems:
|
||||
wScore = r['average'] * len(group.feedbackItems) - 0.00001
|
||||
item['text'] = group.feedbackItems[int(wScore)].text
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
def getTeamResultsForQuestion(self, question, questionnaire):
|
||||
result = dict(average=0.0, stddev=0.0)
|
||||
if self.teamData is None:
|
||||
respManager = Responses(self.context)
|
||||
self.teamData = self.getTeamData(respManager)
|
||||
answerRange = question.answerRange or questionnaire.defaultAnswerRange
|
||||
values = [r.values.get(question) for r in self.teamData]
|
||||
values = [v for v in values if v is not None]
|
||||
if values:
|
||||
average = float(sum(values)) / len(values)
|
||||
if question.revertAnswerOptions:
|
||||
average = answerRange - average - 1
|
||||
devs = [(average - v) for v in values]
|
||||
stddev = math.sqrt(sum(d * d for d in devs) / len(values))
|
||||
average = average * 100 / (answerRange - 1)
|
||||
stddev = stddev * 100 / (answerRange - 1)
|
||||
result['average'] = int(round(average))
|
||||
result['stddev'] = int(round(stddev))
|
||||
texts = [r.texts.get(question) for r in self.teamData]
|
||||
result['texts'] = '<br />'.join([unicode(t) for t in texts if t])
|
||||
return result
|
||||
|
||||
def prefsResults(self, respManager, form, action):
|
||||
result = []
|
||||
data = {}
|
||||
for key, value in form.items():
|
||||
if key.startswith('group_') and value:
|
||||
data[value] = 1
|
||||
respManager.save(data)
|
||||
if action == 'save':
|
||||
self.message = u'Your data have been saved.'
|
||||
return []
|
||||
self.data = data
|
||||
#self.errors = self.check(response)
|
||||
if self.errors:
|
||||
return []
|
||||
for group in self.adapted.getQuestionGroups(self.personId):
|
||||
score = 0
|
||||
for qu in group.questions:
|
||||
value = data.get(qu.uid) or 0
|
||||
if qu.revertAnswerOptions:
|
||||
value = -value
|
||||
score += value
|
||||
result.append(dict(category=group.title, score=score))
|
||||
return result
|
||||
|
||||
def check(self, response):
|
||||
errors = []
|
||||
values = response.values
|
||||
for qu in self.adapted.questions:
|
||||
for qu in self.adapted.getQuestions(self.personId):
|
||||
if qu.required and qu not in values:
|
||||
errors.append('Please answer the obligatory questions.')
|
||||
errors.append(dict(uid=qu.uid,
|
||||
text='Please answer the obligatory questions.'))
|
||||
break
|
||||
qugroups = {}
|
||||
for qugroup in self.adapted.questionGroups:
|
||||
for qugroup in self.adapted.getQuestionGroups(self.personId):
|
||||
qugroups[qugroup] = 0
|
||||
for qu in values:
|
||||
qugroups[qu.questionGroup] += 1
|
||||
|
@ -97,7 +352,12 @@ class SurveyView(ConceptView):
|
|||
if minAnswers in (u'', None):
|
||||
minAnswers = len(qugroup.questions)
|
||||
if count < minAnswers:
|
||||
errors.append('Please answer the minimum number of questions.')
|
||||
if self.adapted.noGrouping:
|
||||
errors.append(dict(uid=qugroup.uid,
|
||||
text='Please answer the highlighted questions.'))
|
||||
else:
|
||||
errors.append(dict(uid=qugroup.uid,
|
||||
text='Please answer the minimum number of questions.'))
|
||||
break
|
||||
return errors
|
||||
|
||||
|
@ -106,7 +366,8 @@ class SurveyView(ConceptView):
|
|||
text = qugroup.description
|
||||
info = None
|
||||
if qugroup.minAnswers in (u'', None):
|
||||
info = translate(_(u'Please answer all questions.'), target_language=lang)
|
||||
info = translate(_(u'Please answer all questions.'),
|
||||
target_language=lang)
|
||||
elif qugroup.minAnswers > 0:
|
||||
info = translate(_(u'Please answer at least $minAnswers questions.',
|
||||
mapping=dict(minAnswers=qugroup.minAnswers)),
|
||||
|
@ -115,16 +376,48 @@ class SurveyView(ConceptView):
|
|||
text = u'<i>%s</i><br />(%s)' % (text, info)
|
||||
return text
|
||||
|
||||
def loadData(self):
|
||||
if self.data is None:
|
||||
respManager = Responses(self.context)
|
||||
respManager.personId = (self.request.form.get('person') or
|
||||
respManager.getPersonId())
|
||||
if self.adapted.teamBasedEvaluation and self.institution:
|
||||
respManager.institutionId = self.getUidForObject(
|
||||
baseObject(self.institution))
|
||||
if self.adapted.questionnaireType == 'person':
|
||||
respManager.referrerId = respManager.getPersonId()
|
||||
self.data = respManager.load()
|
||||
|
||||
def getValues(self, question):
|
||||
setting = None
|
||||
if self.data is None:
|
||||
self.data = Responses(self.context).load()
|
||||
self.loadData()
|
||||
if self.data:
|
||||
setting = self.data.get(question.uid)
|
||||
noAnswer = [dict(value='none', checked=(setting == None),
|
||||
radio=(not question.required))]
|
||||
return noAnswer + [dict(value=i, checked=(setting == i), radio=True)
|
||||
for i in reversed(range(question.answerRange))]
|
||||
if setting is None:
|
||||
setting = 'none'
|
||||
setting = str(setting)
|
||||
result = []
|
||||
for opt in self.answerOptions:
|
||||
value = str(opt['value'])
|
||||
result.append(dict(value=value, checked=(setting == value),
|
||||
title=opt.get('description') or u''))
|
||||
return result
|
||||
|
||||
def getTextValue(self, question):
|
||||
self.loadData()
|
||||
if self.data:
|
||||
return self.data.get(question.uid)
|
||||
|
||||
def getPrefsValue(self, question):
|
||||
self.loadData()
|
||||
if self.data:
|
||||
return self.data.get(question.uid)
|
||||
|
||||
def getCssClass(self, question):
|
||||
cls = ''
|
||||
if self.errors and self.data.get(question.uid) is None:
|
||||
cls = 'error '
|
||||
return cls + 'vpad'
|
||||
|
||||
|
||||
class SurveyCsvExport(NodeView):
|
||||
|
@ -132,36 +425,43 @@ class SurveyCsvExport(NodeView):
|
|||
encoding = 'ISO8859-15'
|
||||
|
||||
def encode(self, text):
|
||||
text.encode(self.encoding)
|
||||
return text.encode(self.encoding)
|
||||
|
||||
@Lazy
|
||||
def questions(self):
|
||||
result = []
|
||||
for idx1, qug in enumerate(adapted(self.virtualTargetObject).questionGroups):
|
||||
for idx1, qug in enumerate(
|
||||
adapted(self.virtualTargetObject).questionGroups):
|
||||
for idx2, qu in enumerate(qug.questions):
|
||||
result.append((idx1, idx2, qug, qu))
|
||||
return result
|
||||
|
||||
@Lazy
|
||||
def columns(self):
|
||||
infoCols = ['Name', 'Timestamp']
|
||||
infoCols = ['Institution', 'Name', 'Timestamp']
|
||||
dataCols = ['%02i-%02i' % (item[0], item[1]) for item in self.questions]
|
||||
return infoCols + dataCols
|
||||
|
||||
def getRows(self):
|
||||
memberPred = self.conceptManager.get('ismember')
|
||||
for tr in Responses(self.virtualTargetObject).getAllTracks():
|
||||
p = adapted(getObjectForUid(tr.userName))
|
||||
name = p and p.title or u'???'
|
||||
name = self.encode(p and p.title or u'???')
|
||||
inst = u''
|
||||
if memberPred is not None:
|
||||
for i in baseObject(p).getParents([memberPred]):
|
||||
inst = self.encode(i.title)
|
||||
break
|
||||
ts = formatTimeStamp(tr.timeStamp)
|
||||
cells = [tr.data.get(qu.uid, -1)
|
||||
for (idx1, idx2, qug, qu) in self.questions]
|
||||
yield [name, ts] + cells
|
||||
yield [inst, name, ts] + cells
|
||||
|
||||
def __call__(self):
|
||||
f = StringIO()
|
||||
writer = csv.writer(f, delimiter=',')
|
||||
writer.writerow(self.columns)
|
||||
for row in self.getRows():
|
||||
for row in sorted(self.getRows()):
|
||||
writer.writerow(row)
|
||||
text = f.getvalue()
|
||||
self.setDownloadHeader(text)
|
||||
|
|
|
@ -12,8 +12,6 @@
|
|||
<zope:class class="loops.knowledge.survey.base.Questionnaire">
|
||||
<require permission="zope.View"
|
||||
interface="loops.knowledge.survey.interfaces.IQuestionnaire" />
|
||||
<require permission="zope.View"
|
||||
attributes="context" />
|
||||
<require permission="zope.ManageContent"
|
||||
set_schema="loops.knowledge.survey.interfaces.IQuestionnaire" />
|
||||
</zope:class>
|
||||
|
@ -25,8 +23,6 @@
|
|||
<zope:class class="loops.knowledge.survey.base.QuestionGroup">
|
||||
<require permission="zope.View"
|
||||
interface="loops.knowledge.survey.interfaces.IQuestionGroup" />
|
||||
<require permission="zope.View"
|
||||
attributes="context" />
|
||||
<require permission="zope.ManageContent"
|
||||
set_schema="loops.knowledge.survey.interfaces.IQuestionGroup" />
|
||||
</zope:class>
|
||||
|
@ -38,8 +34,6 @@
|
|||
<zope:class class="loops.knowledge.survey.base.Question">
|
||||
<require permission="zope.View"
|
||||
interface="loops.knowledge.survey.interfaces.IQuestion" />
|
||||
<require permission="zope.View"
|
||||
attributes="context" />
|
||||
<require permission="zope.ManageContent"
|
||||
set_schema="loops.knowledge.survey.interfaces.IQuestion" />
|
||||
</zope:class>
|
||||
|
@ -51,8 +45,6 @@
|
|||
<zope:class class="loops.knowledge.survey.base.FeedbackItem">
|
||||
<require permission="zope.View"
|
||||
interface="loops.knowledge.survey.interfaces.IFeedbackItem" />
|
||||
<require permission="zope.View"
|
||||
attributes="context" />
|
||||
<require permission="zope.ManageContent"
|
||||
set_schema="loops.knowledge.survey.interfaces.IFeedbackItem" />
|
||||
</zope:class>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2016 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -23,21 +23,82 @@ Interfaces for surveys used in knowledge management.
|
|||
from zope.interface import Interface, Attribute
|
||||
from zope import interface, component, schema
|
||||
|
||||
from cybertools.composer.schema.grid.interfaces import Records
|
||||
from cybertools.knowledge.survey import interfaces
|
||||
from loops.interfaces import IConceptSchema, ILoopsAdapter
|
||||
from loops.util import _
|
||||
from loops.util import _, KeywordVocabulary
|
||||
|
||||
|
||||
class IQuestionnaire(IConceptSchema, interfaces.IQuestionnaire):
|
||||
class IQuestionnaire(ILoopsAdapter, interfaces.IQuestionnaire):
|
||||
""" A collection of questions for setting up a survey.
|
||||
"""
|
||||
|
||||
questionnaireHeader = schema.Text(
|
||||
title=_(u'Questionnaire Header'),
|
||||
description=_(u'Text that will appear at the top of the questionnaire.'),
|
||||
default=u'',
|
||||
missing_value=u'',
|
||||
required=False)
|
||||
|
||||
questionnaireType = schema.Choice(
|
||||
title=_(u'Questionnaire Type'),
|
||||
description=_(u'Select the type of the questionnaire.'),
|
||||
source=KeywordVocabulary((
|
||||
('standard', _(u'Standard Questionnaire')),
|
||||
('person', _(u'Person-related Questionnaire')),
|
||||
('team', _(u'Team-related Questionnaire')),
|
||||
('pref_selection', _(u'Preference Selection')),
|
||||
)),
|
||||
default='standard',
|
||||
required=True)
|
||||
|
||||
defaultAnswerRange = schema.Int(
|
||||
title=_(u'Answer Range'),
|
||||
description=_(u'Number of items (answer options) to select from.'),
|
||||
default=4,
|
||||
required=True)
|
||||
|
||||
answerOptions = Records(
|
||||
title=_(u'Answer Options'),
|
||||
description=_(u'Values to select from with corresponding column '
|
||||
u'labels and descriptions. There should be at '
|
||||
u'least answer range items with numeric values.'),
|
||||
default=[],
|
||||
required=False)
|
||||
|
||||
answerOptions.column_types = [
|
||||
schema.Text(__name__='value', title=u'Value',),
|
||||
schema.Text(__name__='label', title=u'Label'),
|
||||
schema.Text(__name__='description', title=u'Description'),
|
||||
schema.Text(__name__='colspan', title=u'ColSpan'),
|
||||
schema.Text(__name__='cssclass', title=u'CSS Class'),]
|
||||
|
||||
noGrouping = schema.Bool(
|
||||
title=_(u'No Grouping of Questions'),
|
||||
description=_(u'The questions should be presented in a linear manner, '
|
||||
u'not grouped by categories or question groups.'),
|
||||
default=False,
|
||||
required=False)
|
||||
|
||||
teamBasedEvaluation = schema.Bool(
|
||||
title=_(u'Team-based Evaluation'),
|
||||
description=_(u'.'),
|
||||
default=False,
|
||||
required=False)
|
||||
|
||||
#teamBasedEvaluation = Attribute('Team-based Evaluation')
|
||||
|
||||
feedbackColumns = Records(
|
||||
title=_(u'Feedback Columns'),
|
||||
description=_(u'Column definitions for the results table '
|
||||
u'on the feedback page.'),
|
||||
default=[],
|
||||
required=False)
|
||||
|
||||
feedbackColumns.column_types = [
|
||||
schema.Text(__name__='name', title=u'Column Name',),
|
||||
schema.Text(__name__='label', title=u'Column Label'),]
|
||||
|
||||
feedbackHeader = schema.Text(
|
||||
title=_(u'Feedback Header'),
|
||||
description=_(u'Text that will appear at the top of the feedback page.'),
|
||||
|
@ -53,7 +114,7 @@ class IQuestionnaire(IConceptSchema, interfaces.IQuestionnaire):
|
|||
required=False)
|
||||
|
||||
|
||||
class IQuestionGroup(IConceptSchema, interfaces.IQuestionGroup):
|
||||
class IQuestionGroup(ILoopsAdapter, interfaces.IQuestionGroup):
|
||||
""" A group of questions within a questionnaire.
|
||||
"""
|
||||
|
||||
|
@ -65,10 +126,20 @@ class IQuestionGroup(IConceptSchema, interfaces.IQuestionGroup):
|
|||
required=False)
|
||||
|
||||
|
||||
class IQuestion(IConceptSchema, interfaces.IQuestion):
|
||||
class IQuestion(ILoopsAdapter, interfaces.IQuestion):
|
||||
""" A single question within a questionnaire.
|
||||
"""
|
||||
|
||||
questionType = schema.Choice(
|
||||
title=_(u'Question Type'),
|
||||
description=_(u'Select the type of the question.'),
|
||||
source=KeywordVocabulary((
|
||||
('value_selection', _(u'Value Selection')),
|
||||
('text', _(u'Text')),
|
||||
)),
|
||||
default='value_selection',
|
||||
required=True)
|
||||
|
||||
required = schema.Bool(
|
||||
title=_(u'Required'),
|
||||
description=_(u'Question must be answered.'),
|
||||
|
@ -82,7 +153,7 @@ class IQuestion(IConceptSchema, interfaces.IQuestion):
|
|||
required=False)
|
||||
|
||||
|
||||
class IFeedbackItem(IConceptSchema, interfaces.IFeedbackItem):
|
||||
class IFeedbackItem(ILoopsAdapter, interfaces.IFeedbackItem):
|
||||
""" Some text (e.g. a recommendation) or some other kind of information
|
||||
that may be deduced from the res)ponses to a questionnaire.
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2016 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -34,22 +34,52 @@ class Responses(BaseRecordManager):
|
|||
implements(IResponses)
|
||||
|
||||
storageName = 'survey_responses'
|
||||
personId = None
|
||||
institutionId = None
|
||||
referrerId = None
|
||||
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
|
||||
def save(self, data):
|
||||
if self.personId:
|
||||
self.storage.saveUserTrack(self.uid, 0, self.personId, data,
|
||||
update=True, overwrite=True)
|
||||
id = self.personId
|
||||
if self.institutionId:
|
||||
id += '.' + self.institutionId
|
||||
if self.referrerId:
|
||||
id += '.' + self.referrerId
|
||||
self.storage.saveUserTrack(self.uid, 0, id, data,
|
||||
update=True, overwrite=False)
|
||||
|
||||
def load(self):
|
||||
if self.personId:
|
||||
tracks = self.storage.getUserTracks(self.uid, 0, self.personId)
|
||||
def load(self, personId=None, referrerId=None, institutionId=None):
|
||||
if personId is None:
|
||||
personId = self.personId
|
||||
if referrerId is None:
|
||||
referrerId = self.referrerId
|
||||
if institutionId is None:
|
||||
institutionId = self.institutionId
|
||||
if personId:
|
||||
id = personId
|
||||
if institutionId:
|
||||
id += '.' + institutionId
|
||||
if referrerId:
|
||||
id += '.' + referrerId
|
||||
tracks = self.storage.getUserTracks(self.uid, 0, id)
|
||||
if not tracks: # then try without institution
|
||||
tracks = self.storage.getUserTracks(self.uid, 0, personId)
|
||||
if tracks:
|
||||
return tracks[0].data
|
||||
return {}
|
||||
|
||||
def loadRange(self, personId):
|
||||
tracks = self.storage.getUserTracks(self.uid, 0, personId)
|
||||
data = {}
|
||||
for tr in tracks:
|
||||
for k, v in tr.data.items():
|
||||
item = data.setdefault(k, [])
|
||||
item.append(v)
|
||||
return data
|
||||
|
||||
def getAllTracks(self):
|
||||
return self.storage.query(taskId=self.uid)
|
||||
|
||||
|
|
|
@ -4,95 +4,282 @@
|
|||
|
||||
<metal:block define-macro="survey"
|
||||
tal:define="feedback item/results;
|
||||
errors item/errors">
|
||||
questType item/questionnaireType;
|
||||
questMacro python:
|
||||
'quest_' + (questType or 'standard');
|
||||
report request/report|nothing;
|
||||
reportMacro python:
|
||||
'report_' + (report or 'standard');
|
||||
errors item/errors;
|
||||
message item/message;
|
||||
dummy item/update">
|
||||
<metal:title use-macro="item/conceptMacros/concepttitle_only" />
|
||||
<tal:description condition="not:feedback">
|
||||
<metal:title use-macro="item/conceptMacros/conceptdescription" />
|
||||
</tal:description>
|
||||
<div tal:condition="feedback">
|
||||
<h3 i18n:translate="">Feedback</h3>
|
||||
<div tal:define="header item/adapted/feedbackHeader"
|
||||
<div tal:define="header item/adapted/questionnaireHeader"
|
||||
tal:condition="header"
|
||||
tal:content="structure python:item.renderText(header, 'text/restructured')" />
|
||||
<table class="listing">
|
||||
<tr>
|
||||
<th i18n:translate="">Category</th>
|
||||
<th i18n:translate="">Response</th>
|
||||
<th i18n:translate="">%</th>
|
||||
</tr>
|
||||
<tr tal:repeat="fbitem feedback">
|
||||
<td tal:content="fbitem/category" />
|
||||
<td tal:content="fbitem/text" />
|
||||
<td tal:content="fbitem/score" />
|
||||
</tr>
|
||||
</table>
|
||||
<div class="button" id="show_questionnaire">
|
||||
<a href="" onclick="back(); return false"
|
||||
i18n:translate="">
|
||||
Back to Questionnaire</a>
|
||||
<br />
|
||||
</div>
|
||||
<div tal:define="footer item/adapted/feedbackFooter"
|
||||
tal:condition="footer"
|
||||
tal:content="structure python:item.renderText(footer, 'text/restructured')" />
|
||||
tal:content="structure python:
|
||||
item.renderText(header, 'text/restructured')" />
|
||||
</tal:description>
|
||||
|
||||
<div tal:condition="feedback">
|
||||
<metal:block use-macro="item/template/macros/?reportMacro" />
|
||||
</div>
|
||||
<div id="questionnaire"
|
||||
tal:condition="not:feedback">
|
||||
<metal:block use-macro="item/template/macros/?questMacro" />
|
||||
</div>
|
||||
</metal:block>
|
||||
|
||||
|
||||
<metal:block define-macro="quest_standard">
|
||||
<tal:inst condition="item/adapted/teamBasedEvaluation">
|
||||
<metal:inst use-macro="item/knowledge_macros/select_institution" />
|
||||
</tal:inst>
|
||||
<div class="button"
|
||||
tal:define="reports item/teamReports"
|
||||
tal:condition="reports">
|
||||
<b i18n:translate="label_survey_show_report">Show Report</b>:
|
||||
<a tal:repeat="report reports"
|
||||
tal:attributes="href string:${view/requestUrl}?report=${report/name}"
|
||||
i18n:translate=""
|
||||
tal:content="report/label" />
|
||||
<br /><br />
|
||||
</div>
|
||||
<h3 i18n:translate="">Questionnaire</h3>
|
||||
<div class="error"
|
||||
tal:condition="errors">
|
||||
<div tal:repeat="error errors">
|
||||
<span i18n:translate=""
|
||||
tal:content="error" />
|
||||
tal:content="error/text" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="message"
|
||||
tal:condition="message"
|
||||
i18n:translate=""
|
||||
tal:content="message" />
|
||||
<form method="post">
|
||||
<table class="listing">
|
||||
<tal:qugroup repeat="qugroup item/adapted/questionGroups">
|
||||
<tr><td colspan="6"> </td></tr>
|
||||
<input type="hidden" name="person"
|
||||
tal:define="personId request/person|nothing"
|
||||
tal:condition="personId"
|
||||
tal:attributes="value personId" />
|
||||
<tal:group repeat="group item/groups">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td tal:repeat="opt item/answerOptions"> </td></tr>
|
||||
<tr class="vpad">
|
||||
<td tal:define="infoText python:item.getInfoText(qugroup)">
|
||||
<b tal:content="qugroup/title" />
|
||||
<td tal:define="infoText group/infoText">
|
||||
<b i18n:translate=""
|
||||
tal:content="group/title" />
|
||||
<div class="infotext"
|
||||
tal:condition="infoText">
|
||||
<span tal:content="structure infoText" />
|
||||
</div>
|
||||
</td>
|
||||
<td style="text-align: center"
|
||||
i18n:translate="">No answer</td>
|
||||
<td colspan="2"
|
||||
i18n:translate="">Fully applies</td>
|
||||
<td colspan="2"
|
||||
style="text-align: right"
|
||||
i18n:translate="">Does not apply</td>
|
||||
<td tal:repeat="opt python:[opt for opt in item.answerOptions
|
||||
if opt.get('colspan') != '0']"
|
||||
i18n:translate=""
|
||||
i18n:attributes="title"
|
||||
tal:attributes="title opt/description|string:;
|
||||
class python:opt.get('cssclass') or 'center';
|
||||
colspan python:opt.get('colspan')"
|
||||
tal:content="opt/label|string:" />
|
||||
</tr>
|
||||
<tr class="vpad"
|
||||
tal:repeat="question qugroup/questions">
|
||||
<tal:question repeat="question group/questions">
|
||||
<tal:question define="qutype python:
|
||||
question.questionType or 'value_selection'">
|
||||
<metal:question use-macro="item/template/macros/?qutype" />
|
||||
</tal:question>
|
||||
</tal:question>
|
||||
</tal:group>
|
||||
</table>
|
||||
<input type="submit" name="submit" value="Evaluate Questionnaire"
|
||||
i18n:attributes="value" />
|
||||
<input type="submit" name="save" value="Save Data"
|
||||
i18n:attributes="value" />
|
||||
<input type="button" name="reset_responses" value="Reset Responses Entered"
|
||||
i18n:attributes="value; onclick"
|
||||
onclick="if (confirm('Do you really want to reset all response data?')) setRadioButtons('none'); return false" />
|
||||
</form>
|
||||
</metal:block>
|
||||
|
||||
|
||||
<metal:block define-macro="quest_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:condition="value/radio"
|
||||
tal:attributes="
|
||||
name string:question_${question/uid};
|
||||
tal:attributes="name string:question_${question/uid};
|
||||
value value/value;
|
||||
checked value/checked;
|
||||
title string:survey_value_${value/value}" />
|
||||
<span tal:condition="not:value/radio"
|
||||
title="Obligatory question, must be answered"
|
||||
i18n:attributes="title">***
|
||||
</span>
|
||||
title value/title" />
|
||||
</td>
|
||||
</tr>
|
||||
</tal:qugroup>
|
||||
</metal:block>
|
||||
|
||||
|
||||
<metal:block define-macro="text">
|
||||
<tr tal:attributes="class python:item.getCssClass(question)">
|
||||
<td>
|
||||
<div tal:content="question/text" />
|
||||
<textarea style="width: 90%; margin-left: 20px"
|
||||
tal:content="python:item.getTextValue(question)"
|
||||
tal:attributes="name string:question_${question/uid}">
|
||||
</textarea>
|
||||
</td>
|
||||
<td tal:repeat="opt item/answerOptions" />
|
||||
</tr>
|
||||
</metal:block>
|
||||
|
||||
|
||||
<metal:block define-macro="report_standard">
|
||||
<h3 i18n:translate="">Feedback</h3>
|
||||
<div tal:define="header item/adapted/feedbackHeader"
|
||||
tal:condition="header"
|
||||
tal:content="structure python:
|
||||
item.renderText(header, 'text/restructured')" />
|
||||
<table class="listing">
|
||||
<tr>
|
||||
<th i18n:translate="">Category</th>
|
||||
<th tal:repeat="col item/feedbackColumns"
|
||||
i18n:translate=""
|
||||
tal:attributes="class python:
|
||||
col['name'] != 'text' and 'center' or None"
|
||||
tal:content="col/label" />
|
||||
</tr>
|
||||
<tr style="vertical-align: top"
|
||||
tal:repeat="fbitem feedback">
|
||||
<td style="vertical-align: top"
|
||||
tal:content="fbitem/category" />
|
||||
<tal:cols repeat="col item/feedbackColumns">
|
||||
<td style="vertical-align: top"
|
||||
tal:define="name col/name"
|
||||
tal:attributes="class python:name != 'text' and 'center' or None"
|
||||
tal:content="fbitem/?name|string:" />
|
||||
</tal:cols>
|
||||
</tr>
|
||||
</table>
|
||||
<input type="submit" name="submit" value="Evaluate Questionnaire"
|
||||
i18n:attributes="value" />
|
||||
<input type="button" name="reset_responses" value="Reset Responses Entered"
|
||||
i18n:attributes="value"
|
||||
onclick="setRadioButtons('none'); return false" />
|
||||
</form>
|
||||
<p tal:define="teamData item/teamData"
|
||||
tal:condition="teamData">
|
||||
<b><span i18n:translate="">Team Size</span>:
|
||||
<span tal:content="python:len(teamData)" /></b><br />
|
||||
</p>
|
||||
<div class="button" id="show_questionnaire">
|
||||
<a i18n:translate=""
|
||||
tal:attributes="href string:${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>
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# tests.py - loops.knowledge package
|
||||
|
||||
import os
|
||||
import unittest, doctest
|
||||
|
@ -6,6 +5,7 @@ from zope.app.testing import ztapi
|
|||
from zope import component
|
||||
from zope.interface.verify import verifyClass
|
||||
|
||||
from loops.expert.report import IReport, Report
|
||||
from loops.knowledge.qualification.base import Competence
|
||||
from loops.knowledge.survey.base import Questionnaire, Question, FeedbackItem
|
||||
from loops.knowledge.survey.interfaces import IQuestionnaire, IQuestion, \
|
||||
|
@ -18,6 +18,7 @@ importPath = os.path.join(os.path.dirname(__file__), 'data')
|
|||
|
||||
|
||||
def importData(loopsRoot):
|
||||
component.provideAdapter(Report, provides=IReport)
|
||||
baseImportData(loopsRoot, importPath, 'knowledge_de.dmp')
|
||||
|
||||
def importSurvey(loopsRoot):
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2016 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -27,6 +27,7 @@ from zope.proxy import removeAllProxies
|
|||
from zope.security.proxy import removeSecurityProxy
|
||||
from zope.traversing.browser import absoluteURL
|
||||
|
||||
from cybertools.browser.view import URLGetter
|
||||
from cybertools.meta.interfaces import IOptions
|
||||
from cybertools.util import format
|
||||
from loops.common import adapted, baseObject
|
||||
|
@ -42,6 +43,10 @@ class BaseView(object):
|
|||
self.context = removeSecurityProxy(context) # this is the adapted concept!
|
||||
self.request = request
|
||||
|
||||
@property
|
||||
def requestUrl(self):
|
||||
return URLGetter(self.request)
|
||||
|
||||
@Lazy
|
||||
def loopsRoot(self):
|
||||
return self.context.getLoopsRoot()
|
||||
|
@ -86,6 +91,10 @@ class BaseView(object):
|
|||
def title(self):
|
||||
return self.context.title
|
||||
|
||||
@Lazy
|
||||
def headTitle(self):
|
||||
return self.title
|
||||
|
||||
@Lazy
|
||||
def description(self):
|
||||
return self.context.description
|
||||
|
|
|
@ -65,8 +65,9 @@ class LayoutNodeView(Page, BaseView):
|
|||
if self.target is not None:
|
||||
targetView = component.getMultiAdapter((self.target, self.request),
|
||||
name='layout')
|
||||
if targetView.title not in parts:
|
||||
parts.append(targetView.title)
|
||||
title = getattr(targetView, 'headTitle', targetView.title)
|
||||
if title not in parts:
|
||||
parts.append(title)
|
||||
if self.globalOptions('reverseHeadTitle'):
|
||||
parts.reverse()
|
||||
return ' - '.join(parts)
|
||||
|
|
|
@ -50,3 +50,15 @@ class TextView(BaseView):
|
|||
|
||||
def render(self):
|
||||
return self.renderText(self.context.data, self.context.contentType)
|
||||
|
||||
@Lazy
|
||||
def canonicalUrl(self):
|
||||
parents = self.context.context.getParents(
|
||||
[self.conceptManager['standard']])
|
||||
for parent in parents:
|
||||
view = component.getMultiAdapter((adapted(parent),
|
||||
self.request), name='layout')
|
||||
if view:
|
||||
url = getattr(view, 'canonicalUrl')
|
||||
if url:
|
||||
return url
|
||||
|
|
Binary file not shown.
|
@ -1,9 +1,9 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
|
||||
"Project-Id-Version: 0.13.0\n"
|
||||
"Project-Id-Version: 0.13.1\n"
|
||||
"POT-Creation-Date: 2007-05-22 12:00 CET\n"
|
||||
"PO-Revision-Date: 2016-01-27 12:00 CET\n"
|
||||
"PO-Revision-Date: 2017-12-08 12:00 CET\n"
|
||||
"Last-Translator: Helmut Merz <helmutm@cy55.de>\n"
|
||||
"Language-Team: loops developers <helmutm@cy55.de>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
@ -89,6 +89,14 @@ msgstr "Thema ändern"
|
|||
msgid "Please correct the indicated errors."
|
||||
msgstr "Bitte berichtigen Sie die angezeigten Fehler."
|
||||
|
||||
msgid "tooltip_sort_column"
|
||||
msgstr "Nach dieser Spalte sortieren"
|
||||
|
||||
# expert (reporting)
|
||||
|
||||
msgid "Download Data"
|
||||
msgstr "Download als Excel-Datei"
|
||||
|
||||
# blog
|
||||
|
||||
msgid "Edit Blog Post..."
|
||||
|
@ -178,6 +186,30 @@ msgstr "Glossareintrag anlegen."
|
|||
msgid "Answer Range"
|
||||
msgstr "Abstufung Bewertungen"
|
||||
|
||||
msgid "Answer Options"
|
||||
msgstr "Antwortmöglichkeiten"
|
||||
|
||||
msgid "Values to select from with corresponding column labels and descriptions. There should be at least answer range items with numeric values."
|
||||
msgstr "Auszuwählende Werte mit zugehörigen Spaltenüberschriften und Beschreibungen. Es sollte mindestens so viele Einträge mit numerischen Werten geben wie durch das Feld 'Abstufung Bewertungen' vorgegeben."
|
||||
|
||||
msgid "No Grouping of Questions"
|
||||
msgstr "Keine Gruppierung der Fragen"
|
||||
|
||||
msgid "The questions should be presented in a linear manner, not grouped by categories or question groups."
|
||||
msgstr "Die Fragen sollen in linearer Reihenfolge ausgegeben und nicht nach Fragengruppen bzw. Kategorien gruppiert werden."
|
||||
|
||||
msgid "Questionnaire Header"
|
||||
msgstr "Infotext zum Fragebogen"
|
||||
|
||||
msgid "Text that will appear at the top of the questionnaire."
|
||||
msgstr "Text, der vor dem Fragebogen erscheinen soll"
|
||||
|
||||
msgid "Feedback Header"
|
||||
msgstr "Infotext zur Auswertung"
|
||||
|
||||
msgid "Text that will appear at the top of the feedback page."
|
||||
msgstr "Text, der oben auf der Auswertungsseite erscheinen soll."
|
||||
|
||||
msgid "Feedback Footer"
|
||||
msgstr "Auswertungs-Hinweis"
|
||||
|
||||
|
@ -193,6 +225,15 @@ msgstr "Mindestanzahl an Antworten"
|
|||
msgid "Minumum number of questions that have to be answered. Empty means all questions have to be answered."
|
||||
msgstr "Anzahl der Fragen, die mindestens beantwortet werden müssen. Keine Angabe: Es müssen alle Fragen beantwortet werden."
|
||||
|
||||
msgid "Question Type"
|
||||
msgstr "Fragentyp"
|
||||
|
||||
msgid "Select the type of the question."
|
||||
msgstr "Bitte den Typ der Frage auswählen."
|
||||
|
||||
msgid "Value Selection"
|
||||
msgstr "Auswahl Bewertung"
|
||||
|
||||
msgid "Required"
|
||||
msgstr "Pflichtfrage"
|
||||
|
||||
|
@ -205,6 +246,9 @@ msgstr "Negative Polarität"
|
|||
msgid "Value inversion: High selection means low value."
|
||||
msgstr "Invertierung der Bewertung: Hohe gewählte Stufe bedeutet niedriger Wert."
|
||||
|
||||
msgid "Question"
|
||||
msgstr "Frage"
|
||||
|
||||
msgid "Questionnaire"
|
||||
msgstr "Fragebogen"
|
||||
|
||||
|
@ -241,15 +285,30 @@ msgstr "Trifft eher zu"
|
|||
msgid "survey_value_3"
|
||||
msgstr "Trifft für unser Unternehmen voll und ganz zu"
|
||||
|
||||
msgid "label_survey_show_report"
|
||||
msgstr "Auswertung anzeigen"
|
||||
|
||||
msgid "label_survey_report_standard"
|
||||
msgstr "Standard-Auswertung"
|
||||
|
||||
msgid "label_survey_report_questions"
|
||||
msgstr "Einzelfragen-Auswertung"
|
||||
|
||||
msgid "Evaluate Questionnaire"
|
||||
msgstr "Fragebogen auswerten"
|
||||
|
||||
msgid "Save Data"
|
||||
msgstr "Daten speichern"
|
||||
|
||||
msgid "Reset Responses Entered"
|
||||
msgstr "Eingaben zurücksetzen"
|
||||
|
||||
msgid "Back to Questionnaire"
|
||||
msgstr "Zurück zum Fragebogen"
|
||||
|
||||
msgid "Your data have been saved."
|
||||
msgstr "Ihre Daten wurden gespeichert."
|
||||
|
||||
msgid "Please answer at least $minAnswers questions."
|
||||
msgstr "Bitte beantworten Sie mindestens $minAnswers Fragen."
|
||||
|
||||
|
@ -262,10 +321,37 @@ msgstr "Bitte beantworten Sie die Pflichtfragen."
|
|||
msgid "Please answer the minimum number of questions."
|
||||
msgstr "Bitte beantworten Sie die angegebene Mindestanzahl an Fragen je Fragengruppe."
|
||||
|
||||
msgid "Please answer the highlighted questions."
|
||||
msgstr "Bitte beantworten Sie die markierten Fragen."
|
||||
|
||||
msgid "Obligatory question, must be answered"
|
||||
msgstr "Pflichtfrage, muss beantwortet werden"
|
||||
|
||||
# competence (qualification)
|
||||
msgid "Score"
|
||||
msgstr "Ergebnis %"
|
||||
|
||||
msgid "Team Score"
|
||||
msgstr "Durchschnitt Team %"
|
||||
|
||||
msgid "Rank"
|
||||
msgstr "Rang"
|
||||
|
||||
msgid "Team Rank"
|
||||
msgstr "Rang Team"
|
||||
|
||||
msgid "Average"
|
||||
msgstr "Durchschnitt"
|
||||
|
||||
msgid "Deviation"
|
||||
msgstr "Abweichung"
|
||||
|
||||
msgid "Team Size"
|
||||
msgstr "Anzahl der vom Team ausgefüllten Fragebögen"
|
||||
|
||||
msgid "if (confirm('Do you really want to reset all response data?')) setRadioButtons('none'); return false"
|
||||
msgstr "if (confirm('Wollen Sie wirklich alle eingegebenen Daten zurücksetzen?')) setRadioButtons('none'); return false"
|
||||
|
||||
# compentence and qualification management
|
||||
|
||||
msgid "Validity Period (Months)"
|
||||
msgstr "Gültigkeitszeitraum (Monate)"
|
||||
|
@ -471,6 +557,8 @@ msgstr "Wer?"
|
|||
msgid "When?"
|
||||
msgstr "Wann?"
|
||||
|
||||
# personal stuff
|
||||
|
||||
msgid "Favorites"
|
||||
msgstr "Lesezeichen"
|
||||
|
||||
|
@ -519,6 +607,8 @@ msgstr "Anmelden"
|
|||
msgid "Presence"
|
||||
msgstr "Anwesenheit"
|
||||
|
||||
# general
|
||||
|
||||
msgid "Actions"
|
||||
msgstr "Aktionen"
|
||||
|
||||
|
@ -531,9 +621,6 @@ msgstr "Informationen über dieses Objekt"
|
|||
msgid "Information about this object."
|
||||
msgstr "Informationen über dieses Objekt."
|
||||
|
||||
msgid "Send a link to this object by email."
|
||||
msgstr "Einen Link zu diesem Objekt per E-Mail versenden."
|
||||
|
||||
msgid "Edit with external editor."
|
||||
msgstr "Mit 'External Editor' bearbeiten."
|
||||
|
||||
|
@ -792,6 +879,9 @@ msgstr "Benutzer registrieren"
|
|||
msgid "Register new member"
|
||||
msgstr "Neu registrieren"
|
||||
|
||||
msgid "Login name not allowed."
|
||||
msgstr "Die von Ihnen eingegebene Benutzerkennung enthält Sonderzeichen, z. B. Umlaute."
|
||||
|
||||
msgid "Login name already taken."
|
||||
msgstr "Die von Ihnen eingegebene Benutzerkennung ist schon vergeben."
|
||||
|
||||
|
@ -846,6 +936,12 @@ msgstr "Beginn"
|
|||
msgid "End date"
|
||||
msgstr "Ende"
|
||||
|
||||
msgid "Start Day"
|
||||
msgstr "Beginn"
|
||||
|
||||
msgid "End Day"
|
||||
msgstr "Ende"
|
||||
|
||||
msgid "Knowledge"
|
||||
msgstr "Kompetenzen"
|
||||
|
||||
|
@ -918,6 +1014,9 @@ msgstr "Kommentare"
|
|||
msgid "Add Comment"
|
||||
msgstr "Kommentar hinzufügen"
|
||||
|
||||
msgid "Email Address"
|
||||
msgstr "E-Mail-Adresse"
|
||||
|
||||
msgid "Subject"
|
||||
msgstr "Thema"
|
||||
|
||||
|
@ -930,6 +1029,9 @@ msgstr "Objekte löschen"
|
|||
msgid "confirm('Do you really want to delete the selected objects?')"
|
||||
msgstr "confirm('Wollen Sie die ausgewählten Objekte wirklich löschen?')"
|
||||
|
||||
msgid "title_bookTopicView"
|
||||
msgstr "Übersicht"
|
||||
|
||||
# management interface
|
||||
|
||||
msgid "label_type"
|
||||
|
@ -992,6 +1094,21 @@ msgstr "Kalender"
|
|||
msgid "Work Items"
|
||||
msgstr "Aktivitäten"
|
||||
|
||||
msgid "Work Item Type"
|
||||
msgstr "Art der Aktivität"
|
||||
|
||||
msgid "Unit of Work"
|
||||
msgstr "Standard-Aktivität"
|
||||
|
||||
msgid "Scheduled Event"
|
||||
msgstr "Termin"
|
||||
|
||||
msgid "Deadline"
|
||||
msgstr "Deadline"
|
||||
|
||||
msgid "Check-up"
|
||||
msgstr "Überprüfung"
|
||||
|
||||
msgid "Work Items for $title"
|
||||
msgstr "Aktivitäten für $title"
|
||||
|
||||
|
@ -1022,6 +1139,12 @@ msgstr "Dauer/Aufwand"
|
|||
msgid "Duration / Effort (hh:mm)"
|
||||
msgstr "Dauer / Aufwand (hh:mm)"
|
||||
|
||||
msgid "Priority"
|
||||
msgstr "Priorität"
|
||||
|
||||
msgid "Activity"
|
||||
msgstr "Leistungsart"
|
||||
|
||||
msgid "Action"
|
||||
msgstr "Aktion"
|
||||
|
||||
|
@ -1096,6 +1219,9 @@ msgstr "Bemerkung"
|
|||
msgid "desc_transition_comments"
|
||||
msgstr "Notizen zum Statusübergang."
|
||||
|
||||
msgid "contact_states"
|
||||
msgstr "Kontaktstatus"
|
||||
|
||||
# state names
|
||||
|
||||
msgid "accepted"
|
||||
|
@ -1164,6 +1290,12 @@ msgstr "unklassifiziert"
|
|||
msgid "verified"
|
||||
msgstr "verifiziert"
|
||||
|
||||
msgid "prospective"
|
||||
msgstr "künftig"
|
||||
|
||||
msgid "inactive"
|
||||
msgstr "inaktiv"
|
||||
|
||||
# transitions
|
||||
|
||||
msgid "accept"
|
||||
|
@ -1238,6 +1370,15 @@ msgstr "verifizieren"
|
|||
msgid "work"
|
||||
msgstr "bearbeiten"
|
||||
|
||||
msgid "activate"
|
||||
msgstr "aktivieren"
|
||||
|
||||
msgid "inactivate"
|
||||
msgstr "inaktiv setzen"
|
||||
|
||||
msgid "reset"
|
||||
msgstr "zurücksetzen"
|
||||
|
||||
# calendar
|
||||
|
||||
msgid "Monday"
|
||||
|
@ -1305,3 +1446,27 @@ msgstr "Zeitraum"
|
|||
|
||||
msgid "Technology"
|
||||
msgstr "Technik"
|
||||
|
||||
# send mail
|
||||
|
||||
msgid "Send a link to this object by email."
|
||||
msgstr "Einen Link zu diesem Objekt per E-Mail versenden."
|
||||
|
||||
msgid "Send Link by Email"
|
||||
msgstr "Link per E-Mail versenden"
|
||||
|
||||
msgid "Mail Subject"
|
||||
msgstr "Betreff"
|
||||
|
||||
msgid "Mail Body"
|
||||
msgstr "Text"
|
||||
|
||||
msgid "Recipients"
|
||||
msgstr "Empfänger"
|
||||
|
||||
msgid "Additional Recipients"
|
||||
msgstr "Weitere Empfänger"
|
||||
|
||||
msgid "Send email"
|
||||
msgstr "E-Mail senden"
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<tal:actions condition="view/showObjectActions">
|
||||
<div metal:use-macro="views/node_macros/object_actions" /></tal:actions>
|
||||
<h1><a tal:omit-tag="python: level > 1"
|
||||
tal:attributes="href request/URL"
|
||||
tal:attributes="href view/requestUrl"
|
||||
tal:content="item/title">Title</a></h1><br />
|
||||
<p tal:define="url python: view.getUrlForTarget(item)">
|
||||
<a tal:omit-tag="view/isAnonymous"
|
||||
|
|
|
@ -228,6 +228,23 @@ We need a principal for testing the login stuff:
|
|||
>>> pwcView.update()
|
||||
False
|
||||
|
||||
Reset Password
|
||||
--------------
|
||||
|
||||
Invalidates the user account by generating a new password. A mail ist sent to
|
||||
the email address of the person with a link for re-activating the account
|
||||
and enter a new password.
|
||||
|
||||
>>> data = {'loginName': u'dummy',
|
||||
... 'action': 'update'}
|
||||
|
||||
>>> request = TestRequest(form=data)
|
||||
|
||||
>>> from loops.organize.browser.member import PasswordReset
|
||||
>>> pwrView = PasswordReset(menu, request)
|
||||
>>> pwrView.update()
|
||||
True
|
||||
|
||||
|
||||
Pure Person-based Authentication
|
||||
================================
|
||||
|
@ -410,7 +427,7 @@ Send Email to Members
|
|||
>>> form.subject
|
||||
u"loops Notification from '$site'"
|
||||
>>> form.mailBody
|
||||
u'\n\nEvent #1\nhttp://127.0.0.1/loops/views/menu/.113\n\n'
|
||||
u'\n\nEvent #1\nhttp://127.0.0.1/loops/views/menu/.118\n\n'
|
||||
|
||||
|
||||
Show Presence of Other Users
|
||||
|
|
|
@ -45,6 +45,12 @@
|
|||
class="loops.organize.browser.member.PasswordChange"
|
||||
permission="zope.View" />
|
||||
|
||||
<browser:page
|
||||
for="loops.interfaces.INode"
|
||||
name="reset_password.html"
|
||||
class="loops.organize.browser.member.PasswordReset"
|
||||
permission="zope.View" />
|
||||
|
||||
<zope:adapter
|
||||
name="task.html"
|
||||
for="loops.interfaces.IConcept
|
||||
|
@ -89,6 +95,12 @@
|
|||
|
||||
<!-- specialized forms -->
|
||||
|
||||
<browser:page
|
||||
name="create_person.html"
|
||||
for="loops.interfaces.INode"
|
||||
class="loops.organize.browser.party.CreatePersonForm"
|
||||
permission="zope.View" />
|
||||
|
||||
<browser:page
|
||||
name="edit_person.html"
|
||||
for="loops.interfaces.INode"
|
||||
|
@ -146,4 +158,12 @@
|
|||
permission="zope.ManageServices"
|
||||
menu="zmi_views" title="Prefix" />
|
||||
|
||||
<!-- utilities -->
|
||||
|
||||
<browser:page
|
||||
for="loops.interfaces.ILoops"
|
||||
name="fix_person_roles"
|
||||
class="loops.organize.browser.member.FixPersonRoles"
|
||||
permission="zope.ManageServices" />
|
||||
|
||||
</configure>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -87,6 +87,7 @@ class BaseMemberRegistration(NodeView):
|
|||
formErrors = dict(
|
||||
confirm_nomatch=FormError(_(u'Password and password confirmation '
|
||||
u'do not match.')),
|
||||
illegal_loginname=FormError(_('Login name not allowed.')),
|
||||
duplicate_loginname=FormError(_('Login name already taken.')),
|
||||
)
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2011 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2016 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -32,7 +32,7 @@ from cybertools.ajax import innerHtml
|
|||
from cybertools.browser.action import actions
|
||||
from cybertools.browser.form import FormController
|
||||
from loops.browser.action import DialogAction
|
||||
from loops.browser.form import EditConceptForm
|
||||
from loops.browser.form import CreateConceptForm, EditConceptForm
|
||||
from loops.browser.node import NodeView
|
||||
from loops.common import adapted
|
||||
from loops.organize.party import getPersonForUser
|
||||
|
@ -44,7 +44,8 @@ organize_macros = ViewPageTemplateFile('view_macros.pt')
|
|||
actions.register('createPerson', 'portlet', DialogAction,
|
||||
title=_(u'Create Person...'),
|
||||
description=_(u'Create a new person.'),
|
||||
viewName='create_concept.html',
|
||||
#viewName='create_concept.html',
|
||||
viewName='create_person.html',
|
||||
dialogName='createPerson',
|
||||
typeToken='.loops/concepts/person',
|
||||
fixedType=True,
|
||||
|
@ -115,24 +116,35 @@ actions.register('send_email', 'object', DialogAction,
|
|||
)
|
||||
|
||||
|
||||
class EditPersonForm(EditConceptForm):
|
||||
class PersonForm(object):
|
||||
|
||||
@Lazy
|
||||
def presetTypesForAssignment(self):
|
||||
types = list(self.typeManager.listTypes(include=('workspace',)))
|
||||
#assigned = [r.context for r in self.assignments]
|
||||
#types = [t for t in types if t.typeProvider not in assigned]
|
||||
predicates = [n for n in ['standard', 'ismember', 'ismaster', 'isowner']
|
||||
if n in self.conceptManager]
|
||||
return [dict(title=t.title, token=t.tokenForSearch, predicates=predicates)
|
||||
for t in types]
|
||||
|
||||
|
||||
class CreatePersonForm(PersonForm, CreateConceptForm):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class EditPersonForm(PersonForm, EditConceptForm):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class SendEmailForm(NodeView):
|
||||
|
||||
__call__ = innerHtml
|
||||
|
||||
def checkPermissions(self):
|
||||
return (not self.isAnonymous and
|
||||
super(SendEmailForm, self).checkPermissions())
|
||||
|
||||
@property
|
||||
def macro(self):
|
||||
return organize_macros.macros['send_email']
|
||||
|
@ -171,6 +183,10 @@ class SendEmailForm(NodeView):
|
|||
|
||||
@Lazy
|
||||
def subject(self):
|
||||
optionKey = 'organize.sendmail_subject'
|
||||
option = self.globalOptions(optionKey) or self.typeOptions(optionKey)
|
||||
if option:
|
||||
return option[0]
|
||||
menu = self.context.getMenu()
|
||||
zdc = IZopeDublinCore(menu)
|
||||
zdc.languageInfo = self.languageInfo
|
||||
|
@ -181,6 +197,12 @@ class SendEmailForm(NodeView):
|
|||
|
||||
class SendEmail(FormController):
|
||||
|
||||
bccToSender = False
|
||||
|
||||
def checkPermissions(self):
|
||||
return (not self.isAnonymous and
|
||||
super(SendEmail, self).checkPermissions())
|
||||
|
||||
def update(self):
|
||||
form = self.request.form
|
||||
subject = form.get('subject') or u''
|
||||
|
@ -193,7 +215,10 @@ class SendEmail(FormController):
|
|||
msg = MIMEText(message.encode('utf-8'), 'plain', 'utf-8')
|
||||
msg['Subject'] = subject.encode('utf-8')
|
||||
msg['From'] = sender
|
||||
msg['To'] = ', '.join(r.strip() for r in recipients if r.strip())
|
||||
recipients = [r.strip() for r in recipients if r.strip()]
|
||||
msg['To'] = ', '.join(recipients)
|
||||
if self.bccToSender:
|
||||
recipients.append(sender)
|
||||
mailhost = component.getUtility(IMailDelivery, 'Mail')
|
||||
mailhost.send(sender, recipients, msg.as_string())
|
||||
return True
|
||||
|
|
|
@ -123,20 +123,24 @@
|
|||
<div class="heading">
|
||||
<span i18n:translate="">Send Link by Email</span> -
|
||||
<span tal:content="view/target/title"></span></div>
|
||||
<metal:content define-macro="mail_content">
|
||||
<div>
|
||||
<label i18n:translate="" for="subject">Subject</label>
|
||||
<label i18n:translate="" for="subject">Mail Subject</label>
|
||||
<div>
|
||||
<input name="subject" id="subject" style="width: 60em"
|
||||
dojoType="dijit.form.ValidationTextBox" required
|
||||
tal:attributes="value view/subject" /></div>
|
||||
</div>
|
||||
<div>
|
||||
<label i18n:translate="" for="mailbody">Mail Body</label>
|
||||
<label i18n:translate=""
|
||||
for="mailbody">Mail Body</label>
|
||||
<div>
|
||||
<textarea name="mailbody" cols="80" rows="4" id="mailbody"
|
||||
dojoType="dijit.form.SimpleTextarea" style="width: 60em"
|
||||
tal:attributes="rows view/contentHeight|string:4"
|
||||
tal:content="view/mailBody"></textarea></div>
|
||||
</div>
|
||||
</metal:content>
|
||||
<div>
|
||||
<label i18n:translate="">Recipients</label>
|
||||
<div tal:repeat="member view/members">
|
||||
|
@ -152,7 +156,8 @@
|
|||
<span i18n:translate="">Toggle all</span></div>
|
||||
</div>
|
||||
<div>
|
||||
<label i18n:translate="" for="addrecipients">Additional recipients</label>
|
||||
<label i18n:translate=""
|
||||
for="addrecipients">Additional Recipients</label>
|
||||
<div>
|
||||
<textarea name="addrecipients" cols="80" rows="4" id="addrecipients"
|
||||
dojoType="dijit.form.SimpleTextarea"
|
||||
|
|
|
@ -45,6 +45,9 @@ to assign comments to this document.
|
|||
>>> home = views['home']
|
||||
>>> home.target = resources['d001.txt']
|
||||
|
||||
>>> from loops.organize.comment.base import commentStates
|
||||
>>> component.provideUtility(commentStates(), name='organize.commentStates')
|
||||
|
||||
Creating comments
|
||||
-----------------
|
||||
|
||||
|
@ -75,6 +78,12 @@ Viewing comments
|
|||
('My comment', u'... ...', u'john')
|
||||
|
||||
|
||||
Reporting
|
||||
=========
|
||||
|
||||
>>> from loops.organize.comment.report import CommentsOverview
|
||||
|
||||
|
||||
Fin de partie
|
||||
=============
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -18,24 +18,55 @@
|
|||
|
||||
"""
|
||||
Base classes for comments/discussions.
|
||||
|
||||
$Id$
|
||||
"""
|
||||
|
||||
from zope.component import adapts
|
||||
from zope.interface import implements
|
||||
from zope.interface import implementer, implements
|
||||
from zope.traversing.api import getParent
|
||||
|
||||
from cybertools.stateful.definition import StatesDefinition
|
||||
from cybertools.stateful.definition import State, Transition
|
||||
from cybertools.stateful.interfaces import IStatesDefinition
|
||||
from cybertools.tracking.btree import Track
|
||||
from cybertools.tracking.interfaces import ITrackingStorage
|
||||
from cybertools.tracking.comment.interfaces import IComment
|
||||
from loops.organize.comment.interfaces import IComment
|
||||
from loops.organize.stateful.base import Stateful
|
||||
from loops import util
|
||||
|
||||
|
||||
class Comment(Track):
|
||||
@implementer(IStatesDefinition)
|
||||
def commentStates():
|
||||
return StatesDefinition('commentStates',
|
||||
State('new', 'new', ('accept', 'reject'), color='red'),
|
||||
State('public', 'public', ('retract', 'reject'), color='green'),
|
||||
State('rejected', 'rejected', ('accept',), color='grey'),
|
||||
Transition('accept', 'accept', 'public'),
|
||||
Transition('reject', 'reject', 'rejected'),
|
||||
Transition('retract', 'retract', 'new'),
|
||||
initialState='new')
|
||||
|
||||
|
||||
class Comment(Stateful, Track):
|
||||
|
||||
implements(IComment)
|
||||
|
||||
metadata_attributes = Track.metadata_attributes + ('state',)
|
||||
index_attributes = metadata_attributes
|
||||
typeName = 'Comment'
|
||||
typeInterface = IComment
|
||||
statesDefinition = 'organize.commentStates'
|
||||
|
||||
contentType = 'text/restructured'
|
||||
|
||||
def __init__(self, taskId, runId, userName, data):
|
||||
super(Comment, self).__init__(taskId, runId, userName, data)
|
||||
self.state = self.getState() # make initial state persistent
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.data['subject']
|
||||
|
||||
def doTransition(self, action):
|
||||
super(Comment, self).doTransition(action)
|
||||
getParent(self).indexTrack(None, self, 'state')
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2012 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -23,15 +23,17 @@ Definition of view classes and other browser related stuff for comments.
|
|||
from zope import interface, component
|
||||
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||
from zope.cachedescriptors.property import Lazy
|
||||
from zope.security import checkPermission
|
||||
|
||||
from cybertools.browser.action import actions
|
||||
from cybertools.tracking.btree import TrackingStorage
|
||||
from loops.browser.action import DialogAction
|
||||
from loops.browser.action import Action, DialogAction
|
||||
from loops.browser.common import BaseView
|
||||
from loops.browser.form import ObjectForm, EditObject
|
||||
from loops.browser.node import NodeView
|
||||
from loops.organize.comment.base import Comment
|
||||
from loops.organize.party import getPersonForUser
|
||||
from loops.organize.stateful.browser import StateAction
|
||||
from loops.organize.tracking.report import TrackDetails
|
||||
from loops.security.common import canAccessObject
|
||||
from loops.setup import addObject
|
||||
|
@ -50,10 +52,17 @@ class CommentsView(NodeView):
|
|||
|
||||
@Lazy
|
||||
def allowed(self):
|
||||
if self.isAnonymous:
|
||||
if self.virtualTargetObject is None:
|
||||
return False
|
||||
return (self.virtualTargetObject is not None and
|
||||
self.globalOptions('organize.allowComments'))
|
||||
opts = (self.globalOptions('organize.allowComments') or
|
||||
self.typeOptions('organize.allowComments'))
|
||||
if not opts:
|
||||
return False
|
||||
if opts is True:
|
||||
opts = []
|
||||
if self.isAnonymous and not 'all' in opts:
|
||||
return False
|
||||
return True
|
||||
|
||||
@Lazy
|
||||
def addUrl(self):
|
||||
|
@ -76,9 +85,47 @@ class CommentsView(NodeView):
|
|||
result.append(CommentDetails(self, tr))
|
||||
return result
|
||||
|
||||
def getActionsFor(self, comment):
|
||||
if not self.globalOptions('organize.showCommentState'):
|
||||
return []
|
||||
if not checkPermission('loops.ViewRestricted', self.context):
|
||||
return []
|
||||
trackUid = util.getUidForObject(comment.track)
|
||||
url = '%s/.%s/change_state.html' % (
|
||||
self.page.virtualTargetUrl, trackUid)
|
||||
onClick = ("objectDialog('change_state', "
|
||||
"'%s?dialog=change_state"
|
||||
"&target_uid=%s'); return false;" % (url, trackUid))
|
||||
stateAct = StateAction(self,
|
||||
definition='organize.commentStates',
|
||||
stateful=comment.track,
|
||||
url=url,
|
||||
onClick=onClick)
|
||||
actions = [stateAct]
|
||||
if not checkPermission('loops.EditRestricted', self.context):
|
||||
return actions
|
||||
baseUrl = self.page.virtualTargetUrl
|
||||
url = '%s/delete_object?uid=%s' % (baseUrl, trackUid)
|
||||
onClick = _("return confirm('Do you really want to delete this object?')")
|
||||
delAct = Action(self,
|
||||
url=url,
|
||||
description=_('Delete Comment'),
|
||||
icon='cybertools.icons/delete.png',
|
||||
cssClass='icon-action',
|
||||
onClick=onClick)
|
||||
actions.append(delAct)
|
||||
return actions
|
||||
|
||||
|
||||
class CommentDetails(TrackDetails):
|
||||
|
||||
@Lazy
|
||||
def poster(self):
|
||||
name = self.track.data.get('name')
|
||||
if name:
|
||||
return name
|
||||
return self.user['title']
|
||||
|
||||
@Lazy
|
||||
def subject(self):
|
||||
return self.track.data['subject']
|
||||
|
@ -108,6 +155,8 @@ class CreateComment(EditObject):
|
|||
|
||||
@Lazy
|
||||
def personId(self):
|
||||
if self.view.isAnonymous:
|
||||
return self.request.form.get('email')
|
||||
p = getPersonForUser(self.context, self.request)
|
||||
if p is not None:
|
||||
return util.getUidForObject(p)
|
||||
|
@ -129,8 +178,11 @@ class CreateComment(EditObject):
|
|||
if ts is None:
|
||||
ts = addObject(rm, TrackingStorage, 'comments', trackFactory=Comment)
|
||||
uid = util.getUidForObject(self.object)
|
||||
ts.saveUserTrack(uid, 0, self.personId, dict(
|
||||
subject=subject, text=text))
|
||||
data = dict(subject=subject, text=text)
|
||||
for k in ('name', 'email'):
|
||||
if k in form:
|
||||
data[k] = form[k]
|
||||
ts.saveUserTrack(uid, 0, self.personId, data)
|
||||
url = self.view.virtualTargetUrl + '?version=this'
|
||||
self.request.response.redirect(url)
|
||||
return False
|
||||
|
|
|
@ -14,10 +14,17 @@
|
|||
<tal:comment tal:repeat="comment items">
|
||||
<br />
|
||||
<div class="comment">
|
||||
<div class="object-actions"
|
||||
tal:define="actions python:comments.getActionsFor(comment)"
|
||||
tal:condition="actions">
|
||||
<tal:actions repeat="action actions">
|
||||
<metal:action use-macro="action/macro" />
|
||||
</tal:actions>
|
||||
</div>
|
||||
<h3>
|
||||
<span tal:content="comment/subject">Subject</span></h3>
|
||||
<div class="info">
|
||||
<span tal:replace="comment/user/title">John</span>,
|
||||
<span tal:replace="comment/poster">John</span>,
|
||||
<span tal:replace="comment/timeStamp">2007-03-30</span>
|
||||
</div>
|
||||
<p class="content"
|
||||
|
@ -44,6 +51,18 @@
|
|||
<input type="hidden" name="contentType" value="text/restructured" />
|
||||
<div class="heading" i18n:translate="">Add Comment</div>
|
||||
<div>
|
||||
<tal:anonymous condition="view/isAnonymous">
|
||||
<label i18n:translate=""
|
||||
for="comment_name">Name</label>
|
||||
<div><input type="text" name="name" id="comment_name"
|
||||
dojoType="dijit.form.ValidationTextBox" required="true"
|
||||
style="width: 60em" /></div>
|
||||
<label i18n:translate=""
|
||||
for="comment_email">Email Address</label>
|
||||
<div><input type="text" name="email" id="comment_email"
|
||||
dojoType="dijit.form.ValidationTextBox" required="true"
|
||||
style="width: 60em" /></div>
|
||||
</tal:anonymous>
|
||||
<label i18n:translate=""
|
||||
for="comment_subject">Subject</label>
|
||||
<div><input type="text" name="subject" id="comment_subject"
|
||||
|
|
|
@ -12,6 +12,10 @@
|
|||
set_schema="cybertools.tracking.comment.interfaces.IComment" />
|
||||
</zope:class>
|
||||
|
||||
<zope:utility
|
||||
factory="loops.organize.comment.base.commentStates"
|
||||
name="organize.commentStates" />
|
||||
|
||||
<!-- views -->
|
||||
|
||||
<browser:page
|
||||
|
@ -33,4 +37,26 @@
|
|||
factory="loops.organize.comment.browser.CreateComment"
|
||||
permission="zope.View" />
|
||||
|
||||
<!-- reporting -->
|
||||
|
||||
<zope:adapter
|
||||
name="list_comments.html"
|
||||
for="loops.interfaces.IConcept
|
||||
zope.publisher.interfaces.browser.IBrowserRequest"
|
||||
provides="zope.interface.Interface"
|
||||
factory="loops.organize.comment.report.CommentsOverview"
|
||||
permission="zope.View" />
|
||||
|
||||
<zope:adapter
|
||||
name="comments_overview"
|
||||
factory="loops.organize.comment.report.CommentsReportInstance"
|
||||
provides="loops.expert.report.IReportInstance"
|
||||
trusted="True" />
|
||||
<zope:class class="loops.organize.comment.report.CommentsReportInstance">
|
||||
<require permission="zope.View"
|
||||
interface="loops.expert.report.IReportInstance" />
|
||||
<require permission="zope.ManageContent"
|
||||
set_schema="loops.expert.report.IReportInstance" />
|
||||
</zope:class>
|
||||
|
||||
</configure>
|
||||
|
|
26
organize/comment/interfaces.py
Normal file
26
organize/comment/interfaces.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
#
|
||||
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
|
||||
"""
|
||||
Interface definitions for comments - discussions - forums.
|
||||
"""
|
||||
|
||||
from zope.interface import Interface, Attribute
|
||||
from zope import schema
|
||||
|
||||
from cybertools.tracking.comment.interfaces import IComment
|
6
organize/comment/loops_comment_de.dmp
Normal file
6
organize/comment/loops_comment_de.dmp
Normal file
|
@ -0,0 +1,6 @@
|
|||
type(u'report', u'Report', options=u'',
|
||||
typeInterface='loops.expert.report.IReport', viewName=u'')
|
||||
concept(u'comments_overview', u'\xdcbersicht Kommentare', u'report',
|
||||
reportType=u'comments_overview')
|
||||
concept(u'comments', u'Kommentare', u'query', options=u'',
|
||||
viewName=u'list_comments.html')
|
75
organize/comment/report.py
Normal file
75
organize/comment/report.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
#
|
||||
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
|
||||
"""
|
||||
Report views and definitions for comments listings and similar stuff.
|
||||
"""
|
||||
|
||||
from cybertools.util.jeep import Jeep
|
||||
from loops.expert.browser.report import ReportConceptView
|
||||
from loops.expert.field import Field, StateField, TargetField
|
||||
from loops.expert.field import TrackDateField
|
||||
from loops.expert.report import ReportInstance, TrackRow
|
||||
|
||||
|
||||
class CommentsOverview(ReportConceptView):
|
||||
|
||||
reportName = 'comments_overview'
|
||||
|
||||
|
||||
timeStamp = TrackDateField('timeStamp', u'Timestamp',
|
||||
description=u'The date and time the comment was posted.',
|
||||
part='dateTime', descending=True,
|
||||
executionSteps=['sort', 'output'])
|
||||
target = TargetField('taskId', u'Target',
|
||||
description=u'The resource or concept the comment was posted at.',
|
||||
executionSteps=['output'])
|
||||
name = Field('name', u'Name',
|
||||
description=u'The name addres of the poster.',
|
||||
executionSteps=['output'])
|
||||
email = Field('email', u'E-Mail Address',
|
||||
description=u'The email addres of the poster.',
|
||||
executionSteps=['output'])
|
||||
subject = Field('subject', u'Subject',
|
||||
description=u'The subject of the comment.',
|
||||
executionSteps=['output'])
|
||||
state = StateField('state', u'State',
|
||||
description=u'The state of the comment.',
|
||||
cssClass='center',
|
||||
statesDefinition='organize.commentStates',
|
||||
executionSteps=['query', 'sort', 'output'])
|
||||
|
||||
|
||||
class CommentsRow(TrackRow):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CommentsReportInstance(ReportInstance):
|
||||
|
||||
type = "comments_overview"
|
||||
label = u'Comments Overview'
|
||||
|
||||
rowFactory = CommentsRow
|
||||
|
||||
fields = Jeep((timeStamp, target, name, email, subject, state))
|
||||
defaultOutputFields = fields
|
||||
defaultSortCriteria = (state, timeStamp)
|
||||
|
||||
def selectObjects(self, parts):
|
||||
return self.recordManager['comments'].values()
|
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
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -79,8 +79,10 @@ class MemberRegistrationManager(object):
|
|||
if pfName is None:
|
||||
pfName = options(self.principalfolder_key,
|
||||
(self.default_principalfolder,))[0]
|
||||
self.createPrincipal(pfName, userId, password, lastName, firstName,
|
||||
useExisting=useExisting)
|
||||
rc = self.createPrincipal(pfName, userId, password,
|
||||
lastName, firstName, useExisting=useExisting)
|
||||
if rc is not None:
|
||||
return rc
|
||||
if not groups:
|
||||
groups = options(self.groups_key, ())
|
||||
self.setGroupsForPrincipal(pfName, userId, groups=groups)
|
||||
|
@ -90,6 +92,8 @@ class MemberRegistrationManager(object):
|
|||
def createPrincipal(self, pfName, userId, password, lastName,
|
||||
firstName=u'', groups=[], useExisting=False,
|
||||
overwrite=False, **kw):
|
||||
if not self.checkPrincipalId(userId):
|
||||
return dict(fieldName='loginName', error='illegal_loginname')
|
||||
pFolder = getPrincipalFolder(self.context, pfName)
|
||||
if IPersonBasedAuthenticator.providedBy(pFolder):
|
||||
pFolder.setPassword(userId, password)
|
||||
|
@ -125,10 +129,18 @@ class MemberRegistrationManager(object):
|
|||
if gFolder is not None:
|
||||
group = gFolder.get(gName)
|
||||
if group is not None:
|
||||
members = list(group.principals)
|
||||
members = [p for p in group.principals
|
||||
if self.checkPrincipalId(p)]
|
||||
members.append(pFolder.prefix + userId)
|
||||
group.principals = members
|
||||
|
||||
def checkPrincipalId(self, pid):
|
||||
try:
|
||||
pid = str(pid)
|
||||
return True
|
||||
except UnicodeEncodeError:
|
||||
return False
|
||||
|
||||
def createPersonForPrincipal(self, pfName, userId, lastName, firstName=u'',
|
||||
useExisting=False, **kw):
|
||||
concepts = self.context.getConceptManager()
|
||||
|
|
|
@ -24,6 +24,7 @@ from persistent.mapping import PersistentMapping
|
|||
from zope import interface, component
|
||||
from zope.app.principalannotation import annotations
|
||||
from zope.app.security.interfaces import IAuthentication, PrincipalLookupError
|
||||
from zope.app.security.interfaces import IUnauthenticatedPrincipal
|
||||
from zope.component import adapts
|
||||
from zope.interface import implements
|
||||
from zope.cachedescriptors.property import Lazy
|
||||
|
@ -57,11 +58,13 @@ PredicateInterfaceSourceList.predicateInterfaces += (IHasRole,)
|
|||
|
||||
|
||||
def getPersonForUser(context, request=None, principal=None):
|
||||
if context is None:
|
||||
return None
|
||||
if principal is None:
|
||||
if request is None:
|
||||
principal = getCurrentPrincipal()
|
||||
else:
|
||||
if request is not None:
|
||||
principal = getattr(request, 'principal', None)
|
||||
else:
|
||||
principal = getPrincipal(context)
|
||||
if principal is None:
|
||||
return None
|
||||
loops = baseObject(context).getLoopsRoot()
|
||||
|
@ -76,6 +79,15 @@ def getPersonForUser(context, request=None, principal=None):
|
|||
return pa.get(util.getUidForObject(loops))
|
||||
|
||||
|
||||
def getPrincipal(context):
|
||||
principal = getCurrentPrincipal()
|
||||
if principal is not None:
|
||||
if IUnauthenticatedPrincipal.providedBy(principal):
|
||||
return None
|
||||
return principal
|
||||
return None
|
||||
|
||||
|
||||
class Person(AdapterBase, BasePerson):
|
||||
""" typeInterface adapter for concepts of type 'person'.
|
||||
"""
|
||||
|
@ -95,9 +107,11 @@ class Person(AdapterBase, BasePerson):
|
|||
return
|
||||
person = getPersonForUser(self.context, principal=principal)
|
||||
if person is not None and person != self.context:
|
||||
name = getName(person)
|
||||
if name:
|
||||
raise ValueError(
|
||||
'Error when creating user %s: There is already a person (%s) assigned to user %s.'
|
||||
% (getName(self.context), getName(person), userId))
|
||||
'There is already a person (%s) assigned to user %s.'
|
||||
% (getName(person), userId))
|
||||
pa = annotations(principal)
|
||||
loopsId = util.getUidForObject(self.context.getLoopsRoot())
|
||||
ann = pa.get(ANNOTATION_KEY)
|
||||
|
|
|
@ -73,7 +73,7 @@ So we are now ready to query the favorites.
|
|||
|
||||
>>> favs = list(favorites.query(userName=johnCId))
|
||||
>>> favs
|
||||
[<Favorite ['27', 1, '33', '...']: {'type': 'favorite'}>]
|
||||
[<Favorite ['27', 1, '33', '...']: {'type': 'favorite', 'order': 100}>]
|
||||
|
||||
>>> list(favAdapted.list(johnC))
|
||||
['27']
|
||||
|
|
|
@ -57,15 +57,22 @@ class FavoriteView(NodeView):
|
|||
def listFavorites(self):
|
||||
if self.favorites is None:
|
||||
return
|
||||
for uid in self.favorites.list(self.person):
|
||||
self.registerDojoDnd()
|
||||
form = self.request.form
|
||||
if 'favorites_change_order' in form:
|
||||
uids = form.get('favorite_uids')
|
||||
if uids:
|
||||
self.favorites.reorder(uids)
|
||||
for trackUid, uid in self.favorites.listWithTracks(self.person):
|
||||
obj = util.getObjectForUid(uid)
|
||||
if obj is not None:
|
||||
adobj = adapted(obj)
|
||||
yield dict(url=self.getUrlForTarget(obj),
|
||||
uid=uid,
|
||||
title=adobj.favTitle,
|
||||
description=adobj.description,
|
||||
object=obj)
|
||||
title=obj.title,
|
||||
description=obj.description,
|
||||
object=obj,
|
||||
trackUid=trackUid)
|
||||
|
||||
def add(self):
|
||||
if self.favorites is None:
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
<metal:actions define-macro="favorites_portlet"
|
||||
tal:define="view nocall:context/@@favorites_view;
|
||||
targetUid view/targetUid">
|
||||
<div tal:repeat="item view/listFavorites">
|
||||
<span style="float:right" class="delete-item"> <a href="removeFavorite.html"
|
||||
<form method="post">
|
||||
<div dojoType="dojo.dnd.Source" withHandles="true" id="favorites_list">
|
||||
<div class="dojoDndItem dojoDndHandle" style="padding: 0"
|
||||
tal:repeat="item view/listFavorites">
|
||||
<span style="float:right" class="delete-item"> <a
|
||||
tal:attributes="href
|
||||
string:${view/virtualTargetUrl}/removeFavorite.html?id=${item/uid};
|
||||
title string:Remove from favorites"
|
||||
|
@ -10,7 +13,22 @@
|
|||
<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>
|
||||
<input type="submit" style="display: none"
|
||||
name="favorites_change_order" id="favorites_change_order"
|
||||
value="Save Changes"
|
||||
i18n:attributes="value" />
|
||||
<script language="javascript">
|
||||
dojo.subscribe('/dnd/drop', function(data) {
|
||||
if (data.node.id == 'favorites_list') {
|
||||
dojo.byId('favorites_change_order').style.display = ''}});
|
||||
</script>
|
||||
</div>
|
||||
</form>
|
||||
<div id="addFavorite" class="action"
|
||||
tal:condition="targetUid">
|
||||
<a i18n:translate=""
|
||||
|
|
|
@ -41,12 +41,16 @@ class Favorites(object):
|
|||
for item in self.listTracks(person, sortKey, type):
|
||||
yield item.taskId
|
||||
|
||||
def listWithTracks(self, person, sortKey=None, type='favorite'):
|
||||
for item in self.listTracks(person, sortKey, type):
|
||||
yield util.getUidForObject(item), item.taskId
|
||||
|
||||
def listTracks(self, person, sortKey=None, type='favorite'):
|
||||
if person is None:
|
||||
return
|
||||
personUid = util.getUidForObject(person)
|
||||
if sortKey is None:
|
||||
sortKey = lambda x: -x.timeStamp
|
||||
sortKey = lambda x: (x.data.get('order', 100), -x.timeStamp)
|
||||
for item in sorted(self.context.query(userName=personUid), key=sortKey):
|
||||
if type is not None:
|
||||
if item.type != type:
|
||||
|
@ -59,7 +63,7 @@ class Favorites(object):
|
|||
uid = util.getUidForObject(obj)
|
||||
personUid = util.getUidForObject(person)
|
||||
if data is None:
|
||||
data = {'type': 'favorite'}
|
||||
data = {'type': 'favorite', 'order': 100}
|
||||
if nodups:
|
||||
for track in self.context.query(userName=personUid, taskId=uid):
|
||||
if track.type == data['type']: # already present
|
||||
|
@ -78,6 +82,18 @@ class Favorites(object):
|
|||
self.context.removeTrack(track)
|
||||
return changed
|
||||
|
||||
def reorder(self, uids):
|
||||
offset = 0
|
||||
for idx, uid in enumerate(uids):
|
||||
track = util.getObjectForUid(uid)
|
||||
if track is not None:
|
||||
data = track.data
|
||||
order = data.get('order', 100)
|
||||
if order < idx or (order >= 100 and order < idx + 100):
|
||||
offset = 100
|
||||
data['order'] = idx + offset
|
||||
track.data = data
|
||||
|
||||
|
||||
class Favorite(Track):
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue