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

Title + + + + + + + + diff --git a/browser/configure.zcml b/browser/configure.zcml index a82194e..569d618 100644 --- a/browser/configure.zcml +++ b/browser/configure.zcml @@ -125,7 +125,7 @@ @@ -365,7 +365,7 @@ + + + + + tal:define="item nocall:view/targetItem"> @@ -52,7 +52,7 @@ + tal:define="item nocall:view/targetItem">

Object Information


+ + + + + + + + + + + + + +

Feedback

+
+
diff --git a/browser/loops.css b/browser/loops.css index d6b9acc..eabe375 100644 --- a/browser/loops.css +++ b/browser/loops.css @@ -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 { diff --git a/browser/loops.js b/browser/loops.js index 7da13e2..56a3813 100755 --- a/browser/loops.js +++ b/browser/loops.js @@ -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; } }); } diff --git a/browser/node.py b/browser/node.py index 9635360..e1b9864 100755 --- a/browser/node.py +++ b/browser/node.py @@ -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 tag show the current object - traversedNames[-1] = name + # traversedNames[-1] = name + # let tag show the current object + traversedNames.append(name) return name def getTarget(self, name): diff --git a/browser/node_macros.pt b/browser/node_macros.pt index 57c5297..a5f8d75 100644 --- a/browser/node_macros.pt +++ b/browser/node_macros.pt @@ -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')"> The body @@ -41,17 +41,22 @@ - +
+ ondblclick python:item.openEditWindow('configure.html')"> Node Body
- -
- +
+
+
+
+
@@ -328,11 +333,12 @@ + tal:attributes="href string:${view/topMenu/url}/login.html" + i18n:translate="">Log in
diff --git a/browser/resource.py b/browser/resource.py index 7d35c0c..323d92b 100644 --- a/browser/resource.py +++ b/browser/resource.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 @@ -20,6 +20,7 @@ View class for resource objects. """ +import os.path import urllib from zope.cachedescriptors.property import Lazy from zope import component @@ -47,7 +48,7 @@ from loops.browser.common import EditForm, BaseView from loops.browser.concept import BaseRelationView, ConceptRelationView from loops.browser.concept import ConceptConfigureView from loops.browser.node import NodeView, node_macros -from loops.common import adapted, NameChooser, normalizeName +from loops.common import adapted, baseObject, NameChooser, normalizeName from loops.interfaces import IBaseResource, IDocument, ITextDocument from loops.interfaces import IMediaAsset as legacy_IMediaAsset from loops.interfaces import ITypeConcept @@ -196,6 +197,9 @@ class ResourceView(BaseView): context = self.context ct = context.contentType response = self.request.response + if self.typeOptions('x_robots_tag_header', None) is not None: + tagVal = ', '.join(self.typeOptions('x_robots_tag_header')) + response.setHeader('X-Robots-Tag', tagVal) self.recordAccess('show', target=self.uniqueId) if ct.startswith('image/'): #response.setHeader('Cache-Control', 'public,max-age=86400') @@ -214,8 +218,18 @@ class ResourceView(BaseView): data = context.data if useAttachment: if filename is None: - filename = (adapted(self.context).localFilename or + filename = (adapted(self.context).localFilename or getName(self.context)) + if self.typeOptions('use_title_for_download_filename'): + base, ext = os.path.splitext(filename) + filename = context.title + vr = IVersionable(baseObject(context)) + if len(vr.versions) > 0: + filename = vr.generateName(filename, ext, vr.versionId) + else: + if not filename.endswith(ext): + filename += ext + filename = filename.encode('UTF-8') if self.typeOptions('no_normalize_download_filename'): filename = '"%s"' % filename else: @@ -262,11 +276,17 @@ class ResourceView(BaseView): #return util.toUnicode(wp.render(self.request)) return super(ResourceView, self).renderText(text, contentType) + showMore = True + def renderShortText(self): return self.renderDescription() or self.createShortText(self.render()) def createShortText(self, text=None): - return extractFirstPart(text or self.render()) + text = (text or self.render()).strip() + shortText = extractFirstPart(text) + if shortText == text: + self.showMore = False + return shortText def download(self): """ Force download, e.g. of a PDF file """ @@ -471,4 +491,3 @@ class NoteView(DocumentView): def linkUrl(self): ad = self.typeAdapter return ad and ad.linkUrl or '' - diff --git a/browser/resource_macros.pt b/browser/resource_macros.pt index 8a70e4f..c06c93f 100644 --- a/browser/resource_macros.pt +++ b/browser/resource_macros.pt @@ -10,7 +10,7 @@

Title

@@ -51,7 +51,7 @@

Title


@@ -96,6 +96,7 @@
+
diff --git a/browser/skin/__init__.py b/browser/skin/__init__.py index 0babeb0..8ee0148 100644 --- a/browser/skin/__init__.py +++ b/browser/skin/__init__.py @@ -1,6 +1,4 @@ -""" -$Id$ -""" +# package loops.browser.skin from cybertools.browser.liquid import Liquid from cybertools.browser.blue import Blue diff --git a/browser/skin/lobo/body.pt b/browser/skin/lobo/body.pt index 62f529c..e6e7b59 100644 --- a/browser/skin/lobo/body.pt +++ b/browser/skin/lobo/body.pt @@ -69,9 +69,9 @@ metal:define-macro="footer"> © Copyright , - cyberconcepts IT-Consulting Dr. Helmut Merz - (Impressum) + + (Impressum)
Powered by loops · diff --git a/browser/skin/lobo/lobo.css b/browser/skin/lobo/lobo.css index 30fdd1a..f9b572f 100644 --- a/browser/skin/lobo/lobo.css +++ b/browser/skin/lobo/lobo.css @@ -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; diff --git a/browser/skin/lobo/print.css b/browser/skin/lobo/print.css index 19dd977..16fccfa 100644 --- a/browser/skin/lobo/print.css +++ b/browser/skin/lobo/print.css @@ -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; } + diff --git a/browser/target_macros.pt b/browser/target_macros.pt index dce40c0..b87dd62 100644 --- a/browser/target_macros.pt +++ b/browser/target_macros.pt @@ -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"> Create Target + tal:attributes="action view/requestUrl">
Name @@ -113,7 +113,7 @@ + tal:attributes="action view/requestUrl">
= 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): diff --git a/classifier/browser.py b/classifier/browser.py index 3ef92a5..e0fc935 100644 --- a/classifier/browser.py +++ b/classifier/browser.py @@ -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 diff --git a/classifier/tests.py b/classifier/tests.py index e3a7512..e6bb966 100755 --- a/classifier/tests.py +++ b/classifier/tests.py @@ -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." diff --git a/common.py b/common.py index 2051549..3ec247c 100644 --- a/common.py +++ b/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', diff --git a/compound/book/browser.py b/compound/book/browser.py index fb6fcf6..cb4b44e 100644 --- a/compound/book/browser.py +++ b/compound/book/browser.py @@ -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'] diff --git a/compound/book/view_macros.pt b/compound/book/view_macros.pt index c85a69c..65ac6a4 100644 --- a/compound/book/view_macros.pt +++ b/compound/book/view_macros.pt @@ -130,7 +130,8 @@ + textResources textResources|item/textResources; + resources item/otherResources">

Children

@@ -145,14 +146,25 @@ -
-
+ + diff --git a/concept.py b/concept.py index cf3f5ae..cb7226e 100644 --- a/concept.py +++ b/concept.py @@ -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): diff --git a/configure.zcml b/configure.zcml index 701176b..a29445a 100644 --- a/configure.zcml +++ b/configure.zcml @@ -1,6 +1,7 @@ @@ -478,6 +479,19 @@ component="loops.view.NodeTypeSourceList" name="loops.nodeTypeSource" /> + + + + + + diff --git a/data/loops_std_de.dmp b/data/loops_std_de.dmp index 3bb354a..ad10096 100644 --- a/data/loops_std_de.dmp +++ b/data/loops_std_de.dmp @@ -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') diff --git a/data/loops_std_en.dmp b/data/loops_std_en.dmp index a982ee7..f2fadb5 100644 --- a/data/loops_std_en.dmp +++ b/data/loops_std_en.dmp @@ -1,60 +1,99 @@ +# types type(u'query', u'Query', options=u'', typeInterface='loops.expert.concept.IQueryConcept', viewName=u'') +type(u'datatable', u'Data Table', options=u'action.portlet:edit_concept', + typeInterface='loops.table.IDataTable', viewName=u'') type(u'task', u'Task', options=u'', typeInterface='loops.knowledge.interfaces.ITask', viewName=u'') type(u'domain', u'Domain', options=u'', typeInterface=u'', viewName=u'') type(u'classifier', u'Classifier', options=u'', - typeInterface='loops.classifier.interfaces.IClassifier', viewName=u'classifier.html') + typeInterface='loops.classifier.interfaces.IClassifier', + viewName=u'classifier.html') type(u'documenttype', u'Document Type', options=u'', typeInterface=u'', viewName=u'') type(u'extcollection', u'External Collection', options=u'', typeInterface='loops.integrator.interfaces.IExternalCollection', viewName=u'collection.html') +type(u'folder', u'Ordner', options=u'', typeInterface=u'', viewName=u'') type(u'glossaryitem', u'Glossary Item', options=u'', typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'glossaryitem.html') type(u'media_asset', u'Media Asset', - options=u'storage:varsubdir\nstorage_parameters:extfiles/sites_zzz\nasset_transform.minithumb: size(105)\nasset_transform.small: size(230)\nasset_transform.medium: size(480)', typeInterface='loops.media.interfaces.IMediaAsset', viewName=u'image_medium.html') + options=u'storage:varsubdir\nstorage_parameters:extfiles/sites_zzz\nasset_transform.minithumb: size(105)\nasset_transform.small: size(230)\nasset_transform.medium: size(480)', typeInterface='loops.media.interfaces.IMediaAsset', + viewName=u'image_medium.html') type(u'note', u'Note', options=u'', typeInterface='loops.interfaces.INote', viewName='note.html') type(u'person', u'Person', options=u'', typeInterface='loops.knowledge.interfaces.IPerson', viewName=u'') type(u'predicate', u'Predicate', options=u'', typeInterface=u'loops.interfaces.IPredicate', viewName=u'') -type(u'event', u'Event', options=u'', typeInterface='loops.organize.interfaces.ITask', +type(u'event', u'Event', options=u'', + typeInterface='loops.organize.interfaces.ITask', viewName=u'task.html') -type(u'textdocument', u'Text', options=u'', typeInterface='loops.interfaces.ITextDocument', viewName=u'') -type(u'topic', u'Topy', options=u'', typeInterface='loops.knowledge.interfaces.ITopic', - viewName=u'') -type(u'type', u'Type', options=u'', typeInterface='loops.interfaces.ITypeConcept', - viewName=u'') +type(u'textdocument', u'Text', options=u'', + typeInterface='loops.interfaces.ITextDocument', viewName=u'') +type(u'topic', u'Topic', options=u'action.portlet:editTopic,createTopic', + typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'') +type(u'type', u'Type', options=u'', + typeInterface='loops.interfaces.ITypeConcept', viewName=u'') + +#domains +concept(u'general', u'General', u'domain') +concept(u'system', u'System', u'domain') + +# predicates concept(u'depends', u'depends', u'predicate') concept(u'follows', u'follows', u'predicate') -concept(u'general', u'General', u'domain') -concept(u'glossary', u'Glossary', u'query', options=u'', viewName=u'glossary.html') concept(u'hasType', u'has Type', u'predicate') concept(u'ispartof', u'is Part of', u'predicate') concept(u'issubtype', u'is Subtype', u'predicate') concept(u'knows', u'knows', u'predicate') concept(u'ownedby', u'owned by', u'predicate') -concept(u'personal_info', u'Personal Information', u'query', options=u'', - viewName=u'personal_info.html') concept(u'provides', u'provides', u'predicate') concept(u'querytarget', u'is Query Target', u'predicate') concept(u'requires', u'requires', u'predicate') -concept(u'search', u'Search', u'query', options=u'', viewName=u'search') concept(u'standard', u'subobject', u'predicate') -concept(u'system', u'System', u'domain') + +#queries +concept(u'events', u'Events', u'query', options=u'delta:2', + viewName=u'list_events.html') +concept(u'glossary', u'Glossary', u'query', options=u'', viewName=u'glossary.html') +concept(u'personal_info', u'Personal Information', u'query', options=u'', + viewName=u'personal_info.html') +concept(u'participants', u'Participants', u'query', options=u'', + viewName=u'list_children.html') +concept(u'recenct_changes', u'Recent Changes', u'query', + options=u'types:concept:*,resource:*', + viewName=u'recent_changes.html') +concept(u'search', u'Search', u'query', options=u'', viewName=u'search') +concept(u'topics', u'Topics', u'query', options=u'action.portlet:createTopic', + viewName=u'list_children.html') + +# child assignments child(u'general', u'documenttype', u'standard') child(u'general', u'event', u'standard') +child(u'general', u'events', u'standard') +child(u'general', u'participants', u'standard') +child(u'general', u'topics', u'standard') child(u'system', u'classifier', u'standard') child(u'system', u'extcollection', u'standard') child(u'system', u'issubtype', u'standard') child(u'system', u'media_asset', u'standard') child(u'system', u'personal_info', u'standard') -node(u'home', u'Homepage', '', 'menu', body=u'Welcome\n=======) -node(u'participants', u'Participants', u'home', 'page', - body=u'Participants\n============', target=u'concepts/person', - viewName=u'listchildren') -node(u'topics', u'Topics', u'home', 'page', body=u'Topics\n======', - target=u'concepts/topic', viewName=u'listchildren') +child(u'topic', u'topic', u'issubtype', 1) + +# resources +resource(u'homepage', u'Welcome', u'textdocument', + contentType='text/restructured') +resource(u'impressum', u'Legal Information', u'textdocument', + contentType='text/restructured') + +#nodes +node(u'home', u'Home', '', 'menu') +node(u'welcome', u'Welcome', u'home', u'text', + target=u'resources/homepage') +node(u'participants', u'Participants', u'home', 'page', + target=u'concepts/participants') +node(u'topics', u'Topics', u'home', 'page', target=u'concepts/topics') node(u'glossary', u'Glossary', u'home', 'page', target=u'concepts/glossary') node(u'search', u'Search', u'home', 'page', target=u'concepts/search') +node(u'impressum', u'Legal Information', u'home', u'info', + target=u'resources/impressum') diff --git a/data/loops_std_update_de.dmp b/data/loops_std_update_de.dmp new file mode 100644 index 0000000..23c9be3 --- /dev/null +++ b/data/loops_std_update_de.dmp @@ -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') diff --git a/expert/README.txt b/expert/README.txt index d3fd786..35fe082 100644 --- a/expert/README.txt +++ b/expert/README.txt @@ -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())) diff --git a/expert/browser/configure.zcml b/expert/browser/configure.zcml index 5dadb3a..bd82ff6 100644 --- a/expert/browser/configure.zcml +++ b/expert/browser/configure.zcml @@ -91,4 +91,26 @@ factory="loops.expert.browser.report.ResultsConceptView" permission="zope.View" /> + + + + + + diff --git a/expert/browser/export.py b/expert/browser/export.py new file mode 100644 index 0000000..90a4c65 --- /dev/null +++ b/expert/browser/export.py @@ -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 '???' + diff --git a/expert/browser/report.pt b/expert/browser/report.pt index 7a795fd..fef05d5 100755 --- a/expert/browser/report.pt +++ b/expert/browser/report.pt @@ -3,10 +3,13 @@
-
+ +
+
@@ -23,29 +26,65 @@
+
+
+
+
+
+
+ +
- - - - - -
-
- - -
-
- + +
+ + + + + + + +
+
+ + +
+
+ + + +
+ + +
+ +
@@ -113,7 +152,14 @@ - + diff --git a/expert/browser/report.py b/expert/browser/report.py index 940a89b..7c62e76 100755 --- a/expert/browser/report.py +++ b/expert/browser/report.py @@ -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. """ diff --git a/expert/browser/results.pt b/expert/browser/results.pt index 4f66f78..e8997fd 100644 --- a/expert/browser/results.pt +++ b/expert/browser/results.pt @@ -25,29 +25,62 @@
-
+
+
+ +
+
+
+ + +
+
+ + +
+
- - - + + - +
+ + + + + +
- -
+ +
- - + +
@@ -78,6 +111,17 @@ + + + + + + + + + >> t = searchView.typesForSearch() >>> len(t) - 15 + 16 >>> t.getTermByToken('loops:resource:*').title 'Any Resource' >>> t = searchView.conceptTypesForSearch() >>> len(t) - 12 + 13 >>> t.getTermByToken('loops:concept:*').title 'Any Concept' @@ -91,7 +91,7 @@ a controller attribute for the search view. >>> searchView.submitReplacing('1.results', '1.search.form', pageView) 'submitReplacing("1.results", "1.search.form", - "http://127.0.0.1/loops/views/page/.target96/@@searchresults.html");...' + "http://127.0.0.1/loops/views/page/.target.../@@searchresults.html");...' Basic (text/title) search ------------------------- @@ -177,7 +177,7 @@ of the concepts' titles: >>> request = TestRequest(form=form) >>> view = Search(page, request) >>> view.listConcepts() - '{"items": [{"id": "101", "name": "Zope", "label": "Zope (Thema)"}, {"id": "103", "name": "Zope 2", "label": "Zope 2 (Thema)"}, {"id": "105", "name": "Zope 3", "label": "Zope 3 (Thema)"}], "identifier": "id"}' + u"{identifier: 'id', items: [{label: 'Zope (Thema)', name: 'Zope', id: '...'}, {label: 'Zope 2 (Thema)', name: 'Zope 2', id: '...'}, {label: 'Zope 3 (Thema)', name: 'Zope 3', id: '...'}]}" Preset Concept Types on Search Forms ------------------------------------ @@ -219,13 +219,13 @@ and thus include the customer type in the preset search types. >>> searchView.conceptsForType('loops:concept:customer') [{'token': 'none', 'title': u'not selected'}, - {'token': '74', 'title': u'Customer 1'}, - {'token': '76', 'title': u'Customer 2'}, - {'token': '78', 'title': u'Customer 3'}] + {'token': '...', 'title': u'Customer 1'}, + {'token': '...', 'title': u'Customer 2'}, + {'token': '...', 'title': u'Customer 3'}] Let's use this new search option for querying: - >>> form = {'search.4.text_selected': u'74'} + >>> form = {'search.4.text_selected': u'75'} >>> resultsView = SearchResults(page, TestRequest(form=form)) >>> results = list(resultsView.results) >>> results[0].title diff --git a/external/README.txt b/external/README.txt index 19b8a55..4d8c045 100644 --- a/external/README.txt +++ b/external/README.txt @@ -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 -------------------------------------------------- diff --git a/external/pyfunc.py b/external/pyfunc.py index 83a0293..b4f7969 100644 --- a/external/pyfunc.py +++ b/external/pyfunc.py @@ -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: diff --git a/i18n/browser.py b/i18n/browser.py index d01bcb1..733366d 100644 --- a/i18n/browser.py +++ b/i18n/browser.py @@ -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) diff --git a/integrator/browser.py b/integrator/browser.py index aeafa87..e76c012 100644 --- a/integrator/browser.py +++ b/integrator/browser.py @@ -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 diff --git a/integrator/collection.py b/integrator/collection.py index 167974b..b33fe4f 100644 --- a/integrator/collection.py +++ b/integrator/collection.py @@ -24,8 +24,10 @@ file system. from datetime import datetime from logging import getLogger import os, re, stat +import transaction from zope.app.container.interfaces import INameChooser +from zope.app.container.contained import ObjectRemovedEvent from zope.cachedescriptors.property import Lazy from zope import component from zope.component import adapts @@ -51,6 +53,8 @@ from loops.versioning.interfaces import IVersionable TypeInterfaceSourceList.typeInterfaces += (IExternalCollection,) +logger = getLogger('loops.integrator.collection') + class ExternalCollectionAdapter(AdapterBase): """ A concept adapter for accessing an external collection. @@ -66,7 +70,7 @@ class ExternalCollectionAdapter(AdapterBase): newResources = None updateMessage = None - + def getExclude(self): return getattr(self.context, '_exclude', None) or [] def setExclude(self, value): @@ -83,10 +87,11 @@ class ExternalCollectionAdapter(AdapterBase): print '###', vaddr, vobj, vid versions.add(vaddr) new = [] - oldFound = [] + oldFound = set([]) provider = component.getUtility(IExternalCollectionProvider, name=self.providerName or '') #print '*** old', old, versions, self.lastUpdated + changeCount = 0 for addr, mdate in provider.collect(self): #print '***', addr, mdate if addr in versions: @@ -94,8 +99,9 @@ class ExternalCollectionAdapter(AdapterBase): if addr in old: # may be it would be better to return a file's hash # for checking for changes... - oldFound.append(addr) + oldFound.add(addr) if self.lastUpdated is None or (mdate and mdate > self.lastUpdated): + changeCount +=1 obj = old[addr] # update settings and regenerate scale variant for media asset adobj = adapted(obj) @@ -110,29 +116,41 @@ class ExternalCollectionAdapter(AdapterBase): self.updateMessage = message # force reindexing notify(ObjectModifiedEvent(obj)) + if changeCount % 10 == 0: + logger.info('Updated: %i.' % changeCount) + transaction.commit() else: new.append(addr) + logger.info('%i objects updated.' % changeCount) + transaction.commit() if new: self.newResources = provider.createExtFileObjects(self, new) for r in self.newResources: self.context.assignResource(r) + logger.info('%i objects created.' % len(new)) + transaction.commit() for addr in old: if str(addr) not in oldFound: # not part of the collection any more # TODO: only remove from collection but keep object? self.remove(old[addr]) + transaction.commit() for r in self.context.getResources(): adobj = adapted(r) if self.metaInfo != adobj.metaInfo and ( not adobj.metaInfo or self.overwriteMetaInfo): adobj.metaInfo = self.metaInfo self.lastUpdated = datetime.today() + logger.info('External collection updated.') + transaction.commit() def clear(self): for obj in self.context.getResources(): self.remove(obj) def remove(self, obj): + logger.info('Removing object: %s.' % getName(obj)) + notify(ObjectRemovedEvent(obj)) del self.resourceManager[getName(obj)] @Lazy @@ -187,7 +205,7 @@ class DirectoryCollectionProvider(object): for k, v in self.extFileTypeMapping.items()) container = client.context.getLoopsRoot().getResourceManager() directory = self.getDirectory(client) - for addr in addresses: + for idx, addr in enumerate(addresses): name = self.generateName(container, addr) title = self.generateTitle(addr) contentType = guess_content_type(addr, @@ -200,9 +218,8 @@ class DirectoryCollectionProvider(object): if extFileType is None: extFileType = extFileTypes['image/*'] if extFileType is None: - getLogger('loops.integrator.collection.DirectoryCollectionProvider' - ).warn('No external file type found for %r, ' - 'content type: %r' % (name, contentType)) + logger.warn('No external file type found for %r, ' + 'content type: %r' % (name, contentType)) obj = addAndConfigureObject( container, Resource, name, title=title, @@ -219,6 +236,9 @@ class DirectoryCollectionProvider(object): message = client.updateMessage or u'' message += u'
'.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): diff --git a/integrator/collection_macros.pt b/integrator/collection_macros.pt index 8c3e898..57e5a4b 100644 --- a/integrator/collection_macros.pt +++ b/integrator/collection_macros.pt @@ -2,7 +2,7 @@ + tal:condition="item/update"> - diff --git a/knowledge/glossary/view_macros.pt b/knowledge/glossary/view_macros.pt index 88bb440..3219627 100755 --- a/knowledge/glossary/view_macros.pt +++ b/knowledge/glossary/view_macros.pt @@ -4,20 +4,28 @@ tal:define="data item/childrenAlphaGroups">
-
+
A + +
+
+ + A
 
-
+
A
diff --git a/knowledge/knowledge_macros.pt b/knowledge/knowledge_macros.pt index f775b71..f51308b 100644 --- a/knowledge/knowledge_macros.pt +++ b/knowledge/knowledge_macros.pt @@ -1,6 +1,27 @@ + +
+
+ Organisation/Team: + + + +
+ +
+ +
@@ -32,28 +53,32 @@ -

Candidates for Task

- - - - - - - - - - -
CandidateFitKnowledge
- - - - - , -
+
+

Candidates for Task

+ + + + + + + + + + +
CandidateFitKnowledge
+ + + + + , +
+
diff --git a/knowledge/qualification/browser.py b/knowledge/qualification/browser.py index 29b53d6..4a98f70 100644 --- a/knowledge/qualification/browser.py +++ b/knowledge/qualification/browser.py @@ -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 diff --git a/knowledge/qualification/configure.zcml b/knowledge/qualification/configure.zcml index dea246f..e4f02c9 100644 --- a/knowledge/qualification/configure.zcml +++ b/knowledge/qualification/configure.zcml @@ -15,4 +15,50 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/knowledge/qualification/report.py b/knowledge/qualification/report.py new file mode 100644 index 0000000..0bbc0b0 --- /dev/null +++ b/knowledge/qualification/report.py @@ -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) + diff --git a/knowledge/survey/base.py b/knowledge/survey/base.py index cacdcc7..523364e 100644 --- a/knowledge/survey/base.py +++ b/knowledge/survey/base.py @@ -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): diff --git a/knowledge/survey/browser.py b/knowledge/survey/browser.py index d9075d6..4107c94 100644 --- a/knowledge/survey/browser.py +++ b/knowledge/survey/browser.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2013 Helmut Merz helmutm@cy55.de +# Copyright (c) 2016 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -23,6 +23,7 @@ surveys and self-assessments. import csv from cStringIO import StringIO +import math from zope.app.pagetemplate import ViewPageTemplateFile from zope.cachedescriptors.property import Lazy from zope.i18n import translate @@ -31,64 +32,318 @@ from cybertools.knowledge.survey.questionnaire import Response from cybertools.util.date import formatTimeStamp from loops.browser.concept import ConceptView from loops.browser.node import NodeView -from loops.common import adapted +from loops.common import adapted, baseObject +from loops.knowledge.browser import InstitutionMixin from loops.knowledge.survey.response import Responses from loops.organize.party import getPersonForUser +from loops.security.common import checkPermission from loops.util import getObjectForUid from loops.util import _ template = ViewPageTemplateFile('view_macros.pt') -class SurveyView(ConceptView): +class SurveyView(InstitutionMixin, ConceptView): data = None - errors = None + errors = message = None + batchSize = 12 + teamData = None + + template = template + + #adminMaySelectAllInstitutions = False @Lazy def macro(self): self.registerDojo() return template.macros['survey'] + @Lazy + def title(self): + title = self.context.title + if self.personId: + person = adapted(getObjectForUid(self.personId)) + if person is not None: + return '%s: %s' % (title, person.title) + return title + @Lazy def tabview(self): if self.editable: return 'index.html' - def results(self): + def getUrlParamString(self): + qs = super(SurveyView, self).getUrlParamString() + if qs.startswith('?report='): + return '' + return qs + + @Lazy + def personId(self): + return self.request.form.get('person') + + @Lazy + def report(self): + return self.request.form.get('report') + + @Lazy + def questionnaireType(self): + return self.adapted.questionnaireType + + def teamReports(self): + if self.adapted.teamBasedEvaluation: + if checkPermission('loops.ViewRestricted', self.context): + return [dict(name='standard', label='label_survey_report_standard'), + dict(name='questions', + label='label_survey_report_questions')] + + def update(self): + instUid = self.request.form.get('select_institution') + if instUid: + return self.setInstitution(instUid) + + @Lazy + def groups(self): result = [] - response = None + if self.questionnaireType == 'pref_selection': + groups = [g.questions for g in + self.adapted.getQuestionGroups(self.personId)] + questions = [] + for idxg, g in enumerate(groups): + qus = [] + for idxq, qu in enumerate(g): + questions.append((idxg + 3 * idxq, idxg, qu)) + questions.sort() + questions = [item[2] for item in questions] + size = len(questions) + for idx in range(0, size, 3): + result.append(dict(title=u'Question', infoText=None, + questions=questions[idx:idx+3])) + return [g for g in result if len(g['questions']) == 3] + if self.adapted.noGrouping: + questions = list(self.adapted.getQuestions(self.personId)) + questions.sort(key=lambda x: x.title) + size = len(questions) + bs = self.batchSize + for idx in range(0, size, bs): + result.append(dict(title=u'Question', infoText=None, + questions=questions[idx:idx+bs])) + else: + for group in self.adapted.getQuestionGroups(self.personId): + result.append(dict(title=group.title, + infoText=self.getInfoText(group), + questions=group.questions)) + return result + + @Lazy + def answerOptions(self): + opts = self.adapted.answerOptions + if not opts: + opts = [ + dict(value='none', label=u'No answer', + description=u'survey_value_none'), + dict(value=3, label=u'Fully applies', + description=u'survey_value_3'), + dict(value=2, label=u'', description=u'survey_value_2'), + dict(value=1, label=u'', description=u'survey_value_1'), + dict(value=0, label=u'Does not apply', + description=u'survey_value_0'),] + return opts + + @Lazy + def showFeedbackText(self): + sft = self.adapted.showFeedbackText + return sft is None and True or sft + + @Lazy + def feedbackColumns(self): + cols = self.adapted.feedbackColumns + if not cols: + cols = [ + dict(name='text', label=u'Response'), + dict(name='score', label=u'Score')] + if self.report == 'standard': + cols = [c for c in cols if c['name'] in self.teamColumns] + return cols + + teamColumns = ['category', 'average', 'stddev', 'teamRank', 'text'] + + @Lazy + def showTeamResults(self): + for c in self.feedbackColumns: + if c['name'] in ('average', 'teamRank'): + return True + return False + + def getTeamData(self, respManager): + result = [] + pred = [self.conceptManager.get('ismember'), + self.conceptManager.get('ismaster')] + if None in pred: + return result + inst = self.institution + instUid = self.getUidForObject(inst) + if inst: + for c in inst.getChildren(pred): + uid = self.getUidForObject(c) + data = respManager.load(uid, instUid) + if data: + resp = Response(self.adapted, self.personId) + for qu in self.adapted.getQuestions(self.personId): + if qu.questionType in (None, 'value_selection'): + if qu.uid in data: + value = data[qu.uid] + if isinstance(value, int) or value.isdigit(): + resp.values[qu] = int(value) + else: + resp.texts[qu] = data.get(qu.uid) or u'' + qgAvailable = True + for qg in self.adapted.getQuestionGroups(self.personId): + if qg.uid in data: + resp.values[qg] = data[qg.uid] + else: + qgAvailable = False + if not qgAvailable: + values = resp.getGroupedResult() + for v in values: + resp.values[v['group']] = v['score'] + result.append(resp) + return result + + def results(self): + if self.report: + return self.teamResults(self.report) form = self.request.form - if 'submit' in form: - self.data = {} - response = Response(self.adapted, None) - for key, value in form.items(): - if key.startswith('question_'): + action = None + for k in ('submit', 'save'): + if k in form: + action = k + break + if action is None: + return [] + respManager = Responses(self.context) + respManager.personId = (self.request.form.get('person') or + respManager.getPersonId()) + if self.adapted.teamBasedEvaluation and self.institution: + respManager.institutionId = self.getUidForObject( + baseObject(self.institution)) + if self.adapted.questionnaireType == 'person': + respManager.referrerId = respManager.getPersonId() + if self.adapted.questionnaireType == 'pref_selection': + return self.prefsResults(respManager, form, action) + data = {} + response = Response(self.adapted, self.personId) + for key, value in form.items(): + if key.startswith('question_'): + if value != 'none': uid = key[len('question_'):] question = adapted(self.getObjectForUid(uid)) - if value != 'none': + if value.isdigit(): value = int(value) - self.data[uid] = value - response.values[question] = value - Responses(self.context).save(self.data) - self.errors = self.check(response) - if self.errors: - return [] - if response is not None: - result = response.getGroupedResult() - return [dict(category=r[0].title, text=r[1].text, - score=int(round(r[2] * 100))) - for r in result] + data[uid] = value + response.values[question] = value + values = response.getGroupedResult() + for v in values: + data[self.getUidForObject(v['group'])] = v['score'] + self.data = data + self.errors = self.check(response) + if action == 'submit' and not self.errors: + data['state'] = 'active' + else: + data['state'] = 'draft' + respManager.save(data) + if action == 'save': + self.message = u'Your data have been saved.' + return [] + if self.errors: + return [] + result = [dict(category=r['group'].title, text=r['feedback'].text, + score=int(round(r['score'] * 100)), rank=r['rank']) + for r in values] + if self.showTeamResults: + self.teamData = self.getTeamData(respManager) + groups = [r['group'] for r in values] + teamValues = response.getTeamResult(groups, self.teamData) + for idx, r in enumerate(teamValues): + result[idx]['average'] = int(round(r['average'] * 100)) + result[idx]['teamRank'] = r['rank'] + return result + + def teamResults(self, report): + result = [] + respManager = Responses(self.context) + self.teamData = self.getTeamData(respManager) + response = Response(self.adapted, None) + groups = self.adapted.getQuestionGroups(self.personId) + teamValues = response.getTeamResult(groups, self.teamData) + for idx, r in enumerate(teamValues): + group = r['group'] + item = dict(category=group.title, + average=int(round(r['average'] * 100)), + teamRank=r['rank']) + if group.feedbackItems: + wScore = r['average'] * len(group.feedbackItems) - 0.00001 + item['text'] = group.feedbackItems[int(wScore)].text + result.append(item) + return result + + def getTeamResultsForQuestion(self, question, questionnaire): + result = dict(average=0.0, stddev=0.0) + if self.teamData is None: + respManager = Responses(self.context) + self.teamData = self.getTeamData(respManager) + answerRange = question.answerRange or questionnaire.defaultAnswerRange + values = [r.values.get(question) for r in self.teamData] + values = [v for v in values if v is not None] + if values: + average = float(sum(values)) / len(values) + if question.revertAnswerOptions: + average = answerRange - average - 1 + devs = [(average - v) for v in values] + stddev = math.sqrt(sum(d * d for d in devs) / len(values)) + average = average * 100 / (answerRange - 1) + stddev = stddev * 100 / (answerRange - 1) + result['average'] = int(round(average)) + result['stddev'] = int(round(stddev)) + texts = [r.texts.get(question) for r in self.teamData] + result['texts'] = '
'.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'%s
(%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) diff --git a/knowledge/survey/configure.zcml b/knowledge/survey/configure.zcml index a205abd..423889d 100644 --- a/knowledge/survey/configure.zcml +++ b/knowledge/survey/configure.zcml @@ -12,8 +12,6 @@ - @@ -25,8 +23,6 @@ - @@ -38,8 +34,6 @@ - @@ -51,8 +45,6 @@ - diff --git a/knowledge/survey/interfaces.py b/knowledge/survey/interfaces.py index e2adffc..6d2dfed 100644 --- a/knowledge/survey/interfaces.py +++ b/knowledge/survey/interfaces.py @@ -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. """ diff --git a/knowledge/survey/response.py b/knowledge/survey/response.py index 3841c4f..ed2c4b8 100644 --- a/knowledge/survey/response.py +++ b/knowledge/survey/response.py @@ -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) diff --git a/knowledge/survey/view_macros.pt b/knowledge/survey/view_macros.pt index 9e7593c..842a71c 100644 --- a/knowledge/survey/view_macros.pt +++ b/knowledge/survey/view_macros.pt @@ -4,96 +4,283 @@ + 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"> - - -
+ + +
+
+ +
+
+ + + + + +
Category +
+ + + +
+

+ Team Size: +
  +

+ +
+ + + + +

+
+ + + + + + + + + + + + + + + + + + + + +
  
Average
+
+
+
+
+

+ Team Size: +
  +

+ +
diff --git a/knowledge/tests.py b/knowledge/tests.py index 8a23a33..16fee64 100755 --- a/knowledge/tests.py +++ b/knowledge/tests.py @@ -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): diff --git a/layout/browser/base.py b/layout/browser/base.py index b5039b8..2fc7c11 100644 --- a/layout/browser/base.py +++ b/layout/browser/base.py @@ -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 diff --git a/layout/browser/node.py b/layout/browser/node.py index 6e1bb44..b158e8e 100644 --- a/layout/browser/node.py +++ b/layout/browser/node.py @@ -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) diff --git a/layout/browser/resource.py b/layout/browser/resource.py index a6f343a..ef18ba4 100644 --- a/layout/browser/resource.py +++ b/layout/browser/resource.py @@ -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 diff --git a/locales/de/LC_MESSAGES/loops.mo b/locales/de/LC_MESSAGES/loops.mo index a153046..277d8a1 100644 Binary files a/locales/de/LC_MESSAGES/loops.mo and b/locales/de/LC_MESSAGES/loops.mo differ diff --git a/locales/de/LC_MESSAGES/loops.po b/locales/de/LC_MESSAGES/loops.po index b3abae7..080664b 100644 --- a/locales/de/LC_MESSAGES/loops.po +++ b/locales/de/LC_MESSAGES/loops.po @@ -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 \n" "Language-Team: loops developers \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" + diff --git a/media/browser/asset.pt b/media/browser/asset.pt index 24f0524..60c7efc 100644 --- a/media/browser/asset.pt +++ b/media/browser/asset.pt @@ -3,7 +3,7 @@

Title


>> 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 diff --git a/organize/browser/configure.zcml b/organize/browser/configure.zcml index e351efa..8420800 100644 --- a/organize/browser/configure.zcml +++ b/organize/browser/configure.zcml @@ -45,6 +45,12 @@ class="loops.organize.browser.member.PasswordChange" permission="zope.View" /> + + + + + + + diff --git a/organize/browser/member.py b/organize/browser/member.py index 46ff45b..9ffe903 100644 --- a/organize/browser/member.py +++ b/organize/browser/member.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 @@ -87,6 +87,7 @@ class BaseMemberRegistration(NodeView): formErrors = dict( confirm_nomatch=FormError(_(u'Password and password confirmation ' u'do not match.')), + illegal_loginname=FormError(_('Login name not allowed.')), duplicate_loginname=FormError(_('Login name already taken.')), ) @@ -244,7 +245,7 @@ class SecureMemberRegistration(BaseMemberRegistration, CreateForm): regMan = IMemberRegistrationManager(self.context.getLoopsRoot()) pw = generateName() email = form.get('email') - try: + try: result = regMan.register(login, pw, form.get('lastName'), form.get('firstName'), email=email,) diff --git a/organize/browser/party.py b/organize/browser/party.py index 5d89e50..d2f8d5a 100644 --- a/organize/browser/party.py +++ b/organize/browser/party.py @@ -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 diff --git a/organize/browser/view_macros.pt b/organize/browser/view_macros.pt index 1e296bd..30169f2 100644 --- a/organize/browser/view_macros.pt +++ b/organize/browser/view_macros.pt @@ -123,20 +123,24 @@

-
- +
-
-
-
- + +
+
+
-
-
+ +
+
+
+
@@ -152,7 +156,8 @@ Toggle all
- +