Merge branch '2master' of ssh://git.cy55.de/home/git/loops into 2master
This commit is contained in:
		
						commit
						9d9cfd6fc7
					
				
					 136 changed files with 4198 additions and 791 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -1,5 +1,6 @@ | ||||||
| *.pyc | *.pyc | ||||||
| *.pyo | *.pyo | ||||||
|  | dist/ | ||||||
| *.project | *.project | ||||||
| *.pydevproject | *.pydevproject | ||||||
| *.sublime-project | *.sublime-project | ||||||
|  |  | ||||||
							
								
								
									
										11
									
								
								MANIFEST.in
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								MANIFEST.in
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | global-include *.cfg | ||||||
|  | global-include *.css *.js | ||||||
|  | global-include *.gif *.jpg *.png | ||||||
|  | global-include *.dmp | ||||||
|  | global-include *.md *.txt | ||||||
|  | global-include *.mo *.po *.pot | ||||||
|  | global-include *.pdf | ||||||
|  | global-include *.pt | ||||||
|  | global-include *.zcml | ||||||
|  | 
 | ||||||
|  | graft loops/integrator/testdata | ||||||
							
								
								
									
										7
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | # Introduction | ||||||
|  | 
 | ||||||
|  | This is the main part of the code of the semantic  | ||||||
|  | web application platform *loops*, based on  | ||||||
|  | Zope 3 / bluebream. | ||||||
|  | 
 | ||||||
|  | More information: see https://www.cyberconcepts.org. | ||||||
							
								
								
									
										18
									
								
								README.txt
									
										
									
									
									
								
							
							
						
						
									
										18
									
								
								README.txt
									
										
									
									
									
								
							|  | @ -737,7 +737,9 @@ on data provided in this form: | ||||||
| 
 | 
 | ||||||
|   >>> component.provideAdapter(NameChooser) |   >>> component.provideAdapter(NameChooser) | ||||||
|   >>> request = TestRequest(form={'title': u'Test Note', |   >>> request = TestRequest(form={'title': u'Test Note', | ||||||
|   ...                             'form.type': u'.loops/concepts/note'}) |   ...                             'form.type': u'.loops/concepts/note', | ||||||
|  |   ...                             'contentType': u'text/restructured', | ||||||
|  |   ...                             'linkUrl': u'http://'}) | ||||||
|   >>> view = NodeView(m112, request) |   >>> view = NodeView(m112, request) | ||||||
|   >>> cont = CreateObject(view, request) |   >>> cont = CreateObject(view, request) | ||||||
|   >>> cont.update() |   >>> cont.update() | ||||||
|  | @ -802,7 +804,7 @@ The new technique uses the ``fields`` and ``data`` attributes... | ||||||
|   linkText textline False None |   linkText textline False None | ||||||
| 
 | 
 | ||||||
|   >>> view.data |   >>> view.data | ||||||
|   {'linkUrl': u'http://', 'contentType': 'text/restructured', 'data': u'', |   {'linkUrl': u'http://', 'contentType': u'text/restructured', 'data': u'', | ||||||
|    'linkText': u'', 'title': u'Test Note'} |    'linkText': u'', 'title': u'Test Note'} | ||||||
| 
 | 
 | ||||||
| The object is changed via a FormController adapter created for | The object is changed via a FormController adapter created for | ||||||
|  | @ -913,6 +915,12 @@ relates ISO country codes with the full name of the country. | ||||||
|   >>> sorted(adapted(concepts['countries']).data.items()) |   >>> sorted(adapted(concepts['countries']).data.items()) | ||||||
|   [('at', ['Austria']), ('de', ['Germany'])] |   [('at', ['Austria']), ('de', ['Germany'])] | ||||||
| 
 | 
 | ||||||
|  |   >>> countries.dataAsRecords() | ||||||
|  |   [{'value': 'Austria', 'key': 'at'}, {'value': 'Germany', 'key': 'de'}] | ||||||
|  | 
 | ||||||
|  |   >>> countries.getRowsByValue('value', 'Germany') | ||||||
|  |   [{'value': 'Germany', 'key': 'de'}] | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| Caching | Caching | ||||||
| ======= | ======= | ||||||
|  | @ -932,6 +940,12 @@ Security | ||||||
|   >>> from loops.security.browser import admin, audit |   >>> from loops.security.browser import admin, audit | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | Paster Shell Utilities - Repair Scripts | ||||||
|  | ======================================= | ||||||
|  | 
 | ||||||
|  |   >>> from loops.repair.base import removeRecords | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| Import/Export | Import/Export | ||||||
| ============= | ============= | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										35
									
								
								__init__.py
									
										
									
									
									
								
							
							
						
						
									
										35
									
								
								__init__.py
									
										
									
									
									
								
							|  | @ -1,22 +1,17 @@ | ||||||
| # | # package loops | ||||||
| #  Copyright (c) 2008 Helmut Merz helmutm@cy55.de |  | ||||||
| # |  | ||||||
| #  This program is free software; you can redistribute it and/or modify |  | ||||||
| #  it under the terms of the GNU General Public License as published by |  | ||||||
| #  the Free Software Foundation; either version 2 of the License, or |  | ||||||
| #  (at your option) any later version. |  | ||||||
| # |  | ||||||
| #  This program is distributed in the hope that it will be useful, |  | ||||||
| #  but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
| #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
| #  GNU General Public License for more details. |  | ||||||
| # |  | ||||||
| #  You should have received a copy of the GNU General Public License |  | ||||||
| #  along with this program; if not, write to the Free Software |  | ||||||
| #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA |  | ||||||
| # |  | ||||||
| 
 | 
 | ||||||
| """ | # intid monkey patch for avoiding ForbiddenAttribute error | ||||||
|  | 
 | ||||||
|  | from zope import component | ||||||
|  | from zope.intid.interfaces import IIntIds | ||||||
|  | from zope import intid | ||||||
|  | from zope.security.proxy import removeSecurityProxy | ||||||
|  | 
 | ||||||
|  | def queryId(self, ob, default=None): | ||||||
|  |     try: | ||||||
|  |         return self.getId(removeSecurityProxy(ob)) | ||||||
|  |     except KeyError: | ||||||
|  |         return default | ||||||
|  | 
 | ||||||
|  | intid.IntIds.queryId = queryId | ||||||
| 
 | 
 | ||||||
| $Id$ |  | ||||||
| """ |  | ||||||
|  |  | ||||||
							
								
								
									
										4
									
								
								base.py
									
										
									
									
									
								
							
							
						
						
									
										4
									
								
								base.py
									
										
									
									
									
								
							|  | @ -1,5 +1,3 @@ | ||||||
| # -*- coding: UTF-8 -*- |  | ||||||
| # -*- Mode: Python; py-indent-offset: 4 -*- |  | ||||||
| # | # | ||||||
| #  Copyright (c) 2019 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2019 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
|  | @ -19,7 +17,7 @@ | ||||||
| # | # | ||||||
| 
 | 
 | ||||||
| """ | """ | ||||||
| The loops container class. | Implementation of loops root object. | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| from zope.app.container.btree import BTreeContainer | from zope.app.container.btree import BTreeContainer | ||||||
|  |  | ||||||
|  | @ -92,6 +92,8 @@ class DialogAction(Action): | ||||||
|             urlParams['fixed_type'] = 'yes' |             urlParams['fixed_type'] = 'yes' | ||||||
|         if self.viewTitle: |         if self.viewTitle: | ||||||
|             urlParams['view_title'] = self.viewTitle |             urlParams['view_title'] = self.viewTitle | ||||||
|  |         #for k, v in self.page.sortInfo.items(): | ||||||
|  |         #    urlParams['sortinfo_' + k] = v['fparam'] | ||||||
|         urlParams.update(self.addParams) |         urlParams.update(self.addParams) | ||||||
|         if self.target is not None: |         if self.target is not None: | ||||||
|             url = self.page.getUrlForTarget(self.target) |             url = self.page.getUrlForTarget(self.target) | ||||||
|  |  | ||||||
|  | @ -20,13 +20,14 @@ | ||||||
| Common base class for loops browser view classes. | Common base class for loops browser view classes. | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| from cgi import parse_qs, parse_qsl | from cgi import parse_qsl | ||||||
| #import mimetypes   # use more specific assignments from cybertools.text | #import mimetypes   # use more specific assignments from cybertools.text | ||||||
| from datetime import datetime | from datetime import date, datetime | ||||||
| from logging import getLogger | from logging import getLogger | ||||||
| import re | import re | ||||||
| from time import strptime | from time import strptime | ||||||
| from urllib import urlencode | from urllib import urlencode | ||||||
|  | from urlparse import parse_qs | ||||||
| from zope import component | from zope import component | ||||||
| from zope.app.form.browser.interfaces import ITerms | from zope.app.form.browser.interfaces import ITerms | ||||||
| from zope.app.i18n.interfaces import ITranslationDomain | from zope.app.i18n.interfaces import ITranslationDomain | ||||||
|  | @ -62,17 +63,21 @@ from cybertools.stateful.interfaces import IStateful | ||||||
| from cybertools.text import mimetypes | from cybertools.text import mimetypes | ||||||
| from cybertools.typology.interfaces import IType, ITypeManager | from cybertools.typology.interfaces import IType, ITypeManager | ||||||
| from cybertools.util.date import toLocalTime | from cybertools.util.date import toLocalTime | ||||||
|  | from cybertools.util.format import formatDate | ||||||
| from cybertools.util.jeep import Jeep | from cybertools.util.jeep import Jeep | ||||||
| from loops.browser.util import normalizeForUrl | from loops.browser.util import normalizeForUrl | ||||||
| from loops.common import adapted, baseObject | from loops.common import adapted, baseObject | ||||||
| from loops.config.base import DummyOptions | from loops.config.base import DummyOptions | ||||||
| from loops.i18n.browser import I18NView | from loops.i18n.browser import I18NView | ||||||
| from loops.interfaces import IResource, IView, INode, ITypeConcept | from loops.interfaces import IResource, IView, INode, ITypeConcept | ||||||
|  | from loops.organize.personal import favorite | ||||||
|  | from loops.organize.party import getPersonForUser | ||||||
| from loops.organize.tracking import access | from loops.organize.tracking import access | ||||||
| from loops.organize.util import getRolesForPrincipal | from loops.organize.util import getRolesForPrincipal | ||||||
| from loops.resource import Resource | from loops.resource import Resource | ||||||
| from loops.security.common import checkPermission | from loops.security.common import checkPermission | ||||||
| from loops.security.common import canAccessObject, canListObject, canWriteObject | from loops.security.common import canAccessObject, canListObject, canWriteObject | ||||||
|  | from loops.security.common import canEditRestricted | ||||||
| from loops.type import ITypeConcept, LoopsTypeInfo | from loops.type import ITypeConcept, LoopsTypeInfo | ||||||
| from loops import util | from loops import util | ||||||
| from loops.util import _, saveRequest | from loops.util import _, saveRequest | ||||||
|  | @ -137,7 +142,58 @@ class EditForm(form.EditForm): | ||||||
|         return parentUrl + '/contents.html' |         return parentUrl + '/contents.html' | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class BaseView(GenericView, I18NView): | class SortableMixin(object): | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def sortInfo(self): | ||||||
|  |         result = {} | ||||||
|  |         for k, v in self.request.form.items(): | ||||||
|  |             if k.startswith('sortinfo_'): | ||||||
|  |                 tableName = k[len('sortinfo_'):] | ||||||
|  |                 if ',' in v: | ||||||
|  |                     fn, dir = v.split(',') | ||||||
|  |                 else: | ||||||
|  |                     fn = v | ||||||
|  |                     dir = 'asc' | ||||||
|  |                 result[tableName] = dict( | ||||||
|  |                     colName=fn, ascending=(dir=='asc'), fparam=v) | ||||||
|  |         result = favorite.updateSortInfo(getPersonForUser( | ||||||
|  |                         self.context, self.request), self.target, result) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     def isSortableColumn(self, tableName, colName): | ||||||
|  |         return False    # overwrite in subclass | ||||||
|  | 
 | ||||||
|  |     def getSortUrl(self, tableName, colName): | ||||||
|  |         url = str(self.request.URL) | ||||||
|  |         paramChar = '?' in url and '&' or '?' | ||||||
|  |         si = self.sortInfo.get(tableName) | ||||||
|  |         if si is not None and si.get('colName') == colName: | ||||||
|  |             dir = si['ascending'] and 'desc' or 'asc' | ||||||
|  |         else: | ||||||
|  |             dir = 'asc' | ||||||
|  |         return '%s%ssortinfo_%s=%s,%s' % (url, paramChar, tableName, colName, dir) | ||||||
|  | 
 | ||||||
|  |     def getSortParams(self, tableName): | ||||||
|  |         url = str(self.request.URL) | ||||||
|  |         paramChar = '?' in url and '&' or '?' | ||||||
|  |         si = self.sortInfo.get(tableName) | ||||||
|  |         if si is not None: | ||||||
|  |             colName = si['colName'] | ||||||
|  |             dir = si['ascending'] and 'asc' or 'desc' | ||||||
|  |             return '%ssortinfo_%s=%s,%s' % (paramChar, tableName, colName, dir) | ||||||
|  |         return '' | ||||||
|  | 
 | ||||||
|  |     def getSortImage(self, tableName, colName): | ||||||
|  |         si = self.sortInfo.get(tableName) | ||||||
|  |         if si is not None and si.get('colName') == colName: | ||||||
|  |             if si['ascending']: | ||||||
|  |                 return '/@@/cybertools.icons/arrowdown.gif' | ||||||
|  |             else: | ||||||
|  |                 return '/@@/cybertools.icons/arrowup.gif' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class BaseView(GenericView, I18NView, SortableMixin): | ||||||
| 
 | 
 | ||||||
|     actions = {} |     actions = {} | ||||||
|     portlet_actions = [] |     portlet_actions = [] | ||||||
|  | @ -146,6 +202,7 @@ class BaseView(GenericView, I18NView): | ||||||
|     icon = None |     icon = None | ||||||
|     modeName = 'view' |     modeName = 'view' | ||||||
|     isToplevel = False |     isToplevel = False | ||||||
|  |     isVisible = True | ||||||
| 
 | 
 | ||||||
|     def __init__(self, context, request): |     def __init__(self, context, request): | ||||||
|         context = baseObject(context) |         context = baseObject(context) | ||||||
|  | @ -163,6 +220,10 @@ class BaseView(GenericView, I18NView): | ||||||
|             pass |             pass | ||||||
|         saveRequest(request) |         saveRequest(request) | ||||||
| 
 | 
 | ||||||
|  |     def todayFormatted(self): | ||||||
|  |         return formatDate(date.today(), 'date', 'short', | ||||||
|  |                           self.languageInfo.language) | ||||||
|  | 
 | ||||||
|     def checkPermissions(self): |     def checkPermissions(self): | ||||||
|         return canAccessObject(self.context) |         return canAccessObject(self.context) | ||||||
| 
 | 
 | ||||||
|  | @ -214,6 +275,16 @@ class BaseView(GenericView, I18NView): | ||||||
|                 result.append(view) |                 result.append(view) | ||||||
|         return result |         return result | ||||||
| 
 | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def urlParamString(self): | ||||||
|  |         return self.getUrlParamString() | ||||||
|  | 
 | ||||||
|  |     def getUrlParamString(self): | ||||||
|  |         qs = self.request.get('QUERY_STRING') | ||||||
|  |         if qs: | ||||||
|  |             return '?' + qs | ||||||
|  |         return '' | ||||||
|  | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def principalId(self): |     def principalId(self): | ||||||
|         principal = self.request.principal |         principal = self.request.principal | ||||||
|  | @ -347,6 +418,10 @@ class BaseView(GenericView, I18NView): | ||||||
|     def isPartOfPredicate(self): |     def isPartOfPredicate(self): | ||||||
|         return self.conceptManager.get('ispartof') |         return self.conceptManager.get('ispartof') | ||||||
| 
 | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def queryTargetPredicate(self): | ||||||
|  |         return self.conceptManager.get('querytarget') | ||||||
|  | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def memberPredicate(self): |     def memberPredicate(self): | ||||||
|         return self.conceptManager.get('ismember') |         return self.conceptManager.get('ismember') | ||||||
|  | @ -395,6 +470,10 @@ class BaseView(GenericView, I18NView): | ||||||
|     def description(self): |     def description(self): | ||||||
|         return self.adapted.description |         return self.adapted.description | ||||||
| 
 | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def tabTitle(self): | ||||||
|  |         return u'Info' | ||||||
|  | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def additionalInfos(self): |     def additionalInfos(self): | ||||||
|         return [] |         return [] | ||||||
|  | @ -747,6 +826,8 @@ class BaseView(GenericView, I18NView): | ||||||
|         return result |         return result | ||||||
| 
 | 
 | ||||||
|     def checkState(self): |     def checkState(self): | ||||||
|  |         if checkPermission('loops.ManageSite', self.context): | ||||||
|  |             return True | ||||||
|         if not self.allStates: |         if not self.allStates: | ||||||
|             return True |             return True | ||||||
|         for stf in self.allStates: |         for stf in self.allStates: | ||||||
|  | @ -821,6 +902,10 @@ class BaseView(GenericView, I18NView): | ||||||
|     def canAccessRestricted(self): |     def canAccessRestricted(self): | ||||||
|         return checkPermission('loops.ViewRestricted', self.context) |         return checkPermission('loops.ViewRestricted', self.context) | ||||||
| 
 | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def canEditRestricted(self): | ||||||
|  |         return canEditRestricted(self.context) | ||||||
|  | 
 | ||||||
|     def openEditWindow(self, viewName='edit.html'): |     def openEditWindow(self, viewName='edit.html'): | ||||||
|         if self.editable: |         if self.editable: | ||||||
|             if checkPermission('loops.ManageSite', self.context): |             if checkPermission('loops.ManageSite', self.context): | ||||||
|  | @ -943,6 +1028,12 @@ class BaseView(GenericView, I18NView): | ||||||
|         jsCall = 'dojo.require("dojox.image.Lightbox");' |         jsCall = 'dojo.require("dojox.image.Lightbox");' | ||||||
|         self.controller.macros.register('js-execute', jsCall, jsCall=jsCall) |         self.controller.macros.register('js-execute', jsCall, jsCall=jsCall) | ||||||
| 
 | 
 | ||||||
|  |     def registerDojoComboBox(self): | ||||||
|  |         self.registerDojo() | ||||||
|  |         jsCall = ('dojo.require("dijit.form.ComboBox");') | ||||||
|  |         self.controller.macros.register('js-execute', | ||||||
|  |                 'dojo.require.ComboBox', jsCall=jsCall) | ||||||
|  | 
 | ||||||
|     def registerDojoFormAll(self): |     def registerDojoFormAll(self): | ||||||
|         self.registerDojo() |         self.registerDojo() | ||||||
|         self.registerDojoEditor() |         self.registerDojoEditor() | ||||||
|  | @ -996,6 +1087,7 @@ class LoggedIn(object): | ||||||
|             params = parse_qsl(qs) |             params = parse_qsl(qs) | ||||||
|         params = [(k, v) for k, v in params if k != 'loops.messages.top:record'] |         params = [(k, v) for k, v in params if k != 'loops.messages.top:record'] | ||||||
|         params.append(('loops.messages.top:record', message.encode('UTF-8'))) |         params.append(('loops.messages.top:record', message.encode('UTF-8'))) | ||||||
|  |         url = url.encode('utf-8') | ||||||
|         return '%s?%s' % (url, urlencode(params)) |         return '%s?%s' % (url, urlencode(params)) | ||||||
| 
 | 
 | ||||||
| # vocabulary stuff | # vocabulary stuff | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2013 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2016 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -254,18 +254,35 @@ class ConceptView(BaseView): | ||||||
|                 result.append(view) |                 result.append(view) | ||||||
|         return result |         return result | ||||||
| 
 | 
 | ||||||
|  |     def viewModes(self): | ||||||
|  |         modes = Jeep() | ||||||
|  |         current = self.request.form.get('loops.viewName') | ||||||
|  |         parts = (self.options('view_tabs') or  | ||||||
|  |                     self.typeOptions('view_tabs') or []) | ||||||
|  |         if not parts: | ||||||
|  |             return modes | ||||||
|  |         activeMode = None | ||||||
|  |         for p in parts: | ||||||
|  |             view = component.queryMultiAdapter( | ||||||
|  |                         (self.adapted, self.request), name=p) | ||||||
|  |             if view is None: | ||||||
|  |                 view = component.queryMultiAdapter( | ||||||
|  |                             (self.context, self.request), name=p) | ||||||
|  |             if view is None: | ||||||
|  |                 continue | ||||||
|  |             active = (activeMode is None and p == current) | ||||||
|  |             if active: | ||||||
|  |                 activeMode = p | ||||||
|  |             url = '%s?loops.viewName=%s' % (self.targetUrl, p) | ||||||
|  |             modes.append(ViewMode(p, view.tabTitle, url, active)) | ||||||
|  |         if activeMode is None: | ||||||
|  |             modes[0].active = True | ||||||
|  |         return modes | ||||||
|  | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def adapted(self): |     def adapted(self): | ||||||
|         return adapted(self.context, self.languageInfo) |         return adapted(self.context, self.languageInfo) | ||||||
| 
 | 
 | ||||||
|     @Lazy |  | ||||||
|     def title(self): |  | ||||||
|         return self.adapted.title or getName(self.context) |  | ||||||
| 
 |  | ||||||
|     @Lazy |  | ||||||
|     def description(self): |  | ||||||
|         return self.adapted.description |  | ||||||
| 
 |  | ||||||
|     @Lazy |     @Lazy | ||||||
|     def targetUrl(self): |     def targetUrl(self): | ||||||
|         return self.nodeView.getUrlForTarget(self.context) |         return self.nodeView.getUrlForTarget(self.context) | ||||||
|  | @ -282,8 +299,17 @@ class ConceptView(BaseView): | ||||||
|     def breadcrumbsTitle(self): |     def breadcrumbsTitle(self): | ||||||
|         return self.title |         return self.title | ||||||
| 
 | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def showInBreadcrumbs(self): | ||||||
|  |         return (self.options('show_in_breadcrumbs') or  | ||||||
|  |                 self.typeOptions('show_in_breadcrumbs')) | ||||||
|  | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def breadcrumbsParent(self): |     def breadcrumbsParent(self): | ||||||
|  |         for p in self.context.getParents([self.defaultPredicate]): | ||||||
|  |             view = self.nodeView.getViewForTarget(p) | ||||||
|  |             if view.showInBreadcrumbs: | ||||||
|  |                 return view | ||||||
|         return None |         return None | ||||||
| 
 | 
 | ||||||
|     def getData(self, omit=('title', 'description')): |     def getData(self, omit=('title', 'description')): | ||||||
|  | @ -389,7 +415,8 @@ class ConceptView(BaseView): | ||||||
|     children = getChildren |     children = getChildren | ||||||
| 
 | 
 | ||||||
|     def childrenAlphaGroups(self, predicates=None): |     def childrenAlphaGroups(self, predicates=None): | ||||||
|         result = Jeep() |         #result = Jeep() | ||||||
|  |         result = {} | ||||||
|         rels = self.getChildren(predicates=predicates or [self.defaultPredicate], |         rels = self.getChildren(predicates=predicates or [self.defaultPredicate], | ||||||
|                                 topLevelOnly=False, sort=False) |                                 topLevelOnly=False, sort=False) | ||||||
|         rels = sorted(rels, key=lambda r: r.title.lower()) |         rels = sorted(rels, key=lambda r: r.title.lower()) | ||||||
|  | @ -449,7 +476,7 @@ class ConceptView(BaseView): | ||||||
|                 if r.order != pos: |                 if r.order != pos: | ||||||
|                     r.order = pos |                     r.order = pos | ||||||
| 
 | 
 | ||||||
|     def getResources(self): |     def getResources(self, relView=None, sort='default'): | ||||||
|         form = self.request.form |         form = self.request.form | ||||||
|         #if form.get('loops.viewName') == 'index.html' and self.editable: |         #if form.get('loops.viewName') == 'index.html' and self.editable: | ||||||
|         if self.editable: |         if self.editable: | ||||||
|  | @ -458,13 +485,17 @@ class ConceptView(BaseView): | ||||||
|                 tokens = form.get('resources_tokens') |                 tokens = form.get('resources_tokens') | ||||||
|                 if tokens: |                 if tokens: | ||||||
|                     self.reorderResources(tokens) |                     self.reorderResources(tokens) | ||||||
|         from loops.browser.resource import ResourceRelationView |         if relView is None: | ||||||
|  |             from loops.browser.resource import ResourceRelationView | ||||||
|  |             relView = ResourceRelationView | ||||||
|         from loops.organize.personal.browser.filter import FilterView |         from loops.organize.personal.browser.filter import FilterView | ||||||
|         fv = FilterView(self.context, self.request) |         fv = FilterView(self.context, self.request) | ||||||
|         rels = self.context.getResourceRelations() |         rels = self.context.getResourceRelations(sort=sort) | ||||||
|         for r in rels: |         for r in rels: | ||||||
|             if fv.check(r.first): |             if fv.check(r.first): | ||||||
|                 yield ResourceRelationView(r, self.request, contextIsSecond=True) |                 view = relView(r, self.request, contextIsSecond=True) | ||||||
|  |                 if view.checkState(): | ||||||
|  |                     yield view | ||||||
| 
 | 
 | ||||||
|     def resources(self): |     def resources(self): | ||||||
|         return self.getResources() |         return self.getResources() | ||||||
|  |  | ||||||
|  | @ -53,7 +53,7 @@ | ||||||
|     <h1 tal:define="tabview item/tabview|nothing" |     <h1 tal:define="tabview item/tabview|nothing" | ||||||
|         tal:attributes="ondblclick item/openEditWindow"> |         tal:attributes="ondblclick item/openEditWindow"> | ||||||
|       <a tal:omit-tag="python: level > 1" |       <a tal:omit-tag="python: level > 1" | ||||||
|          tal:attributes="href request/URL" |          tal:attributes="href string:${view/requestUrl}${item/urlParamString}" | ||||||
|          tal:content="item/title">Title</a> |          tal:content="item/title">Title</a> | ||||||
|       <a title="Show tabular view" |       <a title="Show tabular view" | ||||||
|          i18n:attributes="title" |          i18n:attributes="title" | ||||||
|  | @ -367,4 +367,21 @@ | ||||||
| </metal:actions> | </metal:actions> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | <metal:sortable define-macro="sortable_column_header" | ||||||
|  |                 tal:define="tableName tableName|nothing"> | ||||||
|  |         <a title="tooltip_sort_column" | ||||||
|  |            tal:define="colName col/name" | ||||||
|  |            tal:omit-tag="python:not item.isSortableColumn(tableName, colName)" | ||||||
|  |            tal:attributes="href python:item.getSortUrl(tableName, colName)" | ||||||
|  |            i18n:attributes="title"> | ||||||
|  |           <span tal:content="col/title" | ||||||
|  |                 tal:attributes="class col/cssClass|nothing" | ||||||
|  |                 i18n:translate="" /> | ||||||
|  |           <img tal:define="src python:item.getSortImage(tableName, colName)" | ||||||
|  |                tal:condition="src" | ||||||
|  |                tal:attributes="src src" /> | ||||||
|  |         </a> | ||||||
|  | </metal:sortable> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| </html> | </html> | ||||||
|  |  | ||||||
|  | @ -125,7 +125,7 @@ | ||||||
| 
 | 
 | ||||||
|   <containerViews |   <containerViews | ||||||
|       for="loops.interfaces.ILoops" |       for="loops.interfaces.ILoops" | ||||||
|       index="zope.View" |       index="zope.ManageSite" | ||||||
|       contents="loops.ManageSite" |       contents="loops.ManageSite" | ||||||
|       add="loops.ManageSite" /> |       add="loops.ManageSite" /> | ||||||
| 
 | 
 | ||||||
|  | @ -365,7 +365,7 @@ | ||||||
| 
 | 
 | ||||||
|   <containerViews |   <containerViews | ||||||
|       for="loops.interfaces.IViewManager" |       for="loops.interfaces.IViewManager" | ||||||
|       index="zope.View" |       index="zope.ManageSite" | ||||||
|       add="loops.ManageSite" /> |       add="loops.ManageSite" /> | ||||||
| 
 | 
 | ||||||
|   <menuItem |   <menuItem | ||||||
|  | @ -571,6 +571,14 @@ | ||||||
|       factory="loops.browser.concept.TabbedPage" |       factory="loops.browser.concept.TabbedPage" | ||||||
|       permission="zope.View" /> |       permission="zope.View" /> | ||||||
| 
 | 
 | ||||||
|  |   <!-- delete object action --> | ||||||
|  | 
 | ||||||
|  |   <page | ||||||
|  |       name="delete_object" | ||||||
|  |       for="loops.interfaces.INode" | ||||||
|  |       class="loops.browser.form.DeleteObject" | ||||||
|  |       permission="zope.ManageContent" /> | ||||||
|  | 
 | ||||||
|   <!-- dialogs/forms (end-user views) --> |   <!-- dialogs/forms (end-user views) --> | ||||||
| 
 | 
 | ||||||
|   <page |   <page | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2012 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2017 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -20,12 +20,13 @@ | ||||||
| Classes for form presentation and processing. | Classes for form presentation and processing. | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
|  | from urllib import urlencode, unquote_plus | ||||||
|  | from zope.app.container.contained import ObjectRemovedEvent | ||||||
| from zope import component, interface, schema | from zope import component, interface, schema | ||||||
| from zope.component import adapts | from zope.component import adapts | ||||||
| from zope.event import notify | from zope.event import notify | ||||||
| from zope.interface import Interface | from zope.interface import Interface | ||||||
| from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent | from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent | ||||||
| 
 |  | ||||||
| from zope.app.container.interfaces import INameChooser | from zope.app.container.interfaces import INameChooser | ||||||
| from zope.app.container.contained import ObjectAddedEvent | from zope.app.container.contained import ObjectAddedEvent | ||||||
| from zope.app.pagetemplate import ViewPageTemplateFile | from zope.app.pagetemplate import ViewPageTemplateFile | ||||||
|  | @ -35,7 +36,7 @@ from zope.publisher.browser import FileUpload | ||||||
| from zope.publisher.interfaces import BadRequest | from zope.publisher.interfaces import BadRequest | ||||||
| from zope.security.interfaces import ForbiddenAttribute, Unauthorized | from zope.security.interfaces import ForbiddenAttribute, Unauthorized | ||||||
| from zope.security.proxy import isinstance, removeSecurityProxy | from zope.security.proxy import isinstance, removeSecurityProxy | ||||||
| from zope.traversing.api import getName | from zope.traversing.api import getName, getParent | ||||||
| 
 | 
 | ||||||
| from cybertools.ajax import innerHtml | from cybertools.ajax import innerHtml | ||||||
| from cybertools.browser.form import FormController | from cybertools.browser.form import FormController | ||||||
|  | @ -68,6 +69,25 @@ from loops.util import _ | ||||||
| from loops.versioning.interfaces import IVersionable | from loops.versioning.interfaces import IVersionable | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | # delete object | ||||||
|  | 
 | ||||||
|  | class DeleteObject(NodeView): | ||||||
|  | 
 | ||||||
|  |     isTopLevel = True | ||||||
|  | 
 | ||||||
|  |     def __call__(self): | ||||||
|  |         # todo: check permission; check security code | ||||||
|  |         form = self.request.form | ||||||
|  |         obj = util.getObjectForUid(form['uid']) | ||||||
|  |         container = getParent(obj) | ||||||
|  |         notify(ObjectRemovedEvent(obj)) | ||||||
|  |         del container[getName(obj)] | ||||||
|  |         message = 'The object requested has been deleted.' | ||||||
|  |         params = [('loops.message', message.encode('UTF-8'))] | ||||||
|  |         nextUrl = '%s?%s' % (self.request.URL[-1], urlencode(params)) | ||||||
|  |         return self.request.response.redirect(nextUrl) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| # forms | # forms | ||||||
| 
 | 
 | ||||||
| class ObjectForm(NodeView): | class ObjectForm(NodeView): | ||||||
|  | @ -162,7 +182,8 @@ class ObjectForm(NodeView): | ||||||
|                 field = self.schema.fields.get(k) |                 field = self.schema.fields.get(k) | ||||||
|                 if field: |                 if field: | ||||||
|                     fi = field.getFieldInstance(self.instance) |                     fi = field.getFieldInstance(self.instance) | ||||||
|                     data[k] = fi.marshall(fi.unmarshall(form[k])) |                     input = unquote_plus(form[k]) | ||||||
|  |                     data[k] = fi.marshall(fi.unmarshall(input)) | ||||||
|                     #data[k] = toUnicode(form[k]) |                     #data[k] = toUnicode(form[k]) | ||||||
|         return data |         return data | ||||||
| 
 | 
 | ||||||
|  | @ -196,15 +217,37 @@ class ObjectForm(NodeView): | ||||||
|     def typeManager(self): |     def typeManager(self): | ||||||
|         return ITypeManager(self.target) |         return ITypeManager(self.target) | ||||||
| 
 | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def targetType(self): | ||||||
|  |         return self.target.getType() | ||||||
|  | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def presetTypesForAssignment(self): |     def presetTypesForAssignment(self): | ||||||
|         types = list(self.typeManager.listTypes(include=('assign',))) |         types = [] | ||||||
|  |         tn = getName(self.targetType) | ||||||
|  |         for t in self.typeManager.listTypes(include=('assign',)): | ||||||
|  |             # check if type is appropriate for the object to be created | ||||||
|  |             opt = IOptions(adapted(t.context))('qualifier_assign_to') | ||||||
|  |             #print '***', t.context.__name__, opt, tn | ||||||
|  |             if not opt or tn in opt: | ||||||
|  |                 types.append(t) | ||||||
|         assigned = [r.context.conceptType for r in self.assignments] |         assigned = [r.context.conceptType for r in self.assignments] | ||||||
|         types = [t for t in types if t.typeProvider not in assigned] |         types = [t for t in types if t.typeProvider not in assigned] | ||||||
|         return [dict(title=t.title, token=t.tokenForSearch) for t in types] |         return [dict(title=t.title, token=t.tokenForSearch) for t in types] | ||||||
| 
 | 
 | ||||||
|     def conceptsForType(self, token): |     def conceptsForType(self, token): | ||||||
|         result = ConceptQuery(self).query(type=token) |         result = ConceptQuery(self).query(type=token) | ||||||
|  |         # check typeOption: include only matching instances | ||||||
|  |         include = [] | ||||||
|  |         type = self.conceptManager[token.split(':')[-1]] | ||||||
|  |         #print '###', token, repr(type) | ||||||
|  |         opt = IOptions(adapted(type))('qualifier_assign_check_parents') | ||||||
|  |         if opt: | ||||||
|  |             for p in self.target.getAllParents([self.defaultPredicate]): | ||||||
|  |                 for c in p.object.getChildren([self.defaultPredicate]): | ||||||
|  |                     include.append(c) | ||||||
|  |         if include: | ||||||
|  |             result = [c for c in result if c in include] | ||||||
|         fv = FilterView(self.context, self.request) |         fv = FilterView(self.context, self.request) | ||||||
|         result = fv.apply(result) |         result = fv.apply(result) | ||||||
|         result.sort(key=lambda x: x.title) |         result.sort(key=lambda x: x.title) | ||||||
|  | @ -288,8 +331,11 @@ class CreateObjectForm(ObjectForm): | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def defaultTypeToken(self): |     def defaultTypeToken(self): | ||||||
|         return (self.controller.params.get('form.create.defaultTypeToken') |         setting = self.controller.params.get('form.create.defaultTypeToken') | ||||||
|                 or '.loops/concepts/textdocument') |         if setting: | ||||||
|  |             return setting | ||||||
|  |         opt = self.globalOptions('form.create.default_type_token') | ||||||
|  |         return opt and opt[0] or '.loops/concepts/textdocument' | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def typeToken(self): |     def typeToken(self): | ||||||
|  | @ -310,6 +356,10 @@ class CreateObjectForm(ObjectForm): | ||||||
|         if typeToken: |         if typeToken: | ||||||
|             return self.loopsRoot.loopsTraverse(typeToken) |             return self.loopsRoot.loopsTraverse(typeToken) | ||||||
| 
 | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def targetType(self): | ||||||
|  |         return self.typeConcept | ||||||
|  | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def adapted(self): |     def adapted(self): | ||||||
|         ad = self.typeInterface(Resource()) |         ad = self.typeInterface(Resource()) | ||||||
|  | @ -423,6 +473,7 @@ class CreateConceptForm(CreateObjectForm): | ||||||
|             return c |             return c | ||||||
|         ad = ti(c) |         ad = ti(c) | ||||||
|         ad.__is_dummy__ = True |         ad.__is_dummy__ = True | ||||||
|  |         ad.__type__ = adapted(self.typeConcept) | ||||||
|         return ad |         return ad | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| <metal:info define-macro="object_info" | <metal:info define-macro="object_info" | ||||||
|             tal:define="item nocall:view/item"> |             tal:define="item nocall:view/targetItem"> | ||||||
|   <table class="object_info" width="400"> |   <table class="object_info" width="400"> | ||||||
|     <tr> |     <tr> | ||||||
|       <td colspan="2"><h2 i18n:translate="">Object Information</h2><br /></td> |       <td colspan="2"><h2 i18n:translate="">Object Information</h2><br /></td> | ||||||
|  | @ -52,7 +52,7 @@ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| <metal:info define-macro="meta_info" | <metal:info define-macro="meta_info" | ||||||
|             tal:define="item nocall:view/item"> |             tal:define="item nocall:view/targetItem"> | ||||||
|   <table class="object_info" width="400"> |   <table class="object_info" width="400"> | ||||||
|     <tr> |     <tr> | ||||||
|       <td colspan="2"> |       <td colspan="2"> | ||||||
|  |  | ||||||
|  | @ -238,18 +238,21 @@ fieldset.box td { | ||||||
|     font-weight: bold; |     font-weight: bold; | ||||||
|     color: #444; |     color: #444; | ||||||
|     padding-top: 0.4em; |     padding-top: 0.4em; | ||||||
|  |     border-bottom: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .content-4 h1, .content-3 h2, .content-2 h3, .content-1 h4, h4 { | .content-4 h1, .content-3 h2, .content-2 h3, .content-1 h4, h4 { | ||||||
|     font-size: 130%; |     font-size: 130%; | ||||||
|     font-weight: normal; |     font-weight: normal; | ||||||
|     padding-top: 0.3em; |     padding-top: 0.3em; | ||||||
|  |     border-bottom: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .content-5 h1, .content-4 h2, .content-3 h3, content-2 h4, h5 { | .content-5 h1, .content-4 h2, .content-3 h3, content-2 h4, h5 { | ||||||
|     font-size: 120%; |     font-size: 120%; | ||||||
|     /* border: none; */ |     /* border: none; */ | ||||||
|     padding-top: 0.2em; |     padding-top: 0.2em; | ||||||
|  |     border-bottom: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .box { | .box { | ||||||
|  |  | ||||||
|  | @ -47,6 +47,35 @@ function showIfIn(node, conditions) { | ||||||
|     }) |     }) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function setIfIn(node, conditions) { | ||||||
|  |     dojo.forEach(conditions, function(cond) { | ||||||
|  |         if (node.value == cond[0]) { | ||||||
|  |             target = dijit.byId(cond[1]); | ||||||
|  |             target.setValue(cond[2]); | ||||||
|  |         } | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function setIf(node, cond, acts) { | ||||||
|  |     if (node.value == cond) { | ||||||
|  |         dojo.forEach(acts, function(act) { | ||||||
|  |             target = dijit.byId(act[0]); | ||||||
|  |             target.setValue(act[1]); | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function setIfN(node, conds, acts) { | ||||||
|  |     dojo.forEach(conds, function(cond) { | ||||||
|  |         if (node.value == cond) { | ||||||
|  |             dojo.forEach(acts, function(act) { | ||||||
|  |                 target = dijit.byId(act[0]); | ||||||
|  |                 target.setValue(act[1]); | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| function destroyWidgets(node) { | function destroyWidgets(node) { | ||||||
|     dojo.forEach(dojo.query('[widgetId]', node), function(n) { |     dojo.forEach(dojo.query('[widgetId]', node), function(n) { | ||||||
|         w = dijit.byNode(n); |         w = dijit.byNode(n); | ||||||
|  | @ -103,7 +132,7 @@ function submitReplacing(targetId, formId, url) { | ||||||
|         mimetype: "text/html", |         mimetype: "text/html", | ||||||
|         load: function(response, ioArgs) { |         load: function(response, ioArgs) { | ||||||
|             replaceNode(response, targetId); |             replaceNode(response, targetId); | ||||||
|             return resonse; |             return response; | ||||||
|         } |         } | ||||||
|     }) |     }) | ||||||
| } | } | ||||||
|  | @ -115,7 +144,7 @@ function xhrSubmitPopup(formId, url) { | ||||||
|         mimetype: "text/html", |         mimetype: "text/html", | ||||||
|         load: function(response, ioArgs) { |         load: function(response, ioArgs) { | ||||||
|             window.close(); |             window.close(); | ||||||
|             return resonse; |             return response; | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2016 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2017 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -86,10 +86,14 @@ class NodeView(BaseView): | ||||||
|         super(NodeView, self).__init__(context, request) |         super(NodeView, self).__init__(context, request) | ||||||
|         self.viewAnnotations.setdefault('nodeView', self) |         self.viewAnnotations.setdefault('nodeView', self) | ||||||
|         self.viewAnnotations.setdefault('node', self.context) |         self.viewAnnotations.setdefault('node', self.context) | ||||||
|         viewConfig = getViewConfiguration(context, request) |         self.setSkin(self.viewConfig.get('skinName')) | ||||||
|         self.setSkin(viewConfig.get('skinName')) |  | ||||||
| 
 | 
 | ||||||
|     def __call__(self, *args, **kw): |     def __call__(self, *args, **kw): | ||||||
|  |         if self.nodeType == 'raw': | ||||||
|  |             vn = self.context.viewName | ||||||
|  |             if vn: | ||||||
|  |                 self.request.response.setHeader('content-type', vn) | ||||||
|  |             return self.context.body | ||||||
|         tv = self.viewAnnotations.get('targetView') |         tv = self.viewAnnotations.get('targetView') | ||||||
|         if tv is not None: |         if tv is not None: | ||||||
|             if tv.isToplevel: |             if tv.isToplevel: | ||||||
|  | @ -98,6 +102,29 @@ class NodeView(BaseView): | ||||||
|             self.controller.setMainPage() |             self.controller.setMainPage() | ||||||
|         return super(NodeView, self).__call__(*args, **kw) |         return super(NodeView, self).__call__(*args, **kw) | ||||||
| 
 | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def viewConfig(self): | ||||||
|  |         return getViewConfiguration(self.context, self.request) | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def viewConfigOptions(self): | ||||||
|  |         result = {} | ||||||
|  |         for opt in self.viewConfig.get('options') or []: | ||||||
|  |             if ':' in opt: | ||||||
|  |                 k, v = opt.split(':', 1) | ||||||
|  |                 result[k] = v.split(',') | ||||||
|  |             else: | ||||||
|  |                 result[opt] = True | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def copyright(self): | ||||||
|  |         cr = self.viewConfigOptions.get('copyright') | ||||||
|  |         if cr: | ||||||
|  |             return cr[0] | ||||||
|  |         cr = self.globalOptions('copyright') | ||||||
|  |         return cr and cr[0] or 'cyberconcepts.org team' | ||||||
|  | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def macro(self): |     def macro(self): | ||||||
|         return self.template.macros['content'] |         return self.template.macros['content'] | ||||||
|  | @ -115,7 +142,9 @@ class NodeView(BaseView): | ||||||
|             parts.extend(getParts(n)) |             parts.extend(getParts(n)) | ||||||
|         return parts |         return parts | ||||||
| 
 | 
 | ||||||
|     def update(self): |     def update(self, topLevel=True): | ||||||
|  |         if topLevel and self.view != self: | ||||||
|  |             return self.view.update(False) | ||||||
|         result = super(NodeView, self).update() |         result = super(NodeView, self).update() | ||||||
|         self.recordAccess() |         self.recordAccess() | ||||||
|         return result |         return result | ||||||
|  | @ -129,7 +158,7 @@ class NodeView(BaseView): | ||||||
|             return [] |             return [] | ||||||
|         menu = self.menu |         menu = self.menu | ||||||
|         data = [dict(label=menu.title, url=menu.url)] |         data = [dict(label=menu.title, url=menu.url)] | ||||||
|         menuItem = self.nearestMenuItem |         menuItem = self.getNearestMenuItem(all=True) | ||||||
|         if menuItem != menu.context: |         if menuItem != menu.context: | ||||||
|             data.append(dict(label=menuItem.title, |             data.append(dict(label=menuItem.title, | ||||||
|                              url=absoluteURL(menuItem, self.request))) |                              url=absoluteURL(menuItem, self.request))) | ||||||
|  | @ -140,6 +169,9 @@ class NodeView(BaseView): | ||||||
|                                     url=absoluteURL(p, self.request))) |                                     url=absoluteURL(p, self.request))) | ||||||
|         if self.virtualTarget: |         if self.virtualTarget: | ||||||
|             data.extend(self.virtualTarget.breadcrumbs()) |             data.extend(self.virtualTarget.breadcrumbs()) | ||||||
|  |         if data and not '?' in data[-1]['url']: | ||||||
|  |             if self.urlParamString: | ||||||
|  |                 data[-1]['url'] += self.urlParamString | ||||||
|         return data |         return data | ||||||
| 
 | 
 | ||||||
|     def viewModes(self): |     def viewModes(self): | ||||||
|  | @ -366,6 +398,10 @@ class NodeView(BaseView): | ||||||
|     def editable(self): |     def editable(self): | ||||||
|         return canWrite(self.context, 'body') |         return canWrite(self.context, 'body') | ||||||
| 
 | 
 | ||||||
|  |     def hasTopPage(self, name): | ||||||
|  |         page = self.topMenu.context.get(name) | ||||||
|  |         return page is not None | ||||||
|  | 
 | ||||||
|     # menu stuff |     # menu stuff | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|  | @ -411,8 +447,9 @@ class NodeView(BaseView): | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def menuItems(self): |     def menuItems(self): | ||||||
|         return [NodeView(child, self.request) |         items = [NodeView(child, self.request).view | ||||||
|                     for child in self.context.getMenuItems()] |                     for child in self.context.getMenuItems()] | ||||||
|  |         return [item for item in items if item.isVisible] | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def parents(self): |     def parents(self): | ||||||
|  | @ -420,10 +457,13 @@ class NodeView(BaseView): | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def nearestMenuItem(self): |     def nearestMenuItem(self): | ||||||
|  |         return self.getNearestMenuItem() | ||||||
|  | 
 | ||||||
|  |     def getNearestMenuItem(self, all=False): | ||||||
|         menu = self.menuObject |         menu = self.menuObject | ||||||
|         menuItem = None |         menuItem = None | ||||||
|         for p in [self.context] + self.parents: |         for p in [self.context] + self.parents: | ||||||
|             if not p.isMenuItem(): |             if not all and not p.isMenuItem(): | ||||||
|                 menuItem = None |                 menuItem = None | ||||||
|             elif menuItem is None: |             elif menuItem is None: | ||||||
|                 menuItem = p |                 menuItem = p | ||||||
|  | @ -469,7 +509,7 @@ class NodeView(BaseView): | ||||||
|     def targetView(self, name='index.html', methodName='show'): |     def targetView(self, name='index.html', methodName='show'): | ||||||
|         if name == 'index.html':    # only when called for default view |         if name == 'index.html':    # only when called for default view | ||||||
|             tv = self.viewAnnotations.get('targetView') |             tv = self.viewAnnotations.get('targetView') | ||||||
|             if tv is not None: |             if tv is not None and callable(tv): | ||||||
|                 return tv() |                 return tv() | ||||||
|         if '?' in name: |         if '?' in name: | ||||||
|             name, params = name.split('?', 1) |             name, params = name.split('?', 1) | ||||||
|  | @ -567,12 +607,21 @@ class NodeView(BaseView): | ||||||
|         """ Return URL of given target view given as .XXX URL. |         """ Return URL of given target view given as .XXX URL. | ||||||
|         """ |         """ | ||||||
|         if isinstance(target, BaseView): |         if isinstance(target, BaseView): | ||||||
|  |             miu = self.getMenuItemUrlForTarget(target.context) | ||||||
|  |             if miu is not None: | ||||||
|  |                 return miu | ||||||
|             return self.makeTargetUrl(self.url, target.uniqueId, target.title) |             return self.makeTargetUrl(self.url, target.uniqueId, target.title) | ||||||
|         else: |         else: | ||||||
|             target = baseObject(target) |             target = baseObject(target) | ||||||
|             return self.makeTargetUrl(self.url, util.getUidForObject(target), |             return self.makeTargetUrl(self.url, util.getUidForObject(target), | ||||||
|                                       target.title) |                                       target.title) | ||||||
| 
 | 
 | ||||||
|  |     def getMenuItemUrlForTarget(self, tobj): | ||||||
|  |         for node in tobj.getClients(): | ||||||
|  |             if node.nodeType == 'page' and node.getMenu() == self.menuObject: | ||||||
|  |                 return absoluteURL(node, self.request) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     def getActions(self, category='object', page=None, target=None): |     def getActions(self, category='object', page=None, target=None): | ||||||
|         actions = [] |         actions = [] | ||||||
|         #self.registerDojo() |         #self.registerDojo() | ||||||
|  | @ -976,7 +1025,8 @@ class NodeTraverser(ItemTraverser): | ||||||
|         if context.nodeType == 'menu': |         if context.nodeType == 'menu': | ||||||
|             setViewConfiguration(context, request) |             setViewConfiguration(context, request) | ||||||
|         if name == '.loops': |         if name == '.loops': | ||||||
|             return self.context.getLoopsRoot() |             name = self.getTargetUid(request) | ||||||
|  |             #return self.context.getLoopsRoot() | ||||||
|         if name.startswith('.'): |         if name.startswith('.'): | ||||||
|             name = self.cleanUpTraversalStack(request, name)[1:] |             name = self.cleanUpTraversalStack(request, name)[1:] | ||||||
|             target = self.getTarget(name) |             target = self.getTarget(name) | ||||||
|  | @ -1008,17 +1058,34 @@ class NodeTraverser(ItemTraverser): | ||||||
|             raise |             raise | ||||||
|         return obj |         return obj | ||||||
| 
 | 
 | ||||||
|  |     def getTargetUid(self, request): | ||||||
|  |         parent = self.context.getLoopsRoot() | ||||||
|  |         stack = request._traversal_stack | ||||||
|  |         for i in range(2): | ||||||
|  |             name = stack.pop() | ||||||
|  |             obj = parent.get(name) | ||||||
|  |             if not obj: | ||||||
|  |                 return name | ||||||
|  |             parent = obj | ||||||
|  |         return '.' + util.getUidForObject(obj) | ||||||
|  | 
 | ||||||
|     def cleanUpTraversalStack(self, request, name): |     def cleanUpTraversalStack(self, request, name): | ||||||
|         traversalStack = request._traversal_stack |         #traversalStack = request._traversal_stack | ||||||
|         while traversalStack and traversalStack[0].startswith('.'): |         #while traversalStack and traversalStack[0].startswith('.'): | ||||||
|             # skip obsolete target references in the url |             # skip obsolete target references in the url | ||||||
|             name = traversalStack.pop(0) |         #    name = traversalStack.pop(0) | ||||||
|         traversedNames = request._traversed_names |         traversedNames = request._traversed_names | ||||||
|         if traversedNames: |         for n in list(traversedNames): | ||||||
|             lastTraversed = traversedNames[-1] |             if n.startswith('.'): | ||||||
|             if lastTraversed.startswith('.') and lastTraversed != name: |                 # remove obsolete target refs | ||||||
|  |                 traversedNames.remove(n) | ||||||
|  |         #if traversedNames: | ||||||
|  |         #    lastTraversed = traversedNames[-1] | ||||||
|  |         #    if lastTraversed.startswith('.') and lastTraversed != name: | ||||||
|                 # let <base .../> tag show the current object |                 # let <base .../> tag show the current object | ||||||
|                 traversedNames[-1] = name |         #        traversedNames[-1] = name | ||||||
|  |         # let <base .../> tag show the current object | ||||||
|  |         traversedNames.append(name) | ||||||
|         return name |         return name | ||||||
| 
 | 
 | ||||||
|     def getTarget(self, name): |     def getTarget(self, name): | ||||||
|  |  | ||||||
|  | @ -30,7 +30,7 @@ | ||||||
|                              item nocall:target" |                              item nocall:target" | ||||||
|                  tal:attributes="class string:content-$level; |                  tal:attributes="class string:content-$level; | ||||||
|                                  id id; |                                  id id; | ||||||
|                                  ondblclick python: target.openEditWindow('configure.html')"> |                                  ondblclick python:target.openEditWindow('configure.html')"> | ||||||
|               <metal:body use-macro="item/macro"> |               <metal:body use-macro="item/macro"> | ||||||
|                 The body |                 The body | ||||||
|               </metal:body> |               </metal:body> | ||||||
|  | @ -41,17 +41,22 @@ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| <metal:body define-macro="conceptbody"> | <metal:body define-macro="conceptbody"> | ||||||
|   <tal:body define="body item/body;"> |   <tal:body define="body item/body; | ||||||
|  |                     itemNum view/itemNum; | ||||||
|  |                     id string:$itemNum.body"> | ||||||
|     <div class="content-1" id="1" |     <div class="content-1" id="1" | ||||||
|          tal:attributes="class string:content-$level; |          tal:attributes="class string:content-$level; | ||||||
|                          id string:${view/itemNum}.body; |                          id string:${view/itemNum}.body; | ||||||
|                          ondblclick python: item.openEditWindow('configure.html')"> |                          ondblclick python:item.openEditWindow('configure.html')"> | ||||||
|       <span tal:content="structure body">Node Body</span> |       <span tal:content="structure body">Node Body</span> | ||||||
|     </div> |     </div> | ||||||
|     <tal:concepts define="item nocall:item/targetObjectView; |     <div tal:define="item nocall:item/targetObjectView; | ||||||
|                           macro item/macro"> |                      macro item/macro"> | ||||||
|       <div metal:use-macro="macro" /> |       <div tal:attributes="class string:content-$level; | ||||||
|     </tal:concepts> |                            id id;"> | ||||||
|  |         <div metal:use-macro="macro" /> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|   </tal:body> |   </tal:body> | ||||||
| </metal:body> | </metal:body> | ||||||
| 
 | 
 | ||||||
|  | @ -328,11 +333,12 @@ | ||||||
| <metal:login define-macro="login"> | <metal:login define-macro="login"> | ||||||
|     <div> |     <div> | ||||||
|       <a href="login.html" |       <a href="login.html" | ||||||
|             i18n:translate="">Log in</a></div> |          tal:attributes="href string:${view/topMenu/url}/login.html" | ||||||
|  |          i18n:translate="">Log in</a></div> | ||||||
|     <div tal:define="register python:view.globalOptions('provideLogin')" |     <div tal:define="register python:view.globalOptions('provideLogin')" | ||||||
|          tal:condition="register"> |          tal:condition="python:register and register != True"> | ||||||
|       <a tal:condition="python:register != True" |       <a tal:define="reg python:register[0]" | ||||||
|          tal:attributes="href python:register[0]" |          tal:attributes="href string:${view/topMenu/url}/$reg" | ||||||
|          i18n:translate="">Register new member</a></div> |          i18n:translate="">Register new member</a></div> | ||||||
| </metal:login> | </metal:login> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2014 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2015 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -20,6 +20,7 @@ | ||||||
| View class for resource objects. | View class for resource objects. | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
|  | import os.path | ||||||
| import urllib | import urllib | ||||||
| from zope.cachedescriptors.property import Lazy | from zope.cachedescriptors.property import Lazy | ||||||
| from zope import component | from zope import component | ||||||
|  | @ -47,7 +48,7 @@ from loops.browser.common import EditForm, BaseView | ||||||
| from loops.browser.concept import BaseRelationView, ConceptRelationView | from loops.browser.concept import BaseRelationView, ConceptRelationView | ||||||
| from loops.browser.concept import ConceptConfigureView | from loops.browser.concept import ConceptConfigureView | ||||||
| from loops.browser.node import NodeView, node_macros | from loops.browser.node import NodeView, node_macros | ||||||
| from loops.common import adapted, NameChooser, normalizeName | from loops.common import adapted, baseObject, NameChooser, normalizeName | ||||||
| from loops.interfaces import IBaseResource, IDocument, ITextDocument | from loops.interfaces import IBaseResource, IDocument, ITextDocument | ||||||
| from loops.interfaces import IMediaAsset as legacy_IMediaAsset | from loops.interfaces import IMediaAsset as legacy_IMediaAsset | ||||||
| from loops.interfaces import ITypeConcept | from loops.interfaces import ITypeConcept | ||||||
|  | @ -196,6 +197,9 @@ class ResourceView(BaseView): | ||||||
|         context = self.context |         context = self.context | ||||||
|         ct = context.contentType |         ct = context.contentType | ||||||
|         response = self.request.response |         response = self.request.response | ||||||
|  |         if self.typeOptions('x_robots_tag_header', None) is not None: | ||||||
|  |             tagVal = ', '.join(self.typeOptions('x_robots_tag_header')) | ||||||
|  |             response.setHeader('X-Robots-Tag', tagVal) | ||||||
|         self.recordAccess('show', target=self.uniqueId) |         self.recordAccess('show', target=self.uniqueId) | ||||||
|         if ct.startswith('image/'): |         if ct.startswith('image/'): | ||||||
|             #response.setHeader('Cache-Control', 'public,max-age=86400') |             #response.setHeader('Cache-Control', 'public,max-age=86400') | ||||||
|  | @ -216,6 +220,16 @@ class ResourceView(BaseView): | ||||||
|             if filename is None: |             if filename is None: | ||||||
|                 filename = (adapted(self.context).localFilename or |                 filename = (adapted(self.context).localFilename or | ||||||
|                                 getName(self.context)) |                                 getName(self.context)) | ||||||
|  |                 if self.typeOptions('use_title_for_download_filename'): | ||||||
|  |                     base, ext = os.path.splitext(filename) | ||||||
|  |                     filename = context.title | ||||||
|  |                     vr = IVersionable(baseObject(context)) | ||||||
|  |                     if len(vr.versions) > 0: | ||||||
|  |                         filename = vr.generateName(filename, ext, vr.versionId) | ||||||
|  |                     else: | ||||||
|  |                         if not filename.endswith(ext): | ||||||
|  |                             filename += ext | ||||||
|  |                     filename = filename.encode('UTF-8') | ||||||
|             if self.typeOptions('no_normalize_download_filename'): |             if self.typeOptions('no_normalize_download_filename'): | ||||||
|                 filename = '"%s"' % filename |                 filename = '"%s"' % filename | ||||||
|             else: |             else: | ||||||
|  | @ -262,11 +276,17 @@ class ResourceView(BaseView): | ||||||
|             #return util.toUnicode(wp.render(self.request)) |             #return util.toUnicode(wp.render(self.request)) | ||||||
|         return super(ResourceView, self).renderText(text, contentType) |         return super(ResourceView, self).renderText(text, contentType) | ||||||
| 
 | 
 | ||||||
|  |     showMore = True | ||||||
|  | 
 | ||||||
|     def renderShortText(self): |     def renderShortText(self): | ||||||
|         return self.renderDescription() or self.createShortText(self.render()) |         return self.renderDescription() or self.createShortText(self.render()) | ||||||
| 
 | 
 | ||||||
|     def createShortText(self, text=None): |     def createShortText(self, text=None): | ||||||
|         return extractFirstPart(text or self.render()) |         text = (text or self.render()).strip() | ||||||
|  |         shortText = extractFirstPart(text) | ||||||
|  |         if shortText == text: | ||||||
|  |             self.showMore = False | ||||||
|  |         return shortText | ||||||
| 
 | 
 | ||||||
|     def download(self): |     def download(self): | ||||||
|         """ Force download, e.g. of a PDF file """ |         """ Force download, e.g. of a PDF file """ | ||||||
|  | @ -471,4 +491,3 @@ class NoteView(DocumentView): | ||||||
|     def linkUrl(self): |     def linkUrl(self): | ||||||
|         ad = self.typeAdapter |         ad = self.typeAdapter | ||||||
|         return ad and ad.linkUrl or '' |         return ad and ad.linkUrl or '' | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ | ||||||
|         <div metal:use-macro="views/node_macros/object_actions" /> |         <div metal:use-macro="views/node_macros/object_actions" /> | ||||||
|       </tal:actions> |       </tal:actions> | ||||||
|       <h1><a tal:omit-tag="python: level > 1" |       <h1><a tal:omit-tag="python: level > 1" | ||||||
|              tal:attributes="href request/URL" |              tal:attributes="href view/requestUrl" | ||||||
|              tal:content="item/title">Title</a></h1> |              tal:content="item/title">Title</a></h1> | ||||||
|       <tal:desc define="description description|item/renderedDescription" |       <tal:desc define="description description|item/renderedDescription" | ||||||
|                 condition="description"> |                 condition="description"> | ||||||
|  | @ -51,7 +51,7 @@ | ||||||
|   <div tal:attributes="ondblclick python: item.openEditWindow('edit.html')"> |   <div tal:attributes="ondblclick python: item.openEditWindow('edit.html')"> | ||||||
|     <div metal:use-macro="views/node_macros/object_actions" /> |     <div metal:use-macro="views/node_macros/object_actions" /> | ||||||
|     <h1><a tal:omit-tag="python: level > 1" |     <h1><a tal:omit-tag="python: level > 1" | ||||||
|            tal:attributes="href request/URL" |            tal:attributes="href view/requestUrl" | ||||||
|            tal:content="item/title">Title</a></h1><br /> |            tal:content="item/title">Title</a></h1><br /> | ||||||
|     <img tal:attributes="src |     <img tal:attributes="src | ||||||
|                 string:${view/url}/.${view/targetId}/view?version=this" /> |                 string:${view/url}/.${view/targetId}/view?version=this" /> | ||||||
|  | @ -96,6 +96,7 @@ | ||||||
|         </a> |         </a> | ||||||
|       </span> |       </span> | ||||||
|     </div> |     </div> | ||||||
|  |     <metal:custom define-slot="custom_info" /> | ||||||
|     <metal:fields use-macro="view/comment_macros/comments" /> |     <metal:fields use-macro="view/comment_macros/comments" /> | ||||||
|   </div> |   </div> | ||||||
| </metal:block> | </metal:block> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,4 @@ | ||||||
| """ | # package loops.browser.skin | ||||||
| $Id$ |  | ||||||
| """ |  | ||||||
| 
 | 
 | ||||||
| from cybertools.browser.liquid import Liquid | from cybertools.browser.liquid import Liquid | ||||||
| from cybertools.browser.blue import Blue | from cybertools.browser.blue import Blue | ||||||
|  |  | ||||||
|  | @ -69,9 +69,9 @@ | ||||||
|          metal:define-macro="footer"> |          metal:define-macro="footer"> | ||||||
|       <metal:footer define-slot="footer"> |       <metal:footer define-slot="footer"> | ||||||
|         © Copyright <span tal:replace="view/currentYear" />, |         © Copyright <span tal:replace="view/currentYear" />, | ||||||
|         cyberconcepts IT-Consulting Dr. Helmut Merz |         <span tal:replace="view/topMenu/copyright" /> | ||||||
|         (<a href="#" |         (<a i18n:translate="" | ||||||
|           tal:attributes="href string:${view/topMenu/url}/impressum">Impressum</a>) |             tal:attributes="href string:${view/topMenu/url}/impressum">Impressum</a>) | ||||||
|         <br /> |         <br /> | ||||||
|         Powered by |         Powered by | ||||||
|         <b><a href="http://www.wissen-statt-suchen.de">loops</a></b> · |         <b><a href="http://www.wissen-statt-suchen.de">loops</a></b> · | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ body { | ||||||
| 
 | 
 | ||||||
| #portlets { | #portlets { | ||||||
|     margin-top: 1em; |     margin-top: 1em; | ||||||
|  |     background-color: #ffffff; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| ul.view-modes { | ul.view-modes { | ||||||
|  | @ -108,6 +109,14 @@ thead th { | ||||||
|     background: none; |     background: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /* printing */ | ||||||
|  | 
 | ||||||
|  | @media print { | ||||||
|  |     .noprint { | ||||||
|  |         display: none; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /* class-specific */ | /* class-specific */ | ||||||
| 
 | 
 | ||||||
| .breadcrumbs td { | .breadcrumbs td { | ||||||
|  | @ -253,10 +262,26 @@ table.records th, table.records td { | ||||||
|     border: 1px solid lightgrey; |     border: 1px solid lightgrey; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | table.report { | ||||||
|  |     position: relative;  | ||||||
|  |     z-index: 99;  | ||||||
|  |     background: white; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | table.report th { | ||||||
|  |     border-bottom: 1px solid #bbbbbb; | ||||||
|  |     font-weight: bold; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| table.report td { | table.report td { | ||||||
|  |     border-bottom: 1px dotted #dddddd; | ||||||
|     vertical-align: top; |     vertical-align: top; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .report-meta table { | ||||||
|  |     width: auto; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| dl.docutils dt { | dl.docutils dt { | ||||||
|     font-weight: bold; |     font-weight: bold; | ||||||
|     margin-top: 0.3em; |     margin-top: 0.3em; | ||||||
|  |  | ||||||
|  | @ -11,8 +11,18 @@ body { | ||||||
|     display: none; |     display: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .breadcrumbs { | ||||||
|  |     display: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .container { | ||||||
|  |     width: auto; | ||||||
|  |     margin: 0; | ||||||
|  |     border: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #content { | #content { | ||||||
| /*  width: 100%; */ |     width: auto; | ||||||
|     width: 80%; |  | ||||||
|     color: Black; |     color: Black; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ | ||||||
|           method="post" name="listing" action="." |           method="post" name="listing" action="." | ||||||
|           tal:define="target nocall:view/target" |           tal:define="target nocall:view/target" | ||||||
|           tal:condition="python: target or items" |           tal:condition="python: target or items" | ||||||
|           tal:attributes="action request/URL"> |           tal:attributes="action view/requestUrl"> | ||||||
|       <input type="hidden" name="action" value="assign" |       <input type="hidden" name="action" value="assign" | ||||||
|              tal:attributes="value action" /> |              tal:attributes="value action" /> | ||||||
|       <table class="listing" summary="Currently assigned" |       <table class="listing" summary="Currently assigned" | ||||||
|  | @ -82,7 +82,7 @@ | ||||||
|   <fieldset> |   <fieldset> | ||||||
|     <legend i18n:translate="">Create Target</legend> |     <legend i18n:translate="">Create Target</legend> | ||||||
|     <form method="post" name="listing" action="." |     <form method="post" name="listing" action="." | ||||||
|         tal:attributes="action request/URL"> |         tal:attributes="action view/requestUrl"> | ||||||
|       <input type="hidden" name="action" value="create" /> |       <input type="hidden" name="action" value="create" /> | ||||||
|       <div class="row"> |       <div class="row"> | ||||||
|         <span i18n:translate="">Name</span> |         <span i18n:translate="">Name</span> | ||||||
|  | @ -113,7 +113,7 @@ | ||||||
| 
 | 
 | ||||||
| <metal:search define-macro="search"> | <metal:search define-macro="search"> | ||||||
|   <form method="post" name="listing" action="." |   <form method="post" name="listing" action="." | ||||||
|         tal:attributes="action request/URL"> |         tal:attributes="action view/requestUrl"> | ||||||
|       <input type="hidden" name="action" value="search" /> |       <input type="hidden" name="action" value="search" /> | ||||||
|       <div class="row" |       <div class="row" | ||||||
|            tal:define="searchTerm request/searchTerm | nothing; |            tal:define="searchTerm request/searchTerm | nothing; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2007 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2015 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -18,8 +18,6 @@ | ||||||
| 
 | 
 | ||||||
| """ | """ | ||||||
| Adapters and others classes for analyzing resources. | Adapters and others classes for analyzing resources. | ||||||
| 
 |  | ||||||
| $Id$ |  | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| from itertools import tee | from itertools import tee | ||||||
|  | @ -41,6 +39,7 @@ from loops.resource import Resource | ||||||
| from loops.setup import addAndConfigureObject | from loops.setup import addAndConfigureObject | ||||||
| from loops.type import TypeInterfaceSourceList | from loops.type import TypeInterfaceSourceList | ||||||
| 
 | 
 | ||||||
|  | logger = getLogger('Classifier') | ||||||
| 
 | 
 | ||||||
| TypeInterfaceSourceList.typeInterfaces += (IClassifier,) | TypeInterfaceSourceList.typeInterfaces += (IClassifier,) | ||||||
| 
 | 
 | ||||||
|  | @ -102,15 +101,15 @@ class Classifier(AdapterBase): | ||||||
|         if resource not in resources: |         if resource not in resources: | ||||||
|             concept.assignResource(resource, predicate) |             concept.assignResource(resource, predicate) | ||||||
|             message = u'Assigning: %s %s %s' |             message = u'Assigning: %s %s %s' | ||||||
|  |             self.log(message % (resource.title, predicate.title, concept.title), 5) | ||||||
|         else: |         else: | ||||||
|             message = u'Already assigned: %s %s %s' |             message = u'Already assigned: %s %s %s' | ||||||
|         self.log(message % (resource.title, predicate.title, concept.title), 4) |             self.log(message % (resource.title, predicate.title, concept.title), 4) | ||||||
| 
 | 
 | ||||||
|     def log(self, message, level=5): |     def log(self, message, level=5): | ||||||
|         if level >= self.logLevel: |         if level >= self.logLevel: | ||||||
|             #print 'Classifier %s:' % getName(self.context), message |             #print 'Classifier %s:' % getName(self.context), message | ||||||
|             getLogger('Classifier').info( |             logger.info(u'%s: %s' % (getName(self.context), message)) | ||||||
|                 u'%s: %s' % (getName(self.context), message)) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Extractor(object): | class Extractor(object): | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2007 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2015 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -18,17 +18,20 @@ | ||||||
| 
 | 
 | ||||||
| """ | """ | ||||||
| View class(es) for resource classifiers. | View class(es) for resource classifiers. | ||||||
| 
 |  | ||||||
| $Id$ |  | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
|  | from logging import getLogger | ||||||
|  | import transaction | ||||||
| from zope import interface, component | from zope import interface, component | ||||||
| from zope.app.pagetemplate import ViewPageTemplateFile | from zope.app.pagetemplate import ViewPageTemplateFile | ||||||
| from zope.cachedescriptors.property import Lazy | from zope.cachedescriptors.property import Lazy | ||||||
|  | from zope.traversing.api import getName | ||||||
| 
 | 
 | ||||||
| from loops.browser.concept import ConceptView | from loops.browser.concept import ConceptView | ||||||
| from loops.common import adapted | from loops.common import adapted | ||||||
| 
 | 
 | ||||||
|  | logger = getLogger('ClassifierView') | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class ClassifierView(ConceptView): | class ClassifierView(ConceptView): | ||||||
| 
 | 
 | ||||||
|  | @ -42,12 +45,18 @@ class ClassifierView(ConceptView): | ||||||
|         if 'update' in self.request.form: |         if 'update' in self.request.form: | ||||||
|             cta = adapted(self.context) |             cta = adapted(self.context) | ||||||
|             if cta is not None: |             if cta is not None: | ||||||
|                 for r in collectResources(self.context): |                 for idx, r in enumerate(collectResources(self.context)): | ||||||
|  |                     if idx % 1000 == 0: | ||||||
|  |                         logger.info('Committing, resource # %s' % idx) | ||||||
|  |                         transaction.commit() | ||||||
|                     cta.process(r) |                     cta.process(r) | ||||||
|  |             logger.info('Finished processing') | ||||||
|  |             transaction.commit() | ||||||
|         return True |         return True | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def collectResources(concept, checkedConcepts=None, result=None): | def collectResources(concept, checkedConcepts=None, result=None): | ||||||
|  |     logger.info('Start collecting resources for %s' % getName(concept)) | ||||||
|     if result is None: |     if result is None: | ||||||
|         result = [] |         result = [] | ||||||
|     if checkedConcepts is None: |     if checkedConcepts is None: | ||||||
|  | @ -59,4 +68,5 @@ def collectResources(concept, checkedConcepts=None, result=None): | ||||||
|         if c not in checkedConcepts: |         if c not in checkedConcepts: | ||||||
|             checkedConcepts.append(c) |             checkedConcepts.append(c) | ||||||
|             collectResources(c, checkedConcepts, result) |             collectResources(c, checkedConcepts, result) | ||||||
|  |     logger.info('Collected %s resources' % len(result)) | ||||||
|     return result |     return result | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| 
 | 
 | ||||||
| import unittest, doctest | import unittest, doctest | ||||||
| from zope.interface.verify import verifyClass | from zope.interface.verify import verifyClass | ||||||
| #from loops.versioning import versionable |  | ||||||
| 
 | 
 | ||||||
| class Test(unittest.TestCase): | class Test(unittest.TestCase): | ||||||
|     "Basic tests for the classifier sub-package." |     "Basic tests for the classifier sub-package." | ||||||
|  |  | ||||||
							
								
								
									
										14
									
								
								common.py
									
										
									
									
									
								
							
							
						
						
									
										14
									
								
								common.py
									
										
									
									
									
								
							|  | @ -235,17 +235,19 @@ class NameChooser(BaseNameChooser): | ||||||
|         return name |         return name | ||||||
| 
 | 
 | ||||||
|     def generateNameFromTitle(self, obj): |     def generateNameFromTitle(self, obj): | ||||||
|         title = obj.title |         return generateNameFromTitle(obj.title) | ||||||
|         if len(title) > 15: |  | ||||||
|             words = title.split() |  | ||||||
|             if len(words) > 1: |  | ||||||
|                 title = '_'.join((words[0], words[-1])) |  | ||||||
|         return self.normalizeName(title) |  | ||||||
| 
 | 
 | ||||||
|     def normalizeName(self, baseName): |     def normalizeName(self, baseName): | ||||||
|         return normalizeName(baseName) |         return normalizeName(baseName) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def generateNameFromTitle(title): | ||||||
|  |         if len(title) > 15: | ||||||
|  |             words = title.split() | ||||||
|  |             if len(words) > 1: | ||||||
|  |                 title = '_'.join((words[0], words[-1])) | ||||||
|  |         return normalizeName(title) | ||||||
|  | 
 | ||||||
| def normalizeName(baseName): | def normalizeName(baseName): | ||||||
|     specialCharacters = { |     specialCharacters = { | ||||||
|         '\xc4': 'Ae', '\xe4': 'ae', '\xd6': 'Oe', '\xf6': 'oe', |         '\xc4': 'Ae', '\xe4': 'ae', '\xd6': 'Oe', '\xf6': 'oe', | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2013 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2017 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -34,6 +34,7 @@ from loops.browser.concept import ConceptRelationView as \ | ||||||
|     BaseConceptRelationView |     BaseConceptRelationView | ||||||
| from loops.browser.resource import ResourceView as BaseResourceView | from loops.browser.resource import ResourceView as BaseResourceView | ||||||
| from loops.common import adapted, baseObject | from loops.common import adapted, baseObject | ||||||
|  | from loops.util import _ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| standard_template = standard.standard_template | standard_template = standard.standard_template | ||||||
|  | @ -54,42 +55,6 @@ class Base(object): | ||||||
|     def sectionType(self): |     def sectionType(self): | ||||||
|         return self.conceptManager['section'] |         return self.conceptManager['section'] | ||||||
| 
 | 
 | ||||||
|     @Lazy |  | ||||||
|     def isPartOfPredicate(self): |  | ||||||
|         return self.conceptManager['ispartof'] |  | ||||||
| 
 |  | ||||||
|     @Lazy |  | ||||||
|     def showNavigation(self): |  | ||||||
|         return self.typeOptions.show_navigation |  | ||||||
| 
 |  | ||||||
|     @Lazy |  | ||||||
|     def breadcrumbsParent(self): |  | ||||||
|         for p in self.context.getParents([self.isPartOfPredicate]): |  | ||||||
|             return self.nodeView.getViewForTarget(p) |  | ||||||
| 
 |  | ||||||
|     @Lazy |  | ||||||
|     def neighbours(self): |  | ||||||
|         pred = succ = None |  | ||||||
|         parent = self.breadcrumbsParent |  | ||||||
|         if parent is not None: |  | ||||||
|             myself = None |  | ||||||
|             children = list(parent.context.getChildren([self.isPartOfPredicate])) |  | ||||||
|             for idx, c in enumerate(children): |  | ||||||
|                 if c == self.context: |  | ||||||
|                     if idx > 0: |  | ||||||
|                         pred = self.nodeView.getViewForTarget(children[idx-1]) |  | ||||||
|                     if idx < len(children) - 1: |  | ||||||
|                         succ = self.nodeView.getViewForTarget(children[idx+1]) |  | ||||||
|         return pred, succ |  | ||||||
| 
 |  | ||||||
|     @Lazy |  | ||||||
|     def predecessor(self): |  | ||||||
|         return self.neighbours[0] |  | ||||||
| 
 |  | ||||||
|     @Lazy |  | ||||||
|     def successor(self): |  | ||||||
|         return self.neighbours[1] |  | ||||||
| 
 |  | ||||||
|     @Lazy |     @Lazy | ||||||
|     def tabview(self): |     def tabview(self): | ||||||
|         if self.editable: |         if self.editable: | ||||||
|  | @ -107,6 +72,7 @@ class Base(object): | ||||||
|     @Lazy |     @Lazy | ||||||
|     def textResources(self): |     def textResources(self): | ||||||
|         self.images = [[]] |         self.images = [[]] | ||||||
|  |         self.otherResources = [] | ||||||
|         result = [] |         result = [] | ||||||
|         idx = 0 |         idx = 0 | ||||||
|         for rv in self.getResources(): |         for rv in self.getResources(): | ||||||
|  | @ -115,7 +81,7 @@ class Base(object): | ||||||
|                         idx += 1 |                         idx += 1 | ||||||
|                         result.append(rv) |                         result.append(rv) | ||||||
|                         self.images.append([]) |                         self.images.append([]) | ||||||
|             else: |             elif rv.context.contentType.startswith('image/'): | ||||||
|                 self.registerDojoLightbox() |                 self.registerDojoLightbox() | ||||||
|                 url = self.nodeView.getUrlForTarget(rv.context) |                 url = self.nodeView.getUrlForTarget(rv.context) | ||||||
|                 src = '%s/mediaasset.html?v=small' % url |                 src = '%s/mediaasset.html?v=small' % url | ||||||
|  | @ -123,6 +89,8 @@ class Base(object): | ||||||
|                 img = dict(src=src, fullImageUrl=fullSrc, title=rv.title, |                 img = dict(src=src, fullImageUrl=fullSrc, title=rv.title, | ||||||
|                            description=rv.description, url=url, object=rv) |                            description=rv.description, url=url, object=rv) | ||||||
|                 self.images[idx].append(img) |                 self.images[idx].append(img) | ||||||
|  |             else: | ||||||
|  |                 self.otherResources.append(rv) | ||||||
|         return result |         return result | ||||||
| 
 | 
 | ||||||
|     def getDocumentTypeForResource(self, r): |     def getDocumentTypeForResource(self, r): | ||||||
|  | @ -178,9 +146,47 @@ class SectionView(Base, ConceptView): | ||||||
|     def macro(self): |     def macro(self): | ||||||
|         return book_template.macros['section'] |         return book_template.macros['section'] | ||||||
| 
 | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def isPartOfPredicate(self): | ||||||
|  |         return self.conceptManager['ispartof'] | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def breadcrumbsParent(self): | ||||||
|  |         for p in self.context.getParents([self.isPartOfPredicate]): | ||||||
|  |             return self.nodeView.getViewForTarget(p) | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def showNavigation(self): | ||||||
|  |         return self.typeOptions.show_navigation | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def neighbours(self): | ||||||
|  |         pred = succ = None | ||||||
|  |         parent = self.breadcrumbsParent | ||||||
|  |         if parent is not None: | ||||||
|  |             myself = None | ||||||
|  |             children = list(parent.context.getChildren([self.isPartOfPredicate])) | ||||||
|  |             for idx, c in enumerate(children): | ||||||
|  |                 if c == self.context: | ||||||
|  |                     if idx > 0: | ||||||
|  |                         pred = self.nodeView.getViewForTarget(children[idx-1]) | ||||||
|  |                     if idx < len(children) - 1: | ||||||
|  |                         succ = self.nodeView.getViewForTarget(children[idx+1]) | ||||||
|  |         return pred, succ | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def predecessor(self): | ||||||
|  |         return self.neighbours[0] | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def successor(self): | ||||||
|  |         return self.neighbours[1] | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class TopicView(Base, ConceptView): | class TopicView(Base, ConceptView): | ||||||
| 
 | 
 | ||||||
|  |     tabTitle = _(u'title_bookTopicView') | ||||||
|  | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def macro(self): |     def macro(self): | ||||||
|         return book_template.macros['topic'] |         return book_template.macros['topic'] | ||||||
|  |  | ||||||
|  | @ -130,7 +130,8 @@ | ||||||
| 
 | 
 | ||||||
| <metal:topic define-macro="topic" | <metal:topic define-macro="topic" | ||||||
|              tal:define="children children|python:list(item.children()); |              tal:define="children children|python:list(item.children()); | ||||||
|                          textResources textResources|item/textResources"> |                          textResources textResources|item/textResources; | ||||||
|  |                          resources item/otherResources"> | ||||||
|   <metal:info use-macro="view/concept_macros/concepttitle" /> |   <metal:info use-macro="view/concept_macros/concepttitle" /> | ||||||
|   <h2 i18n:translate="" |   <h2 i18n:translate="" | ||||||
|       tal:condition="children">Children</h2> |       tal:condition="children">Children</h2> | ||||||
|  | @ -145,14 +146,25 @@ | ||||||
|         <a tal:attributes="href python:view.getUrlForTarget(related.context)" |         <a tal:attributes="href python:view.getUrlForTarget(related.context)" | ||||||
|            tal:content="related/title" /> |            tal:content="related/title" /> | ||||||
|       </h3> |       </h3> | ||||||
|       <div> |       <div tal:define="shortText related/renderShortText"> | ||||||
|         <div tal:replace="structure related/renderShortText" /> |         <div tal:replace="structure shortText" /> | ||||||
|         <p> |         <p> | ||||||
|           <a i18n:translate="" |           <a i18n:translate="" | ||||||
|               tal:attributes="href python:view.getUrlForTarget(related.context)"> |              tal:condition="related/showMore" | ||||||
|  |              tal:attributes="href python:view.getUrlForTarget(related.context)"> | ||||||
|             more...</a></p> |             more...</a></p> | ||||||
|  |         <div tal:repeat="image python: | ||||||
|  |                             item.images[repeat['related'].index() + 1]"> | ||||||
|  |           <a dojoType="dojox.image.Lightbox" group="mediasset" | ||||||
|  |              i18n:attributes="title" | ||||||
|  |              tal:attributes="href image/fullImageUrl; | ||||||
|  |                              title image/title"> | ||||||
|  |             <img tal:attributes="src image/src; | ||||||
|  |                                  alt image/title" /></a> | ||||||
|  |         </div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|  |   <metal:info use-macro="view/concept_macros/conceptresources" /> | ||||||
| </metal:topic> | </metal:topic> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -45,7 +45,7 @@ from cybertools.typology.interfaces import IType, ITypeManager | ||||||
| from cybertools.util.jeep import Jeep | from cybertools.util.jeep import Jeep | ||||||
| 
 | 
 | ||||||
| from loops.base import ParentInfo | from loops.base import ParentInfo | ||||||
| from loops.common import adapted, AdapterBase | from loops.common import adapted, baseObject, AdapterBase | ||||||
| from loops.i18n.common import I18NValue | from loops.i18n.common import I18NValue | ||||||
| from loops.interfaces import IConcept, IConceptRelation, IConceptView | from loops.interfaces import IConcept, IConceptRelation, IConceptView | ||||||
| from loops.interfaces import IResource | from loops.interfaces import IResource | ||||||
|  | @ -490,14 +490,14 @@ class IndexAttributes(object): | ||||||
|             title = u'' |             title = u'' | ||||||
|         if isinstance(title, I18NValue): |         if isinstance(title, I18NValue): | ||||||
|             title = ' '.join(title.values()) |             title = ' '.join(title.values()) | ||||||
|         return ' '.join((getName(context), title)).strip() |         return ' '.join((getName(baseObject(context)), title)).strip() | ||||||
| 
 | 
 | ||||||
|     def date(self): |     def date(self): | ||||||
|         if self.adaptedIndexAttributes is not None: |         if self.adaptedIndexAttributes is not None: | ||||||
|             return self.adaptedIndexAttributes.date() |             return self.adaptedIndexAttributes.date() | ||||||
| 
 | 
 | ||||||
|     def creators(self): |     def creators(self): | ||||||
|         cr = IZopeDublinCore(self.context).creators or [] |         cr = IZopeDublinCore(baseObject(self.context)).creators or [] | ||||||
|         pau = component.getUtility(IAuthentication) |         pau = component.getUtility(IAuthentication) | ||||||
|         creators = [] |         creators = [] | ||||||
|         for c in cr: |         for c in cr: | ||||||
|  | @ -514,7 +514,7 @@ class IndexAttributes(object): | ||||||
|     def identifier(self): |     def identifier(self): | ||||||
|         id = getattr(self.adapted, 'identifier', None) |         id = getattr(self.adapted, 'identifier', None) | ||||||
|         if id is None: |         if id is None: | ||||||
|             return getName(self.context) |             return getName(baseObject(self.context)) | ||||||
|         return id |         return id | ||||||
| 
 | 
 | ||||||
|     def keywords(self): |     def keywords(self): | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| <configure | <configure | ||||||
|    xmlns="http://namespaces.zope.org/zope" |    xmlns="http://namespaces.zope.org/zope" | ||||||
|    xmlns:i18n="http://namespaces.zope.org/i18n" |    xmlns:i18n="http://namespaces.zope.org/i18n" | ||||||
|  |    xmlns:browser="http://namespaces.zope.org/browser" | ||||||
|    i18n_domain="loops"> |    i18n_domain="loops"> | ||||||
| 
 | 
 | ||||||
|   <i18n:registerTranslations directory="locales" /> |   <i18n:registerTranslations directory="locales" /> | ||||||
|  | @ -478,6 +479,19 @@ | ||||||
|       component="loops.view.NodeTypeSourceList" |       component="loops.view.NodeTypeSourceList" | ||||||
|       name="loops.nodeTypeSource" /> |       name="loops.nodeTypeSource" /> | ||||||
| 
 | 
 | ||||||
|  |   <!-- Markdown support --> | ||||||
|  | 
 | ||||||
|  |   <utility | ||||||
|  |       component="loops.util.MarkdownSourceFactory" | ||||||
|  |       name="loops.util.markdown" | ||||||
|  |       /> | ||||||
|  | 
 | ||||||
|  |   <browser:view | ||||||
|  |       name="" | ||||||
|  |       for="loops.util.IMarkdownSource" | ||||||
|  |       class="loops.util.MarkdownToHTMLRenderer" | ||||||
|  |       permission="zope.Public" /> | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|   <include package=".browser" /> |   <include package=".browser" /> | ||||||
|   <include package=".classifier" /> |   <include package=".classifier" /> | ||||||
|  |  | ||||||
|  | @ -1,11 +1,14 @@ | ||||||
| # types | # types | ||||||
| type(u'query', u'Abfrage', options=u'', | type(u'query', u'Abfrage', options=u'', | ||||||
|     typeInterface='loops.expert.concept.IQueryConcept', viewName=u'') |     typeInterface='loops.expert.concept.IQueryConcept', viewName=u'') | ||||||
|  | type(u'datatable', u'Datentabelle', options=u'action.portlet:edit_concept', | ||||||
|  |     typeInterface='loops.table.IDataTable', viewName=u'') | ||||||
| type(u'task', u'Aufgabe', options=u'', | type(u'task', u'Aufgabe', options=u'', | ||||||
|     typeInterface='loops.knowledge.interfaces.ITask', viewName=u'') |     typeInterface='loops.knowledge.interfaces.ITask', viewName=u'') | ||||||
| type(u'domain', u'Bereich', options=u'', typeInterface=u'', viewName=u'') | type(u'domain', u'Bereich', options=u'', typeInterface=u'', viewName=u'') | ||||||
| type(u'classifier', u'Classifier', options=u'', | type(u'classifier', u'Classifier', options=u'', | ||||||
|     typeInterface='loops.classifier.interfaces.IClassifier', viewName=u'classifier.html') |     typeInterface='loops.classifier.interfaces.IClassifier',  | ||||||
|  |     viewName=u'classifier.html') | ||||||
| type(u'documenttype', u'Dokumentenart', options=u'', typeInterface=u'', viewName=u'') | type(u'documenttype', u'Dokumentenart', options=u'', typeInterface=u'', viewName=u'') | ||||||
| type(u'extcollection', u'External Collection', options=u'', | type(u'extcollection', u'External Collection', options=u'', | ||||||
|     typeInterface='loops.integrator.interfaces.IExternalCollection', |     typeInterface='loops.integrator.interfaces.IExternalCollection', | ||||||
|  | @ -14,20 +17,23 @@ type(u'folder', u'Ordner', options=u'', typeInterface=u'', viewName=u'') | ||||||
| type(u'glossaryitem', u'Glossareintrag', options=u'', | type(u'glossaryitem', u'Glossareintrag', options=u'', | ||||||
|     typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'glossaryitem.html') |     typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'glossaryitem.html') | ||||||
| type(u'media_asset', u'Media Asset', | type(u'media_asset', u'Media Asset', | ||||||
|     options=u'storage:varsubdir\nstorage_parameters:extfiles/sites_zzz\nasset_transform.minithumb: size(105)\nasset_transform.small: size(230)\nasset_transform.medium: size(480)', typeInterface='loops.media.interfaces.IMediaAsset', viewName=u'image_medium.html') |     options=u'storage:varsubdir\nstorage_parameters:extfiles/sites_zzz\nasset_transform.minithumb: size(105)\nasset_transform.small: size(230)\nasset_transform.medium: size(480)', typeInterface='loops.media.interfaces.IMediaAsset',  | ||||||
|  |     viewName=u'image_medium.html') | ||||||
| type(u'note', u'Notiz', options=u'', typeInterface='loops.interfaces.INote', | type(u'note', u'Notiz', options=u'', typeInterface='loops.interfaces.INote', | ||||||
|     viewName='note.html') |     viewName='note.html') | ||||||
| type(u'person', u'Person', options=u'', | type(u'person', u'Person', options=u'', | ||||||
|     typeInterface='loops.knowledge.interfaces.IPerson', viewName=u'') |     typeInterface='loops.knowledge.interfaces.IPerson', viewName=u'') | ||||||
| type(u'predicate', u'Prädikat', options=u'', | type(u'predicate', u'Prädikat', options=u'', | ||||||
|     typeInterface=u'loops.interfaces.IPredicate', viewName=u'') |     typeInterface=u'loops.interfaces.IPredicate', viewName=u'') | ||||||
| type(u'event', u'Termin', options=u'', typeInterface='loops.organize.interfaces.ITask', | type(u'event', u'Termin', options=u'',  | ||||||
|  |     typeInterface='loops.organize.interfaces.ITask', | ||||||
|     viewName=u'task.html') |     viewName=u'task.html') | ||||||
| type(u'textdocument', u'Text', options=u'', typeInterface='loops.interfaces.ITextDocument', viewName=u'') | type(u'textdocument', u'Text', options=u'',  | ||||||
| type(u'topic', u'Thema', options=u'action.portlet:createTopic,editTopic', |     typeInterface='loops.interfaces.ITextDocument', viewName=u'') | ||||||
|  | type(u'topic', u'Thema', options=u'action.portlet:editTopic,createTopic', | ||||||
|     typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'') |     typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'') | ||||||
| type(u'type', u'Typ', options=u'', typeInterface='loops.interfaces.ITypeConcept', | type(u'type', u'Typ', options=u'',  | ||||||
|     viewName=u'') |     typeInterface='loops.interfaces.ITypeConcept', viewName=u'') | ||||||
| 
 | 
 | ||||||
| # domains | # domains | ||||||
| concept(u'general', u'Allgemein', u'domain') | concept(u'general', u'Allgemein', u'domain') | ||||||
|  | @ -74,16 +80,20 @@ child(u'system', u'media_asset', u'standard') | ||||||
| child(u'system', u'personal_info', u'standard') | child(u'system', u'personal_info', u'standard') | ||||||
| child(u'topic', u'topic', u'issubtype', 1) | child(u'topic', u'topic', u'issubtype', 1) | ||||||
| 
 | 
 | ||||||
| resource(u'homepage', u'Willkommen', u'textdocument', contentType='text/restructured') | # resources | ||||||
| resource(u'impressum', u'Impressum', u'textdocument', contentType='text/restructured') | resource(u'homepage', u'Willkommen', u'textdocument',  | ||||||
|  |     contentType='text/restructured') | ||||||
|  | resource(u'impressum', u'Impressum', u'textdocument',  | ||||||
|  |     contentType='text/restructured') | ||||||
| 
 | 
 | ||||||
| #nodes | #nodes | ||||||
| node(u'home', u'Startseite', '', 'menu') | node(u'home', u'Startseite', '', 'menu') | ||||||
| node(u'willkommen', u'Willkommen', u'home', u'text') | node(u'willkommen', u'Willkommen', u'home', u'text', | ||||||
| node(u'willkommen', u'Willkommen', u'home/willkommen', u'text', |  | ||||||
|     target=u'resources/homepage') |     target=u'resources/homepage') | ||||||
| node(u'participants', u'Teilnehmer', u'home', 'page', target=u'concepts/participants') | node(u'participants', u'Teilnehmer', u'home', 'page',  | ||||||
|  |     target=u'concepts/participants') | ||||||
| node(u'topics', u'Themen', u'home', 'page', target=u'concepts/topics') | node(u'topics', u'Themen', u'home', 'page', target=u'concepts/topics') | ||||||
| node(u'glossary', u'Glossar', u'home', 'page', target=u'concepts/glossary') | node(u'glossary', u'Glossar', u'home', 'page', target=u'concepts/glossary') | ||||||
| node(u'search', u'Suche', u'home', 'page', target=u'concepts/search') | node(u'search', u'Suche', u'home', 'page', target=u'concepts/search') | ||||||
| node(u'impressum', u'Impressum', u'home', u'info', target=u'resources/impressum') | node(u'impressum', u'Impressum', u'home', u'info',  | ||||||
|  |     target=u'resources/impressum') | ||||||
|  |  | ||||||
|  | @ -1,60 +1,99 @@ | ||||||
|  | # types | ||||||
| type(u'query', u'Query', options=u'', | type(u'query', u'Query', options=u'', | ||||||
|     typeInterface='loops.expert.concept.IQueryConcept', viewName=u'') |     typeInterface='loops.expert.concept.IQueryConcept', viewName=u'') | ||||||
|  | type(u'datatable', u'Data Table', options=u'action.portlet:edit_concept', | ||||||
|  |     typeInterface='loops.table.IDataTable', viewName=u'') | ||||||
| type(u'task', u'Task', options=u'', | type(u'task', u'Task', options=u'', | ||||||
|     typeInterface='loops.knowledge.interfaces.ITask', viewName=u'') |     typeInterface='loops.knowledge.interfaces.ITask', viewName=u'') | ||||||
| type(u'domain', u'Domain', options=u'', typeInterface=u'', viewName=u'') | type(u'domain', u'Domain', options=u'', typeInterface=u'', viewName=u'') | ||||||
| type(u'classifier', u'Classifier', options=u'', | type(u'classifier', u'Classifier', options=u'', | ||||||
|     typeInterface='loops.classifier.interfaces.IClassifier', viewName=u'classifier.html') |     typeInterface='loops.classifier.interfaces.IClassifier',  | ||||||
|  |     viewName=u'classifier.html') | ||||||
| type(u'documenttype', u'Document Type', options=u'', typeInterface=u'', viewName=u'') | type(u'documenttype', u'Document Type', options=u'', typeInterface=u'', viewName=u'') | ||||||
| type(u'extcollection', u'External Collection', options=u'', | type(u'extcollection', u'External Collection', options=u'', | ||||||
|     typeInterface='loops.integrator.interfaces.IExternalCollection', |     typeInterface='loops.integrator.interfaces.IExternalCollection', | ||||||
|     viewName=u'collection.html') |     viewName=u'collection.html') | ||||||
|  | type(u'folder', u'Ordner', options=u'', typeInterface=u'', viewName=u'') | ||||||
| type(u'glossaryitem', u'Glossary Item', options=u'', | type(u'glossaryitem', u'Glossary Item', options=u'', | ||||||
|     typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'glossaryitem.html') |     typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'glossaryitem.html') | ||||||
| type(u'media_asset', u'Media Asset', | type(u'media_asset', u'Media Asset', | ||||||
|     options=u'storage:varsubdir\nstorage_parameters:extfiles/sites_zzz\nasset_transform.minithumb: size(105)\nasset_transform.small: size(230)\nasset_transform.medium: size(480)', typeInterface='loops.media.interfaces.IMediaAsset', viewName=u'image_medium.html') |     options=u'storage:varsubdir\nstorage_parameters:extfiles/sites_zzz\nasset_transform.minithumb: size(105)\nasset_transform.small: size(230)\nasset_transform.medium: size(480)', typeInterface='loops.media.interfaces.IMediaAsset',  | ||||||
|  |     viewName=u'image_medium.html') | ||||||
| type(u'note', u'Note', options=u'', typeInterface='loops.interfaces.INote', | type(u'note', u'Note', options=u'', typeInterface='loops.interfaces.INote', | ||||||
|     viewName='note.html') |     viewName='note.html') | ||||||
| type(u'person', u'Person', options=u'', | type(u'person', u'Person', options=u'', | ||||||
|     typeInterface='loops.knowledge.interfaces.IPerson', viewName=u'') |     typeInterface='loops.knowledge.interfaces.IPerson', viewName=u'') | ||||||
| type(u'predicate', u'Predicate', options=u'', | type(u'predicate', u'Predicate', options=u'', | ||||||
|     typeInterface=u'loops.interfaces.IPredicate', viewName=u'') |     typeInterface=u'loops.interfaces.IPredicate', viewName=u'') | ||||||
| type(u'event', u'Event', options=u'', typeInterface='loops.organize.interfaces.ITask', | type(u'event', u'Event', options=u'',  | ||||||
|  |     typeInterface='loops.organize.interfaces.ITask', | ||||||
|     viewName=u'task.html') |     viewName=u'task.html') | ||||||
| type(u'textdocument', u'Text', options=u'', typeInterface='loops.interfaces.ITextDocument', viewName=u'') | type(u'textdocument', u'Text', options=u'',  | ||||||
| type(u'topic', u'Topy', options=u'', typeInterface='loops.knowledge.interfaces.ITopic', |     typeInterface='loops.interfaces.ITextDocument', viewName=u'') | ||||||
|     viewName=u'') | type(u'topic', u'Topic', options=u'action.portlet:editTopic,createTopic', | ||||||
| type(u'type', u'Type', options=u'', typeInterface='loops.interfaces.ITypeConcept', |     typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'') | ||||||
|     viewName=u'') | type(u'type', u'Type', options=u'',  | ||||||
|  |     typeInterface='loops.interfaces.ITypeConcept', viewName=u'') | ||||||
|  | 
 | ||||||
|  | #domains | ||||||
|  | concept(u'general', u'General', u'domain') | ||||||
|  | concept(u'system', u'System', u'domain') | ||||||
|  | 
 | ||||||
|  | # predicates | ||||||
| concept(u'depends', u'depends', u'predicate') | concept(u'depends', u'depends', u'predicate') | ||||||
| concept(u'follows', u'follows', u'predicate') | concept(u'follows', u'follows', u'predicate') | ||||||
| concept(u'general', u'General', u'domain') |  | ||||||
| concept(u'glossary', u'Glossary', u'query', options=u'', viewName=u'glossary.html') |  | ||||||
| concept(u'hasType', u'has Type', u'predicate') | concept(u'hasType', u'has Type', u'predicate') | ||||||
| concept(u'ispartof', u'is Part of', u'predicate') | concept(u'ispartof', u'is Part of', u'predicate') | ||||||
| concept(u'issubtype', u'is Subtype', u'predicate') | concept(u'issubtype', u'is Subtype', u'predicate') | ||||||
| concept(u'knows', u'knows', u'predicate') | concept(u'knows', u'knows', u'predicate') | ||||||
| concept(u'ownedby', u'owned by', u'predicate') | concept(u'ownedby', u'owned by', u'predicate') | ||||||
| concept(u'personal_info', u'Personal Information', u'query', options=u'', |  | ||||||
|     viewName=u'personal_info.html') |  | ||||||
| concept(u'provides', u'provides', u'predicate') | concept(u'provides', u'provides', u'predicate') | ||||||
| concept(u'querytarget', u'is Query Target', u'predicate') | concept(u'querytarget', u'is Query Target', u'predicate') | ||||||
| concept(u'requires', u'requires', u'predicate') | concept(u'requires', u'requires', u'predicate') | ||||||
| concept(u'search', u'Search', u'query', options=u'', viewName=u'search') |  | ||||||
| concept(u'standard', u'subobject', u'predicate') | concept(u'standard', u'subobject', u'predicate') | ||||||
| concept(u'system', u'System', u'domain') | 
 | ||||||
|  | #queries | ||||||
|  | concept(u'events', u'Events', u'query', options=u'delta:2', | ||||||
|  |     viewName=u'list_events.html') | ||||||
|  | concept(u'glossary', u'Glossary', u'query', options=u'', viewName=u'glossary.html') | ||||||
|  | concept(u'personal_info', u'Personal Information', u'query', options=u'', | ||||||
|  |     viewName=u'personal_info.html') | ||||||
|  | concept(u'participants', u'Participants', u'query', options=u'', | ||||||
|  |     viewName=u'list_children.html') | ||||||
|  | concept(u'recenct_changes', u'Recent Changes', u'query', | ||||||
|  |     options=u'types:concept:*,resource:*', | ||||||
|  |     viewName=u'recent_changes.html') | ||||||
|  | concept(u'search', u'Search', u'query', options=u'', viewName=u'search') | ||||||
|  | concept(u'topics', u'Topics', u'query', options=u'action.portlet:createTopic', | ||||||
|  |     viewName=u'list_children.html') | ||||||
|  | 
 | ||||||
|  | # child assignments | ||||||
| child(u'general', u'documenttype', u'standard') | child(u'general', u'documenttype', u'standard') | ||||||
| child(u'general', u'event', u'standard') | child(u'general', u'event', u'standard') | ||||||
|  | child(u'general', u'events', u'standard') | ||||||
|  | child(u'general', u'participants', u'standard') | ||||||
|  | child(u'general', u'topics', u'standard') | ||||||
| child(u'system', u'classifier', u'standard') | child(u'system', u'classifier', u'standard') | ||||||
| child(u'system', u'extcollection', u'standard') | child(u'system', u'extcollection', u'standard') | ||||||
| child(u'system', u'issubtype', u'standard') | child(u'system', u'issubtype', u'standard') | ||||||
| child(u'system', u'media_asset', u'standard') | child(u'system', u'media_asset', u'standard') | ||||||
| child(u'system', u'personal_info', u'standard') | child(u'system', u'personal_info', u'standard') | ||||||
| node(u'home', u'Homepage', '', 'menu', body=u'Welcome\n=======) | child(u'topic', u'topic', u'issubtype', 1) | ||||||
|  | 
 | ||||||
|  | # resources | ||||||
|  | resource(u'homepage', u'Welcome', u'textdocument',  | ||||||
|  |     contentType='text/restructured') | ||||||
|  | resource(u'impressum', u'Legal Information', u'textdocument',  | ||||||
|  |     contentType='text/restructured') | ||||||
|  | 
 | ||||||
|  | #nodes | ||||||
|  | node(u'home', u'Home', '', 'menu') | ||||||
|  | node(u'welcome', u'Welcome', u'home', u'text', | ||||||
|  |     target=u'resources/homepage') | ||||||
| node(u'participants', u'Participants', u'home', 'page',  | node(u'participants', u'Participants', u'home', 'page',  | ||||||
|     body=u'Participants\n============', target=u'concepts/person', |     target=u'concepts/participants') | ||||||
|     viewName=u'listchildren') | node(u'topics', u'Topics', u'home', 'page', target=u'concepts/topics') | ||||||
| node(u'topics', u'Topics', u'home', 'page', body=u'Topics\n======', |  | ||||||
|     target=u'concepts/topic', viewName=u'listchildren') |  | ||||||
| node(u'glossary', u'Glossary', u'home', 'page', target=u'concepts/glossary') | node(u'glossary', u'Glossary', u'home', 'page', target=u'concepts/glossary') | ||||||
| node(u'search', u'Search', u'home', 'page', target=u'concepts/search') | node(u'search', u'Search', u'home', 'page', target=u'concepts/search') | ||||||
|  | node(u'impressum', u'Legal Information', u'home', u'info',  | ||||||
|  |     target=u'resources/impressum') | ||||||
|  |  | ||||||
							
								
								
									
										9
									
								
								data/loops_std_update_de.dmp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								data/loops_std_update_de.dmp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | ||||||
|  | # update for old loops sites | ||||||
|  | 
 | ||||||
|  | type(u'datatable', u'Datentabelle', options=u'action.portlet:edit_concept', | ||||||
|  |     typeInterface='loops.table.IDataTable', viewName=u'') | ||||||
|  | 
 | ||||||
|  | concept(u'issubtype', u'is Subtype', u'predicate') | ||||||
|  | 
 | ||||||
|  | child(u'general', u'issubtype', u'datatable') | ||||||
|  | child(u'system', u'issubtype', u'standard') | ||||||
|  | @ -27,7 +27,7 @@ configuration): | ||||||
|   >>> concepts, resources, views = t.setup() |   >>> concepts, resources, views = t.setup() | ||||||
| 
 | 
 | ||||||
|   >>> len(concepts) + len(resources) |   >>> len(concepts) + len(resources) | ||||||
|   36 |   38 | ||||||
| 
 | 
 | ||||||
|   >>> loopsRoot = site['loops'] |   >>> loopsRoot = site['loops'] | ||||||
| 
 | 
 | ||||||
|  | @ -47,11 +47,11 @@ Type- and text-based queries | ||||||
|   >>> from loops.expert import query |   >>> from loops.expert import query | ||||||
|   >>> qu = query.Title('ty*') |   >>> qu = query.Title('ty*') | ||||||
|   >>> list(qu.apply()) |   >>> list(qu.apply()) | ||||||
|   [0, 2, 65] |   [0, 2, 70] | ||||||
| 
 | 
 | ||||||
|   >>> qu = query.Type('loops:*') |   >>> qu = query.Type('loops:*') | ||||||
|   >>> len(list(qu.apply())) |   >>> len(list(qu.apply())) | ||||||
|   36 |   38 | ||||||
| 
 | 
 | ||||||
|   >>> qu = query.Type('loops:concept:predicate') |   >>> qu = query.Type('loops:concept:predicate') | ||||||
|   >>> len(list(qu.apply())) |   >>> len(list(qu.apply())) | ||||||
|  |  | ||||||
|  | @ -91,4 +91,26 @@ | ||||||
|         factory="loops.expert.browser.report.ResultsConceptView" |         factory="loops.expert.browser.report.ResultsConceptView" | ||||||
|         permission="zope.View" /> |         permission="zope.View" /> | ||||||
| 
 | 
 | ||||||
|  |   <zope:adapter | ||||||
|  |         name="concept_report_embedded.html" | ||||||
|  |         for="loops.interfaces.IConcept | ||||||
|  |              zope.publisher.interfaces.browser.IBrowserRequest" | ||||||
|  |         provides="zope.interface.Interface" | ||||||
|  |         factory="loops.expert.browser.report.EmbeddedReportConceptView" | ||||||
|  |         permission="zope.View" /> | ||||||
|  | 
 | ||||||
|  |   <zope:adapter | ||||||
|  |         name="concept_results_embedded.html" | ||||||
|  |         for="loops.interfaces.IConcept | ||||||
|  |              zope.publisher.interfaces.browser.IBrowserRequest" | ||||||
|  |         provides="zope.interface.Interface" | ||||||
|  |         factory="loops.expert.browser.report.EmbeddedResultsConceptView" | ||||||
|  |         permission="zope.View" /> | ||||||
|  | 
 | ||||||
|  |   <browser:page | ||||||
|  |         name="concept_results.csv" | ||||||
|  |         for="loops.organize.interfaces.IConceptSchema" | ||||||
|  |         class="loops.expert.browser.export.ResultsConceptCSVExport" | ||||||
|  |         permission="zope.View" /> | ||||||
|  | 
 | ||||||
| </configure> | </configure> | ||||||
|  |  | ||||||
							
								
								
									
										167
									
								
								expert/browser/export.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								expert/browser/export.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,167 @@ | ||||||
|  | # | ||||||
|  | #  Copyright (c) 2017 Helmut Merz helmutm@cy55.de | ||||||
|  | # | ||||||
|  | #  This program is free software; you can redistribute it and/or modify | ||||||
|  | #  it under the terms of the GNU General Public License as published by | ||||||
|  | #  the Free Software Foundation; either version 2 of the License, or | ||||||
|  | #  (at your option) any later version. | ||||||
|  | # | ||||||
|  | #  This program is distributed in the hope that it will be useful, | ||||||
|  | #  but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | #  GNU General Public License for more details. | ||||||
|  | # | ||||||
|  | #  You should have received a copy of the GNU General Public License | ||||||
|  | #  along with this program; if not, write to the Free Software | ||||||
|  | #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA | ||||||
|  | # | ||||||
|  | 
 | ||||||
|  | """ | ||||||
|  | View classes for export of report results. | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | import csv | ||||||
|  | from cStringIO import StringIO | ||||||
|  | import os | ||||||
|  | import time | ||||||
|  | from zope.cachedescriptors.property import Lazy | ||||||
|  | from zope.i18n import translate | ||||||
|  | from zope.i18nmessageid import Message | ||||||
|  | from zope.traversing.api import getName | ||||||
|  | 
 | ||||||
|  | from cybertools.meta.interfaces import IOptions | ||||||
|  | from cybertools.util.date import formatTimeStamp | ||||||
|  | from loops.common import adapted, normalizeName | ||||||
|  | from loops.expert.browser.report import ResultsConceptView | ||||||
|  | from loops.interfaces import ILoopsObject | ||||||
|  | from loops.util import _, getVarDirectory | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     from main.config import office_data | ||||||
|  | except ImportError: | ||||||
|  |     office_data = None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ResultsConceptCSVExport(ResultsConceptView): | ||||||
|  | 
 | ||||||
|  |     isToplevel = True | ||||||
|  |     reportMode = 'export' | ||||||
|  | 
 | ||||||
|  |     delimiter = ';' | ||||||
|  |     #encoding = 'UTF-8' | ||||||
|  |     #encoding = 'ISO8859-15' | ||||||
|  |     #encoding = 'CP852' | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def encoding(self): | ||||||
|  |         enc = self.globalOptions('csv_encoding') | ||||||
|  |         if enc: | ||||||
|  |             return enc[0] | ||||||
|  |         return 'UTF-8' | ||||||
|  | 
 | ||||||
|  |     def getFileName(self): | ||||||
|  |         return normalizeName(self.context.title) | ||||||
|  | 
 | ||||||
|  |     def getColumnTitle(self, field): | ||||||
|  |         lang = self.languageInfo.language | ||||||
|  |         title = field.title | ||||||
|  |         if not isinstance(title, Message): | ||||||
|  |             title = _(title) | ||||||
|  |         return encode(translate(title, target_language=lang),  | ||||||
|  |                       self.encoding) | ||||||
|  | 
 | ||||||
|  |     def getFilenames(self): | ||||||
|  |         """@return (data_fn, result_fn)""" | ||||||
|  |         repName = getName(self.report.context) | ||||||
|  |         ts = formatTimeStamp(None, format='%y%m%d%H%M%S') | ||||||
|  |         name = '-'.join((ts, repName)) | ||||||
|  |         return (name + '.csv',  | ||||||
|  |                 name + '.xlsx') | ||||||
|  | 
 | ||||||
|  |     def getOfficeTemplatePath(self): | ||||||
|  |         for res in self.report.context.getResources(): | ||||||
|  |             return adapted(res).getDataPath() | ||||||
|  | 
 | ||||||
|  |     def renderCsv(self, scriptfn, datapath, tplpath, respath): | ||||||
|  |         callable = os.path.join(office_data['script_path'], scriptfn) | ||||||
|  |         command = ' '.join((callable, datapath, tplpath, respath)) | ||||||
|  |         #print '***', command | ||||||
|  |         os.popen(command).read() | ||||||
|  | 
 | ||||||
|  |     def __call__(self): | ||||||
|  |         fields = self.displayedColumns | ||||||
|  |         fieldNames = [f.name for f in fields] | ||||||
|  |         reportOptions = IOptions(self.report) | ||||||
|  |         csvRenderer = reportOptions('csv_renderer') | ||||||
|  |         if not csvRenderer: | ||||||
|  |             csvRenderer = self.globalOptions('csv_renderer') | ||||||
|  |         if csvRenderer: | ||||||
|  |             tplpath = self.getOfficeTemplatePath() | ||||||
|  |             #print '***', csvRenderer, office_data, tplpath | ||||||
|  |             if None in (tplpath, office_data): | ||||||
|  |                 csvRenderer = None | ||||||
|  |         if csvRenderer: | ||||||
|  |             csvRenderer = csvRenderer[0] | ||||||
|  |             datafn, resfn = self.getFilenames() | ||||||
|  |             datapath = os.path.join(office_data['data_path'], datafn) | ||||||
|  |             respath = os.path.join(office_data['result_path'], resfn) | ||||||
|  |             output = open(datapath, 'w') | ||||||
|  |         else: | ||||||
|  |             output = StringIO() | ||||||
|  |         writer = csv.DictWriter(output, fieldNames, delimiter=self.delimiter) | ||||||
|  |         if csvRenderer: | ||||||
|  |             output.write(self.delimiter.join([f.name for f in fields]) + '\n') | ||||||
|  |         else: | ||||||
|  |             output.write(self.delimiter.join( | ||||||
|  |                 [self.getColumnTitle(f) for f in fields]) + '\n') | ||||||
|  |         results = self.reportInstance.getResults() | ||||||
|  |         for row in results: | ||||||
|  |             data = {} | ||||||
|  |             for f in fields: | ||||||
|  |                 lang = self.languageInfo.language | ||||||
|  |                 value = f.getExportValue(row, 'csv', lang) | ||||||
|  |                 if ILoopsObject.providedBy(value): | ||||||
|  |                     value = value.title | ||||||
|  |                 value = encode(value, self.encoding) | ||||||
|  |                 data[f.name] = value | ||||||
|  |             writer.writerow(data) | ||||||
|  |         if csvRenderer: | ||||||
|  |             output.close() | ||||||
|  |             self.renderCsv(csvRenderer, datapath, tplpath, respath) | ||||||
|  |             input = open(respath, 'rb') | ||||||
|  |             text = input.read() | ||||||
|  |             input.close() | ||||||
|  |             self.setDownloadHeader(text, | ||||||
|  |                 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', | ||||||
|  |                 'xlsx') | ||||||
|  |         else: | ||||||
|  |             text = output.getvalue() | ||||||
|  |             self.setDownloadHeader(text) | ||||||
|  |         return text | ||||||
|  | 
 | ||||||
|  |     def setDownloadHeader(self, text, ctype='text/csv', ext='csv'): | ||||||
|  |         response = self.request.response | ||||||
|  |         response.setHeader('Content-Disposition', | ||||||
|  |                            'attachment; filename=%s.%s' % | ||||||
|  |                                 (self.getFileName(), ext)) | ||||||
|  |         response.setHeader('Cache-Control', '') | ||||||
|  |         response.setHeader('Pragma', '') | ||||||
|  |         response.setHeader('Content-Type', ctype) | ||||||
|  |         response.setHeader('Content-Length', len(text)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def encode(text, encoding): | ||||||
|  |     if not isinstance(text, unicode): | ||||||
|  |         return text | ||||||
|  |     try: | ||||||
|  |         return text.encode(encoding) | ||||||
|  |     except UnicodeEncodeError: | ||||||
|  |         result = [] | ||||||
|  |         for c in text: | ||||||
|  |             try: | ||||||
|  |                 result.append(c.encode(encoding)) | ||||||
|  |             except UnicodeEncodeError: | ||||||
|  |                 result.append('?') | ||||||
|  |         return ''.join(result) | ||||||
|  |     return '???' | ||||||
|  | 
 | ||||||
|  | @ -3,10 +3,13 @@ | ||||||
| 
 | 
 | ||||||
| <div metal:define-macro="main"> | <div metal:define-macro="main"> | ||||||
|   <div tal:define="report item/reportInstance; |   <div tal:define="report item/reportInstance; | ||||||
|                    reportView nocall:item" |                    reportView nocall:item; | ||||||
|  |                    renderer item/resultsRenderer" | ||||||
|        tal:attributes="class string:content-$level;"> |        tal:attributes="class string:content-$level;"> | ||||||
|     <div metal:use-macro="item/report_macros/header" /> |     <div metal:use-macro="item/report_macros/header" /> | ||||||
|     <div metal:use-macro="item/resultsRenderer" /> |     <tal:renderer condition="renderer"> | ||||||
|  |       <div metal:use-macro="renderer" /> | ||||||
|  |     </tal:renderer> | ||||||
|   </div> |   </div> | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
|  | @ -23,29 +26,65 @@ | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | <div metal:define-macro="embedded_report"> | ||||||
|  |   <div tal:define="report item/reportInstance; | ||||||
|  |                    reportView nocall:item" | ||||||
|  |        tal:attributes="class string:content-$level;"> | ||||||
|  |     <div metal:use-macro="item/report_macros/header" /> | ||||||
|  |     <div metal:use-macro="item/resultsRenderer" /> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| <div metal:define-macro="header"> | <div metal:define-macro="header"> | ||||||
|       <metal:block use-macro="view/concept_macros/concepttitle" /> |     <metal:block use-macro="view/concept_macros/concepttitle" /> | ||||||
|       <form method="get" name="report_data" class="report-meta"> |     <form method="get" name="report_data" class="report-meta"> | ||||||
|         <input type="hidden" name="show_results" value="True" /> |       <input type="hidden" name="show_results" value="True" /> | ||||||
|         <tal:hidden define="params item/dynamicParams" |       <tal:hidden define="params item/dynamicParams"> | ||||||
|                     tal:condition="nothing"> |         <input type="hidden" | ||||||
|           <input type="hidden" |                tal:repeat="name params" | ||||||
|                  tal:repeat="name params" |                tal:condition="nothing" | ||||||
|                  tal:attributes="name name; |                tal:attributes="name name; | ||||||
|                                  value params/?name" /></tal:hidden> |                                value params/?name" /> | ||||||
|         <div metal:use-macro="item/report_macros/params" /> |         <input type="hidden" | ||||||
|         <div metal:define-macro="buttons"> |                tal:define="viewName request/loops.viewName|nothing" | ||||||
|           <input type="submit" name="report_execute" value="Execute Report" |                tal:condition="viewName" | ||||||
|                  tal:attributes="value item/reportExecuteTitle|string:Execute Report" |                tal:attributes="name string:loops.viewName; | ||||||
|                  i18n:attributes="value" /> |                                value viewName" /> | ||||||
|           <input type="submit" |         <input type="hidden" | ||||||
|                  tal:condition="item/reportDownload" |                tal:define="sortinfo request/sortinfo_results|nothing" | ||||||
|                  tal:attributes="name string:${item/reportDownload}:method; |                tal:condition="sortinfo" | ||||||
|                                  value item/reportDownloadTitle" |                tal:attributes="name string:sortinfo_results; | ||||||
|                  i18n:attributes="value"  /> |                                value sortinfo" /> | ||||||
|         </div> |         <input type="hidden" name="report_name" | ||||||
|         <br /> |                tal:define="reportName item/reportName" | ||||||
|       </form> |                tal:condition="reportName" | ||||||
|  |                tal:attributes="value reportName" /> | ||||||
|  |       </tal:hidden> | ||||||
|  |       <div metal:use-macro="item/report_macros/params" /> | ||||||
|  |       <div metal:define-macro="buttons"> | ||||||
|  |         <input type="submit" name="report_execute" value="Execute Report" | ||||||
|  |                onclick="this.form.action = ''" | ||||||
|  |                tal:attributes="value item/reportExecuteTitle|string:Execute Report" | ||||||
|  |                tal:condition="item/queryFields" | ||||||
|  |                i18n:attributes="value" /> | ||||||
|  |         <input type="submit" name="report_download" | ||||||
|  |                tal:condition="item/reportDownload" | ||||||
|  |                tal:attributes="value item/reportDownloadTitle; | ||||||
|  |                                onclick string: | ||||||
|  |                       this.form.action = '${item/reportDownload}'" | ||||||
|  |                i18n:attributes="value"  /> | ||||||
|  |       </div> | ||||||
|  |       <br /> | ||||||
|  |     </form> | ||||||
|  |     <tal:ignore condition="nothing"> | ||||||
|  |       <tal:list condition="renderer"> | ||||||
|  |         <div metal:use-macro="renderer" /> | ||||||
|  |       </tal:list> | ||||||
|  |       <tal:list condition="not:renderer"> | ||||||
|  |         <div metal:use-macro="view/concept_macros/conceptchildren" /> | ||||||
|  |       </tal:list> | ||||||
|  |     </tal:ignore> | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -113,7 +152,14 @@ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| <metal:field define-macro="selection"> | <metal:field define-macro="selection"> | ||||||
|   <metal:use use-macro="item/report_macros/textline" /> |   <select tal:attributes="name name"> | ||||||
|  |     <option /> | ||||||
|  |     <option tal:repeat="opt python:field.getVocabularyItems( | ||||||
|  |                   context=item.adapted, request=request)" | ||||||
|  |             tal:attributes="value opt/token; | ||||||
|  |                             selected python:value == opt['token']" | ||||||
|  |             tal:content="opt/title" /> | ||||||
|  |   </select> | ||||||
| </metal:field> | </metal:field> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2011 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2016 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -20,6 +20,7 @@ | ||||||
| View classes for reporting. | View classes for reporting. | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
|  | from logging import getLogger | ||||||
| from urllib import urlencode | from urllib import urlencode | ||||||
| from zope import interface, component | from zope import interface, component | ||||||
| from zope.app.pagetemplate import ViewPageTemplateFile | from zope.app.pagetemplate import ViewPageTemplateFile | ||||||
|  | @ -46,6 +47,10 @@ class ReportView(ConceptView): | ||||||
|     """ A view for defining (editing) a report. |     """ A view for defining (editing) a report. | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|  |     resultsRenderer = None  # to be defined by subclass | ||||||
|  |     reportDownload = None | ||||||
|  |     reportName = None | ||||||
|  | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def report_macros(self): |     def report_macros(self): | ||||||
|         return self.controller.mergeTemplateMacros('report', report_template) |         return self.controller.mergeTemplateMacros('report', report_template) | ||||||
|  | @ -55,10 +60,33 @@ class ReportView(ConceptView): | ||||||
|     def macro(self): |     def macro(self): | ||||||
|         return self.report_macros['main'] |         return self.report_macros['main'] | ||||||
| 
 | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def tabTitle(self): | ||||||
|  |         return self.report.title | ||||||
|  | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def dynamicParams(self): |     def dynamicParams(self): | ||||||
|         return self.request.form |         return self.request.form | ||||||
| 
 | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def report(self): | ||||||
|  |         return self.adapted | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def reportInstance(self): | ||||||
|  |         instance = component.getAdapter(self.report, IReportInstance, | ||||||
|  |                                         name=self.report.reportType) | ||||||
|  |         instance.view = self | ||||||
|  |         return instance | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def queryFields(self): | ||||||
|  |         ri = self.reportInstance | ||||||
|  |         qf = ri.getAllQueryFields() | ||||||
|  |         if ri.userSettings: | ||||||
|  |             return [f for f in qf if f in ri.userSettings] | ||||||
|  |         return qf | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class ResultsView(NodeView): | class ResultsView(NodeView): | ||||||
| 
 | 
 | ||||||
|  | @ -107,13 +135,6 @@ class ResultsView(NodeView): | ||||||
|     def report(self): |     def report(self): | ||||||
|         return adapted(self.virtualTargetObject) |         return adapted(self.virtualTargetObject) | ||||||
| 
 | 
 | ||||||
|     @Lazy |  | ||||||
|     def reportInstance(self): |  | ||||||
|         instance = component.getAdapter(self.report, IReportInstance, |  | ||||||
|                                         name=self.report.reportType) |  | ||||||
|         instance.view = self |  | ||||||
|         return instance |  | ||||||
| 
 |  | ||||||
|     #@Lazy |     #@Lazy | ||||||
|     def results(self): |     def results(self): | ||||||
|         return self.reportInstance.getResults(self.params) |         return self.reportInstance.getResults(self.params) | ||||||
|  | @ -139,6 +160,8 @@ class ResultsConceptView(ConceptView): | ||||||
|     """ View on a concept using the results of a report. |     """ View on a concept using the results of a report. | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|  |     logger = getLogger ('ResultsConceptView') | ||||||
|  | 
 | ||||||
|     reportName = None   # define in subclass if applicable |     reportName = None   # define in subclass if applicable | ||||||
|     reportDownload = None |     reportDownload = None | ||||||
|     reportType = None   # set for using special report instance adapter |     reportType = None   # set for using special report instance adapter | ||||||
|  | @ -169,6 +192,9 @@ class ResultsConceptView(ConceptView): | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def reportName(self): |     def reportName(self): | ||||||
|  |         rn = self.request.form.get('report_name') | ||||||
|  |         if rn is not None: | ||||||
|  |             return rn | ||||||
|         return (self.getOptions('report_name') or [None])[0] |         return (self.getOptions('report_name') or [None])[0] | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|  | @ -179,7 +205,10 @@ class ResultsConceptView(ConceptView): | ||||||
|     @Lazy |     @Lazy | ||||||
|     def report(self): |     def report(self): | ||||||
|         if self.reportName: |         if self.reportName: | ||||||
|             return adapted(self.conceptManager[self.reportName]) |             report = adapted(self.conceptManager.get(self.reportName)) | ||||||
|  |             if report is None: | ||||||
|  |                 self.logger.warn("Report '%s' not found." % self.reportName) | ||||||
|  |             return report | ||||||
|         reports = self.context.getParents([self.hasReportPredicate]) |         reports = self.context.getParents([self.hasReportPredicate]) | ||||||
|         if not reports: |         if not reports: | ||||||
|             type = self.context.conceptType |             type = self.context.conceptType | ||||||
|  | @ -193,6 +222,13 @@ class ResultsConceptView(ConceptView): | ||||||
|         ri = component.getAdapter(self.report, IReportInstance, |         ri = component.getAdapter(self.report, IReportInstance, | ||||||
|                                   name=reportType) |                                   name=reportType) | ||||||
|         ri.view = self |         ri.view = self | ||||||
|  |         if not ri.sortCriteria: | ||||||
|  |             si = self.sortInfo.get('results') | ||||||
|  |             if si is not None: | ||||||
|  |                 fnames = (si['colName'],) | ||||||
|  |                 ri.sortCriteria = [f for f in ri.getSortFields()  | ||||||
|  |                                      if f.name in fnames] | ||||||
|  |                 ri.sortDescending = not si['ascending'] | ||||||
|         return ri |         return ri | ||||||
| 
 | 
 | ||||||
|     def results(self): |     def results(self): | ||||||
|  | @ -207,6 +243,35 @@ class ResultsConceptView(ConceptView): | ||||||
|     def getColumnRenderer(self, col): |     def getColumnRenderer(self, col): | ||||||
|         return self.result_macros[col.renderer] |         return self.result_macros[col.renderer] | ||||||
| 
 | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def downloadLink(self, format='csv'): | ||||||
|  |         opt = self.options('download_' + format) | ||||||
|  |         if not opt: | ||||||
|  |             opt = self.typeOptions('download_' + format) | ||||||
|  |         if opt: | ||||||
|  |             return '/'.join((self.nodeView.virtualTargetUrl, opt[0])) | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def reportDownload(self): | ||||||
|  |         return self.downloadLink | ||||||
|  | 
 | ||||||
|  |     def isSortableColumn(self, tableName, colName): | ||||||
|  |         if tableName == 'results': | ||||||
|  |             if colName in [f.name for f in self.reportInstance.getSortFields()]: | ||||||
|  |                 return True | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class EmbeddedResultsConceptView(ResultsConceptView): | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def macro(self): | ||||||
|  |         return self.result_macros['embedded_content'] | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def title(self): | ||||||
|  |         return self.report.title | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class ReportConceptView(ResultsConceptView, ReportView): | class ReportConceptView(ResultsConceptView, ReportView): | ||||||
|     """ View on a concept using a report. |     """ View on a concept using a report. | ||||||
|  | @ -229,6 +294,17 @@ class ReportConceptView(ResultsConceptView, ReportView): | ||||||
|         return qf |         return qf | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class EmbeddedReportConceptView(ReportConceptView): | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def macro(self): | ||||||
|  |         return self.report_macros['embedded_report'] | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def title(self): | ||||||
|  |         return self.report.title | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class ReportParamsView(ReportConceptView): | class ReportParamsView(ReportConceptView): | ||||||
|     """ Report view allowing to enter parameters before executing the report. |     """ Report view allowing to enter parameters before executing the report. | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|  | @ -25,29 +25,62 @@ | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| <div metal:define-macro="results"> | <div metal:define-macro="embedded_content" | ||||||
|  |      tal:define="report item/reportInstance; | ||||||
|  |                  reportView nocall:item"> | ||||||
|  |   <div tal:attributes="class string:content-$level;"> | ||||||
|  |       <metal:block use-macro="view/concept_macros/concepttitle_only" /> | ||||||
|  |   </div> | ||||||
|  |   <div metal:use-macro="item/resultsRenderer" /> | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | <div metal:define-macro="results" | ||||||
|  |      tal:define="tableName string:results"> | ||||||
|  |   <br /> | ||||||
|  |   <tal:download condition="nothing"> | ||||||
|  |     <div class="button"> | ||||||
|  |       <a i18n:translate="" | ||||||
|  |          tal:define="dl string:${item/downloadLink}${item/urlParamString}; | ||||||
|  |                      params python:item.getSortParams(tableName)" | ||||||
|  |          tal:attributes="href dl">Download Data</a> | ||||||
|  |     </div> | ||||||
|  |     <br /> | ||||||
|  |   </tal:download> | ||||||
|   <table class="report" |   <table class="report" | ||||||
|          tal:define="results reportView/results"> |          tal:define="results reportView/results"> | ||||||
|     <tr> |     <tr> | ||||||
|         <th tal:repeat="col results/displayedColumns" |       <th style="white-space: nowrap" | ||||||
|             tal:content="col/title" |           tal:repeat="col results/displayedColumns"> | ||||||
|             tal:attributes="class col/cssClass" |         <a title="tooltip_sort_column" | ||||||
|             i18n:translate="" /> |            tal:define="colName col/name" | ||||||
|  |            tal:omit-tag="python:not item.isSortableColumn(tableName, colName)" | ||||||
|  |            tal:attributes="href python:item.getSortUrl(tableName, colName)" | ||||||
|  |            i18n:attributes="title"> | ||||||
|  |           <span tal:content="col/title" | ||||||
|  |                 tal:attributes="class col/cssClass" | ||||||
|  |                 i18n:translate="" /> | ||||||
|  |           <img tal:define="src python:item.getSortImage(tableName, colName)" | ||||||
|  |                tal:condition="src" | ||||||
|  |                tal:attributes="src src" /> | ||||||
|  |         </a> | ||||||
|  |       </th> | ||||||
|     </tr> |     </tr> | ||||||
|     <tr tal:repeat="row results"> |     <tr tal:repeat="row results" | ||||||
|         <td tal:repeat="col results/displayedColumns" |         tal:attributes="class python:(repeat['row'].index() % 2) and 'even' or 'odd'"> | ||||||
|             tal:attributes="class col/cssClass"> |       <td tal:repeat="col results/displayedColumns" | ||||||
|             <metal:column use-macro="python: |           tal:attributes="class col/cssClass"> | ||||||
|                             reportView.getColumnRenderer(col)" /> |           <metal:column use-macro="python: | ||||||
|         </td> |                           reportView.getColumnRenderer(col)" /> | ||||||
|  |       </td> | ||||||
|     </tr> |     </tr> | ||||||
|     <tr tal:define="row nocall:results/totals" |     <tr tal:define="row nocall:results/totals" | ||||||
|         tal:condition="nocall:row"> |         tal:condition="nocall:row"> | ||||||
|         <td tal:repeat="col results/displayedColumns" |       <td tal:repeat="col results/displayedColumns" | ||||||
|             tal:attributes="class col/cssClass"> |           tal:attributes="class col/cssClass"> | ||||||
|             <metal:column use-macro="python: |           <metal:column use-macro="python: | ||||||
|                             reportView.getColumnRenderer(col)" /> |                           reportView.getColumnRenderer(col)" /> | ||||||
|         </td> |       </td> | ||||||
|     </tr> |     </tr> | ||||||
|   </table> |   </table> | ||||||
| </div> | </div> | ||||||
|  | @ -78,6 +111,17 @@ | ||||||
| </metal:state> | </metal:state> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | <metal:state define-macro="workitem_state"> | ||||||
|  |   <tal:column define="value python:col.getDisplayValue(row)" | ||||||
|  |               condition="value"> | ||||||
|  |     <tal:action repeat="action value/actions"> | ||||||
|  |       <metal:action tal:condition="action" | ||||||
|  |                     use-macro="action/macro" /> | ||||||
|  |     </tal:action> | ||||||
|  |   </tal:column> | ||||||
|  | </metal:state> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| <metal:target define-macro="target"> | <metal:target define-macro="target"> | ||||||
|   <tal:column define="value python:col.getDisplayValue(row)"> |   <tal:column define="value python:col.getDisplayValue(row)"> | ||||||
|     <a tal:omit-tag="python: |     <a tal:omit-tag="python: | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2013 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2015 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -93,7 +93,8 @@ class Search(ConceptView): | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def showActions(self): |     def showActions(self): | ||||||
|         return checkPermission('loops.ManageSite', self.context) |         perm = (self.globalOptions('delete_permission') or ['loops.ManageSite'])[0] | ||||||
|  |         return checkPermission(perm, self.context) | ||||||
|         #return canWriteObject(self.context) |         #return canWriteObject(self.context) | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|  | @ -168,7 +169,7 @@ class Search(ConceptView): | ||||||
|         title = request.get('name') |         title = request.get('name') | ||||||
|         if title == '*': |         if title == '*': | ||||||
|             title = None |             title = None | ||||||
|         types = request.get('searchType') |         #types = request.get('searchType') | ||||||
|         data = [] |         data = [] | ||||||
|         types = self.getTypes() |         types = self.getTypes() | ||||||
|         if title or types: |         if title or types: | ||||||
|  | @ -305,8 +306,8 @@ class Search(ConceptView): | ||||||
|             for state in states: |             for state in states: | ||||||
|                 if stf.state == state: |                 if stf.state == state: | ||||||
|                     break |                     break | ||||||
|                 else: |             else: | ||||||
|                     return False |                 return False | ||||||
|         return True |         return True | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,3 @@ | ||||||
| <!-- $Id$ --> |  | ||||||
| 
 |  | ||||||
| <configure | <configure | ||||||
|    xmlns="http://namespaces.zope.org/zope" |    xmlns="http://namespaces.zope.org/zope" | ||||||
|    xmlns:browser="http://namespaces.zope.org/browser" |    xmlns:browser="http://namespaces.zope.org/browser" | ||||||
|  |  | ||||||
							
								
								
									
										132
									
								
								expert/field.py
									
										
									
									
									
								
							
							
						
						
									
										132
									
								
								expert/field.py
									
										
									
									
									
								
							|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2014 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2015 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -22,6 +22,7 @@ Field definitions for reports. | ||||||
| 
 | 
 | ||||||
| from zope.app.form.browser.interfaces import ITerms | from zope.app.form.browser.interfaces import ITerms | ||||||
| from zope import component | from zope import component | ||||||
|  | from zope.i18n import translate | ||||||
| from zope.i18n.locales import locales | from zope.i18n.locales import locales | ||||||
| from zope.schema.interfaces import IVocabularyFactory, IContextSourceBinder | from zope.schema.interfaces import IVocabularyFactory, IContextSourceBinder | ||||||
| 
 | 
 | ||||||
|  | @ -29,18 +30,32 @@ from cybertools.composer.report.field import Field as BaseField | ||||||
| from cybertools.composer.report.field import TableCellStyle | from cybertools.composer.report.field import TableCellStyle | ||||||
| from cybertools.composer.report.result import ResultSet | from cybertools.composer.report.result import ResultSet | ||||||
| from cybertools.stateful.interfaces import IStateful, IStatesDefinition | from cybertools.stateful.interfaces import IStateful, IStatesDefinition | ||||||
| from cybertools.util.date import timeStamp2Date | from cybertools.util.date import timeStamp2Date, timeStamp2ISO | ||||||
|  | from cybertools.util.format import formatDate | ||||||
| from loops.common import baseObject | from loops.common import baseObject | ||||||
| from loops.expert.report import ReportInstance | from loops.expert.report import ReportInstance | ||||||
|  | from loops.organize.work.browser import WorkItemDetails | ||||||
| from loops import util | from loops import util | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Field(BaseField): | class Field(BaseField): | ||||||
| 
 | 
 | ||||||
|  |     def getContext(self, row): | ||||||
|  |         return row.context | ||||||
|  | 
 | ||||||
|     def getSelectValue(self, row): |     def getSelectValue(self, row): | ||||||
|         return self.getValue(row) |         return self.getValue(row) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class StringField(Field): | ||||||
|  | 
 | ||||||
|  |     def getSelectValue(self, row): | ||||||
|  |         return self.getValue(row).strip() | ||||||
|  | 
 | ||||||
|  |     def getSortValue(self, row): | ||||||
|  |         return self.getValue(row).strip() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class TextField(Field): | class TextField(Field): | ||||||
| 
 | 
 | ||||||
|     format = 'text/restructured' |     format = 'text/restructured' | ||||||
|  | @ -104,11 +119,16 @@ class IntegerField(Field): | ||||||
| 
 | 
 | ||||||
| class DateField(Field): | class DateField(Field): | ||||||
| 
 | 
 | ||||||
|     fieldType='date', |     fieldType='date' | ||||||
|     format = ('date', 'short') |     format = ('date', 'short') | ||||||
|     renderer = cssClass = 'center' |     renderer = cssClass = 'center' | ||||||
|     dbtype = 'date' |     dbtype = 'date' | ||||||
| 
 | 
 | ||||||
|  |     def getValue(self, row): | ||||||
|  |         if getattr(row.parent.context.view, 'reportMode', None) == 'export': | ||||||
|  |             return self.getDisplayValue(row) | ||||||
|  |         super(DateField, self).getValue(row) | ||||||
|  | 
 | ||||||
|     def getDisplayValue(self, row): |     def getDisplayValue(self, row): | ||||||
|         value = self.getRawValue(row) |         value = self.getRawValue(row) | ||||||
|         if not value: |         if not value: | ||||||
|  | @ -127,16 +147,17 @@ class DateField(Field): | ||||||
| 
 | 
 | ||||||
| class StateField(Field): | class StateField(Field): | ||||||
| 
 | 
 | ||||||
|     statesDefinition = 'workItemStates' |     statesDefinition = None | ||||||
|     renderer = 'state' |     renderer = 'state' | ||||||
| 
 | 
 | ||||||
|     def getDisplayValue(self, row): |     def getDisplayValue(self, row): | ||||||
|         if IStateful.providedBy(row.context): |         context = self.getContext(row) | ||||||
|             stf = row.context |         if IStateful.providedBy(context): | ||||||
|         elif row.context is None: |             stf = context | ||||||
|  |         elif context is None: | ||||||
|             return None |             return None | ||||||
|         else: |         else: | ||||||
|             stf = component.getAdapter(baseObject(row.context), IStateful, |             stf = component.getAdapter(context, IStateful,  | ||||||
|                                         name=self.statesDefinition) |                                         name=self.statesDefinition) | ||||||
|         stateObject = stf.getStateObject() |         stateObject = stf.getStateObject() | ||||||
|         icon = stateObject.icon or 'led%s.png' % stateObject.color |         icon = stateObject.icon or 'led%s.png' % stateObject.color | ||||||
|  | @ -147,9 +168,32 @@ class StateField(Field): | ||||||
|         return util._(text) |         return util._(text) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class WorkItemStateField(Field): | ||||||
|  | 
 | ||||||
|  |     statesDefinition = 'workItemStates' | ||||||
|  |     renderer = 'workitem_state' | ||||||
|  | 
 | ||||||
|  |     def getValue(self, row): | ||||||
|  |         view = row.parent.context.view | ||||||
|  |         if getattr(view, 'reportMode', None) == 'export': | ||||||
|  |             stateObject = row.context.getStateObject() | ||||||
|  |             lang = view.languageInfo.language | ||||||
|  |             return translate(util._(stateObject.title), target_language=lang) | ||||||
|  |         return super(WorkItemStateField, self).getValue(row) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def getDisplayValue(self, row): | ||||||
|  |         if row.context is None: | ||||||
|  |             return None | ||||||
|  |         details = WorkItemDetails(row.parent.context.view, row.context) | ||||||
|  |         return dict(actions=details.actions()) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class VocabularyField(Field): | class VocabularyField(Field): | ||||||
| 
 | 
 | ||||||
|     vocabulary = None |     vocabulary = None | ||||||
|  |     sourceList = None | ||||||
|  |     fieldType = 'selection' | ||||||
| 
 | 
 | ||||||
|     def getDisplayValue(self, row): |     def getDisplayValue(self, row): | ||||||
|         value = self.getRawValue(row) |         value = self.getRawValue(row) | ||||||
|  | @ -160,9 +204,11 @@ class VocabularyField(Field): | ||||||
|             if str(item['token']) == str(value): |             if str(item['token']) == str(value): | ||||||
|                 return item['title'] |                 return item['title'] | ||||||
| 
 | 
 | ||||||
|     def getVocabularyItems(self, row): |     def getVocabularyItems(self, row=None, context=None, request=None): | ||||||
|         context = row.context |         if context is  None: | ||||||
|         request = row.parent.context.view.request |             context = row.context | ||||||
|  |         if request is None: | ||||||
|  |             request = row.parent.context.view.request | ||||||
|         voc = self.vocabulary |         voc = self.vocabulary | ||||||
|         if isinstance(voc, basestring): |         if isinstance(voc, basestring): | ||||||
|             terms = self.getVocabularyTerms(voc, context, request) |             terms = self.getVocabularyTerms(voc, context, request) | ||||||
|  | @ -171,7 +217,10 @@ class VocabularyField(Field): | ||||||
|             voc = voc.splitlines() |             voc = voc.splitlines() | ||||||
|             return [dict(token=t, title=t) for t in voc if t.strip()] |             return [dict(token=t, title=t) for t in voc if t.strip()] | ||||||
|         elif IContextSourceBinder.providedBy(voc): |         elif IContextSourceBinder.providedBy(voc): | ||||||
|             source = voc(row.parent.context) |             if row is not None: | ||||||
|  |                 source = voc(row.parent.context) | ||||||
|  |             else: | ||||||
|  |                 source = voc(context) | ||||||
|             terms = component.queryMultiAdapter((source, request), ITerms) |             terms = component.queryMultiAdapter((source, request), ITerms) | ||||||
|             if terms is not None: |             if terms is not None: | ||||||
|                 termsList = [terms.getTerm(value) for value in source] |                 termsList = [terms.getTerm(value) for value in source] | ||||||
|  | @ -233,6 +282,14 @@ class RelationField(Field): | ||||||
| 
 | 
 | ||||||
| class TargetField(RelationField): | class TargetField(RelationField): | ||||||
| 
 | 
 | ||||||
|  |     def getSortValue(self, row): | ||||||
|  |         value = self.getRawValue(row) | ||||||
|  |         if value is not None: | ||||||
|  |             value = util.getObjectForUid(value) | ||||||
|  |             if value is not None: | ||||||
|  |                 if value.title is not None: | ||||||
|  |                    return value.title.split() | ||||||
|  | 
 | ||||||
|     def getValue(self, row): |     def getValue(self, row): | ||||||
|         value = self.getRawValue(row) |         value = self.getRawValue(row) | ||||||
|         if value is None: |         if value is None: | ||||||
|  | @ -247,6 +304,57 @@ class MultiLineField(Field): | ||||||
|     def getValue(self, row): |     def getValue(self, row): | ||||||
|         return self.getRawValue(row) |         return self.getRawValue(row) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | # track fields | ||||||
|  | 
 | ||||||
|  | class TrackDateField(Field): | ||||||
|  | 
 | ||||||
|  |     fieldType = 'date' | ||||||
|  |     part = 'date' | ||||||
|  |     format = 'short' | ||||||
|  |     descending = False | ||||||
|  |     cssClass = 'right' | ||||||
|  | 
 | ||||||
|  |     def getValue(self, row): | ||||||
|  |         reportMode = getattr(row.parent.context.view, 'reportMode', None) | ||||||
|  |         if reportMode == 'export': | ||||||
|  |             return self.getDisplayValue(row) | ||||||
|  |         value = self.getRawValue(row) | ||||||
|  |         if not value: | ||||||
|  |             return None | ||||||
|  |         return timeStamp2Date(value) | ||||||
|  | 
 | ||||||
|  |     def getDisplayValue(self, row): | ||||||
|  |         value = self.getRawValue(row) | ||||||
|  |         if value: | ||||||
|  |             value = timeStamp2Date(value) | ||||||
|  |             view = row.parent.context.view | ||||||
|  |             return formatDate(value, self.part, self.format, | ||||||
|  |                               view.languageInfo.language) | ||||||
|  |         return u'' | ||||||
|  | 
 | ||||||
|  |     def getSelectValue(self, row): | ||||||
|  |         value = self.getRawValue(row) | ||||||
|  |         if not value: | ||||||
|  |             return '' | ||||||
|  |         return timeStamp2ISO(value)[:10] | ||||||
|  | 
 | ||||||
|  |     def getSortValue(self, row): | ||||||
|  |         value = self.getRawValue(row) | ||||||
|  |         if value and self.descending: | ||||||
|  |             return -value | ||||||
|  |         return value or None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TrackDateTimeField(TrackDateField): | ||||||
|  | 
 | ||||||
|  |     part = 'dateTime' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TrackTimeField(TrackDateField): | ||||||
|  | 
 | ||||||
|  |     part = 'time' | ||||||
|  | 
 | ||||||
|     def getDisplayValues(self, row): |     def getDisplayValues(self, row): | ||||||
|         value = self.getValue(row) |         value = self.getValue(row) | ||||||
|         if not isinstance(value, (list, tuple)): |         if not isinstance(value, (list, tuple)): | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2014 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2017 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -35,6 +35,7 @@ from cybertools.composer.report.interfaces import IReportParams | ||||||
| from cybertools.composer.report.result import ResultSet, Row | from cybertools.composer.report.result import ResultSet, Row | ||||||
| from cybertools.util.jeep import Jeep | from cybertools.util.jeep import Jeep | ||||||
| from loops.common import AdapterBase | from loops.common import AdapterBase | ||||||
|  | from loops.expert.concept import IQueryConcept, QueryConcept | ||||||
| from loops.interfaces import ILoopsAdapter | from loops.interfaces import ILoopsAdapter | ||||||
| from loops.type import TypeInterfaceSourceList | from loops.type import TypeInterfaceSourceList | ||||||
| from loops import util | from loops import util | ||||||
|  | @ -43,7 +44,7 @@ from loops.util import _ | ||||||
| 
 | 
 | ||||||
| # interfaces | # interfaces | ||||||
| 
 | 
 | ||||||
| class IReport(ILoopsAdapter, IReportParams): | class IReport(ILoopsAdapter, IReportParams, IQueryConcept): | ||||||
|     """ The report adapter for the persistent object (concept) that stores |     """ The report adapter for the persistent object (concept) that stores | ||||||
|         the report in the concept map. |         the report in the concept map. | ||||||
|     """ |     """ | ||||||
|  | @ -66,7 +67,7 @@ class IReportInstance(IBaseReport): | ||||||
| 
 | 
 | ||||||
| # report concept adapter and instances | # report concept adapter and instances | ||||||
| 
 | 
 | ||||||
| class Report(AdapterBase): | class Report(QueryConcept): | ||||||
| 
 | 
 | ||||||
|     implements(IReport) |     implements(IReport) | ||||||
| 
 | 
 | ||||||
|  | @ -88,6 +89,7 @@ class ReportInstance(BaseReport): | ||||||
|     #headerRowFactory = Row |     #headerRowFactory = Row | ||||||
| 
 | 
 | ||||||
|     view = None     # set upon creation |     view = None     # set upon creation | ||||||
|  |     #headerRowFactory = Row | ||||||
| 
 | 
 | ||||||
|     def __init__(self, context): |     def __init__(self, context): | ||||||
|         self.context = context |         self.context = context | ||||||
|  | @ -120,7 +122,9 @@ class ReportInstance(BaseReport): | ||||||
|         result = list(self.selectObjects(parts))  # may modify parts |         result = list(self.selectObjects(parts))  # may modify parts | ||||||
|         qc = CompoundQueryCriteria(parts) |         qc = CompoundQueryCriteria(parts) | ||||||
|         return ResultSet(self, result, rowFactory=self.rowFactory, |         return ResultSet(self, result, rowFactory=self.rowFactory, | ||||||
|                          sortCriteria=self.getSortCriteria(), queryCriteria=qc, |                          sortCriteria=self.getSortCriteria(),  | ||||||
|  |                          sortDescending=self.sortDescending, | ||||||
|  |                          queryCriteria=qc, | ||||||
|                          limits=limits) |                          limits=limits) | ||||||
| 
 | 
 | ||||||
|     def selectObjects(self, parts): |     def selectObjects(self, parts): | ||||||
|  | @ -173,3 +177,15 @@ class DefaultConceptReportInstance(ReportInstance): | ||||||
| 
 | 
 | ||||||
|     label = u'Default Concept Report' |     label = u'Default Concept Report' | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | # specialized rows | ||||||
|  | 
 | ||||||
|  | class TrackRow(Row): | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def getContextAttr(obj, attr): | ||||||
|  |         if attr in obj.context.metadata_attributes: | ||||||
|  |             return getattr(obj.context, attr) | ||||||
|  |         return obj.context.data.get(attr) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -66,13 +66,13 @@ zcml in real life: | ||||||
| 
 | 
 | ||||||
|   >>> t = searchView.typesForSearch() |   >>> t = searchView.typesForSearch() | ||||||
|   >>> len(t) |   >>> len(t) | ||||||
|   15 |   16 | ||||||
|   >>> t.getTermByToken('loops:resource:*').title |   >>> t.getTermByToken('loops:resource:*').title | ||||||
|   'Any Resource' |   'Any Resource' | ||||||
| 
 | 
 | ||||||
|   >>> t = searchView.conceptTypesForSearch() |   >>> t = searchView.conceptTypesForSearch() | ||||||
|   >>> len(t) |   >>> len(t) | ||||||
|   12 |   13 | ||||||
|   >>> t.getTermByToken('loops:concept:*').title |   >>> t.getTermByToken('loops:concept:*').title | ||||||
|   'Any Concept' |   'Any Concept' | ||||||
| 
 | 
 | ||||||
|  | @ -91,7 +91,7 @@ a controller attribute for the search view. | ||||||
| 
 | 
 | ||||||
|   >>> searchView.submitReplacing('1.results', '1.search.form', pageView) |   >>> searchView.submitReplacing('1.results', '1.search.form', pageView) | ||||||
|   'submitReplacing("1.results", "1.search.form", |   'submitReplacing("1.results", "1.search.form", | ||||||
|        "http://127.0.0.1/loops/views/page/.target96/@@searchresults.html");...' |        "http://127.0.0.1/loops/views/page/.target.../@@searchresults.html");...' | ||||||
| 
 | 
 | ||||||
| Basic (text/title) search | Basic (text/title) search | ||||||
| ------------------------- | ------------------------- | ||||||
|  | @ -177,7 +177,7 @@ of the concepts' titles: | ||||||
|   >>> request = TestRequest(form=form) |   >>> request = TestRequest(form=form) | ||||||
|   >>> view = Search(page, request) |   >>> view = Search(page, request) | ||||||
|   >>> view.listConcepts() |   >>> view.listConcepts() | ||||||
|   '{"items": [{"id": "101", "name": "Zope", "label": "Zope (Thema)"}, {"id": "103", "name": "Zope 2", "label": "Zope 2 (Thema)"}, {"id": "105", "name": "Zope 3", "label": "Zope 3 (Thema)"}], "identifier": "id"}' |   '{"items": [{"id": "...", "name": "Zope", "label": "Zope (Thema)"}, {"id": "...", "name": "Zope 2", "label": "Zope 2 (Thema)"}, {"id": "...", "name": "Zope 3", "label": "Zope 3 (Thema)"}], "identifier": "id"}' | ||||||
| 
 | 
 | ||||||
| Preset Concept Types on Search Forms | Preset Concept Types on Search Forms | ||||||
| ------------------------------------ | ------------------------------------ | ||||||
|  | @ -219,13 +219,13 @@ and thus include the customer type in the preset search types. | ||||||
| 
 | 
 | ||||||
|   >>> searchView.conceptsForType('loops:concept:customer') |   >>> searchView.conceptsForType('loops:concept:customer') | ||||||
|   [{'token': 'none', 'title': u'not selected'}, |   [{'token': 'none', 'title': u'not selected'}, | ||||||
|    {'token': '74', 'title': u'Customer 1'}, |    {'token': '...', 'title': u'Customer 1'}, | ||||||
|    {'token': '76', 'title': u'Customer 2'}, |    {'token': '...', 'title': u'Customer 2'}, | ||||||
|    {'token': '78', 'title': u'Customer 3'}] |    {'token': '...', 'title': u'Customer 3'}] | ||||||
| 
 | 
 | ||||||
| Let's use this new search option for querying: | Let's use this new search option for querying: | ||||||
| 
 | 
 | ||||||
|   >>> form = {'search.4.text_selected': u'74'} |   >>> form = {'search.4.text_selected': u'75'} | ||||||
|   >>> resultsView = SearchResults(page, TestRequest(form=form)) |   >>> resultsView = SearchResults(page, TestRequest(form=form)) | ||||||
|   >>> results = list(resultsView.results) |   >>> results = list(resultsView.results) | ||||||
|   >>> results[0].title |   >>> results[0].title | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								external/README.txt
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								external/README.txt
									
										
									
									
										vendored
									
									
								
							|  | @ -17,7 +17,7 @@ Let's set up a loops site with basic and example concepts and resources. | ||||||
|   >>> concepts, resources, views = t.setup() |   >>> concepts, resources, views = t.setup() | ||||||
|   >>> loopsRoot = site['loops'] |   >>> loopsRoot = site['loops'] | ||||||
|   >>> len(concepts), len(resources), len(views) |   >>> len(concepts), len(resources), len(views) | ||||||
|   (33, 3, 1) |   (35, 3, 1) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| Importing loops Objects | Importing loops Objects | ||||||
|  | @ -44,7 +44,7 @@ Creating the corresponding objects | ||||||
|   >>> loader = Loader(loopsRoot) |   >>> loader = Loader(loopsRoot) | ||||||
|   >>> loader.load(elements) |   >>> loader.load(elements) | ||||||
|   >>> len(concepts), len(resources), len(views) |   >>> len(concepts), len(resources), len(views) | ||||||
|   (34, 3, 1) |   (36, 3, 1) | ||||||
| 
 | 
 | ||||||
|   >>> from loops.common import adapted |   >>> from loops.common import adapted | ||||||
|   >>> adMyquery = adapted(concepts['myquery']) |   >>> adMyquery = adapted(concepts['myquery']) | ||||||
|  | @ -131,7 +131,7 @@ Extracting elements | ||||||
|   >>> extractor = Extractor(loopsRoot, os.path.join(dataDirectory, 'export')) |   >>> extractor = Extractor(loopsRoot, os.path.join(dataDirectory, 'export')) | ||||||
|   >>> elements = list(extractor.extract()) |   >>> elements = list(extractor.extract()) | ||||||
|   >>> len(elements) |   >>> len(elements) | ||||||
|   66 |   69 | ||||||
| 
 | 
 | ||||||
| Writing object information to the external storage | Writing object information to the external storage | ||||||
| -------------------------------------------------- | -------------------------------------------------- | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								external/pyfunc.py
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								external/pyfunc.py
									
										
									
									
										vendored
									
									
								
							|  | @ -44,11 +44,15 @@ class PyReader(object): | ||||||
| 
 | 
 | ||||||
| class InputProcessor(dict): | class InputProcessor(dict): | ||||||
| 
 | 
 | ||||||
|  |     _constants = dict(True=True, False=False) | ||||||
|  | 
 | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         self.elements = [] |         self.elements = [] | ||||||
|         self['__builtins__'] = {}   # security! |         self['__builtins__'] = dict()   # security! | ||||||
| 
 | 
 | ||||||
|     def __getitem__(self, key): |     def __getitem__(self, key): | ||||||
|  |         if key in self._constants: | ||||||
|  |             return self._constants[key] | ||||||
|         def factory(*args, **kw): |         def factory(*args, **kw): | ||||||
|             element = elementTypes[key](*args, **kw) |             element = elementTypes[key](*args, **kw) | ||||||
|             if key in toplevelElements: |             if key in toplevelElements: | ||||||
|  |  | ||||||
|  | @ -98,8 +98,9 @@ class I18NView(object): | ||||||
|         return adapted(self.context, self.languageInfo) |         return adapted(self.context, self.languageInfo) | ||||||
| 
 | 
 | ||||||
|     def checkLanguage(self): |     def checkLanguage(self): | ||||||
|         session = ISession(self.request)[packageId] |         #session = ISession(self.request)[packageId] | ||||||
|         lang = session.get('language') or self.languageInfo.language |         #lang = session.get('language') or self.languageInfo.language | ||||||
|  |         lang = self.languageInfo.language | ||||||
|         if lang: |         if lang: | ||||||
|             self.setLanguage(lang) |             self.setLanguage(lang) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -44,5 +44,7 @@ class ExternalCollectionView(ConceptView): | ||||||
|             cta.update() |             cta.update() | ||||||
|             if cta.updateMessage is not None: |             if cta.updateMessage is not None: | ||||||
|                 self.request.form['message'] = cta.updateMessage |                 self.request.form['message'] = cta.updateMessage | ||||||
|  |             if 'no_show_page' in self.request.form: | ||||||
|  |                 return False | ||||||
|         return True |         return True | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -24,8 +24,10 @@ file system. | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from logging import getLogger | from logging import getLogger | ||||||
| import os, re, stat | import os, re, stat | ||||||
|  | import transaction | ||||||
| 
 | 
 | ||||||
| from zope.app.container.interfaces import INameChooser | from zope.app.container.interfaces import INameChooser | ||||||
|  | from zope.app.container.contained import ObjectRemovedEvent | ||||||
| from zope.cachedescriptors.property import Lazy | from zope.cachedescriptors.property import Lazy | ||||||
| from zope import component | from zope import component | ||||||
| from zope.component import adapts | from zope.component import adapts | ||||||
|  | @ -51,6 +53,8 @@ from loops.versioning.interfaces import IVersionable | ||||||
| 
 | 
 | ||||||
| TypeInterfaceSourceList.typeInterfaces += (IExternalCollection,) | TypeInterfaceSourceList.typeInterfaces += (IExternalCollection,) | ||||||
| 
 | 
 | ||||||
|  | logger = getLogger('loops.integrator.collection') | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class ExternalCollectionAdapter(AdapterBase): | class ExternalCollectionAdapter(AdapterBase): | ||||||
|     """ A concept adapter for accessing an external collection. |     """ A concept adapter for accessing an external collection. | ||||||
|  | @ -83,10 +87,11 @@ class ExternalCollectionAdapter(AdapterBase): | ||||||
|                     print '###', vaddr, vobj, vid |                     print '###', vaddr, vobj, vid | ||||||
|                     versions.add(vaddr) |                     versions.add(vaddr) | ||||||
|         new = [] |         new = [] | ||||||
|         oldFound = [] |         oldFound = set([]) | ||||||
|         provider = component.getUtility(IExternalCollectionProvider, |         provider = component.getUtility(IExternalCollectionProvider, | ||||||
|                                         name=self.providerName or '') |                                         name=self.providerName or '') | ||||||
|         #print '*** old', old, versions, self.lastUpdated |         #print '*** old', old, versions, self.lastUpdated | ||||||
|  |         changeCount = 0 | ||||||
|         for addr, mdate in provider.collect(self): |         for addr, mdate in provider.collect(self): | ||||||
|             #print '***', addr, mdate |             #print '***', addr, mdate | ||||||
|             if addr in versions: |             if addr in versions: | ||||||
|  | @ -94,8 +99,9 @@ class ExternalCollectionAdapter(AdapterBase): | ||||||
|             if addr in old: |             if addr in old: | ||||||
|                 # may be it would be better to return a file's hash |                 # may be it would be better to return a file's hash | ||||||
|                 # for checking for changes... |                 # for checking for changes... | ||||||
|                 oldFound.append(addr) |                 oldFound.add(addr) | ||||||
|                 if self.lastUpdated is None or (mdate and mdate > self.lastUpdated): |                 if self.lastUpdated is None or (mdate and mdate > self.lastUpdated): | ||||||
|  |                     changeCount +=1 | ||||||
|                     obj = old[addr] |                     obj = old[addr] | ||||||
|                     # update settings and regenerate scale variant for media asset |                     # update settings and regenerate scale variant for media asset | ||||||
|                     adobj = adapted(obj) |                     adobj = adapted(obj) | ||||||
|  | @ -110,29 +116,41 @@ class ExternalCollectionAdapter(AdapterBase): | ||||||
|                         self.updateMessage = message |                         self.updateMessage = message | ||||||
|                     # force reindexing |                     # force reindexing | ||||||
|                     notify(ObjectModifiedEvent(obj)) |                     notify(ObjectModifiedEvent(obj)) | ||||||
|  |                     if changeCount % 10 == 0: | ||||||
|  |                         logger.info('Updated: %i.' % changeCount) | ||||||
|  |                         transaction.commit() | ||||||
|             else: |             else: | ||||||
|                 new.append(addr) |                 new.append(addr) | ||||||
|  |         logger.info('%i objects updated.' % changeCount) | ||||||
|  |         transaction.commit() | ||||||
|         if new: |         if new: | ||||||
|             self.newResources = provider.createExtFileObjects(self, new) |             self.newResources = provider.createExtFileObjects(self, new) | ||||||
|             for r in self.newResources: |             for r in self.newResources: | ||||||
|                 self.context.assignResource(r) |                 self.context.assignResource(r) | ||||||
|  |         logger.info('%i objects created.' % len(new)) | ||||||
|  |         transaction.commit() | ||||||
|         for addr in old: |         for addr in old: | ||||||
|             if str(addr) not in oldFound: |             if str(addr) not in oldFound: | ||||||
|                 # not part of the collection any more |                 # not part of the collection any more | ||||||
|                 # TODO: only remove from collection but keep object? |                 # TODO: only remove from collection but keep object? | ||||||
|                 self.remove(old[addr]) |                 self.remove(old[addr]) | ||||||
|  |         transaction.commit() | ||||||
|         for r in self.context.getResources(): |         for r in self.context.getResources(): | ||||||
|             adobj = adapted(r) |             adobj = adapted(r) | ||||||
|             if self.metaInfo != adobj.metaInfo and ( |             if self.metaInfo != adobj.metaInfo and ( | ||||||
|                                     not adobj.metaInfo or self.overwriteMetaInfo): |                                     not adobj.metaInfo or self.overwriteMetaInfo): | ||||||
|                     adobj.metaInfo = self.metaInfo |                     adobj.metaInfo = self.metaInfo | ||||||
|         self.lastUpdated = datetime.today() |         self.lastUpdated = datetime.today() | ||||||
|  |         logger.info('External collection updated.') | ||||||
|  |         transaction.commit() | ||||||
| 
 | 
 | ||||||
|     def clear(self): |     def clear(self): | ||||||
|         for obj in self.context.getResources(): |         for obj in self.context.getResources(): | ||||||
|             self.remove(obj) |             self.remove(obj) | ||||||
| 
 | 
 | ||||||
|     def remove(self, obj): |     def remove(self, obj): | ||||||
|  |         logger.info('Removing object: %s.' % getName(obj)) | ||||||
|  |         notify(ObjectRemovedEvent(obj)) | ||||||
|         del self.resourceManager[getName(obj)] |         del self.resourceManager[getName(obj)] | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|  | @ -187,7 +205,7 @@ class DirectoryCollectionProvider(object): | ||||||
|                                 for k, v in self.extFileTypeMapping.items()) |                                 for k, v in self.extFileTypeMapping.items()) | ||||||
|         container = client.context.getLoopsRoot().getResourceManager() |         container = client.context.getLoopsRoot().getResourceManager() | ||||||
|         directory = self.getDirectory(client) |         directory = self.getDirectory(client) | ||||||
|         for addr in addresses: |         for idx, addr in enumerate(addresses): | ||||||
|             name = self.generateName(container, addr) |             name = self.generateName(container, addr) | ||||||
|             title = self.generateTitle(addr) |             title = self.generateTitle(addr) | ||||||
|             contentType = guess_content_type(addr, |             contentType = guess_content_type(addr, | ||||||
|  | @ -200,9 +218,8 @@ class DirectoryCollectionProvider(object): | ||||||
|                 if extFileType is None: |                 if extFileType is None: | ||||||
|                     extFileType = extFileTypes['image/*'] |                     extFileType = extFileTypes['image/*'] | ||||||
|             if extFileType is None: |             if extFileType is None: | ||||||
|                 getLogger('loops.integrator.collection.DirectoryCollectionProvider' |                 logger.warn('No external file type found for %r, ' | ||||||
|                             ).warn('No external file type found for %r, ' |                             'content type: %r' % (name, contentType)) | ||||||
|                                    'content type: %r' % (name, contentType)) |  | ||||||
|             obj = addAndConfigureObject( |             obj = addAndConfigureObject( | ||||||
|                             container, Resource, name, |                             container, Resource, name, | ||||||
|                             title=title, |                             title=title, | ||||||
|  | @ -219,6 +236,9 @@ class DirectoryCollectionProvider(object): | ||||||
|                 message = client.updateMessage or u'' |                 message = client.updateMessage or u'' | ||||||
|                 message += u'<br />'.join(adobj.processingErrors) |                 message += u'<br />'.join(adobj.processingErrors) | ||||||
|                 client.updateMessage = message |                 client.updateMessage = message | ||||||
|  |             if idx and idx % 10 == 0: | ||||||
|  |                 logger.info('Created: %i.' % idx) | ||||||
|  |                 transaction.commit() | ||||||
|             yield obj |             yield obj | ||||||
| 
 | 
 | ||||||
|     def getDirectory(self, client): |     def getDirectory(self, client): | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| <metal:block define-macro="render_collection" | <metal:block define-macro="render_collection" | ||||||
|              tal:define="dummy item/update"> |              tal:condition="item/update"> | ||||||
| 
 | 
 | ||||||
|   <metal:block use-macro="view/concept_macros/conceptdata"> |   <metal:block use-macro="view/concept_macros/conceptdata"> | ||||||
|     <metal:fill tal:condition="item/editable" |     <metal:fill tal:condition="item/editable" | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| 
 | 
 | ||||||
| import unittest, doctest | import unittest, doctest | ||||||
| from zope.interface.verify import verifyClass | from zope.interface.verify import verifyClass | ||||||
| #from loops.versioning import versionable |  | ||||||
| 
 | 
 | ||||||
| class Test(unittest.TestCase): | class Test(unittest.TestCase): | ||||||
|     "Basic tests for the loops.integrator.content package." |     "Basic tests for the loops.integrator.content package." | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2011 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2014 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -18,8 +18,6 @@ | ||||||
| 
 | 
 | ||||||
| """ | """ | ||||||
| Integrator interfaces. | Integrator interfaces. | ||||||
| 
 |  | ||||||
| $Id$ |  | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| from zope.interface import Interface, Attribute | from zope.interface import Interface, Attribute | ||||||
|  | @ -133,3 +131,5 @@ class IOfficeFile(IExternalFile): | ||||||
|         It provides access to the document content and properties. |         It provides access to the document content and properties. | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|  |     documentPropertiesAccessible = Attribute( | ||||||
|  |             'Are document properties accessible?') | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2013 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2014 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -26,7 +26,7 @@ from lxml import etree | ||||||
| import os | import os | ||||||
| import shutil | import shutil | ||||||
| from time import strptime | from time import strptime | ||||||
| from zipfile import ZipFile | from zipfile import ZipFile, BadZipfile | ||||||
| from zope.cachedescriptors.property import Lazy | from zope.cachedescriptors.property import Lazy | ||||||
| from zope import component | from zope import component | ||||||
| from zope.component import adapts | from zope.component import adapts | ||||||
|  | @ -52,12 +52,22 @@ class OfficeFile(ExternalFileAdapter): | ||||||
| 
 | 
 | ||||||
|     implements(IOfficeFile) |     implements(IOfficeFile) | ||||||
| 
 | 
 | ||||||
|  |     _adapterAttributes = (ExternalFileAdapter._adapterAttributes +  | ||||||
|  |                             ('documentPropertiesAccessible',)) | ||||||
|  | 
 | ||||||
|     propertyMap = {u'Revision:': 'version'} |     propertyMap = {u'Revision:': 'version'} | ||||||
|     propFileName = 'docProps/custom.xml' |     propFileName = 'docProps/custom.xml' | ||||||
|     corePropFileName = 'docProps/core.xml' |     corePropFileName = 'docProps/core.xml' | ||||||
|     fileExtensions = ('.docm', '.docx', 'dotm', 'dotx', 'pptx', 'potx', 'ppsx', |     fileExtensions = ('.docm', '.docx', 'dotm', 'dotx', 'pptx', 'potx', 'ppsx', | ||||||
|                       '.xlsm', '.xlsx', '.xltm', '.xltx') |                       '.xlsm', '.xlsx', '.xltm', '.xltx') | ||||||
| 
 | 
 | ||||||
|  |     def getDocumentPropertiesAccessible(self): | ||||||
|  |         return getattr(self.context, '_documentPropertiesAccessible', True) | ||||||
|  |     def setDocumentPropertiesAccessible(self, value): | ||||||
|  |         self.context._documentPropertiesAccessible = value | ||||||
|  |     documentPropertiesAccessible = property( | ||||||
|  |                 getDocumentPropertiesAccessible, setDocumentPropertiesAccessible) | ||||||
|  | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def logger(self): |     def logger(self): | ||||||
|         return getLogger('loops.integrator.office.base.OfficeFile') |         return getLogger('loops.integrator.office.base.OfficeFile') | ||||||
|  | @ -79,14 +89,19 @@ class OfficeFile(ExternalFileAdapter): | ||||||
|     def docPropertyDom(self): |     def docPropertyDom(self): | ||||||
|         fn = self.docFilename |         fn = self.docFilename | ||||||
|         result = dict(core=[], custom=[]) |         result = dict(core=[], custom=[]) | ||||||
|  |         if not os.path.exists(fn): | ||||||
|  |             # may happen before file has been created | ||||||
|  |             return result | ||||||
|         root, ext = os.path.splitext(fn) |         root, ext = os.path.splitext(fn) | ||||||
|         if not ext.lower() in self.fileExtensions: |         if not ext.lower() in self.fileExtensions: | ||||||
|             return result |             return result | ||||||
|         try: |         try: | ||||||
|             zf = ZipFile(fn, 'r') |             zf = ZipFile(fn, 'r') | ||||||
|         except IOError, e: |             self.documentPropertiesAccessible = True | ||||||
|  |         except (IOError, BadZipfile), e: | ||||||
|             from logging import getLogger |             from logging import getLogger | ||||||
|             self.logger.warn(e) |             self.logger.warn(e) | ||||||
|  |             self.documentPropertiesAccessible = False | ||||||
|             return result |             return result | ||||||
|         if self.corePropFileName not in zf.namelist(): |         if self.corePropFileName not in zf.namelist(): | ||||||
|             self.logger.warn('Core properties not found in file %s.' % |             self.logger.warn('Core properties not found in file %s.' % | ||||||
|  | @ -123,6 +138,8 @@ class OfficeFile(ExternalFileAdapter): | ||||||
|         attributes = {} |         attributes = {} | ||||||
|         # get dc:description from core.xml |         # get dc:description from core.xml | ||||||
|         desc = self.getCoreProperty('description') |         desc = self.getCoreProperty('description') | ||||||
|  |         if not self.documentPropertiesAccessible: | ||||||
|  |             return | ||||||
|         if desc is not None: |         if desc is not None: | ||||||
|             attributes['comments'] = desc |             attributes['comments'] = desc | ||||||
|         dom = self.docPropertyDom['custom'] |         dom = self.docPropertyDom['custom'] | ||||||
|  |  | ||||||
|  | @ -39,6 +39,7 @@ class ExternalSourceInfo(object): | ||||||
|     adapts(ILoopsObject) |     adapts(ILoopsObject) | ||||||
| 
 | 
 | ||||||
|     def __init__(self, context): |     def __init__(self, context): | ||||||
|  |         #import pdb; pdb.set_trace() | ||||||
|         self.context = self.__parent__ = context |         self.context = self.__parent__ = context | ||||||
| 
 | 
 | ||||||
|     def getSourceInfo(self): |     def getSourceInfo(self): | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| 
 | 
 | ||||||
| import unittest, doctest | import unittest, doctest | ||||||
| from zope.interface.verify import verifyClass | from zope.interface.verify import verifyClass | ||||||
| #from loops.versioning import versionable |  | ||||||
| 
 | 
 | ||||||
| class Test(unittest.TestCase): | class Test(unittest.TestCase): | ||||||
|     "Basic tests for the integrator sub-package." |     "Basic tests for the integrator sub-package." | ||||||
|  |  | ||||||
|  | @ -402,7 +402,7 @@ class IDocumentSchema(IResourceSchema): | ||||||
|     contentType = schema.Choice( |     contentType = schema.Choice( | ||||||
|                 title=_(u'Content Type'), |                 title=_(u'Content Type'), | ||||||
|                 description=_(u'Content type (format) of the data field'), |                 description=_(u'Content type (format) of the data field'), | ||||||
|                 values=('text/restructured', 'text/structured', 'text/html', |                 values=('text/markdown', 'text/restructured', 'text/structured', 'text/html', | ||||||
|                         'text/plain', 'text/xml', 'text/css'), |                         'text/plain', 'text/xml', 'text/css'), | ||||||
|                 default='text/restructured', |                 default='text/restructured', | ||||||
|                 required=True) |                 required=True) | ||||||
|  | @ -968,5 +968,3 @@ class IViewConfiguratorSchema(Interface): | ||||||
|         value_type=schema.TextLine(), |         value_type=schema.TextLine(), | ||||||
|         default=[], |         default=[], | ||||||
|         required=False) |         required=False) | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2013 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2015 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -30,8 +30,13 @@ from cybertools.typology.interfaces import IType | ||||||
| from loops.browser.action import DialogAction | from loops.browser.action import DialogAction | ||||||
| from loops.browser.common import BaseView | from loops.browser.common import BaseView | ||||||
| from loops.browser.concept import ConceptView | from loops.browser.concept import ConceptView | ||||||
|  | from loops.common import adapted | ||||||
| from loops.knowledge.interfaces import IPerson, ITask | from loops.knowledge.interfaces import IPerson, ITask | ||||||
| from loops.organize.party import getPersonForUser | from loops.organize.party import getPersonForUser | ||||||
|  | from loops.organize.personal import favorite | ||||||
|  | from loops.organize.personal.interfaces import IFavorites | ||||||
|  | from loops.security.common import checkPermission | ||||||
|  | from loops import util | ||||||
| from loops.util import _ | from loops.util import _ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -69,6 +74,63 @@ actions.register('createQualification', 'portlet', DialogAction, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class InstitutionMixin(object): | ||||||
|  | 
 | ||||||
|  |     knowledge_macros = knowledge_macros | ||||||
|  | 
 | ||||||
|  |     adminMaySelectAllInstitutions = True | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def institutionType(self): | ||||||
|  |         return self.conceptManager['institution'] | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def institutions(self): | ||||||
|  |         if self.adminMaySelectAllInstitutions: | ||||||
|  |             if checkPermission('loops.ManageWorkspaces', self.context): | ||||||
|  |                 return self.getAllInstitutions() | ||||||
|  |         result = [] | ||||||
|  |         p = getPersonForUser(self.context, self.request) | ||||||
|  |         if p is None: | ||||||
|  |             return result | ||||||
|  |         for parent in p.getParents( | ||||||
|  |                 [self.memberPredicate, self.masterPredicate]): | ||||||
|  |             if parent.conceptType == self.institutionType: | ||||||
|  |                 result.append(dict( | ||||||
|  |                         object=adapted(parent),  | ||||||
|  |                         title=parent.title, | ||||||
|  |                         uid=util.getUidForObject(parent))) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     def getAllInstitutions(self): | ||||||
|  |         insts = self.institutionType.getChildren([self.typePredicate]) | ||||||
|  |         return [dict(object=adapted(inst), | ||||||
|  |                      title=inst.title, | ||||||
|  |                      uid=util.getUidForObject(inst)) for inst in insts] | ||||||
|  | 
 | ||||||
|  |     def setInstitution(self, uid): | ||||||
|  |         inst = util.getObjectForUid(uid) | ||||||
|  |         person = getPersonForUser(self.context, self.request) | ||||||
|  |         favorite.setInstitution(person, inst) | ||||||
|  |         self.institution = inst | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|  |     def getSavedInstitution(self): | ||||||
|  |         person = getPersonForUser(self.context, self.request) | ||||||
|  |         favorites = IFavorites(self.loopsRoot.getRecordManager()['favorites']) | ||||||
|  |         for inst in favorites.list(person, type='institution'): | ||||||
|  |             return adapted(util.getObjectForUid(inst)) | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def institution(self): | ||||||
|  |         saved = self.getSavedInstitution() | ||||||
|  |         for inst in self.institutions: | ||||||
|  |             if inst['object'] == saved: | ||||||
|  |                 return inst['object'] | ||||||
|  |         if self.institutions: | ||||||
|  |             return self.institutions[0]['object'] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class MyKnowledge(ConceptView): | class MyKnowledge(ConceptView): | ||||||
| 
 | 
 | ||||||
|     template = template |     template = template | ||||||
|  |  | ||||||
|  | @ -1,9 +1,11 @@ | ||||||
| type(u'competence', u'Kompetenz', viewName=u'',  | type(u'competence', u'Qualifikation', viewName=u'',  | ||||||
|     typeInterface=u'loops.knowledge.qualification.interfaces.ICompetence',  |     typeInterface=u'loops.knowledge.qualification.interfaces.ICompetence',  | ||||||
|     options=u'action.portlet:create_subtype,edit_concept') |     options=u'action.portlet:create_subtype,edit_concept') | ||||||
| type(u'person', u'Person', viewName=u'',  | type(u'person', u'Person', viewName=u'',  | ||||||
|     typeInterface=u'loops.knowledge.interfaces.IPerson',  |     typeInterface=u'loops.knowledge.interfaces.IPerson',  | ||||||
|     options=u'action.portlet:createQualification,editPerson') |     options=u'action.portlet:createQualification,editPerson') | ||||||
|  | type(u'report', u'Report', viewName=u'',  | ||||||
|  |     typeInterface='loops.expert.report.IReport') | ||||||
| type(u'task', u'Aufgabe', viewName=u'',  | type(u'task', u'Aufgabe', viewName=u'',  | ||||||
|     typeInterface=u'loops.knowledge.interfaces.ITask',  |     typeInterface=u'loops.knowledge.interfaces.ITask',  | ||||||
|     options=u'action.portlet:createTask,editTask') |     options=u'action.portlet:createTask,editTask') | ||||||
|  | @ -26,6 +28,10 @@ concept(u'requires', u'requires', u'predicate') | ||||||
| concept(u'issubtype', u'is Subtype', u'predicate', options=u'hide_children', | concept(u'issubtype', u'is Subtype', u'predicate', options=u'hide_children', | ||||||
|     predicateInterface='loops.interfaces.IIsSubtype') |     predicateInterface='loops.interfaces.IIsSubtype') | ||||||
| 
 | 
 | ||||||
|  | # reports | ||||||
|  | concept(u'qualification_overview', u'Qualification Overview', u'report', | ||||||
|  |     reportType=u'qualification_overview') | ||||||
|  | 
 | ||||||
| # structure | # structure | ||||||
| child(u'general', u'competence', u'standard') | child(u'general', u'competence', u'standard') | ||||||
| child(u'general', u'depends', u'standard') | child(u'general', u'depends', u'standard') | ||||||
|  | @ -38,6 +44,7 @@ child(u'general', u'topic', u'standard') | ||||||
| #child(u'general', u'training', u'standard') | #child(u'general', u'training', u'standard') | ||||||
| 
 | 
 | ||||||
| child(u'system', u'issubtype', u'standard') | child(u'system', u'issubtype', u'standard') | ||||||
|  | child(u'system', u'report', u'standard') | ||||||
| 
 | 
 | ||||||
| child(u'competence', u'competence', u'issubtype') | child(u'competence', u'competence', u'issubtype') | ||||||
| #child(u'competence', u'training', u'issubtype', usePredicate=u'provides') | #child(u'competence', u'training', u'issubtype', usePredicate=u'provides') | ||||||
|  |  | ||||||
|  | @ -1,6 +1,14 @@ | ||||||
| type(u'competence', u'Kompetenz', viewName=u'',  | type(u'competence', u'Qualifikation', viewName=u'',  | ||||||
|     typeInterface=u'loops.knowledge.qualification.interfaces.ICompetence',  |     typeInterface=u'loops.knowledge.qualification.interfaces.ICompetence',  | ||||||
|     options=u'action.portlet:create_subtype,edit_concept') |     options=u'action.portlet:create_subtype,edit_concept') | ||||||
|  | type(u'ipskill', u'Kompetenz', viewName=u'',  | ||||||
|  |     options=u'action.portlet:edit_concept') | ||||||
|  | type(u'ipskillsrequired', u'Soll-Profil', viewName=u'',  | ||||||
|  |     options=u'action.portlet:edit_concept') | ||||||
|  | type(u'jobposition', u'Stelle', viewName=u'',  | ||||||
|  |     options=u'action.portlet:edit_concept') | ||||||
|  | type(u'report', u'Report', viewName=u'',  | ||||||
|  |     typeInterface='loops.expert.report.IReport') | ||||||
| # type(u'person', u'Person', viewName=u'',  | # type(u'person', u'Person', viewName=u'',  | ||||||
| #     typeInterface=u'loops.knowledge.interfaces.IPerson',  | #     typeInterface=u'loops.knowledge.interfaces.IPerson',  | ||||||
| #     options=u'action.portlet:editPerson') | #     options=u'action.portlet:editPerson') | ||||||
|  | @ -26,9 +34,16 @@ concept(u'requires', u'requires', u'predicate') | ||||||
| concept(u'issubtype', u'is Subtype', u'predicate', options=u'hide_children', | concept(u'issubtype', u'is Subtype', u'predicate', options=u'hide_children', | ||||||
|     predicateInterface='loops.interfaces.IIsSubtype') |     predicateInterface='loops.interfaces.IIsSubtype') | ||||||
| 
 | 
 | ||||||
|  | # reports | ||||||
|  | concept(u'qualification_overview', u'Qualification Overview', u'report', | ||||||
|  |     reportType=u'qualification_overview') | ||||||
|  | 
 | ||||||
| # structure | # structure | ||||||
| child(u'general', u'competence', u'standard') | child(u'general', u'competence', u'standard') | ||||||
| child(u'general', u'depends', u'standard') | child(u'general', u'depends', u'standard') | ||||||
|  | child(u'general', u'ipskill', u'standard') | ||||||
|  | child(u'general', u'ipskillsrequired', u'standard') | ||||||
|  | child(u'general', u'jobposition', u'standard') | ||||||
| child(u'general', u'knows', u'standard') | child(u'general', u'knows', u'standard') | ||||||
| #child(u'general', u'person', u'standard') | #child(u'general', u'person', u'standard') | ||||||
| child(u'general', u'provides', u'standard') | child(u'general', u'provides', u'standard') | ||||||
|  | @ -38,6 +53,7 @@ child(u'general', u'requires', u'standard') | ||||||
| #child(u'general', u'training', u'standard') | #child(u'general', u'training', u'standard') | ||||||
| 
 | 
 | ||||||
| child(u'system', u'issubtype', u'standard') | child(u'system', u'issubtype', u'standard') | ||||||
|  | child(u'system', u'report', u'standard') | ||||||
| 
 | 
 | ||||||
| child(u'competence', u'competence', u'issubtype') | child(u'competence', u'competence', u'issubtype') | ||||||
| #child(u'competence', u'training', u'issubtype', usePredicate=u'provides') | #child(u'competence', u'training', u'issubtype', usePredicate=u'provides') | ||||||
|  |  | ||||||
|  | @ -1,5 +1,3 @@ | ||||||
| <!-- $Id$ --> |  | ||||||
| 
 |  | ||||||
| <configure | <configure | ||||||
|    xmlns:zope="http://namespaces.zope.org/zope" |    xmlns:zope="http://namespaces.zope.org/zope" | ||||||
|    xmlns="http://namespaces.zope.org/browser" |    xmlns="http://namespaces.zope.org/browser" | ||||||
|  | @ -28,14 +26,14 @@ | ||||||
|       name="create_glossaryitem.html" |       name="create_glossaryitem.html" | ||||||
|       for="loops.interfaces.INode" |       for="loops.interfaces.INode" | ||||||
|       class="loops.knowledge.glossary.browser.CreateGlossaryItemForm" |       class="loops.knowledge.glossary.browser.CreateGlossaryItemForm" | ||||||
|       permission="zope.ManageContent" |       permission="zope.View" | ||||||
|       /> |       /> | ||||||
| 
 | 
 | ||||||
|   <page |   <page | ||||||
|       name="edit_glossaryitem.html" |       name="edit_glossaryitem.html" | ||||||
|       for="loops.interfaces.INode" |       for="loops.interfaces.INode" | ||||||
|       class="loops.knowledge.glossary.browser.EditGlossaryItemForm" |       class="loops.knowledge.glossary.browser.EditGlossaryItemForm" | ||||||
|       permission="zope.ManageContent" |       permission="zope.View" | ||||||
|       /> |       /> | ||||||
| 
 | 
 | ||||||
|   <zope:adapter |   <zope:adapter | ||||||
|  | @ -43,7 +41,7 @@ | ||||||
|       for="loops.browser.node.NodeView |       for="loops.browser.node.NodeView | ||||||
|            zope.publisher.interfaces.browser.IBrowserRequest" |            zope.publisher.interfaces.browser.IBrowserRequest" | ||||||
|       factory="loops.knowledge.glossary.browser.CreateGlossaryItem" |       factory="loops.knowledge.glossary.browser.CreateGlossaryItem" | ||||||
|       permission="zope.ManageContent" |       permission="zope.View" | ||||||
|       /> |       /> | ||||||
| 
 | 
 | ||||||
|   <zope:adapter |   <zope:adapter | ||||||
|  | @ -51,7 +49,7 @@ | ||||||
|       for="loops.browser.node.NodeView |       for="loops.browser.node.NodeView | ||||||
|            zope.publisher.interfaces.browser.IBrowserRequest" |            zope.publisher.interfaces.browser.IBrowserRequest" | ||||||
|       factory="loops.knowledge.glossary.browser.EditGlossaryItem" |       factory="loops.knowledge.glossary.browser.EditGlossaryItem" | ||||||
|       permission="zope.ManageContent" |       permission="zope.View" | ||||||
|       /> |       /> | ||||||
| 
 | 
 | ||||||
| </configure> | </configure> | ||||||
|  |  | ||||||
|  | @ -4,20 +4,28 @@ | ||||||
|              tal:define="data item/childrenAlphaGroups"> |              tal:define="data item/childrenAlphaGroups"> | ||||||
|   <metal:title use-macro="item/conceptMacros/concepttitle" /> |   <metal:title use-macro="item/conceptMacros/concepttitle" /> | ||||||
|   <div><a name="top"> </a></div> |   <div><a name="top"> </a></div> | ||||||
|   <div> |   <div tal:condition="nothing"> | ||||||
|     <span tal:repeat="letter python: [chr(c) for c in range(ord('A'), ord('Z')+1)]" |     <span tal:repeat="letter python: [chr(c) for c in range(ord('A'), ord('Z')+1)]" | ||||||
|           class="navlink"> |           class="navlink"> | ||||||
|       <a href="#" |       <a href="#" | ||||||
|          tal:omit-tag="python: letter not in data.keys()" |          tal:omit-tag="python: letter not in data.keys()" | ||||||
|          tal:attributes="href string:${request/URL/-1}#$letter" |          tal:attributes="href string:${view/requestUrl/-1}#$letter" | ||||||
|  |          tal:content="letter">A</a> | ||||||
|  |     </span> | ||||||
|  |   </div> | ||||||
|  |   <div> | ||||||
|  |     <span tal:repeat="letter python:sorted(data.keys())" | ||||||
|  |           class="navlink"> | ||||||
|  |       <a href="#" | ||||||
|  |          tal:attributes="href string:${view/requestUrl/-1}#$letter" | ||||||
|          tal:content="letter">A</a> |          tal:content="letter">A</a> | ||||||
|     </span> |     </span> | ||||||
|   </div> |   </div> | ||||||
|   <div> </div> |   <div> </div> | ||||||
|   <div tal:repeat="letter data/keys"> |   <div tal:repeat="letter python:sorted(data.keys())"> | ||||||
|     <div class="subtitle"><a name="A" href="#top" |     <div class="subtitle"><a name="A" href="#top" | ||||||
|            tal:attributes="name letter; |            tal:attributes="name letter; | ||||||
|                            href string:${request/URL/-1}#top" |                            href string:${view/requestUrl/-1}#top" | ||||||
|            tal:content="letter">A</a> |            tal:content="letter">A</a> | ||||||
|     </div> |     </div> | ||||||
|     <div tal:repeat="related data/?letter|python:[]"> |     <div tal:repeat="related data/?letter|python:[]"> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,27 @@ | ||||||
| <html i18n:domain="loops"> | <html i18n:domain="loops"> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | <metal:institution define-macro="select_institution"> | ||||||
|  |   <form method="post"> | ||||||
|  |     <div style="font-size: 120%; padding: 10px 0 10px 0"> | ||||||
|  |       <span i18n:translate="">Organisation/Team</span>: | ||||||
|  |       <b tal:content="item/institution/title" /> | ||||||
|  |       <img tal:condition="python:len(item.institutions) > 1" | ||||||
|  |            src="/@@/cybertools.icons/application_edit.png" | ||||||
|  |            onclick="dojo.byId('select_institution').style.display = 'inline'" /> | ||||||
|  |       <select name="select_institution" id="select_institution" | ||||||
|  |               style="display: none" | ||||||
|  |               onchange="submit()"> | ||||||
|  |         <option tal:repeat="inst item/institutions" | ||||||
|  |                 tal:content="inst/title" | ||||||
|  |                 tal:attributes="value inst/uid; | ||||||
|  |                                 selected python:inst['object'] == item.institution" /> | ||||||
|  |       </select> | ||||||
|  |     </div> | ||||||
|  |   </form> | ||||||
|  | </metal:institution> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| <metal:providers define-macro="requirement_providers"> | <metal:providers define-macro="requirement_providers"> | ||||||
|   <metal:block use-macro="view/concept_macros/conceptdata" /> |   <metal:block use-macro="view/concept_macros/conceptdata" /> | ||||||
|   <div> |   <div> | ||||||
|  | @ -32,28 +53,32 @@ | ||||||
| 
 | 
 | ||||||
| <metal:candidates define-macro="requirement_candidates"> | <metal:candidates define-macro="requirement_candidates"> | ||||||
|   <metal:block use-macro="view/concept_macros/conceptdata" /> |   <metal:block use-macro="view/concept_macros/conceptdata" /> | ||||||
|   <h3 i18n:translate="">Candidates for Task</h3> |   <div class="candidates"  | ||||||
|   <table class="listing"> |        tal:define="candidates item/adapted/getCandidates" | ||||||
|     <tr> |        tal:condition="candidates"> | ||||||
|       <th i18n:translate="">Candidate</th> |     <h3 i18n:translate="">Candidates for Task</h3> | ||||||
|       <th i18n:translate="" |     <table class="listing"> | ||||||
|           title="coverage" |       <tr> | ||||||
|           i18n:attributes="title description_fit">Fit</th> |         <th i18n:translate="">Candidate</th> | ||||||
|       <th i18n:translate="">Knowledge</th> |         <th i18n:translate="" | ||||||
|     </tr> |             title="coverage" | ||||||
|     <tr tal:repeat="candidate item/adapted/getCandidates"> |             i18n:attributes="title description_fit">Fit</th> | ||||||
|       <td tal:define="person candidate/person"> |         <th i18n:translate="">Knowledge</th> | ||||||
|         <b tal:omit-tag="python:candidate['fit'] < 1.0"> |       </tr> | ||||||
|           <a tal:attributes="href python:view.getUrlForTarget(person.context)" |       <tr tal:repeat="candidate item/adapted/getCandidates"> | ||||||
|              tal:content="person/title" /></b></td> |         <td tal:define="person candidate/person"> | ||||||
|       <td tal:content="candidate/fit" /> |           <b tal:omit-tag="python:candidate['fit'] < 1.0"> | ||||||
|       <td> |             <a tal:attributes="href python:view.getUrlForTarget(person.context)" | ||||||
|         <tal:knowledge tal:repeat="ke candidate/required"> |                tal:content="person/title" /></b></td> | ||||||
|           <a tal:attributes="href python:view.getUrlForTarget(ke.context)" |         <td tal:content="candidate/fit" /> | ||||||
|              tal:content="ke/title" /><tal:sep condition="not:repeat/ke/end">, </tal:sep> |         <td> | ||||||
|         </tal:knowledge></td> |           <tal:knowledge tal:repeat="ke candidate/required"> | ||||||
|     </tr> |             <a tal:attributes="href python:view.getUrlForTarget(ke.context)" | ||||||
|   </table> |                tal:content="ke/title" /><tal:sep condition="not:repeat/ke/end">, </tal:sep> | ||||||
|  |           </tal:knowledge></td> | ||||||
|  |       </tr> | ||||||
|  |     </table> | ||||||
|  |   </div> | ||||||
| </metal:candidates> | </metal:candidates> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2013 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2015 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -18,19 +18,25 @@ | ||||||
| 
 | 
 | ||||||
| """ | """ | ||||||
| Definition of view classes and other browser related stuff for the | Definition of view classes and other browser related stuff for the | ||||||
| loops.knowledge package. | loops.knowledge.qualification package. | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| from zope import interface, component | from zope import interface, component | ||||||
| from zope.app.pagetemplate import ViewPageTemplateFile | from zope.app.pagetemplate import ViewPageTemplateFile | ||||||
| from zope.cachedescriptors.property import Lazy | from zope.cachedescriptors.property import Lazy | ||||||
| 
 | 
 | ||||||
|  | from loops.browser.concept import ConceptView | ||||||
|  | from loops.expert.browser.export import ResultsConceptCSVExport | ||||||
| from loops.expert.browser.report import ResultsConceptView | from loops.expert.browser.report import ResultsConceptView | ||||||
| from loops.knowledge.browser import template, knowledge_macros | from loops.organize.party import getPersonForUser | ||||||
| from loops.knowledge.qualification.base import QualificationRecord | from loops.util import _ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class PersonQualificationView(ResultsConceptView): | class Qualifications(ResultsConceptView): | ||||||
| 
 | 
 | ||||||
|     pass |     # obsolete because we can directly use ResultsConceptView | ||||||
|  | 
 | ||||||
|  |     #reportName = 'qualification_overview' | ||||||
|  | 
 | ||||||
|  |     pass    # report assigned to query via hasReport relation | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -15,4 +15,50 @@ | ||||||
| 
 | 
 | ||||||
|   <!-- views --> |   <!-- views --> | ||||||
| 
 | 
 | ||||||
|  |   <zope:adapter | ||||||
|  |         name="qualifications.html" | ||||||
|  |         for="loops.interfaces.IConcept | ||||||
|  |              zope.publisher.interfaces.browser.IBrowserRequest" | ||||||
|  |         provides="zope.interface.Interface" | ||||||
|  |         factory="loops.knowledge.qualification.browser.Qualifications" | ||||||
|  |         permission="zope.View" /> | ||||||
|  | 
 | ||||||
|  |   <!-- reports --> | ||||||
|  | 
 | ||||||
|  |   <zope:adapter  | ||||||
|  |       name="qualification_overview" | ||||||
|  |       factory="loops.knowledge.qualification.report.QualificationOverview" | ||||||
|  |       provides="loops.expert.report.IReportInstance" | ||||||
|  |       trusted="True" /> | ||||||
|  |   <zope:class class="loops.knowledge.qualification.report.QualificationOverview"> | ||||||
|  |     <require permission="zope.View" | ||||||
|  |              interface="loops.expert.report.IReportInstance" /> | ||||||
|  |     <require permission="zope.ManageContent" | ||||||
|  |              set_schema="loops.expert.report.IReportInstance" /> | ||||||
|  |   </zope:class> | ||||||
|  | 
 | ||||||
|  |   <zope:adapter  | ||||||
|  |       name="qualifications" | ||||||
|  |       factory="loops.knowledge.qualification.report.Qualifications" | ||||||
|  |       provides="loops.expert.report.IReportInstance" | ||||||
|  |       trusted="True" /> | ||||||
|  |   <zope:class class="loops.knowledge.qualification.report.Qualifications"> | ||||||
|  |     <require permission="zope.View" | ||||||
|  |              interface="loops.expert.report.IReportInstance" /> | ||||||
|  |     <require permission="zope.ManageContent" | ||||||
|  |              set_schema="loops.expert.report.IReportInstance" /> | ||||||
|  |   </zope:class> | ||||||
|  | 
 | ||||||
|  |   <zope:adapter  | ||||||
|  |       name="person_qualifications" | ||||||
|  |       factory="loops.knowledge.qualification.report.PersonQualifications" | ||||||
|  |       provides="loops.expert.report.IReportInstance" | ||||||
|  |       trusted="True" /> | ||||||
|  |   <zope:class class="loops.knowledge.qualification.report.PersonQualifications"> | ||||||
|  |     <require permission="zope.View" | ||||||
|  |              interface="loops.expert.report.IReportInstance" /> | ||||||
|  |     <require permission="zope.ManageContent" | ||||||
|  |              set_schema="loops.expert.report.IReportInstance" /> | ||||||
|  |   </zope:class> | ||||||
|  | 
 | ||||||
| </configure> | </configure> | ||||||
|  |  | ||||||
							
								
								
									
										133
									
								
								knowledge/qualification/report.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								knowledge/qualification/report.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,133 @@ | ||||||
|  | # | ||||||
|  | #  Copyright (c) 2015 Helmut Merz helmutm@cy55.de | ||||||
|  | # | ||||||
|  | #  This program is free software; you can redistribute it and/or modify | ||||||
|  | #  it under the terms of the GNU General Public License as published by | ||||||
|  | #  the Free Software Foundation; either version 2 of the License, or | ||||||
|  | #  (at your option) any later version. | ||||||
|  | # | ||||||
|  | #  This program is distributed in the hope that it will be useful, | ||||||
|  | #  but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | #  GNU General Public License for more details. | ||||||
|  | # | ||||||
|  | #  You should have received a copy of the GNU General Public License | ||||||
|  | #  along with this program; if not, write to the Free Software | ||||||
|  | #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA | ||||||
|  | # | ||||||
|  | 
 | ||||||
|  | """ | ||||||
|  | Qualification management report definitions. | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | from zope.cachedescriptors.property import Lazy | ||||||
|  | 
 | ||||||
|  | from cybertools.composer.report.base import LeafQueryCriteria, CompoundQueryCriteria | ||||||
|  | from cybertools.util.jeep import Jeep | ||||||
|  | from loops.expert.report import ReportInstance | ||||||
|  | from loops.organize.work.report import WorkRow | ||||||
|  | from loops.organize.work.report import deadline, day, task, party, state | ||||||
|  | from loops.organize.work.report import dayStart, dayEnd | ||||||
|  | from loops.organize.work.report import workTitle, workDescription | ||||||
|  | from loops.organize.work.report import partyState | ||||||
|  | from loops import util | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class QualificationOverview(ReportInstance): | ||||||
|  | 
 | ||||||
|  |     type = 'qualification_overview' | ||||||
|  |     label = u'Qualification Overview' | ||||||
|  | 
 | ||||||
|  |     rowFactory = WorkRow | ||||||
|  | 
 | ||||||
|  |     fields = Jeep((task, party, workTitle, dayStart, dayEnd, state, | ||||||
|  |                    partyState,)) # +deadline? | ||||||
|  | 
 | ||||||
|  |     defaultOutputFields = Jeep(list(fields)[:-1]) | ||||||
|  |     defaultSortCriteria = (party, task,) | ||||||
|  | 
 | ||||||
|  |     def getOptions(self, option): | ||||||
|  |         return self.view.options(option) | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def states(self): | ||||||
|  |         return self.getOptions('report_select_state' or ('planned',)) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def queryCriteria(self): | ||||||
|  |         crit = self.context.queryCriteria or [] | ||||||
|  |         f = self.fields.partyState | ||||||
|  |         crit.append( | ||||||
|  |             LeafQueryCriteria(f.name, f.operator, 'active', f)) | ||||||
|  |         return CompoundQueryCriteria(crit)     | ||||||
|  | 
 | ||||||
|  |     def selectObjects(self, parts): | ||||||
|  |         result = [] | ||||||
|  |         workItems = self.recordManager['work'] | ||||||
|  |         pred = self.conceptManager['querytarget'] | ||||||
|  |         types = self.view.context.getChildren([pred]) | ||||||
|  |         for t in types: | ||||||
|  |             for c in t.getChildren([self.view.typePredicate]): | ||||||
|  |                 uid = util.getUidForObject(c) | ||||||
|  |                 for wi in workItems.query(taskId=uid, state=self.states): | ||||||
|  |                     result.append(wi) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Qualifications(QualificationOverview): | ||||||
|  | 
 | ||||||
|  |     type = 'qualifications' | ||||||
|  |     label = u'Qualifications' | ||||||
|  | 
 | ||||||
|  |     taskTypeNames = ('competence',) | ||||||
|  | 
 | ||||||
|  |     def getOptions(self, option): | ||||||
|  |         return self.view.typeOptions(option) | ||||||
|  | 
 | ||||||
|  |     def selectObjects(self, parts): | ||||||
|  |         result = [] | ||||||
|  |         workItems = self.recordManager['work'] | ||||||
|  |         target = self.view.context | ||||||
|  |         tasks = [target] + self.getAllSubtasks(target) | ||||||
|  |         for t in tasks: | ||||||
|  |                 uid = util.getUidForObject(t) | ||||||
|  |                 for wi in workItems.query(taskId=uid, state=self.states): | ||||||
|  |                     result.append(wi) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     def getAllSubtasks(self, concept): | ||||||
|  |         result = [] | ||||||
|  |         for c in concept.getChildren([self.view.defaultPredicate]): | ||||||
|  |             if c.conceptType in self.taskTypes: | ||||||
|  |                 result.append(c) | ||||||
|  |             result.extend(self.getAllSubtasks(c)) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def taskTypes(self): | ||||||
|  |         return [c for c in [self.conceptManager.get(name) | ||||||
|  |                                 for name in self.taskTypeNames] | ||||||
|  |                   if c is not None] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PersonQualifications(QualificationOverview): | ||||||
|  | 
 | ||||||
|  |     type = 'person_qualifications' | ||||||
|  |     label = u'Qualifications for Person' | ||||||
|  | 
 | ||||||
|  |     defaultSortCriteria = (task,) | ||||||
|  | 
 | ||||||
|  |     def getOptions(self, option): | ||||||
|  |         return self.view.typeOptions(option) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def queryCriteria(self): | ||||||
|  |         crit = self.context.queryCriteria or [] | ||||||
|  |         return CompoundQueryCriteria(crit)     | ||||||
|  | 
 | ||||||
|  |     def selectObjects(self, parts): | ||||||
|  |         workItems = self.recordManager['work'] | ||||||
|  |         person = self.view.context | ||||||
|  |         uid = util.getUidForObject(person) | ||||||
|  |         return workItems.query(userName=uid, state=self.states) | ||||||
|  | 
 | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2013 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2016 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -41,16 +41,34 @@ class Questionnaire(AdapterBase, Questionnaire): | ||||||
| 
 | 
 | ||||||
|     _contextAttributes = list(IQuestionnaire) |     _contextAttributes = list(IQuestionnaire) | ||||||
|     _adapterAttributes = AdapterBase._adapterAttributes + ( |     _adapterAttributes = AdapterBase._adapterAttributes + ( | ||||||
|  |                 'teamBasedEvaluation',  | ||||||
|                 'questionGroups', 'questions', 'responses',) |                 'questionGroups', 'questions', 'responses',) | ||||||
|     _noexportAttributes = _adapterAttributes |     _noexportAttributes = _adapterAttributes | ||||||
| 
 | 
 | ||||||
|  |     def getTeamBasedEvaluation(self): | ||||||
|  |         return (self.questionnaireType == 'team' or | ||||||
|  |                     getattr(self.context, '_teamBasedEvaluation', False)) | ||||||
|  |     def setTeamBasedEvaluation(self, value): | ||||||
|  |         if not value and getattr(self.context, '_teamBasedEvaluation', False): | ||||||
|  |             self.context._teamBasedEvaluation = False | ||||||
|  |     teamBasedEvaluation = property(getTeamBasedEvaluation, setTeamBasedEvaluation) | ||||||
|  | 
 | ||||||
|     @property |     @property | ||||||
|     def questionGroups(self): |     def questionGroups(self): | ||||||
|  |         return self.getQuestionGroups() | ||||||
|  | 
 | ||||||
|  |     def getAllQuestionGroups(self, personId=None): | ||||||
|         return [adapted(c) for c in self.context.getChildren()] |         return [adapted(c) for c in self.context.getChildren()] | ||||||
| 
 | 
 | ||||||
|  |     def getQuestionGroups(self, personId=None): | ||||||
|  |         return self.getAllQuestionGroups() | ||||||
|  | 
 | ||||||
|     @property |     @property | ||||||
|     def questions(self): |     def questions(self): | ||||||
|         for qug in self.questionGroups: |         return self.getQuestions() | ||||||
|  | 
 | ||||||
|  |     def getQuestions(self, personId=None): | ||||||
|  |         for qug in self.getQuestionGroups(personId): | ||||||
|             for qu in qug.questions: |             for qu in qug.questions: | ||||||
|                 #qu.questionnaire = self |                 #qu.questionnaire = self | ||||||
|                 yield qu |                 yield qu | ||||||
|  | @ -65,12 +83,18 @@ class QuestionGroup(AdapterBase, QuestionGroup): | ||||||
|                 'questionnaire', 'questions', 'feedbackItems') |                 'questionnaire', 'questions', 'feedbackItems') | ||||||
|     _noexportAttributes = _adapterAttributes |     _noexportAttributes = _adapterAttributes | ||||||
| 
 | 
 | ||||||
|     @property |     def getQuestionnaires(self): | ||||||
|     def questionnaire(self): |         result = [] | ||||||
|         for p in self.context.getParents(): |         for p in self.context.getParents(): | ||||||
|             ap = adapted(p) |             ap = adapted(p) | ||||||
|             if IQuestionnaire.providedBy(ap): |             if IQuestionnaire.providedBy(ap): | ||||||
|                 return ap |                 result.append(ap)             | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def questionnaire(self): | ||||||
|  |         for qu in self.getQuestionnaires(): | ||||||
|  |             return qu | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def subobjects(self): |     def subobjects(self): | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2013 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2016 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -23,6 +23,7 @@ surveys and self-assessments. | ||||||
| 
 | 
 | ||||||
| import csv | import csv | ||||||
| from cStringIO import StringIO | from cStringIO import StringIO | ||||||
|  | import math | ||||||
| from zope.app.pagetemplate import ViewPageTemplateFile | from zope.app.pagetemplate import ViewPageTemplateFile | ||||||
| from zope.cachedescriptors.property import Lazy | from zope.cachedescriptors.property import Lazy | ||||||
| from zope.i18n import translate | from zope.i18n import translate | ||||||
|  | @ -31,64 +32,318 @@ from cybertools.knowledge.survey.questionnaire import Response | ||||||
| from cybertools.util.date import formatTimeStamp | from cybertools.util.date import formatTimeStamp | ||||||
| from loops.browser.concept import ConceptView | from loops.browser.concept import ConceptView | ||||||
| from loops.browser.node import NodeView | from loops.browser.node import NodeView | ||||||
| from loops.common import adapted | from loops.common import adapted, baseObject | ||||||
|  | from loops.knowledge.browser import InstitutionMixin | ||||||
| from loops.knowledge.survey.response import Responses | from loops.knowledge.survey.response import Responses | ||||||
| from loops.organize.party import getPersonForUser | from loops.organize.party import getPersonForUser | ||||||
|  | from loops.security.common import checkPermission | ||||||
| from loops.util import getObjectForUid | from loops.util import getObjectForUid | ||||||
| from loops.util import _ | from loops.util import _ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| template = ViewPageTemplateFile('view_macros.pt') | template = ViewPageTemplateFile('view_macros.pt') | ||||||
| 
 | 
 | ||||||
| class SurveyView(ConceptView): | class SurveyView(InstitutionMixin, ConceptView): | ||||||
| 
 | 
 | ||||||
|     data = None |     data = None | ||||||
|     errors = None |     errors = message = None | ||||||
|  |     batchSize = 12 | ||||||
|  |     teamData = None | ||||||
|  | 
 | ||||||
|  |     template = template | ||||||
|  | 
 | ||||||
|  |     #adminMaySelectAllInstitutions = False | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def macro(self): |     def macro(self): | ||||||
|         self.registerDojo() |         self.registerDojo() | ||||||
|         return template.macros['survey'] |         return template.macros['survey'] | ||||||
| 
 | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def title(self): | ||||||
|  |         title = self.context.title | ||||||
|  |         if self.personId: | ||||||
|  |             person = adapted(getObjectForUid(self.personId)) | ||||||
|  |             if person is not None: | ||||||
|  |                 return '%s: %s' % (title, person.title) | ||||||
|  |         return title | ||||||
|  | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def tabview(self): |     def tabview(self): | ||||||
|         if self.editable: |         if self.editable: | ||||||
|             return 'index.html' |             return 'index.html' | ||||||
| 
 | 
 | ||||||
|     def results(self): |     def getUrlParamString(self): | ||||||
|  |         qs = super(SurveyView, self).getUrlParamString() | ||||||
|  |         if qs.startswith('?report='): | ||||||
|  |             return '' | ||||||
|  |         return qs | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def personId(self): | ||||||
|  |         return self.request.form.get('person') | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def report(self): | ||||||
|  |         return self.request.form.get('report') | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def questionnaireType(self): | ||||||
|  |         return self.adapted.questionnaireType | ||||||
|  | 
 | ||||||
|  |     def teamReports(self): | ||||||
|  |         if self.adapted.teamBasedEvaluation: | ||||||
|  |             if checkPermission('loops.ViewRestricted', self.context): | ||||||
|  |                 return [dict(name='standard', label='label_survey_report_standard'), | ||||||
|  |                         dict(name='questions',  | ||||||
|  |                              label='label_survey_report_questions')] | ||||||
|  | 
 | ||||||
|  |     def update(self): | ||||||
|  |         instUid = self.request.form.get('select_institution') | ||||||
|  |         if instUid: | ||||||
|  |             return self.setInstitution(instUid) | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def groups(self): | ||||||
|         result = [] |         result = [] | ||||||
|         response = None |         if self.questionnaireType == 'pref_selection': | ||||||
|  |             groups = [g.questions for g in  | ||||||
|  |                 self.adapted.getQuestionGroups(self.personId)] | ||||||
|  |             questions = [] | ||||||
|  |             for idxg, g in enumerate(groups): | ||||||
|  |                 qus = [] | ||||||
|  |                 for idxq, qu in enumerate(g): | ||||||
|  |                     questions.append((idxg + 3 * idxq, idxg, qu)) | ||||||
|  |             questions.sort() | ||||||
|  |             questions = [item[2] for item in questions] | ||||||
|  |             size = len(questions) | ||||||
|  |             for idx in range(0, size, 3): | ||||||
|  |                 result.append(dict(title=u'Question', infoText=None,  | ||||||
|  |                                    questions=questions[idx:idx+3])) | ||||||
|  |             return [g for g in result if len(g['questions']) == 3] | ||||||
|  |         if self.adapted.noGrouping: | ||||||
|  |             questions = list(self.adapted.getQuestions(self.personId)) | ||||||
|  |             questions.sort(key=lambda x: x.title) | ||||||
|  |             size = len(questions) | ||||||
|  |             bs = self.batchSize | ||||||
|  |             for idx in range(0, size, bs): | ||||||
|  |                 result.append(dict(title=u'Question', infoText=None,  | ||||||
|  |                                    questions=questions[idx:idx+bs])) | ||||||
|  |         else: | ||||||
|  |             for group in self.adapted.getQuestionGroups(self.personId): | ||||||
|  |                 result.append(dict(title=group.title,  | ||||||
|  |                                    infoText=self.getInfoText(group), | ||||||
|  |                                    questions=group.questions)) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def answerOptions(self): | ||||||
|  |         opts = self.adapted.answerOptions | ||||||
|  |         if not opts: | ||||||
|  |             opts = [ | ||||||
|  |                 dict(value='none', label=u'No answer',  | ||||||
|  |                         description=u'survey_value_none'), | ||||||
|  |                 dict(value=3, label=u'Fully applies',  | ||||||
|  |                         description=u'survey_value_3'), | ||||||
|  |                 dict(value=2, label=u'', description=u'survey_value_2'), | ||||||
|  |                 dict(value=1, label=u'', description=u'survey_value_1'), | ||||||
|  |                 dict(value=0, label=u'Does not apply',  | ||||||
|  |                         description=u'survey_value_0'),] | ||||||
|  |         return opts | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def showFeedbackText(self): | ||||||
|  |         sft = self.adapted.showFeedbackText | ||||||
|  |         return sft is None and True or sft | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def feedbackColumns(self): | ||||||
|  |         cols = self.adapted.feedbackColumns | ||||||
|  |         if not cols: | ||||||
|  |             cols = [ | ||||||
|  |                 dict(name='text', label=u'Response'), | ||||||
|  |                 dict(name='score', label=u'Score')] | ||||||
|  |         if self.report == 'standard': | ||||||
|  |             cols = [c for c in cols if c['name'] in self.teamColumns] | ||||||
|  |         return cols | ||||||
|  | 
 | ||||||
|  |     teamColumns = ['category', 'average', 'stddev', 'teamRank', 'text'] | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def showTeamResults(self): | ||||||
|  |         for c in self.feedbackColumns: | ||||||
|  |             if c['name'] in ('average', 'teamRank'): | ||||||
|  |                 return True | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def getTeamData(self, respManager): | ||||||
|  |         result = [] | ||||||
|  |         pred = [self.conceptManager.get('ismember'), | ||||||
|  |                 self.conceptManager.get('ismaster')] | ||||||
|  |         if None in pred: | ||||||
|  |             return result | ||||||
|  |         inst = self.institution | ||||||
|  |         instUid = self.getUidForObject(inst) | ||||||
|  |         if inst: | ||||||
|  |             for c in inst.getChildren(pred): | ||||||
|  |                 uid = self.getUidForObject(c) | ||||||
|  |                 data = respManager.load(uid, instUid) | ||||||
|  |                 if data: | ||||||
|  |                     resp = Response(self.adapted, self.personId) | ||||||
|  |                     for qu in self.adapted.getQuestions(self.personId): | ||||||
|  |                         if qu.questionType in (None, 'value_selection'): | ||||||
|  |                             if qu.uid in data: | ||||||
|  |                                 value = data[qu.uid] | ||||||
|  |                                 if isinstance(value, int) or value.isdigit(): | ||||||
|  |                                     resp.values[qu] = int(value) | ||||||
|  |                         else: | ||||||
|  |                             resp.texts[qu] = data.get(qu.uid) or u'' | ||||||
|  |                     qgAvailable = True | ||||||
|  |                     for qg in self.adapted.getQuestionGroups(self.personId): | ||||||
|  |                         if qg.uid in data: | ||||||
|  |                             resp.values[qg] = data[qg.uid] | ||||||
|  |                         else: | ||||||
|  |                             qgAvailable = False | ||||||
|  |                     if not qgAvailable: | ||||||
|  |                         values = resp.getGroupedResult() | ||||||
|  |                         for v in values: | ||||||
|  |                             resp.values[v['group']] = v['score'] | ||||||
|  |                     result.append(resp) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     def results(self): | ||||||
|  |         if self.report: | ||||||
|  |             return self.teamResults(self.report) | ||||||
|         form = self.request.form |         form = self.request.form | ||||||
|         if 'submit' in form: |         action = None | ||||||
|             self.data = {} |         for k in ('submit', 'save'): | ||||||
|             response = Response(self.adapted, None) |             if k in form: | ||||||
|             for key, value in form.items(): |                 action = k | ||||||
|                 if key.startswith('question_'): |                 break | ||||||
|  |         if action is None: | ||||||
|  |             return [] | ||||||
|  |         respManager = Responses(self.context) | ||||||
|  |         respManager.personId = (self.request.form.get('person') or  | ||||||
|  |                                 respManager.getPersonId()) | ||||||
|  |         if self.adapted.teamBasedEvaluation and self.institution: | ||||||
|  |             respManager.institutionId = self.getUidForObject( | ||||||
|  |                                             baseObject(self.institution)) | ||||||
|  |         if self.adapted.questionnaireType == 'person': | ||||||
|  |             respManager.referrerId = respManager.getPersonId() | ||||||
|  |         if self.adapted.questionnaireType == 'pref_selection': | ||||||
|  |             return self.prefsResults(respManager, form, action) | ||||||
|  |         data = {} | ||||||
|  |         response = Response(self.adapted, self.personId) | ||||||
|  |         for key, value in form.items(): | ||||||
|  |             if key.startswith('question_'): | ||||||
|  |                 if value != 'none': | ||||||
|                     uid = key[len('question_'):] |                     uid = key[len('question_'):] | ||||||
|                     question = adapted(self.getObjectForUid(uid)) |                     question = adapted(self.getObjectForUid(uid)) | ||||||
|                     if value != 'none': |                     if value.isdigit(): | ||||||
|                         value = int(value) |                         value = int(value) | ||||||
|                         self.data[uid] = value |                     data[uid] = value | ||||||
|                         response.values[question] = value |                     response.values[question] = value | ||||||
|             Responses(self.context).save(self.data) |         values = response.getGroupedResult() | ||||||
|             self.errors = self.check(response) |         for v in values: | ||||||
|             if self.errors: |             data[self.getUidForObject(v['group'])] = v['score'] | ||||||
|                 return [] |         self.data = data | ||||||
|         if response is not None: |         self.errors = self.check(response) | ||||||
|             result = response.getGroupedResult() |         if action == 'submit' and not self.errors: | ||||||
|         return [dict(category=r[0].title, text=r[1].text,  |             data['state'] = 'active' | ||||||
|                             score=int(round(r[2] * 100))) |         else: | ||||||
|                         for r in result] |             data['state'] = 'draft' | ||||||
|  |         respManager.save(data) | ||||||
|  |         if action == 'save': | ||||||
|  |             self.message = u'Your data have been saved.' | ||||||
|  |             return [] | ||||||
|  |         if self.errors: | ||||||
|  |             return [] | ||||||
|  |         result = [dict(category=r['group'].title, text=r['feedback'].text,  | ||||||
|  |                        score=int(round(r['score'] * 100)), rank=r['rank'])  | ||||||
|  |                     for r in values] | ||||||
|  |         if self.showTeamResults: | ||||||
|  |             self.teamData = self.getTeamData(respManager) | ||||||
|  |             groups = [r['group'] for r in values] | ||||||
|  |             teamValues = response.getTeamResult(groups, self.teamData) | ||||||
|  |             for idx, r in enumerate(teamValues): | ||||||
|  |                 result[idx]['average'] = int(round(r['average'] * 100)) | ||||||
|  |                 result[idx]['teamRank'] = r['rank'] | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     def teamResults(self, report): | ||||||
|  |         result = [] | ||||||
|  |         respManager = Responses(self.context) | ||||||
|  |         self.teamData = self.getTeamData(respManager) | ||||||
|  |         response = Response(self.adapted, None) | ||||||
|  |         groups = self.adapted.getQuestionGroups(self.personId) | ||||||
|  |         teamValues = response.getTeamResult(groups, self.teamData) | ||||||
|  |         for idx, r in enumerate(teamValues): | ||||||
|  |             group = r['group'] | ||||||
|  |             item = dict(category=group.title, | ||||||
|  |                         average=int(round(r['average'] * 100)), | ||||||
|  |                         teamRank=r['rank']) | ||||||
|  |             if group.feedbackItems: | ||||||
|  |                 wScore = r['average'] * len(group.feedbackItems) - 0.00001 | ||||||
|  |                 item['text'] = group.feedbackItems[int(wScore)].text | ||||||
|  |             result.append(item) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     def getTeamResultsForQuestion(self, question, questionnaire): | ||||||
|  |         result = dict(average=0.0, stddev=0.0) | ||||||
|  |         if self.teamData is None: | ||||||
|  |             respManager = Responses(self.context) | ||||||
|  |             self.teamData = self.getTeamData(respManager) | ||||||
|  |         answerRange = question.answerRange or questionnaire.defaultAnswerRange | ||||||
|  |         values = [r.values.get(question) for r in self.teamData] | ||||||
|  |         values = [v for v in values if v is not None] | ||||||
|  |         if values: | ||||||
|  |             average = float(sum(values)) / len(values) | ||||||
|  |             if question.revertAnswerOptions: | ||||||
|  |                 average = answerRange - average - 1 | ||||||
|  |             devs = [(average - v) for v in values] | ||||||
|  |             stddev = math.sqrt(sum(d * d for d in devs) / len(values)) | ||||||
|  |             average = average * 100 / (answerRange - 1) | ||||||
|  |             stddev = stddev * 100 / (answerRange - 1) | ||||||
|  |             result['average'] = int(round(average)) | ||||||
|  |             result['stddev'] = int(round(stddev)) | ||||||
|  |         texts = [r.texts.get(question) for r in self.teamData] | ||||||
|  |         result['texts'] = '<br />'.join([unicode(t) for t in texts if t]) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     def prefsResults(self, respManager, form, action): | ||||||
|  |         result = [] | ||||||
|  |         data = {} | ||||||
|  |         for key, value in form.items(): | ||||||
|  |             if key.startswith('group_') and value: | ||||||
|  |                 data[value] = 1 | ||||||
|  |         respManager.save(data) | ||||||
|  |         if action == 'save': | ||||||
|  |             self.message = u'Your data have been saved.' | ||||||
|  |             return [] | ||||||
|  |         self.data = data | ||||||
|  |         #self.errors = self.check(response) | ||||||
|  |         if self.errors: | ||||||
|  |             return [] | ||||||
|  |         for group in self.adapted.getQuestionGroups(self.personId): | ||||||
|  |             score = 0 | ||||||
|  |             for qu in group.questions: | ||||||
|  |                 value = data.get(qu.uid) or 0 | ||||||
|  |                 if qu.revertAnswerOptions: | ||||||
|  |                     value = -value | ||||||
|  |                 score += value | ||||||
|  |             result.append(dict(category=group.title, score=score)) | ||||||
|  |         return result | ||||||
| 
 | 
 | ||||||
|     def check(self, response): |     def check(self, response): | ||||||
|         errors = [] |         errors = [] | ||||||
|         values = response.values |         values = response.values | ||||||
|         for qu in self.adapted.questions: |         for qu in self.adapted.getQuestions(self.personId): | ||||||
|             if qu.required and qu not in values: |             if qu.required and qu not in values: | ||||||
|                 errors.append('Please answer the obligatory questions.') |                 errors.append(dict(uid=qu.uid, | ||||||
|  |                     text='Please answer the obligatory questions.')) | ||||||
|                 break |                 break | ||||||
|         qugroups = {} |         qugroups = {} | ||||||
|         for qugroup in self.adapted.questionGroups: |         for qugroup in self.adapted.getQuestionGroups(self.personId): | ||||||
|             qugroups[qugroup] = 0 |             qugroups[qugroup] = 0 | ||||||
|         for qu in values: |         for qu in values: | ||||||
|             qugroups[qu.questionGroup] += 1 |             qugroups[qu.questionGroup] += 1 | ||||||
|  | @ -97,7 +352,12 @@ class SurveyView(ConceptView): | ||||||
|             if minAnswers in (u'', None): |             if minAnswers in (u'', None): | ||||||
|                 minAnswers = len(qugroup.questions) |                 minAnswers = len(qugroup.questions) | ||||||
|             if count < minAnswers: |             if count < minAnswers: | ||||||
|                 errors.append('Please answer the minimum number of questions.') |                 if self.adapted.noGrouping: | ||||||
|  |                     errors.append(dict(uid=qugroup.uid, | ||||||
|  |                         text='Please answer the highlighted questions.')) | ||||||
|  |                 else: | ||||||
|  |                     errors.append(dict(uid=qugroup.uid, | ||||||
|  |                         text='Please answer the minimum number of questions.')) | ||||||
|                 break |                 break | ||||||
|         return errors |         return errors | ||||||
| 
 | 
 | ||||||
|  | @ -106,7 +366,8 @@ class SurveyView(ConceptView): | ||||||
|         text = qugroup.description |         text = qugroup.description | ||||||
|         info = None |         info = None | ||||||
|         if qugroup.minAnswers in (u'', None): |         if qugroup.minAnswers in (u'', None): | ||||||
|             info = translate(_(u'Please answer all questions.'), target_language=lang) |             info = translate(_(u'Please answer all questions.'),  | ||||||
|  |                 target_language=lang) | ||||||
|         elif qugroup.minAnswers > 0: |         elif qugroup.minAnswers > 0: | ||||||
|             info = translate(_(u'Please answer at least $minAnswers questions.', |             info = translate(_(u'Please answer at least $minAnswers questions.', | ||||||
|                                mapping=dict(minAnswers=qugroup.minAnswers)), |                                mapping=dict(minAnswers=qugroup.minAnswers)), | ||||||
|  | @ -115,16 +376,48 @@ class SurveyView(ConceptView): | ||||||
|             text = u'<i>%s</i><br />(%s)' % (text, info) |             text = u'<i>%s</i><br />(%s)' % (text, info) | ||||||
|         return text |         return text | ||||||
| 
 | 
 | ||||||
|  |     def loadData(self): | ||||||
|  |         if self.data is None: | ||||||
|  |             respManager = Responses(self.context) | ||||||
|  |             respManager.personId = (self.request.form.get('person') or  | ||||||
|  |                                     respManager.getPersonId()) | ||||||
|  |             if self.adapted.teamBasedEvaluation and self.institution: | ||||||
|  |                 respManager.institutionId = self.getUidForObject( | ||||||
|  |                                                 baseObject(self.institution)) | ||||||
|  |             if self.adapted.questionnaireType == 'person': | ||||||
|  |                 respManager.referrerId = respManager.getPersonId() | ||||||
|  |             self.data = respManager.load() | ||||||
|  | 
 | ||||||
|     def getValues(self, question): |     def getValues(self, question): | ||||||
|         setting = None |         setting = None | ||||||
|         if self.data is None: |         self.loadData() | ||||||
|             self.data = Responses(self.context).load() |  | ||||||
|         if self.data: |         if self.data: | ||||||
|             setting = self.data.get(question.uid) |             setting = self.data.get(question.uid) | ||||||
|         noAnswer = [dict(value='none', checked=(setting == None), |         if setting is None: | ||||||
|                          radio=(not question.required))] |             setting = 'none' | ||||||
|         return noAnswer + [dict(value=i, checked=(setting == i), radio=True)  |         setting = str(setting) | ||||||
|                                 for i in reversed(range(question.answerRange))] |         result = [] | ||||||
|  |         for opt in self.answerOptions: | ||||||
|  |             value = str(opt['value']) | ||||||
|  |             result.append(dict(value=value, checked=(setting == value),  | ||||||
|  |                                title=opt.get('description') or u'')) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     def getTextValue(self, question): | ||||||
|  |         self.loadData() | ||||||
|  |         if self.data: | ||||||
|  |             return self.data.get(question.uid) | ||||||
|  | 
 | ||||||
|  |     def getPrefsValue(self, question): | ||||||
|  |         self.loadData() | ||||||
|  |         if self.data: | ||||||
|  |             return self.data.get(question.uid) | ||||||
|  | 
 | ||||||
|  |     def getCssClass(self, question): | ||||||
|  |         cls = '' | ||||||
|  |         if self.errors and self.data.get(question.uid) is None: | ||||||
|  |             cls = 'error ' | ||||||
|  |         return cls + 'vpad' | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class SurveyCsvExport(NodeView): | class SurveyCsvExport(NodeView): | ||||||
|  | @ -132,36 +425,43 @@ class SurveyCsvExport(NodeView): | ||||||
|     encoding = 'ISO8859-15' |     encoding = 'ISO8859-15' | ||||||
| 
 | 
 | ||||||
|     def encode(self, text): |     def encode(self, text): | ||||||
|         text.encode(self.encoding) |         return text.encode(self.encoding) | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def questions(self): |     def questions(self): | ||||||
|         result = [] |         result = [] | ||||||
|         for idx1, qug in enumerate(adapted(self.virtualTargetObject).questionGroups): |         for idx1, qug in enumerate( | ||||||
|  |                     adapted(self.virtualTargetObject).questionGroups): | ||||||
|             for idx2, qu in enumerate(qug.questions): |             for idx2, qu in enumerate(qug.questions): | ||||||
|                 result.append((idx1, idx2, qug, qu)) |                 result.append((idx1, idx2, qug, qu)) | ||||||
|         return result |         return result | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def columns(self): |     def columns(self): | ||||||
|         infoCols = ['Name', 'Timestamp'] |         infoCols = ['Institution', 'Name', 'Timestamp'] | ||||||
|         dataCols = ['%02i-%02i' % (item[0], item[1]) for item in self.questions] |         dataCols = ['%02i-%02i' % (item[0], item[1]) for item in self.questions] | ||||||
|         return infoCols + dataCols |         return infoCols + dataCols | ||||||
| 
 | 
 | ||||||
|     def getRows(self): |     def getRows(self): | ||||||
|  |         memberPred = self.conceptManager.get('ismember') | ||||||
|         for tr in Responses(self.virtualTargetObject).getAllTracks(): |         for tr in Responses(self.virtualTargetObject).getAllTracks(): | ||||||
|             p = adapted(getObjectForUid(tr.userName)) |             p = adapted(getObjectForUid(tr.userName)) | ||||||
|             name = p and p.title or u'???' |             name = self.encode(p and p.title or u'???') | ||||||
|  |             inst = u'' | ||||||
|  |             if memberPred is not None: | ||||||
|  |                 for i in baseObject(p).getParents([memberPred]): | ||||||
|  |                     inst = self.encode(i.title) | ||||||
|  |                     break | ||||||
|             ts = formatTimeStamp(tr.timeStamp) |             ts = formatTimeStamp(tr.timeStamp) | ||||||
|             cells = [tr.data.get(qu.uid, -1)  |             cells = [tr.data.get(qu.uid, -1)  | ||||||
|                         for (idx1, idx2, qug, qu) in self.questions] |                         for (idx1, idx2, qug, qu) in self.questions] | ||||||
|             yield [name, ts] + cells |             yield [inst, name, ts] + cells | ||||||
| 
 | 
 | ||||||
|     def __call__(self): |     def __call__(self): | ||||||
|         f = StringIO() |         f = StringIO() | ||||||
|         writer = csv.writer(f, delimiter=',') |         writer = csv.writer(f, delimiter=',') | ||||||
|         writer.writerow(self.columns) |         writer.writerow(self.columns) | ||||||
|         for row in self.getRows(): |         for row in sorted(self.getRows()): | ||||||
|             writer.writerow(row) |             writer.writerow(row) | ||||||
|         text = f.getvalue() |         text = f.getvalue() | ||||||
|         self.setDownloadHeader(text) |         self.setDownloadHeader(text) | ||||||
|  |  | ||||||
|  | @ -12,8 +12,6 @@ | ||||||
|   <zope:class class="loops.knowledge.survey.base.Questionnaire"> |   <zope:class class="loops.knowledge.survey.base.Questionnaire"> | ||||||
|     <require permission="zope.View" |     <require permission="zope.View" | ||||||
|              interface="loops.knowledge.survey.interfaces.IQuestionnaire" /> |              interface="loops.knowledge.survey.interfaces.IQuestionnaire" /> | ||||||
|     <require permission="zope.View" |  | ||||||
|              attributes="context" /> |  | ||||||
|     <require permission="zope.ManageContent" |     <require permission="zope.ManageContent" | ||||||
|              set_schema="loops.knowledge.survey.interfaces.IQuestionnaire" /> |              set_schema="loops.knowledge.survey.interfaces.IQuestionnaire" /> | ||||||
|   </zope:class> |   </zope:class> | ||||||
|  | @ -25,8 +23,6 @@ | ||||||
|   <zope:class class="loops.knowledge.survey.base.QuestionGroup"> |   <zope:class class="loops.knowledge.survey.base.QuestionGroup"> | ||||||
|     <require permission="zope.View" |     <require permission="zope.View" | ||||||
|              interface="loops.knowledge.survey.interfaces.IQuestionGroup" /> |              interface="loops.knowledge.survey.interfaces.IQuestionGroup" /> | ||||||
|     <require permission="zope.View" |  | ||||||
|              attributes="context" /> |  | ||||||
|     <require permission="zope.ManageContent" |     <require permission="zope.ManageContent" | ||||||
|              set_schema="loops.knowledge.survey.interfaces.IQuestionGroup" /> |              set_schema="loops.knowledge.survey.interfaces.IQuestionGroup" /> | ||||||
|   </zope:class> |   </zope:class> | ||||||
|  | @ -38,8 +34,6 @@ | ||||||
|   <zope:class class="loops.knowledge.survey.base.Question"> |   <zope:class class="loops.knowledge.survey.base.Question"> | ||||||
|     <require permission="zope.View" |     <require permission="zope.View" | ||||||
|              interface="loops.knowledge.survey.interfaces.IQuestion" /> |              interface="loops.knowledge.survey.interfaces.IQuestion" /> | ||||||
|     <require permission="zope.View" |  | ||||||
|              attributes="context" /> |  | ||||||
|     <require permission="zope.ManageContent" |     <require permission="zope.ManageContent" | ||||||
|              set_schema="loops.knowledge.survey.interfaces.IQuestion" /> |              set_schema="loops.knowledge.survey.interfaces.IQuestion" /> | ||||||
|   </zope:class> |   </zope:class> | ||||||
|  | @ -51,8 +45,6 @@ | ||||||
|   <zope:class class="loops.knowledge.survey.base.FeedbackItem"> |   <zope:class class="loops.knowledge.survey.base.FeedbackItem"> | ||||||
|     <require permission="zope.View" |     <require permission="zope.View" | ||||||
|              interface="loops.knowledge.survey.interfaces.IFeedbackItem" /> |              interface="loops.knowledge.survey.interfaces.IFeedbackItem" /> | ||||||
|     <require permission="zope.View" |  | ||||||
|              attributes="context" /> |  | ||||||
|     <require permission="zope.ManageContent" |     <require permission="zope.ManageContent" | ||||||
|              set_schema="loops.knowledge.survey.interfaces.IFeedbackItem" /> |              set_schema="loops.knowledge.survey.interfaces.IFeedbackItem" /> | ||||||
|   </zope:class> |   </zope:class> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2013 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2016 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -23,21 +23,82 @@ Interfaces for surveys used in knowledge management. | ||||||
| from zope.interface import Interface, Attribute | from zope.interface import Interface, Attribute | ||||||
| from zope import interface, component, schema | from zope import interface, component, schema | ||||||
| 
 | 
 | ||||||
|  | from cybertools.composer.schema.grid.interfaces import Records | ||||||
| from cybertools.knowledge.survey import interfaces | from cybertools.knowledge.survey import interfaces | ||||||
| from loops.interfaces import IConceptSchema, ILoopsAdapter | from loops.interfaces import IConceptSchema, ILoopsAdapter | ||||||
| from loops.util import _ | from loops.util import _, KeywordVocabulary | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class IQuestionnaire(IConceptSchema, interfaces.IQuestionnaire): | class IQuestionnaire(ILoopsAdapter, interfaces.IQuestionnaire): | ||||||
|     """ A collection of questions for setting up a survey. |     """ A collection of questions for setting up a survey. | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|  |     questionnaireHeader = schema.Text( | ||||||
|  |         title=_(u'Questionnaire Header'), | ||||||
|  |         description=_(u'Text that will appear at the top of the questionnaire.'), | ||||||
|  |         default=u'', | ||||||
|  |         missing_value=u'', | ||||||
|  |         required=False) | ||||||
|  | 
 | ||||||
|  |     questionnaireType = schema.Choice( | ||||||
|  |         title=_(u'Questionnaire Type'), | ||||||
|  |         description=_(u'Select the type of the questionnaire.'), | ||||||
|  |         source=KeywordVocabulary(( | ||||||
|  |                 ('standard', _(u'Standard Questionnaire')), | ||||||
|  |                 ('person', _(u'Person-related Questionnaire')), | ||||||
|  |                 ('team', _(u'Team-related Questionnaire')), | ||||||
|  |                 ('pref_selection', _(u'Preference Selection')), | ||||||
|  |             )), | ||||||
|  |         default='standard', | ||||||
|  |         required=True) | ||||||
|  | 
 | ||||||
|     defaultAnswerRange = schema.Int( |     defaultAnswerRange = schema.Int( | ||||||
|         title=_(u'Answer Range'), |         title=_(u'Answer Range'), | ||||||
|         description=_(u'Number of items (answer options) to select from.'), |         description=_(u'Number of items (answer options) to select from.'), | ||||||
|         default=4, |         default=4, | ||||||
|         required=True) |         required=True) | ||||||
| 
 | 
 | ||||||
|  |     answerOptions = Records( | ||||||
|  |         title=_(u'Answer Options'), | ||||||
|  |         description=_(u'Values to select from with corresponding column ' | ||||||
|  |                         u'labels and descriptions. There should be at ' | ||||||
|  |                         u'least answer range items with numeric values.'), | ||||||
|  |         default=[], | ||||||
|  |         required=False) | ||||||
|  | 
 | ||||||
|  |     answerOptions.column_types = [ | ||||||
|  |             schema.Text(__name__='value', title=u'Value',), | ||||||
|  |             schema.Text(__name__='label', title=u'Label'), | ||||||
|  |             schema.Text(__name__='description', title=u'Description'), | ||||||
|  |             schema.Text(__name__='colspan', title=u'ColSpan'), | ||||||
|  |             schema.Text(__name__='cssclass', title=u'CSS Class'),] | ||||||
|  | 
 | ||||||
|  |     noGrouping = schema.Bool( | ||||||
|  |         title=_(u'No Grouping of Questions'), | ||||||
|  |         description=_(u'The questions should be presented in a linear manner, ' | ||||||
|  |                         u'not grouped by categories or question groups.'), | ||||||
|  |         default=False, | ||||||
|  |         required=False) | ||||||
|  | 
 | ||||||
|  |     teamBasedEvaluation = schema.Bool( | ||||||
|  |         title=_(u'Team-based Evaluation'), | ||||||
|  |         description=_(u'.'), | ||||||
|  |         default=False, | ||||||
|  |         required=False) | ||||||
|  | 
 | ||||||
|  |     #teamBasedEvaluation = Attribute('Team-based Evaluation') | ||||||
|  | 
 | ||||||
|  |     feedbackColumns = Records( | ||||||
|  |         title=_(u'Feedback Columns'), | ||||||
|  |         description=_(u'Column definitions for the results table ' | ||||||
|  |                         u'on the feedback page.'), | ||||||
|  |         default=[], | ||||||
|  |         required=False) | ||||||
|  | 
 | ||||||
|  |     feedbackColumns.column_types = [ | ||||||
|  |             schema.Text(__name__='name', title=u'Column Name',), | ||||||
|  |             schema.Text(__name__='label', title=u'Column Label'),] | ||||||
|  | 
 | ||||||
|     feedbackHeader = schema.Text( |     feedbackHeader = schema.Text( | ||||||
|         title=_(u'Feedback Header'), |         title=_(u'Feedback Header'), | ||||||
|         description=_(u'Text that will appear at the top of the feedback page.'), |         description=_(u'Text that will appear at the top of the feedback page.'), | ||||||
|  | @ -53,7 +114,7 @@ class IQuestionnaire(IConceptSchema, interfaces.IQuestionnaire): | ||||||
|         required=False) |         required=False) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class IQuestionGroup(IConceptSchema, interfaces.IQuestionGroup): | class IQuestionGroup(ILoopsAdapter, interfaces.IQuestionGroup): | ||||||
|     """ A group of questions within a questionnaire. |     """ A group of questions within a questionnaire. | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|  | @ -65,10 +126,20 @@ class IQuestionGroup(IConceptSchema, interfaces.IQuestionGroup): | ||||||
|         required=False) |         required=False) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class IQuestion(IConceptSchema, interfaces.IQuestion): | class IQuestion(ILoopsAdapter, interfaces.IQuestion): | ||||||
|     """ A single question within a questionnaire. |     """ A single question within a questionnaire. | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|  |     questionType = schema.Choice( | ||||||
|  |         title=_(u'Question Type'), | ||||||
|  |         description=_(u'Select the type of the question.'), | ||||||
|  |         source=KeywordVocabulary(( | ||||||
|  |                 ('value_selection', _(u'Value Selection')), | ||||||
|  |                 ('text', _(u'Text')), | ||||||
|  |             )), | ||||||
|  |         default='value_selection', | ||||||
|  |         required=True) | ||||||
|  | 
 | ||||||
|     required = schema.Bool( |     required = schema.Bool( | ||||||
|         title=_(u'Required'), |         title=_(u'Required'), | ||||||
|         description=_(u'Question must be answered.'), |         description=_(u'Question must be answered.'), | ||||||
|  | @ -82,7 +153,7 @@ class IQuestion(IConceptSchema, interfaces.IQuestion): | ||||||
|         required=False) |         required=False) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class IFeedbackItem(IConceptSchema, interfaces.IFeedbackItem): | class IFeedbackItem(ILoopsAdapter, interfaces.IFeedbackItem): | ||||||
|     """ Some text (e.g. a recommendation) or some other kind of information |     """ Some text (e.g. a recommendation) or some other kind of information | ||||||
|         that may be deduced from the res)ponses to a questionnaire. |         that may be deduced from the res)ponses to a questionnaire. | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2013 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2016 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -34,22 +34,52 @@ class Responses(BaseRecordManager): | ||||||
|     implements(IResponses) |     implements(IResponses) | ||||||
| 
 | 
 | ||||||
|     storageName = 'survey_responses' |     storageName = 'survey_responses' | ||||||
|  |     personId = None | ||||||
|  |     institutionId = None | ||||||
|  |     referrerId = None | ||||||
| 
 | 
 | ||||||
|     def __init__(self, context): |     def __init__(self, context): | ||||||
|         self.context = context |         self.context = context | ||||||
| 
 | 
 | ||||||
|     def save(self, data): |     def save(self, data): | ||||||
|         if self.personId: |         if self.personId: | ||||||
|             self.storage.saveUserTrack(self.uid, 0, self.personId, data,  |             id = self.personId | ||||||
|                                         update=True, overwrite=True) |             if self.institutionId: | ||||||
|  |                 id += '.' + self.institutionId | ||||||
|  |             if self.referrerId: | ||||||
|  |                 id += '.' + self.referrerId | ||||||
|  |             self.storage.saveUserTrack(self.uid, 0, id, data,  | ||||||
|  |                                         update=True, overwrite=False) | ||||||
| 
 | 
 | ||||||
|     def load(self): |     def load(self, personId=None, referrerId=None, institutionId=None): | ||||||
|         if self.personId: |         if personId is None: | ||||||
|             tracks = self.storage.getUserTracks(self.uid, 0, self.personId) |             personId = self.personId | ||||||
|  |         if referrerId is None: | ||||||
|  |             referrerId = self.referrerId | ||||||
|  |         if institutionId is None: | ||||||
|  |             institutionId = self.institutionId | ||||||
|  |         if personId: | ||||||
|  |             id = personId | ||||||
|  |             if institutionId: | ||||||
|  |                 id += '.' + institutionId | ||||||
|  |             if referrerId: | ||||||
|  |                 id += '.' + referrerId | ||||||
|  |             tracks = self.storage.getUserTracks(self.uid, 0, id) | ||||||
|  |             if not tracks:  # then try without institution | ||||||
|  |                 tracks = self.storage.getUserTracks(self.uid, 0, personId) | ||||||
|             if tracks: |             if tracks: | ||||||
|                 return tracks[0].data |                 return tracks[0].data | ||||||
|         return {} |         return {} | ||||||
| 
 | 
 | ||||||
|  |     def loadRange(self, personId): | ||||||
|  |         tracks = self.storage.getUserTracks(self.uid, 0, personId) | ||||||
|  |         data = {} | ||||||
|  |         for tr in tracks: | ||||||
|  |             for k, v in tr.data.items(): | ||||||
|  |                 item = data.setdefault(k, []) | ||||||
|  |                 item.append(v) | ||||||
|  |         return data | ||||||
|  | 
 | ||||||
|     def getAllTracks(self): |     def getAllTracks(self): | ||||||
|         return self.storage.query(taskId=self.uid) |         return self.storage.query(taskId=self.uid) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,96 +4,283 @@ | ||||||
| 
 | 
 | ||||||
| <metal:block define-macro="survey" | <metal:block define-macro="survey" | ||||||
|               tal:define="feedback item/results; |               tal:define="feedback item/results; | ||||||
|                           errors item/errors"> |                           questType item/questionnaireType; | ||||||
|  |                           questMacro python: | ||||||
|  |                               'quest_' + (questType or 'standard'); | ||||||
|  |                           report request/report|nothing; | ||||||
|  |                           reportMacro python: | ||||||
|  |                               'report_' + (report or 'standard'); | ||||||
|  |                           errors item/errors; | ||||||
|  |                           message item/message; | ||||||
|  |                           dummy item/update"> | ||||||
|   <metal:title use-macro="item/conceptMacros/concepttitle_only" /> |   <metal:title use-macro="item/conceptMacros/concepttitle_only" /> | ||||||
|   <tal:description condition="not:feedback"> |   <tal:description condition="not:feedback"> | ||||||
|     <metal:title use-macro="item/conceptMacros/conceptdescription" /> |     <div tal:define="header item/adapted/questionnaireHeader" | ||||||
|   </tal:description> |  | ||||||
|   <div tal:condition="feedback"> |  | ||||||
|     <h3 i18n:translate="">Feedback</h3> |  | ||||||
|     <div tal:define="header item/adapted/feedbackHeader" |  | ||||||
|          tal:condition="header" |          tal:condition="header" | ||||||
|          tal:content="structure python:item.renderText(header, 'text/restructured')" /> |          tal:content="structure python: | ||||||
|     <table class="listing"> |                         item.renderText(header, 'text/restructured')" /> | ||||||
|       <tr> |   </tal:description> | ||||||
|         <th i18n:translate="">Category</th> | 
 | ||||||
|         <th i18n:translate="">Response</th> |   <div tal:condition="feedback"> | ||||||
|         <th i18n:translate="">%</th> |       <metal:block use-macro="item/template/macros/?reportMacro" /> | ||||||
|       </tr> |  | ||||||
|       <tr tal:repeat="fbitem feedback"> |  | ||||||
|         <td tal:content="fbitem/category" /> |  | ||||||
|         <td tal:content="fbitem/text" /> |  | ||||||
|         <td tal:content="fbitem/score" /> |  | ||||||
|       </tr> |  | ||||||
|     </table> |  | ||||||
|     <div class="button" id="show_questionnaire"> |  | ||||||
|       <a href="" onclick="back(); return false" |  | ||||||
|          i18n:translate=""> |  | ||||||
|         Back to Questionnaire</a> |  | ||||||
|       <br /> |  | ||||||
|     </div> |  | ||||||
|     <div tal:define="footer item/adapted/feedbackFooter" |  | ||||||
|          tal:condition="footer" |  | ||||||
|          tal:content="structure python:item.renderText(footer, 'text/restructured')" /> |  | ||||||
|   </div> |   </div> | ||||||
|   <div id="questionnaire" |   <div id="questionnaire" | ||||||
|        tal:condition="not:feedback"> |        tal:condition="not:feedback"> | ||||||
|  |       <metal:block use-macro="item/template/macros/?questMacro" /> | ||||||
|  |   </div> | ||||||
|  | </metal:block> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | <metal:block define-macro="quest_standard"> | ||||||
|  |     <tal:inst condition="item/adapted/teamBasedEvaluation"> | ||||||
|  |       <metal:inst use-macro="item/knowledge_macros/select_institution" /> | ||||||
|  |     </tal:inst> | ||||||
|  |     <div class="button" | ||||||
|  |          tal:define="reports item/teamReports" | ||||||
|  |          tal:condition="reports"> | ||||||
|  |       <b i18n:translate="label_survey_show_report">Show Report</b>:   | ||||||
|  |       <a tal:repeat="report reports" | ||||||
|  |          tal:attributes="href string:${view/requestUrl}?report=${report/name}" | ||||||
|  |          i18n:translate="" | ||||||
|  |          tal:content="report/label" /> | ||||||
|  |       <br /><br /> | ||||||
|  |     </div> | ||||||
|     <h3 i18n:translate="">Questionnaire</h3> |     <h3 i18n:translate="">Questionnaire</h3> | ||||||
|     <div class="error" |     <div class="error" | ||||||
|          tal:condition="errors"> |          tal:condition="errors"> | ||||||
|       <div tal:repeat="error errors"> |       <div tal:repeat="error errors"> | ||||||
|         <span i18n:translate="" |         <span i18n:translate="" | ||||||
|               tal:content="error" /> |               tal:content="error/text" /> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  |     <div class="message" | ||||||
|  |          tal:condition="message" | ||||||
|  |          i18n:translate="" | ||||||
|  |          tal:content="message" /> | ||||||
|     <form method="post"> |     <form method="post"> | ||||||
|       <table class="listing"> |       <table class="listing"> | ||||||
|         <tal:qugroup repeat="qugroup item/adapted/questionGroups"> |         <input type="hidden" name="person" | ||||||
|           <tr><td colspan="6"> </td></tr> |                tal:define="personId request/person|nothing" | ||||||
|  |                tal:condition="personId" | ||||||
|  |                tal:attributes="value personId" /> | ||||||
|  |         <tal:group repeat="group item/groups"> | ||||||
|  |           <tr> | ||||||
|  |             <td> </td> | ||||||
|  |             <td tal:repeat="opt item/answerOptions"> </td></tr> | ||||||
|           <tr class="vpad"> |           <tr class="vpad"> | ||||||
|             <td tal:define="infoText python:item.getInfoText(qugroup)"> |             <td tal:define="infoText group/infoText"> | ||||||
|               <b tal:content="qugroup/title" /> |               <b i18n:translate="" | ||||||
|  |                  tal:content="group/title" /> | ||||||
|               <div class="infotext" |               <div class="infotext" | ||||||
|                    tal:condition="infoText"> |                    tal:condition="infoText"> | ||||||
|                 <span tal:content="structure infoText" /> |                 <span tal:content="structure infoText" /> | ||||||
|               </div> |               </div> | ||||||
|             </td> |             </td> | ||||||
|             <td style="text-align: center" |             <td tal:repeat="opt python:[opt for opt in item.answerOptions | ||||||
|                 i18n:translate="">No answer</td> |                                             if opt.get('colspan') != '0']" | ||||||
|             <td colspan="2" |                 i18n:translate="" | ||||||
|                 i18n:translate="">Fully applies</td> |                 i18n:attributes="title" | ||||||
|             <td colspan="2" |                 tal:attributes="title opt/description|string:; | ||||||
|                 style="text-align: right" |                                 class python:opt.get('cssclass') or 'center'; | ||||||
|                 i18n:translate="">Does not apply</td> |                                 colspan python:opt.get('colspan')" | ||||||
|  |                 tal:content="opt/label|string:" /> | ||||||
|           </tr> |           </tr> | ||||||
|           <tr class="vpad" |           <tal:question repeat="question group/questions"> | ||||||
|               tal:repeat="question qugroup/questions"> |             <tal:question define="qutype python: | ||||||
|             <td tal:content="question/text" /> |                               question.questionType or 'value_selection'"> | ||||||
|             <td style="white-space: nowrap; text-align: center" |               <metal:question use-macro="item/template/macros/?qutype" /> | ||||||
|                 tal:repeat="value python:item.getValues(question)"> |             </tal:question> | ||||||
|                 <input type="radio" |           </tal:question> | ||||||
|                        i18n:attributes="title" |         </tal:group> | ||||||
|                        tal:condition="value/radio"       |  | ||||||
|                        tal:attributes=" |  | ||||||
|                             name string:question_${question/uid}; |  | ||||||
|                             value value/value; |  | ||||||
|                             checked value/checked; |  | ||||||
|                             title string:survey_value_${value/value}" /> |  | ||||||
|                 <span tal:condition="not:value/radio" |  | ||||||
|                       title="Obligatory question, must be answered" |  | ||||||
|                       i18n:attributes="title">*** |  | ||||||
|                 </span> |  | ||||||
|             </td> |  | ||||||
|           </tr> |  | ||||||
|         </tal:qugroup> |  | ||||||
|       </table> |       </table> | ||||||
|       <input type="submit" name="submit" value="Evaluate Questionnaire" |       <input type="submit" name="submit" value="Evaluate Questionnaire" | ||||||
|              i18n:attributes="value" /> |              i18n:attributes="value" /> | ||||||
|  |       <input type="submit" name="save" value="Save Data" | ||||||
|  |              i18n:attributes="value" /> | ||||||
|       <input type="button" name="reset_responses" value="Reset Responses Entered" |       <input type="button" name="reset_responses" value="Reset Responses Entered" | ||||||
|              i18n:attributes="value" |              i18n:attributes="value; onclick" | ||||||
|              onclick="setRadioButtons('none'); return false" /> |              onclick="if (confirm('Do you really want to reset all response data?')) setRadioButtons('none'); return false" /> | ||||||
|     </form> |     </form> | ||||||
|   </div> | </metal:block> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | <metal:block define-macro="quest_person"> | ||||||
|  |   <metal:block use-macro="item/template/macros/quest_standard" /> | ||||||
|  | </metal:block> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | <metal:block define-macro="quest_team"> | ||||||
|  |   <metal:block use-macro="item/template/macros/quest_standard" /> | ||||||
|  | </metal:block> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | <metal:block define-macro="quest_pref_selection"> | ||||||
|  |     <h3 i18n:translate="">Questionnaire</h3> | ||||||
|  |     <div class="error" | ||||||
|  |          tal:condition="errors"> | ||||||
|  |       <div tal:repeat="error errors"> | ||||||
|  |         <span i18n:translate="" | ||||||
|  |               tal:content="error/text" /> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |     <div class="message" | ||||||
|  |          tal:condition="message" | ||||||
|  |          i18n:translate="" | ||||||
|  |          tal:content="message" /> | ||||||
|  |     <form method="post"> | ||||||
|  |       <table class="listing"> | ||||||
|  |         <input type="hidden" name="person" | ||||||
|  |                tal:define="personId request/person|nothing" | ||||||
|  |                tal:condition="personId" | ||||||
|  |                tal:attributes="value personId" /> | ||||||
|  |         <tal:group repeat="group item/groups"> | ||||||
|  |           <tr><td> </td><td> </td></tr> | ||||||
|  |           <tal:question repeat="question group/questions"> | ||||||
|  |             <tr tal:attributes="class python:item.getCssClass(question)"> | ||||||
|  |               <td tal:content="question/text" /> | ||||||
|  |               <td tal:define="value python:item.getPrefsValue(question)"> | ||||||
|  |                 <input type="radio" | ||||||
|  |                        tal:attributes="name string:group_${repeat/group/index}; | ||||||
|  |                                        value question/uid; | ||||||
|  |                                        checked value" /> | ||||||
|  |               </td> | ||||||
|  |             </tr> | ||||||
|  |           </tal:question> | ||||||
|  |         </tal:group> | ||||||
|  |       </table> | ||||||
|  |       <input type="submit" name="submit" value="Evaluate Questionnaire" | ||||||
|  |              i18n:attributes="value" /> | ||||||
|  |       <input type="submit" name="save" value="Save Data" | ||||||
|  |              i18n:attributes="value" /> | ||||||
|  |       <input type="button" name="reset_responses" value="Reset Responses Entered" | ||||||
|  |              i18n:attributes="value; onclick" | ||||||
|  |              onclick="if (confirm('Do you really want to reset all response data?')) setRadioButtons('none'); return false" /> | ||||||
|  |     </form> | ||||||
|  | </metal:block> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | <metal:block define-macro="value_selection"> | ||||||
|  |   <tr tal:attributes="class python:item.getCssClass(question)"> | ||||||
|  |     <td tal:content="question/text" /> | ||||||
|  |     <td style="white-space: nowrap; text-align: center" | ||||||
|  |         tal:repeat="value python:item.getValues(question)"> | ||||||
|  |         <input type="radio" | ||||||
|  |                i18n:attributes="title" | ||||||
|  |                tal:attributes="name string:question_${question/uid}; | ||||||
|  |                                value value/value; | ||||||
|  |                                checked value/checked; | ||||||
|  |                                title value/title" /> | ||||||
|  |     </td> | ||||||
|  |   </tr> | ||||||
|  | </metal:block> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | <metal:block define-macro="text"> | ||||||
|  |   <tr tal:attributes="class python:item.getCssClass(question)"> | ||||||
|  |     <td> | ||||||
|  |       <div tal:content="question/text" /> | ||||||
|  |       <textarea style="width: 90%; margin-left: 20px" | ||||||
|  |                 tal:content="python:item.getTextValue(question)" | ||||||
|  |                 tal:attributes="name string:question_${question/uid}"> | ||||||
|  |       </textarea> | ||||||
|  |     </td> | ||||||
|  |     <td tal:repeat="opt item/answerOptions" /> | ||||||
|  |   </tr> | ||||||
|  | </metal:block> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | <metal:block define-macro="report_standard"> | ||||||
|  |     <h3 i18n:translate="">Feedback</h3> | ||||||
|  |     <div tal:define="header item/adapted/feedbackHeader" | ||||||
|  |          tal:condition="header" | ||||||
|  |          tal:content="structure python: | ||||||
|  |                         item.renderText(header, 'text/restructured')" /> | ||||||
|  |     <table class="listing"> | ||||||
|  |       <tr> | ||||||
|  |         <th i18n:translate="">Category</th> | ||||||
|  |         <th tal:repeat="col item/feedbackColumns" | ||||||
|  |             i18n:translate="" | ||||||
|  |             tal:attributes="class python:  | ||||||
|  |                               col['name'] != 'text' and 'center' or None" | ||||||
|  |             tal:content="col/label" /> | ||||||
|  |       </tr> | ||||||
|  |       <tr style="vertical-align: top" | ||||||
|  |           tal:repeat="fbitem feedback"> | ||||||
|  |         <td  style="vertical-align: top" | ||||||
|  |              tal:content="fbitem/category" /> | ||||||
|  |         <tal:cols repeat="col item/feedbackColumns"> | ||||||
|  |           <td style="vertical-align: top" | ||||||
|  |               tal:define="name col/name" | ||||||
|  |               tal:attributes="class python:name != 'text' and 'center' or None" | ||||||
|  |               tal:content="fbitem/?name|string:" /> | ||||||
|  |         </tal:cols> | ||||||
|  |       </tr> | ||||||
|  |     </table> | ||||||
|  |     <p tal:define="teamData item/teamData" | ||||||
|  |        tal:condition="teamData"> | ||||||
|  |       <b><span i18n:translate="">Team Size</span>: | ||||||
|  |         <span tal:content="python:len(teamData)" /></b><br />  | ||||||
|  |     </p> | ||||||
|  |     <div class="button" id="show_questionnaire"> | ||||||
|  |       <a i18n:translate="" | ||||||
|  |          tal:attributes="href string:${view/requestUrl}${item/urlParamString}"> | ||||||
|  |         Back to Questionnaire</a> | ||||||
|  |       <br /> | ||||||
|  |     </div> | ||||||
|  |     <div tal:define="footer item/adapted/feedbackFooter" | ||||||
|  |          tal:condition="footer" | ||||||
|  |          tal:content="structure python: | ||||||
|  |                         item.renderText(footer, 'text/restructured')" /> | ||||||
|  | </metal:block> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | <metal:block define-macro="report_questions"> | ||||||
|  |     <h3 i18n:translate="label_survey_report_questions"></h3> | ||||||
|  |     <div> | ||||||
|  |       <table class="listing"> | ||||||
|  |         <tal:group repeat="group item/groups"> | ||||||
|  |           <tr> | ||||||
|  |             <td> </td> | ||||||
|  |             <td> </td> | ||||||
|  |             <!--<td> </td>--> | ||||||
|  |           </tr> | ||||||
|  |           <tr class="vpad"> | ||||||
|  |             <td><b tal:content="group/title" /></td> | ||||||
|  |             <td i18n:translate="">Average</td> | ||||||
|  |             <!--<td i18n:translate="">Deviation</td>--> | ||||||
|  |           </tr> | ||||||
|  |           <tr tal:repeat="question group/questions"> | ||||||
|  |             <tal:question  | ||||||
|  |                   define="qutype python: | ||||||
|  |                               question.questionType or 'value_selection'; | ||||||
|  |                           data python: | ||||||
|  |                             item.getTeamResultsForQuestion(question, item.adapted)"> | ||||||
|  |               <td> | ||||||
|  |                 <div tal:content="question/text" /> | ||||||
|  |                 <div style="width: 90%; margin-left: 20px" | ||||||
|  |                      tal:condition="python:qutype == 'text'" | ||||||
|  |                      tal:content="structure data/texts" /> | ||||||
|  |               </td> | ||||||
|  |               <td class="center"> | ||||||
|  |                 <span tal:condition="python:qutype == 'value_selection'" | ||||||
|  |                       tal:content="data/average" /></td> | ||||||
|  |               <!--<td class="center"> | ||||||
|  |                 <span tal:condition="python:qutype == 'value_selection'" | ||||||
|  |                       tal:content="data/stddev" /></td>--> | ||||||
|  |             </tal:question> | ||||||
|  |           </tr> | ||||||
|  |         </tal:group> | ||||||
|  |       </table> | ||||||
|  |       <p tal:define="teamData item/teamData" | ||||||
|  |          tal:condition="teamData"> | ||||||
|  |         <b><span i18n:translate="">Team Size</span>: | ||||||
|  |           <span tal:content="python:len(teamData)" /></b><br />  | ||||||
|  |       </p> | ||||||
|  |       <div class="button" id="show_questionnaire"> | ||||||
|  |         <a i18n:translate="" | ||||||
|  |            tal:attributes="href string:${view/requestUrl}${item/urlParamString}"> | ||||||
|  |           Back to Questionnaire</a></div> | ||||||
|  |     </div> | ||||||
| </metal:block> | </metal:block> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| # tests.py - loops.knowledge package |  | ||||||
| 
 | 
 | ||||||
| import os | import os | ||||||
| import unittest, doctest | import unittest, doctest | ||||||
|  | @ -6,6 +5,7 @@ from zope.app.testing import ztapi | ||||||
| from zope import component | from zope import component | ||||||
| from zope.interface.verify import verifyClass | from zope.interface.verify import verifyClass | ||||||
| 
 | 
 | ||||||
|  | from loops.expert.report import IReport, Report | ||||||
| from loops.knowledge.qualification.base import Competence | from loops.knowledge.qualification.base import Competence | ||||||
| from loops.knowledge.survey.base import Questionnaire, Question, FeedbackItem | from loops.knowledge.survey.base import Questionnaire, Question, FeedbackItem | ||||||
| from loops.knowledge.survey.interfaces import IQuestionnaire, IQuestion, \ | from loops.knowledge.survey.interfaces import IQuestionnaire, IQuestion, \ | ||||||
|  | @ -18,6 +18,7 @@ importPath = os.path.join(os.path.dirname(__file__), 'data') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def importData(loopsRoot): | def importData(loopsRoot): | ||||||
|  |     component.provideAdapter(Report, provides=IReport) | ||||||
|     baseImportData(loopsRoot, importPath, 'knowledge_de.dmp') |     baseImportData(loopsRoot, importPath, 'knowledge_de.dmp') | ||||||
| 
 | 
 | ||||||
| def importSurvey(loopsRoot): | def importSurvey(loopsRoot): | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2013 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2016 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -27,6 +27,7 @@ from zope.proxy import removeAllProxies | ||||||
| from zope.security.proxy import removeSecurityProxy | from zope.security.proxy import removeSecurityProxy | ||||||
| from zope.traversing.browser import absoluteURL | from zope.traversing.browser import absoluteURL | ||||||
| 
 | 
 | ||||||
|  | from cybertools.browser.view import URLGetter | ||||||
| from cybertools.meta.interfaces import IOptions | from cybertools.meta.interfaces import IOptions | ||||||
| from cybertools.util import format | from cybertools.util import format | ||||||
| from loops.common import adapted, baseObject | from loops.common import adapted, baseObject | ||||||
|  | @ -42,6 +43,10 @@ class BaseView(object): | ||||||
|         self.context = removeSecurityProxy(context)  # this is the adapted concept! |         self.context = removeSecurityProxy(context)  # this is the adapted concept! | ||||||
|         self.request = request |         self.request = request | ||||||
| 
 | 
 | ||||||
|  |     @property | ||||||
|  |     def requestUrl(self): | ||||||
|  |         return URLGetter(self.request) | ||||||
|  | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def loopsRoot(self): |     def loopsRoot(self): | ||||||
|         return self.context.getLoopsRoot() |         return self.context.getLoopsRoot() | ||||||
|  | @ -86,6 +91,10 @@ class BaseView(object): | ||||||
|     def title(self): |     def title(self): | ||||||
|         return self.context.title |         return self.context.title | ||||||
| 
 | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def headTitle(self): | ||||||
|  |         return self.title | ||||||
|  | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def description(self): |     def description(self): | ||||||
|         return self.context.description |         return self.context.description | ||||||
|  |  | ||||||
|  | @ -65,8 +65,9 @@ class LayoutNodeView(Page, BaseView): | ||||||
|         if self.target is not None: |         if self.target is not None: | ||||||
|             targetView = component.getMultiAdapter((self.target, self.request), |             targetView = component.getMultiAdapter((self.target, self.request), | ||||||
|                                                    name='layout') |                                                    name='layout') | ||||||
|             if targetView.title not in parts: |             title = getattr(targetView, 'headTitle', targetView.title) | ||||||
|                 parts.append(targetView.title) |             if title not in parts: | ||||||
|  |                 parts.append(title) | ||||||
|         if self.globalOptions('reverseHeadTitle'): |         if self.globalOptions('reverseHeadTitle'): | ||||||
|             parts.reverse() |             parts.reverse() | ||||||
|         return ' - '.join(parts) |         return ' - '.join(parts) | ||||||
|  |  | ||||||
|  | @ -50,3 +50,15 @@ class TextView(BaseView): | ||||||
| 
 | 
 | ||||||
|     def render(self): |     def render(self): | ||||||
|         return self.renderText(self.context.data, self.context.contentType) |         return self.renderText(self.context.data, self.context.contentType) | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def canonicalUrl(self): | ||||||
|  |         parents = self.context.context.getParents( | ||||||
|  |             [self.conceptManager['standard']]) | ||||||
|  |         for parent in parents: | ||||||
|  |             view = component.getMultiAdapter((adapted(parent), | ||||||
|  |                                               self.request), name='layout') | ||||||
|  |             if view: | ||||||
|  |                 url = getattr(view, 'canonicalUrl') | ||||||
|  |                 if url: | ||||||
|  |                     return url | ||||||
|  |  | ||||||
										
											Binary file not shown.
										
									
								
							|  | @ -1,9 +1,9 @@ | ||||||
| msgid "" | msgid "" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
| "Project-Id-Version: 0.13.0\n" | "Project-Id-Version: 0.13.1\n" | ||||||
| "POT-Creation-Date: 2007-05-22 12:00 CET\n" | "POT-Creation-Date: 2007-05-22 12:00 CET\n" | ||||||
| "PO-Revision-Date: 2016-01-27 12:00 CET\n" | "PO-Revision-Date: 2017-12-08 12:00 CET\n" | ||||||
| "Last-Translator: Helmut Merz <helmutm@cy55.de>\n" | "Last-Translator: Helmut Merz <helmutm@cy55.de>\n" | ||||||
| "Language-Team: loops developers <helmutm@cy55.de>\n" | "Language-Team: loops developers <helmutm@cy55.de>\n" | ||||||
| "MIME-Version: 1.0\n" | "MIME-Version: 1.0\n" | ||||||
|  | @ -89,6 +89,14 @@ msgstr "Thema ändern" | ||||||
| msgid "Please correct the indicated errors." | msgid "Please correct the indicated errors." | ||||||
| msgstr "Bitte berichtigen Sie die angezeigten Fehler." | msgstr "Bitte berichtigen Sie die angezeigten Fehler." | ||||||
| 
 | 
 | ||||||
|  | msgid "tooltip_sort_column" | ||||||
|  | msgstr "Nach dieser Spalte sortieren" | ||||||
|  | 
 | ||||||
|  | # expert (reporting) | ||||||
|  | 
 | ||||||
|  | msgid "Download Data" | ||||||
|  | msgstr "Download als Excel-Datei" | ||||||
|  | 
 | ||||||
| # blog | # blog | ||||||
| 
 | 
 | ||||||
| msgid "Edit Blog Post..." | msgid "Edit Blog Post..." | ||||||
|  | @ -178,6 +186,30 @@ msgstr "Glossareintrag anlegen." | ||||||
| msgid "Answer Range" | msgid "Answer Range" | ||||||
| msgstr "Abstufung Bewertungen" | msgstr "Abstufung Bewertungen" | ||||||
| 
 | 
 | ||||||
|  | msgid "Answer Options" | ||||||
|  | msgstr "Antwortmöglichkeiten" | ||||||
|  | 
 | ||||||
|  | msgid "Values to select from with corresponding column labels and descriptions. There should be at least answer range items with numeric values." | ||||||
|  | msgstr "Auszuwählende Werte mit zugehörigen Spaltenüberschriften und Beschreibungen. Es sollte mindestens so viele Einträge mit numerischen Werten geben wie durch das Feld 'Abstufung Bewertungen' vorgegeben." | ||||||
|  | 
 | ||||||
|  | msgid "No Grouping of Questions" | ||||||
|  | msgstr "Keine Gruppierung der Fragen" | ||||||
|  | 
 | ||||||
|  | msgid "The questions should be presented in a linear manner, not grouped by categories or question groups." | ||||||
|  | msgstr "Die Fragen sollen in linearer Reihenfolge ausgegeben und nicht nach Fragengruppen bzw. Kategorien gruppiert werden." | ||||||
|  | 
 | ||||||
|  | msgid "Questionnaire Header" | ||||||
|  | msgstr "Infotext zum Fragebogen" | ||||||
|  | 
 | ||||||
|  | msgid "Text that will appear at the top of the questionnaire." | ||||||
|  | msgstr "Text, der vor dem Fragebogen erscheinen soll" | ||||||
|  | 
 | ||||||
|  | msgid "Feedback Header" | ||||||
|  | msgstr "Infotext zur Auswertung" | ||||||
|  | 
 | ||||||
|  | msgid "Text that will appear at the top of the feedback page." | ||||||
|  | msgstr "Text, der oben auf der Auswertungsseite erscheinen soll." | ||||||
|  | 
 | ||||||
| msgid "Feedback Footer" | msgid "Feedback Footer" | ||||||
| msgstr "Auswertungs-Hinweis" | msgstr "Auswertungs-Hinweis" | ||||||
| 
 | 
 | ||||||
|  | @ -193,6 +225,15 @@ msgstr "Mindestanzahl an Antworten" | ||||||
| msgid "Minumum number of questions that have to be answered. Empty means all questions have to be answered." | msgid "Minumum number of questions that have to be answered. Empty means all questions have to be answered." | ||||||
| msgstr "Anzahl der Fragen, die mindestens beantwortet werden müssen. Keine Angabe: Es müssen alle Fragen beantwortet werden." | msgstr "Anzahl der Fragen, die mindestens beantwortet werden müssen. Keine Angabe: Es müssen alle Fragen beantwortet werden." | ||||||
| 
 | 
 | ||||||
|  | msgid "Question Type" | ||||||
|  | msgstr "Fragentyp" | ||||||
|  | 
 | ||||||
|  | msgid "Select the type of the question." | ||||||
|  | msgstr "Bitte den Typ der Frage auswählen." | ||||||
|  | 
 | ||||||
|  | msgid "Value Selection" | ||||||
|  | msgstr "Auswahl Bewertung" | ||||||
|  | 
 | ||||||
| msgid "Required" | msgid "Required" | ||||||
| msgstr "Pflichtfrage" | msgstr "Pflichtfrage" | ||||||
| 
 | 
 | ||||||
|  | @ -205,6 +246,9 @@ msgstr "Negative Polarität" | ||||||
| msgid "Value inversion: High selection means low value." | msgid "Value inversion: High selection means low value." | ||||||
| msgstr "Invertierung der Bewertung: Hohe gewählte Stufe bedeutet niedriger Wert." | msgstr "Invertierung der Bewertung: Hohe gewählte Stufe bedeutet niedriger Wert." | ||||||
| 
 | 
 | ||||||
|  | msgid "Question" | ||||||
|  | msgstr "Frage" | ||||||
|  | 
 | ||||||
| msgid "Questionnaire" | msgid "Questionnaire" | ||||||
| msgstr "Fragebogen" | msgstr "Fragebogen" | ||||||
| 
 | 
 | ||||||
|  | @ -241,15 +285,30 @@ msgstr "Trifft eher zu" | ||||||
| msgid "survey_value_3" | msgid "survey_value_3" | ||||||
| msgstr "Trifft für unser Unternehmen voll und ganz zu" | msgstr "Trifft für unser Unternehmen voll und ganz zu" | ||||||
| 
 | 
 | ||||||
|  | msgid "label_survey_show_report" | ||||||
|  | msgstr "Auswertung anzeigen" | ||||||
|  | 
 | ||||||
|  | msgid "label_survey_report_standard" | ||||||
|  | msgstr "Standard-Auswertung" | ||||||
|  | 
 | ||||||
|  | msgid "label_survey_report_questions" | ||||||
|  | msgstr "Einzelfragen-Auswertung" | ||||||
|  | 
 | ||||||
| msgid "Evaluate Questionnaire" | msgid "Evaluate Questionnaire" | ||||||
| msgstr "Fragebogen auswerten" | msgstr "Fragebogen auswerten" | ||||||
| 
 | 
 | ||||||
|  | msgid "Save Data" | ||||||
|  | msgstr "Daten speichern" | ||||||
|  | 
 | ||||||
| msgid "Reset Responses Entered" | msgid "Reset Responses Entered" | ||||||
| msgstr "Eingaben zurücksetzen" | msgstr "Eingaben zurücksetzen" | ||||||
| 
 | 
 | ||||||
| msgid "Back to Questionnaire" | msgid "Back to Questionnaire" | ||||||
| msgstr "Zurück zum Fragebogen" | msgstr "Zurück zum Fragebogen" | ||||||
| 
 | 
 | ||||||
|  | msgid "Your data have been saved." | ||||||
|  | msgstr "Ihre Daten wurden gespeichert." | ||||||
|  | 
 | ||||||
| msgid "Please answer at least $minAnswers questions." | msgid "Please answer at least $minAnswers questions." | ||||||
| msgstr "Bitte beantworten Sie mindestens $minAnswers Fragen." | msgstr "Bitte beantworten Sie mindestens $minAnswers Fragen." | ||||||
| 
 | 
 | ||||||
|  | @ -262,10 +321,37 @@ msgstr "Bitte beantworten Sie die Pflichtfragen." | ||||||
| msgid "Please answer the minimum number of questions." | msgid "Please answer the minimum number of questions." | ||||||
| msgstr "Bitte beantworten Sie die angegebene Mindestanzahl an Fragen je Fragengruppe." | msgstr "Bitte beantworten Sie die angegebene Mindestanzahl an Fragen je Fragengruppe." | ||||||
| 
 | 
 | ||||||
|  | msgid "Please answer the highlighted questions." | ||||||
|  | msgstr "Bitte beantworten Sie die markierten Fragen." | ||||||
|  | 
 | ||||||
| msgid "Obligatory question, must be answered" | msgid "Obligatory question, must be answered" | ||||||
| msgstr "Pflichtfrage, muss beantwortet werden" | msgstr "Pflichtfrage, muss beantwortet werden" | ||||||
| 
 | 
 | ||||||
| # competence (qualification) | msgid "Score" | ||||||
|  | msgstr "Ergebnis %" | ||||||
|  | 
 | ||||||
|  | msgid "Team Score" | ||||||
|  | msgstr "Durchschnitt Team %" | ||||||
|  | 
 | ||||||
|  | msgid "Rank" | ||||||
|  | msgstr "Rang" | ||||||
|  | 
 | ||||||
|  | msgid "Team Rank" | ||||||
|  | msgstr "Rang Team" | ||||||
|  | 
 | ||||||
|  | msgid "Average" | ||||||
|  | msgstr "Durchschnitt" | ||||||
|  | 
 | ||||||
|  | msgid "Deviation" | ||||||
|  | msgstr "Abweichung" | ||||||
|  | 
 | ||||||
|  | msgid "Team Size" | ||||||
|  | msgstr "Anzahl der vom Team ausgefüllten Fragebögen" | ||||||
|  | 
 | ||||||
|  | msgid "if (confirm('Do you really want to reset all response data?')) setRadioButtons('none'); return false" | ||||||
|  | msgstr "if (confirm('Wollen Sie wirklich alle eingegebenen Daten zurücksetzen?')) setRadioButtons('none'); return false" | ||||||
|  | 
 | ||||||
|  | # compentence and qualification management | ||||||
| 
 | 
 | ||||||
| msgid "Validity Period (Months)" | msgid "Validity Period (Months)" | ||||||
| msgstr "Gültigkeitszeitraum (Monate)" | msgstr "Gültigkeitszeitraum (Monate)" | ||||||
|  | @ -471,6 +557,8 @@ msgstr "Wer?" | ||||||
| msgid "When?" | msgid "When?" | ||||||
| msgstr "Wann?" | msgstr "Wann?" | ||||||
| 
 | 
 | ||||||
|  | # personal stuff | ||||||
|  | 
 | ||||||
| msgid "Favorites" | msgid "Favorites" | ||||||
| msgstr "Lesezeichen" | msgstr "Lesezeichen" | ||||||
| 
 | 
 | ||||||
|  | @ -519,6 +607,8 @@ msgstr "Anmelden" | ||||||
| msgid "Presence" | msgid "Presence" | ||||||
| msgstr "Anwesenheit" | msgstr "Anwesenheit" | ||||||
| 
 | 
 | ||||||
|  | # general | ||||||
|  | 
 | ||||||
| msgid "Actions" | msgid "Actions" | ||||||
| msgstr "Aktionen" | msgstr "Aktionen" | ||||||
| 
 | 
 | ||||||
|  | @ -531,9 +621,6 @@ msgstr "Informationen über dieses Objekt" | ||||||
| msgid "Information about this object." | msgid "Information about this object." | ||||||
| msgstr "Informationen über dieses Objekt." | msgstr "Informationen über dieses Objekt." | ||||||
| 
 | 
 | ||||||
| msgid "Send a link to this object by email." |  | ||||||
| msgstr "Einen Link zu diesem Objekt per E-Mail versenden." |  | ||||||
| 
 |  | ||||||
| msgid "Edit with external editor." | msgid "Edit with external editor." | ||||||
| msgstr "Mit 'External Editor' bearbeiten." | msgstr "Mit 'External Editor' bearbeiten." | ||||||
| 
 | 
 | ||||||
|  | @ -792,6 +879,9 @@ msgstr "Benutzer registrieren" | ||||||
| msgid "Register new member" | msgid "Register new member" | ||||||
| msgstr "Neu registrieren" | msgstr "Neu registrieren" | ||||||
| 
 | 
 | ||||||
|  | msgid "Login name not allowed." | ||||||
|  | msgstr "Die von Ihnen eingegebene Benutzerkennung enthält Sonderzeichen, z. B. Umlaute." | ||||||
|  | 
 | ||||||
| msgid "Login name already taken." | msgid "Login name already taken." | ||||||
| msgstr "Die von Ihnen eingegebene Benutzerkennung ist schon vergeben." | msgstr "Die von Ihnen eingegebene Benutzerkennung ist schon vergeben." | ||||||
| 
 | 
 | ||||||
|  | @ -846,6 +936,12 @@ msgstr "Beginn" | ||||||
| msgid "End date" | msgid "End date" | ||||||
| msgstr "Ende" | msgstr "Ende" | ||||||
| 
 | 
 | ||||||
|  | msgid "Start Day" | ||||||
|  | msgstr "Beginn" | ||||||
|  | 
 | ||||||
|  | msgid "End Day" | ||||||
|  | msgstr "Ende" | ||||||
|  | 
 | ||||||
| msgid "Knowledge" | msgid "Knowledge" | ||||||
| msgstr "Kompetenzen" | msgstr "Kompetenzen" | ||||||
| 
 | 
 | ||||||
|  | @ -918,6 +1014,9 @@ msgstr "Kommentare" | ||||||
| msgid "Add Comment" | msgid "Add Comment" | ||||||
| msgstr "Kommentar hinzufügen" | msgstr "Kommentar hinzufügen" | ||||||
| 
 | 
 | ||||||
|  | msgid "Email Address" | ||||||
|  | msgstr "E-Mail-Adresse" | ||||||
|  | 
 | ||||||
| msgid "Subject" | msgid "Subject" | ||||||
| msgstr "Thema" | msgstr "Thema" | ||||||
| 
 | 
 | ||||||
|  | @ -930,6 +1029,9 @@ msgstr "Objekte löschen" | ||||||
| msgid "confirm('Do you really want to delete the selected objects?')" | msgid "confirm('Do you really want to delete the selected objects?')" | ||||||
| msgstr "confirm('Wollen Sie die ausgewählten Objekte wirklich löschen?')" | msgstr "confirm('Wollen Sie die ausgewählten Objekte wirklich löschen?')" | ||||||
| 
 | 
 | ||||||
|  | msgid "title_bookTopicView" | ||||||
|  | msgstr "Übersicht" | ||||||
|  | 
 | ||||||
| # management interface | # management interface | ||||||
| 
 | 
 | ||||||
| msgid "label_type" | msgid "label_type" | ||||||
|  | @ -992,6 +1094,21 @@ msgstr "Kalender" | ||||||
| msgid "Work Items" | msgid "Work Items" | ||||||
| msgstr "Aktivitäten" | msgstr "Aktivitäten" | ||||||
| 
 | 
 | ||||||
|  | msgid "Work Item Type" | ||||||
|  | msgstr "Art der Aktivität" | ||||||
|  | 
 | ||||||
|  | msgid "Unit of Work" | ||||||
|  | msgstr "Standard-Aktivität" | ||||||
|  | 
 | ||||||
|  | msgid "Scheduled Event" | ||||||
|  | msgstr "Termin" | ||||||
|  | 
 | ||||||
|  | msgid "Deadline" | ||||||
|  | msgstr "Deadline" | ||||||
|  | 
 | ||||||
|  | msgid "Check-up" | ||||||
|  | msgstr "Überprüfung" | ||||||
|  | 
 | ||||||
| msgid "Work Items for $title" | msgid "Work Items for $title" | ||||||
| msgstr "Aktivitäten für $title" | msgstr "Aktivitäten für $title" | ||||||
| 
 | 
 | ||||||
|  | @ -1022,6 +1139,12 @@ msgstr "Dauer/Aufwand" | ||||||
| msgid "Duration / Effort (hh:mm)" | msgid "Duration / Effort (hh:mm)" | ||||||
| msgstr "Dauer / Aufwand (hh:mm)" | msgstr "Dauer / Aufwand (hh:mm)" | ||||||
| 
 | 
 | ||||||
|  | msgid "Priority" | ||||||
|  | msgstr "Priorität" | ||||||
|  | 
 | ||||||
|  | msgid "Activity" | ||||||
|  | msgstr "Leistungsart" | ||||||
|  | 
 | ||||||
| msgid "Action" | msgid "Action" | ||||||
| msgstr "Aktion" | msgstr "Aktion" | ||||||
| 
 | 
 | ||||||
|  | @ -1096,6 +1219,9 @@ msgstr "Bemerkung" | ||||||
| msgid "desc_transition_comments" | msgid "desc_transition_comments" | ||||||
| msgstr "Notizen zum Statusübergang." | msgstr "Notizen zum Statusübergang." | ||||||
| 
 | 
 | ||||||
|  | msgid "contact_states" | ||||||
|  | msgstr "Kontaktstatus" | ||||||
|  | 
 | ||||||
| # state names | # state names | ||||||
| 
 | 
 | ||||||
| msgid "accepted" | msgid "accepted" | ||||||
|  | @ -1164,6 +1290,12 @@ msgstr "unklassifiziert" | ||||||
| msgid "verified" | msgid "verified" | ||||||
| msgstr "verifiziert" | msgstr "verifiziert" | ||||||
| 
 | 
 | ||||||
|  | msgid "prospective" | ||||||
|  | msgstr "künftig" | ||||||
|  | 
 | ||||||
|  | msgid "inactive" | ||||||
|  | msgstr "inaktiv" | ||||||
|  | 
 | ||||||
| # transitions | # transitions | ||||||
| 
 | 
 | ||||||
| msgid "accept" | msgid "accept" | ||||||
|  | @ -1238,6 +1370,15 @@ msgstr "verifizieren" | ||||||
| msgid "work" | msgid "work" | ||||||
| msgstr "bearbeiten" | msgstr "bearbeiten" | ||||||
| 
 | 
 | ||||||
|  | msgid "activate" | ||||||
|  | msgstr "aktivieren" | ||||||
|  | 
 | ||||||
|  | msgid "inactivate" | ||||||
|  | msgstr "inaktiv setzen" | ||||||
|  | 
 | ||||||
|  | msgid "reset" | ||||||
|  | msgstr "zurücksetzen" | ||||||
|  | 
 | ||||||
| # calendar | # calendar | ||||||
| 
 | 
 | ||||||
| msgid "Monday" | msgid "Monday" | ||||||
|  | @ -1305,3 +1446,27 @@ msgstr "Zeitraum" | ||||||
| 
 | 
 | ||||||
| msgid "Technology" | msgid "Technology" | ||||||
| msgstr "Technik" | msgstr "Technik" | ||||||
|  | 
 | ||||||
|  | # send mail | ||||||
|  | 
 | ||||||
|  | msgid "Send a link to this object by email." | ||||||
|  | msgstr "Einen Link zu diesem Objekt per E-Mail versenden." | ||||||
|  | 
 | ||||||
|  | msgid "Send Link by Email" | ||||||
|  | msgstr "Link per E-Mail versenden" | ||||||
|  | 
 | ||||||
|  | msgid "Mail Subject" | ||||||
|  | msgstr "Betreff" | ||||||
|  | 
 | ||||||
|  | msgid "Mail Body" | ||||||
|  | msgstr "Text" | ||||||
|  | 
 | ||||||
|  | msgid "Recipients" | ||||||
|  | msgstr "Empfänger" | ||||||
|  | 
 | ||||||
|  | msgid "Additional Recipients" | ||||||
|  | msgstr "Weitere Empfänger" | ||||||
|  | 
 | ||||||
|  | msgid "Send email" | ||||||
|  | msgstr "E-Mail senden" | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
|     <tal:actions condition="view/showObjectActions"> |     <tal:actions condition="view/showObjectActions"> | ||||||
|       <div metal:use-macro="views/node_macros/object_actions" /></tal:actions> |       <div metal:use-macro="views/node_macros/object_actions" /></tal:actions> | ||||||
|     <h1><a tal:omit-tag="python: level > 1" |     <h1><a tal:omit-tag="python: level > 1" | ||||||
|            tal:attributes="href request/URL" |            tal:attributes="href view/requestUrl" | ||||||
|            tal:content="item/title">Title</a></h1><br /> |            tal:content="item/title">Title</a></h1><br /> | ||||||
|     <p tal:define="url python: view.getUrlForTarget(item)"> |     <p tal:define="url python: view.getUrlForTarget(item)"> | ||||||
|       <a tal:omit-tag="view/isAnonymous" |       <a tal:omit-tag="view/isAnonymous" | ||||||
|  |  | ||||||
|  | @ -228,6 +228,23 @@ We need a principal for testing the login stuff: | ||||||
|   >>> pwcView.update() |   >>> pwcView.update() | ||||||
|   False |   False | ||||||
| 
 | 
 | ||||||
|  | Reset Password | ||||||
|  | -------------- | ||||||
|  | 
 | ||||||
|  | Invalidates the user account by generating a new password. A mail ist sent to | ||||||
|  | the email address of the person with a link for re-activating the account | ||||||
|  | and enter a new password. | ||||||
|  | 
 | ||||||
|  |   >>> data = {'loginName': u'dummy', | ||||||
|  |   ...         'action': 'update'} | ||||||
|  | 
 | ||||||
|  |   >>> request = TestRequest(form=data) | ||||||
|  | 
 | ||||||
|  |   >>> from loops.organize.browser.member import PasswordReset | ||||||
|  |   >>> pwrView = PasswordReset(menu, request) | ||||||
|  |   >>> pwrView.update() | ||||||
|  |   True | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| Pure Person-based Authentication | Pure Person-based Authentication | ||||||
| ================================ | ================================ | ||||||
|  | @ -410,7 +427,7 @@ Send Email to Members | ||||||
|   >>> form.subject |   >>> form.subject | ||||||
|   u"loops Notification from '$site'" |   u"loops Notification from '$site'" | ||||||
|   >>> form.mailBody |   >>> form.mailBody | ||||||
|   u'\n\nEvent #1\nhttp://127.0.0.1/loops/views/menu/.113\n\n' |   u'\n\nEvent #1\nhttp://127.0.0.1/loops/views/menu/.118\n\n' | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| Show Presence of Other Users | Show Presence of Other Users | ||||||
|  |  | ||||||
|  | @ -45,6 +45,12 @@ | ||||||
|       class="loops.organize.browser.member.PasswordChange" |       class="loops.organize.browser.member.PasswordChange" | ||||||
|       permission="zope.View" /> |       permission="zope.View" /> | ||||||
| 
 | 
 | ||||||
|  |   <browser:page | ||||||
|  |       for="loops.interfaces.INode" | ||||||
|  |       name="reset_password.html" | ||||||
|  |       class="loops.organize.browser.member.PasswordReset" | ||||||
|  |       permission="zope.View" /> | ||||||
|  | 
 | ||||||
|   <zope:adapter |   <zope:adapter | ||||||
|       name="task.html" |       name="task.html" | ||||||
|       for="loops.interfaces.IConcept |       for="loops.interfaces.IConcept | ||||||
|  | @ -89,6 +95,12 @@ | ||||||
| 
 | 
 | ||||||
|   <!-- specialized forms --> |   <!-- specialized forms --> | ||||||
| 
 | 
 | ||||||
|  |   <browser:page | ||||||
|  |       name="create_person.html" | ||||||
|  |       for="loops.interfaces.INode" | ||||||
|  |       class="loops.organize.browser.party.CreatePersonForm" | ||||||
|  |       permission="zope.View" /> | ||||||
|  | 
 | ||||||
|   <browser:page |   <browser:page | ||||||
|       name="edit_person.html" |       name="edit_person.html" | ||||||
|       for="loops.interfaces.INode" |       for="loops.interfaces.INode" | ||||||
|  | @ -146,4 +158,12 @@ | ||||||
|       permission="zope.ManageServices" |       permission="zope.ManageServices" | ||||||
|       menu="zmi_views" title="Prefix" /> |       menu="zmi_views" title="Prefix" /> | ||||||
| 
 | 
 | ||||||
|  |   <!-- utilities --> | ||||||
|  | 
 | ||||||
|  |   <browser:page | ||||||
|  |       for="loops.interfaces.ILoops" | ||||||
|  |       name="fix_person_roles" | ||||||
|  |       class="loops.organize.browser.member.FixPersonRoles" | ||||||
|  |       permission="zope.ManageServices" /> | ||||||
|  | 
 | ||||||
| </configure> | </configure> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2014 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2015 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -87,6 +87,7 @@ class BaseMemberRegistration(NodeView): | ||||||
|     formErrors = dict( |     formErrors = dict( | ||||||
|         confirm_nomatch=FormError(_(u'Password and password confirmation ' |         confirm_nomatch=FormError(_(u'Password and password confirmation ' | ||||||
|                                     u'do not match.')), |                                     u'do not match.')), | ||||||
|  |         illegal_loginname=FormError(_('Login name not allowed.')), | ||||||
|         duplicate_loginname=FormError(_('Login name already taken.')), |         duplicate_loginname=FormError(_('Login name already taken.')), | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2011 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2016 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -32,7 +32,7 @@ from cybertools.ajax import innerHtml | ||||||
| from cybertools.browser.action import actions | from cybertools.browser.action import actions | ||||||
| from cybertools.browser.form import FormController | from cybertools.browser.form import FormController | ||||||
| from loops.browser.action import DialogAction | from loops.browser.action import DialogAction | ||||||
| from loops.browser.form import EditConceptForm | from loops.browser.form import CreateConceptForm, EditConceptForm | ||||||
| from loops.browser.node import NodeView | from loops.browser.node import NodeView | ||||||
| from loops.common import adapted | from loops.common import adapted | ||||||
| from loops.organize.party import getPersonForUser | from loops.organize.party import getPersonForUser | ||||||
|  | @ -44,7 +44,8 @@ organize_macros = ViewPageTemplateFile('view_macros.pt') | ||||||
| actions.register('createPerson', 'portlet', DialogAction, | actions.register('createPerson', 'portlet', DialogAction, | ||||||
|         title=_(u'Create Person...'), |         title=_(u'Create Person...'), | ||||||
|         description=_(u'Create a new person.'), |         description=_(u'Create a new person.'), | ||||||
|         viewName='create_concept.html', |         #viewName='create_concept.html', | ||||||
|  |         viewName='create_person.html', | ||||||
|         dialogName='createPerson', |         dialogName='createPerson', | ||||||
|         typeToken='.loops/concepts/person', |         typeToken='.loops/concepts/person', | ||||||
|         fixedType=True, |         fixedType=True, | ||||||
|  | @ -115,24 +116,35 @@ actions.register('send_email', 'object', DialogAction, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class EditPersonForm(EditConceptForm): | class PersonForm(object): | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def presetTypesForAssignment(self): |     def presetTypesForAssignment(self): | ||||||
|         types = list(self.typeManager.listTypes(include=('workspace',))) |         types = list(self.typeManager.listTypes(include=('workspace',))) | ||||||
|         #assigned = [r.context for r in self.assignments] |  | ||||||
|         #types = [t for t in types if t.typeProvider not in assigned] |  | ||||||
|         predicates = [n for n in ['standard', 'ismember', 'ismaster', 'isowner'] |         predicates = [n for n in ['standard', 'ismember', 'ismaster', 'isowner'] | ||||||
|                         if n in self.conceptManager] |                         if n in self.conceptManager] | ||||||
|         return [dict(title=t.title, token=t.tokenForSearch, predicates=predicates) |         return [dict(title=t.title, token=t.tokenForSearch, predicates=predicates) | ||||||
|                         for t in types] |                         for t in types] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class CreatePersonForm(PersonForm, CreateConceptForm): | ||||||
|  | 
 | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class EditPersonForm(PersonForm, EditConceptForm): | ||||||
|  | 
 | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class SendEmailForm(NodeView): | class SendEmailForm(NodeView): | ||||||
| 
 | 
 | ||||||
|     __call__ = innerHtml |     __call__ = innerHtml | ||||||
| 
 | 
 | ||||||
|  |     def checkPermissions(self): | ||||||
|  |         return (not self.isAnonymous and  | ||||||
|  |                 super(SendEmailForm, self).checkPermissions()) | ||||||
|  | 
 | ||||||
|     @property |     @property | ||||||
|     def macro(self): |     def macro(self): | ||||||
|         return organize_macros.macros['send_email'] |         return organize_macros.macros['send_email'] | ||||||
|  | @ -171,6 +183,10 @@ class SendEmailForm(NodeView): | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def subject(self): |     def subject(self): | ||||||
|  |         optionKey = 'organize.sendmail_subject' | ||||||
|  |         option = self.globalOptions(optionKey) or self.typeOptions(optionKey) | ||||||
|  |         if option: | ||||||
|  |             return option[0] | ||||||
|         menu = self.context.getMenu() |         menu = self.context.getMenu() | ||||||
|         zdc = IZopeDublinCore(menu) |         zdc = IZopeDublinCore(menu) | ||||||
|         zdc.languageInfo = self.languageInfo |         zdc.languageInfo = self.languageInfo | ||||||
|  | @ -181,6 +197,12 @@ class SendEmailForm(NodeView): | ||||||
| 
 | 
 | ||||||
| class SendEmail(FormController): | class SendEmail(FormController): | ||||||
| 
 | 
 | ||||||
|  |     bccToSender = False | ||||||
|  | 
 | ||||||
|  |     def checkPermissions(self): | ||||||
|  |         return (not self.isAnonymous and  | ||||||
|  |                 super(SendEmail, self).checkPermissions()) | ||||||
|  | 
 | ||||||
|     def update(self): |     def update(self): | ||||||
|         form = self.request.form |         form = self.request.form | ||||||
|         subject = form.get('subject') or u'' |         subject = form.get('subject') or u'' | ||||||
|  | @ -193,7 +215,10 @@ class SendEmail(FormController): | ||||||
|         msg = MIMEText(message.encode('utf-8'), 'plain', 'utf-8') |         msg = MIMEText(message.encode('utf-8'), 'plain', 'utf-8') | ||||||
|         msg['Subject'] = subject.encode('utf-8') |         msg['Subject'] = subject.encode('utf-8') | ||||||
|         msg['From'] = sender |         msg['From'] = sender | ||||||
|         msg['To'] = ', '.join(r.strip() for r in recipients if r.strip()) |         recipients = [r.strip() for r in recipients if r.strip()] | ||||||
|  |         msg['To'] = ', '.join(recipients) | ||||||
|  |         if self.bccToSender: | ||||||
|  |             recipients.append(sender) | ||||||
|         mailhost = component.getUtility(IMailDelivery, 'Mail') |         mailhost = component.getUtility(IMailDelivery, 'Mail') | ||||||
|         mailhost.send(sender, recipients, msg.as_string()) |         mailhost.send(sender, recipients, msg.as_string()) | ||||||
|         return True |         return True | ||||||
|  |  | ||||||
|  | @ -123,20 +123,24 @@ | ||||||
|       <div class="heading"> |       <div class="heading"> | ||||||
|         <span i18n:translate="">Send Link by Email</span> - |         <span i18n:translate="">Send Link by Email</span> - | ||||||
|         <span tal:content="view/target/title"></span></div> |         <span tal:content="view/target/title"></span></div> | ||||||
|       <div> |       <metal:content define-macro="mail_content"> | ||||||
|         <label i18n:translate="" for="subject">Subject</label> |  | ||||||
|         <div> |         <div> | ||||||
|           <input name="subject" id="subject" style="width: 60em" |           <label i18n:translate="" for="subject">Mail Subject</label> | ||||||
|                  dojoType="dijit.form.ValidationTextBox" required |           <div> | ||||||
|                  tal:attributes="value view/subject" /></div> |             <input name="subject" id="subject" style="width: 60em" | ||||||
|       </div> |                    dojoType="dijit.form.ValidationTextBox" required | ||||||
|       <div> |                    tal:attributes="value view/subject" /></div> | ||||||
|         <label i18n:translate="" for="mailbody">Mail Body</label> |         </div> | ||||||
|         <div> |         <div> | ||||||
|           <textarea name="mailbody" cols="80" rows="4" id="mailbody" |           <label i18n:translate=""  | ||||||
|                     dojoType="dijit.form.SimpleTextarea" style="width: 60em" |                  for="mailbody">Mail Body</label> | ||||||
|                     tal:content="view/mailBody"></textarea></div> |           <div> | ||||||
|       </div> |             <textarea name="mailbody" cols="80" rows="4" id="mailbody" | ||||||
|  |                       dojoType="dijit.form.SimpleTextarea" style="width: 60em" | ||||||
|  |                       tal:attributes="rows view/contentHeight|string:4" | ||||||
|  |                       tal:content="view/mailBody"></textarea></div> | ||||||
|  |         </div> | ||||||
|  |       </metal:content> | ||||||
|       <div> |       <div> | ||||||
|         <label i18n:translate="">Recipients</label> |         <label i18n:translate="">Recipients</label> | ||||||
|         <div tal:repeat="member view/members"> |         <div tal:repeat="member view/members"> | ||||||
|  | @ -152,7 +156,8 @@ | ||||||
|           <span i18n:translate="">Toggle all</span></div> |           <span i18n:translate="">Toggle all</span></div> | ||||||
|       </div> |       </div> | ||||||
|       <div> |       <div> | ||||||
|         <label i18n:translate="" for="addrecipients">Additional recipients</label> |         <label i18n:translate=""  | ||||||
|  |                for="addrecipients">Additional Recipients</label> | ||||||
|         <div> |         <div> | ||||||
|           <textarea name="addrecipients" cols="80" rows="4" id="addrecipients" |           <textarea name="addrecipients" cols="80" rows="4" id="addrecipients" | ||||||
|                     dojoType="dijit.form.SimpleTextarea" |                     dojoType="dijit.form.SimpleTextarea" | ||||||
|  |  | ||||||
|  | @ -45,6 +45,9 @@ to assign comments to this document. | ||||||
|   >>> home = views['home'] |   >>> home = views['home'] | ||||||
|   >>> home.target = resources['d001.txt'] |   >>> home.target = resources['d001.txt'] | ||||||
| 
 | 
 | ||||||
|  |   >>> from loops.organize.comment.base import commentStates | ||||||
|  |   >>> component.provideUtility(commentStates(), name='organize.commentStates') | ||||||
|  | 
 | ||||||
| Creating comments | Creating comments | ||||||
| ----------------- | ----------------- | ||||||
| 
 | 
 | ||||||
|  | @ -75,6 +78,12 @@ Viewing comments | ||||||
|   ('My comment', u'... ...', u'john') |   ('My comment', u'... ...', u'john') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | Reporting | ||||||
|  | ========= | ||||||
|  | 
 | ||||||
|  |   >>> from loops.organize.comment.report import CommentsOverview | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| Fin de partie | Fin de partie | ||||||
| ============= | ============= | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2008 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2014 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -18,24 +18,55 @@ | ||||||
| 
 | 
 | ||||||
| """ | """ | ||||||
| Base classes for comments/discussions. | Base classes for comments/discussions. | ||||||
| 
 |  | ||||||
| $Id$ |  | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| from zope.component import adapts | from zope.component import adapts | ||||||
| from zope.interface import implements | from zope.interface import implementer, implements | ||||||
|  | from zope.traversing.api import getParent | ||||||
| 
 | 
 | ||||||
|  | from cybertools.stateful.definition import StatesDefinition | ||||||
|  | from cybertools.stateful.definition import State, Transition | ||||||
|  | from cybertools.stateful.interfaces import IStatesDefinition | ||||||
| from cybertools.tracking.btree import Track | from cybertools.tracking.btree import Track | ||||||
| from cybertools.tracking.interfaces import ITrackingStorage | from cybertools.tracking.interfaces import ITrackingStorage | ||||||
| from cybertools.tracking.comment.interfaces import IComment | from loops.organize.comment.interfaces import IComment | ||||||
|  | from loops.organize.stateful.base import Stateful | ||||||
| from loops import util | from loops import util | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Comment(Track): | @implementer(IStatesDefinition) | ||||||
|  | def commentStates(): | ||||||
|  |     return StatesDefinition('commentStates', | ||||||
|  |         State('new', 'new', ('accept', 'reject'), color='red'), | ||||||
|  |         State('public', 'public', ('retract', 'reject'), color='green'), | ||||||
|  |         State('rejected', 'rejected', ('accept',), color='grey'), | ||||||
|  |         Transition('accept', 'accept', 'public'), | ||||||
|  |         Transition('reject', 'reject', 'rejected'), | ||||||
|  |         Transition('retract', 'retract', 'new'), | ||||||
|  |         initialState='new') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Comment(Stateful, Track): | ||||||
| 
 | 
 | ||||||
|     implements(IComment) |     implements(IComment) | ||||||
| 
 | 
 | ||||||
|  |     metadata_attributes = Track.metadata_attributes + ('state',) | ||||||
|  |     index_attributes = metadata_attributes | ||||||
|     typeName = 'Comment' |     typeName = 'Comment' | ||||||
|  |     typeInterface = IComment | ||||||
|  |     statesDefinition = 'organize.commentStates' | ||||||
| 
 | 
 | ||||||
|     contentType = 'text/restructured' |     contentType = 'text/restructured' | ||||||
| 
 | 
 | ||||||
|  |     def __init__(self, taskId, runId, userName, data): | ||||||
|  |         super(Comment, self).__init__(taskId, runId, userName, data) | ||||||
|  |         self.state = self.getState()    # make initial state persistent | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def title(self): | ||||||
|  |         return self.data['subject'] | ||||||
|  | 
 | ||||||
|  |     def doTransition(self, action): | ||||||
|  |         super(Comment, self).doTransition(action) | ||||||
|  |         getParent(self).indexTrack(None, self, 'state') | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2012 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2014 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -23,15 +23,17 @@ Definition of view classes and other browser related stuff for comments. | ||||||
| from zope import interface, component | from zope import interface, component | ||||||
| from zope.app.pagetemplate import ViewPageTemplateFile | from zope.app.pagetemplate import ViewPageTemplateFile | ||||||
| from zope.cachedescriptors.property import Lazy | from zope.cachedescriptors.property import Lazy | ||||||
|  | from zope.security import checkPermission | ||||||
| 
 | 
 | ||||||
| from cybertools.browser.action import actions | from cybertools.browser.action import actions | ||||||
| from cybertools.tracking.btree import TrackingStorage | from cybertools.tracking.btree import TrackingStorage | ||||||
| from loops.browser.action import DialogAction | from loops.browser.action import Action, DialogAction | ||||||
| from loops.browser.common import BaseView | from loops.browser.common import BaseView | ||||||
| from loops.browser.form import ObjectForm, EditObject | from loops.browser.form import ObjectForm, EditObject | ||||||
| from loops.browser.node import NodeView | from loops.browser.node import NodeView | ||||||
| from loops.organize.comment.base import Comment | from loops.organize.comment.base import Comment | ||||||
| from loops.organize.party import getPersonForUser | from loops.organize.party import getPersonForUser | ||||||
|  | from loops.organize.stateful.browser import StateAction | ||||||
| from loops.organize.tracking.report import TrackDetails | from loops.organize.tracking.report import TrackDetails | ||||||
| from loops.security.common import canAccessObject | from loops.security.common import canAccessObject | ||||||
| from loops.setup import addObject | from loops.setup import addObject | ||||||
|  | @ -50,10 +52,17 @@ class CommentsView(NodeView): | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def allowed(self): |     def allowed(self): | ||||||
|         if self.isAnonymous: |         if self.virtualTargetObject is None: | ||||||
|             return False |             return False | ||||||
|         return (self.virtualTargetObject is not None and |         opts = (self.globalOptions('organize.allowComments') or | ||||||
|                     self.globalOptions('organize.allowComments')) |                 self.typeOptions('organize.allowComments')) | ||||||
|  |         if not opts: | ||||||
|  |             return False | ||||||
|  |         if opts is True: | ||||||
|  |             opts = [] | ||||||
|  |         if self.isAnonymous and not 'all' in opts: | ||||||
|  |             return False | ||||||
|  |         return True | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def addUrl(self): |     def addUrl(self): | ||||||
|  | @ -76,9 +85,47 @@ class CommentsView(NodeView): | ||||||
|             result.append(CommentDetails(self, tr)) |             result.append(CommentDetails(self, tr)) | ||||||
|         return result |         return result | ||||||
| 
 | 
 | ||||||
|  |     def getActionsFor(self, comment): | ||||||
|  |         if not self.globalOptions('organize.showCommentState'): | ||||||
|  |             return [] | ||||||
|  |         if not checkPermission('loops.ViewRestricted', self.context): | ||||||
|  |             return [] | ||||||
|  |         trackUid = util.getUidForObject(comment.track) | ||||||
|  |         url = '%s/.%s/change_state.html' % ( | ||||||
|  |                     self.page.virtualTargetUrl, trackUid) | ||||||
|  |         onClick = ("objectDialog('change_state', " | ||||||
|  |                     "'%s?dialog=change_state" | ||||||
|  |                     "&target_uid=%s'); return false;" % (url, trackUid)) | ||||||
|  |         stateAct = StateAction(self,  | ||||||
|  |                         definition='organize.commentStates',  | ||||||
|  |                         stateful=comment.track, | ||||||
|  |                         url=url, | ||||||
|  |                         onClick=onClick) | ||||||
|  |         actions = [stateAct] | ||||||
|  |         if not checkPermission('loops.EditRestricted', self.context): | ||||||
|  |             return actions | ||||||
|  |         baseUrl = self.page.virtualTargetUrl | ||||||
|  |         url = '%s/delete_object?uid=%s' % (baseUrl, trackUid) | ||||||
|  |         onClick = _("return confirm('Do you really want to delete this object?')") | ||||||
|  |         delAct = Action(self,  | ||||||
|  |                         url=url, | ||||||
|  |                         description=_('Delete Comment'), | ||||||
|  |                         icon='cybertools.icons/delete.png', | ||||||
|  |                         cssClass='icon-action', | ||||||
|  |                         onClick=onClick) | ||||||
|  |         actions.append(delAct) | ||||||
|  |         return actions | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class CommentDetails(TrackDetails): | class CommentDetails(TrackDetails): | ||||||
| 
 | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def poster(self): | ||||||
|  |         name = self.track.data.get('name') | ||||||
|  |         if name: | ||||||
|  |             return name | ||||||
|  |         return self.user['title'] | ||||||
|  | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def subject(self): |     def subject(self): | ||||||
|         return self.track.data['subject'] |         return self.track.data['subject'] | ||||||
|  | @ -108,6 +155,8 @@ class CreateComment(EditObject): | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def personId(self): |     def personId(self): | ||||||
|  |         if self.view.isAnonymous: | ||||||
|  |             return self.request.form.get('email') | ||||||
|         p = getPersonForUser(self.context, self.request) |         p = getPersonForUser(self.context, self.request) | ||||||
|         if p is not None: |         if p is not None: | ||||||
|             return util.getUidForObject(p) |             return util.getUidForObject(p) | ||||||
|  | @ -129,8 +178,11 @@ class CreateComment(EditObject): | ||||||
|         if ts is None: |         if ts is None: | ||||||
|             ts = addObject(rm, TrackingStorage, 'comments', trackFactory=Comment) |             ts = addObject(rm, TrackingStorage, 'comments', trackFactory=Comment) | ||||||
|         uid = util.getUidForObject(self.object) |         uid = util.getUidForObject(self.object) | ||||||
|         ts.saveUserTrack(uid, 0, self.personId, dict( |         data = dict(subject=subject, text=text) | ||||||
|                 subject=subject, text=text)) |         for k in ('name', 'email'): | ||||||
|  |             if k in form: | ||||||
|  |                 data[k] = form[k] | ||||||
|  |         ts.saveUserTrack(uid, 0, self.personId, data) | ||||||
|         url = self.view.virtualTargetUrl + '?version=this' |         url = self.view.virtualTargetUrl + '?version=this' | ||||||
|         self.request.response.redirect(url) |         self.request.response.redirect(url) | ||||||
|         return False |         return False | ||||||
|  |  | ||||||
|  | @ -14,10 +14,17 @@ | ||||||
|     <tal:comment tal:repeat="comment items"> |     <tal:comment tal:repeat="comment items"> | ||||||
|       <br /> |       <br /> | ||||||
|       <div class="comment"> |       <div class="comment"> | ||||||
|  |         <div class="object-actions" | ||||||
|  |              tal:define="actions python:comments.getActionsFor(comment)" | ||||||
|  |              tal:condition="actions"> | ||||||
|  |           <tal:actions repeat="action actions"> | ||||||
|  |             <metal:action use-macro="action/macro" /> | ||||||
|  |           </tal:actions> | ||||||
|  |         </div> | ||||||
|         <h3> |         <h3> | ||||||
|           <span tal:content="comment/subject">Subject</span></h3> |           <span tal:content="comment/subject">Subject</span></h3> | ||||||
|         <div class="info"> |         <div class="info"> | ||||||
|           <span tal:replace="comment/user/title">John</span>, |           <span tal:replace="comment/poster">John</span>, | ||||||
|           <span tal:replace="comment/timeStamp">2007-03-30</span> |           <span tal:replace="comment/timeStamp">2007-03-30</span> | ||||||
|         </div> |         </div> | ||||||
|         <p class="content" |         <p class="content" | ||||||
|  | @ -44,6 +51,18 @@ | ||||||
|       <input type="hidden" name="contentType" value="text/restructured" /> |       <input type="hidden" name="contentType" value="text/restructured" /> | ||||||
|       <div class="heading" i18n:translate="">Add Comment</div> |       <div class="heading" i18n:translate="">Add Comment</div> | ||||||
|       <div> |       <div> | ||||||
|  |         <tal:anonymous condition="view/isAnonymous"> | ||||||
|  |           <label i18n:translate="" | ||||||
|  |                  for="comment_name">Name</label> | ||||||
|  |           <div><input type="text" name="name" id="comment_name" | ||||||
|  |                       dojoType="dijit.form.ValidationTextBox" required="true" | ||||||
|  |                       style="width: 60em" /></div> | ||||||
|  |           <label i18n:translate="" | ||||||
|  |                  for="comment_email">Email Address</label> | ||||||
|  |           <div><input type="text" name="email" id="comment_email" | ||||||
|  |                       dojoType="dijit.form.ValidationTextBox" required="true" | ||||||
|  |                       style="width: 60em" /></div> | ||||||
|  |         </tal:anonymous> | ||||||
|         <label i18n:translate="" |         <label i18n:translate="" | ||||||
|                for="comment_subject">Subject</label> |                for="comment_subject">Subject</label> | ||||||
|         <div><input type="text" name="subject" id="comment_subject" |         <div><input type="text" name="subject" id="comment_subject" | ||||||
|  |  | ||||||
|  | @ -12,6 +12,10 @@ | ||||||
|              set_schema="cybertools.tracking.comment.interfaces.IComment" /> |              set_schema="cybertools.tracking.comment.interfaces.IComment" /> | ||||||
|   </zope:class> |   </zope:class> | ||||||
| 
 | 
 | ||||||
|  |   <zope:utility | ||||||
|  |         factory="loops.organize.comment.base.commentStates" | ||||||
|  |         name="organize.commentStates" /> | ||||||
|  | 
 | ||||||
|   <!-- views --> |   <!-- views --> | ||||||
| 
 | 
 | ||||||
|   <browser:page |   <browser:page | ||||||
|  | @ -33,4 +37,26 @@ | ||||||
|       factory="loops.organize.comment.browser.CreateComment" |       factory="loops.organize.comment.browser.CreateComment" | ||||||
|       permission="zope.View" /> |       permission="zope.View" /> | ||||||
| 
 | 
 | ||||||
|  |   <!-- reporting --> | ||||||
|  | 
 | ||||||
|  |   <zope:adapter | ||||||
|  |       name="list_comments.html" | ||||||
|  |       for="loops.interfaces.IConcept | ||||||
|  |            zope.publisher.interfaces.browser.IBrowserRequest" | ||||||
|  |       provides="zope.interface.Interface" | ||||||
|  |       factory="loops.organize.comment.report.CommentsOverview" | ||||||
|  |       permission="zope.View" /> | ||||||
|  | 
 | ||||||
|  |   <zope:adapter  | ||||||
|  |       name="comments_overview" | ||||||
|  |       factory="loops.organize.comment.report.CommentsReportInstance" | ||||||
|  |       provides="loops.expert.report.IReportInstance" | ||||||
|  |       trusted="True" /> | ||||||
|  |   <zope:class class="loops.organize.comment.report.CommentsReportInstance"> | ||||||
|  |     <require permission="zope.View" | ||||||
|  |              interface="loops.expert.report.IReportInstance" /> | ||||||
|  |     <require permission="zope.ManageContent" | ||||||
|  |              set_schema="loops.expert.report.IReportInstance" /> | ||||||
|  |   </zope:class> | ||||||
|  | 
 | ||||||
| </configure> | </configure> | ||||||
|  |  | ||||||
							
								
								
									
										26
									
								
								organize/comment/interfaces.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								organize/comment/interfaces.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | ||||||
|  | # | ||||||
|  | #  Copyright (c) 2014 Helmut Merz helmutm@cy55.de | ||||||
|  | # | ||||||
|  | #  This program is free software; you can redistribute it and/or modify | ||||||
|  | #  it under the terms of the GNU General Public License as published by | ||||||
|  | #  the Free Software Foundation; either version 2 of the License, or | ||||||
|  | #  (at your option) any later version. | ||||||
|  | # | ||||||
|  | #  This program is distributed in the hope that it will be useful, | ||||||
|  | #  but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | #  GNU General Public License for more details. | ||||||
|  | # | ||||||
|  | #  You should have received a copy of the GNU General Public License | ||||||
|  | #  along with this program; if not, write to the Free Software | ||||||
|  | #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA | ||||||
|  | # | ||||||
|  | 
 | ||||||
|  | """ | ||||||
|  | Interface definitions for comments - discussions - forums. | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | from zope.interface import Interface, Attribute | ||||||
|  | from zope import schema | ||||||
|  | 
 | ||||||
|  | from cybertools.tracking.comment.interfaces import IComment | ||||||
							
								
								
									
										6
									
								
								organize/comment/loops_comment_de.dmp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								organize/comment/loops_comment_de.dmp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | type(u'report', u'Report', options=u'', | ||||||
|  |     typeInterface='loops.expert.report.IReport', viewName=u'') | ||||||
|  | concept(u'comments_overview', u'\xdcbersicht Kommentare', u'report', | ||||||
|  |     reportType=u'comments_overview') | ||||||
|  | concept(u'comments', u'Kommentare', u'query', options=u'',  | ||||||
|  |     viewName=u'list_comments.html') | ||||||
							
								
								
									
										75
									
								
								organize/comment/report.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								organize/comment/report.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,75 @@ | ||||||
|  | # | ||||||
|  | #  Copyright (c) 2014 Helmut Merz helmutm@cy55.de | ||||||
|  | # | ||||||
|  | #  This program is free software; you can redistribute it and/or modify | ||||||
|  | #  it under the terms of the GNU General Public License as published by | ||||||
|  | #  the Free Software Foundation; either version 2 of the License, or | ||||||
|  | #  (at your option) any later version. | ||||||
|  | # | ||||||
|  | #  This program is distributed in the hope that it will be useful, | ||||||
|  | #  but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | #  GNU General Public License for more details. | ||||||
|  | # | ||||||
|  | #  You should have received a copy of the GNU General Public License | ||||||
|  | #  along with this program; if not, write to the Free Software | ||||||
|  | #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA | ||||||
|  | # | ||||||
|  | 
 | ||||||
|  | """ | ||||||
|  | Report views and definitions for comments listings and similar stuff. | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | from cybertools.util.jeep import Jeep | ||||||
|  | from loops.expert.browser.report import ReportConceptView | ||||||
|  | from loops.expert.field import Field, StateField, TargetField | ||||||
|  | from loops.expert.field import TrackDateField | ||||||
|  | from loops.expert.report import ReportInstance, TrackRow | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class CommentsOverview(ReportConceptView): | ||||||
|  | 
 | ||||||
|  |     reportName = 'comments_overview' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | timeStamp = TrackDateField('timeStamp', u'Timestamp', | ||||||
|  |                 description=u'The date and time the comment was posted.', | ||||||
|  |                 part='dateTime', descending=True, | ||||||
|  |                 executionSteps=['sort', 'output']) | ||||||
|  | target = TargetField('taskId', u'Target', | ||||||
|  |                 description=u'The resource or concept the comment was posted at.', | ||||||
|  |                 executionSteps=['output']) | ||||||
|  | name = Field('name', u'Name', | ||||||
|  |                 description=u'The name addres of the poster.', | ||||||
|  |                 executionSteps=['output']) | ||||||
|  | email = Field('email', u'E-Mail Address', | ||||||
|  |                 description=u'The email addres of the poster.', | ||||||
|  |                 executionSteps=['output']) | ||||||
|  | subject = Field('subject', u'Subject', | ||||||
|  |                 description=u'The subject of the comment.', | ||||||
|  |                 executionSteps=['output']) | ||||||
|  | state = StateField('state', u'State', | ||||||
|  |                 description=u'The state of the comment.', | ||||||
|  |                 cssClass='center', | ||||||
|  |                 statesDefinition='organize.commentStates', | ||||||
|  |                 executionSteps=['query', 'sort', 'output']) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class CommentsRow(TrackRow): | ||||||
|  | 
 | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class CommentsReportInstance(ReportInstance): | ||||||
|  | 
 | ||||||
|  |     type = "comments_overview" | ||||||
|  |     label = u'Comments Overview' | ||||||
|  | 
 | ||||||
|  |     rowFactory = CommentsRow | ||||||
|  | 
 | ||||||
|  |     fields = Jeep((timeStamp, target, name, email, subject, state)) | ||||||
|  |     defaultOutputFields = fields | ||||||
|  |     defaultSortCriteria = (state, timeStamp) | ||||||
|  | 
 | ||||||
|  |     def selectObjects(self, parts): | ||||||
|  |         return self.recordManager['comments'].values() | ||||||
							
								
								
									
										4
									
								
								organize/data/organize_work_reports_de.dmp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								organize/data/organize_work_reports_de.dmp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | concept(u'work_statement', u'Leistungsabrechnung', u'report', | ||||||
|  |     report_type=u'work_report') | ||||||
|  | concept(u'work_plan', u'Aktivitätenplanung', u'report', | ||||||
|  |     report_type=u'work_plan_report') | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| # | # | ||||||
| #  Copyright (c) 2013 Helmut Merz helmutm@cy55.de | #  Copyright (c) 2015 Helmut Merz helmutm@cy55.de | ||||||
| # | # | ||||||
| #  This program is free software; you can redistribute it and/or modify | #  This program is free software; you can redistribute it and/or modify | ||||||
| #  it under the terms of the GNU General Public License as published by | #  it under the terms of the GNU General Public License as published by | ||||||
|  | @ -79,8 +79,10 @@ class MemberRegistrationManager(object): | ||||||
|         if pfName is None: |         if pfName is None: | ||||||
|             pfName = options(self.principalfolder_key, |             pfName = options(self.principalfolder_key, | ||||||
|                              (self.default_principalfolder,))[0] |                              (self.default_principalfolder,))[0] | ||||||
|         self.createPrincipal(pfName, userId, password, lastName, firstName,  |         rc = self.createPrincipal(pfName, userId, password,  | ||||||
|                              useExisting=useExisting) |                          lastName, firstName, useExisting=useExisting) | ||||||
|  |         if rc is not None: | ||||||
|  |             return rc | ||||||
|         if not groups: |         if not groups: | ||||||
|             groups = options(self.groups_key, ()) |             groups = options(self.groups_key, ()) | ||||||
|         self.setGroupsForPrincipal(pfName, userId,  groups=groups) |         self.setGroupsForPrincipal(pfName, userId,  groups=groups) | ||||||
|  | @ -90,6 +92,8 @@ class MemberRegistrationManager(object): | ||||||
|     def createPrincipal(self, pfName, userId, password, lastName, |     def createPrincipal(self, pfName, userId, password, lastName, | ||||||
|                               firstName=u'', groups=[], useExisting=False, |                               firstName=u'', groups=[], useExisting=False, | ||||||
|                               overwrite=False, **kw): |                               overwrite=False, **kw): | ||||||
|  |         if not self.checkPrincipalId(userId): | ||||||
|  |             return dict(fieldName='loginName', error='illegal_loginname')             | ||||||
|         pFolder = getPrincipalFolder(self.context, pfName) |         pFolder = getPrincipalFolder(self.context, pfName) | ||||||
|         if IPersonBasedAuthenticator.providedBy(pFolder): |         if IPersonBasedAuthenticator.providedBy(pFolder): | ||||||
|              pFolder.setPassword(userId, password) |              pFolder.setPassword(userId, password) | ||||||
|  | @ -125,10 +129,18 @@ class MemberRegistrationManager(object): | ||||||
|             if gFolder is not None: |             if gFolder is not None: | ||||||
|                 group = gFolder.get(gName) |                 group = gFolder.get(gName) | ||||||
|                 if group is not None: |                 if group is not None: | ||||||
|                     members = list(group.principals) |                     members = [p for p in group.principals  | ||||||
|  |                                  if self.checkPrincipalId(p)] | ||||||
|                     members.append(pFolder.prefix + userId) |                     members.append(pFolder.prefix + userId) | ||||||
|                     group.principals = members |                     group.principals = members | ||||||
| 
 | 
 | ||||||
|  |     def checkPrincipalId(self, pid): | ||||||
|  |         try: | ||||||
|  |             pid = str(pid) | ||||||
|  |             return True | ||||||
|  |         except UnicodeEncodeError: | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|     def createPersonForPrincipal(self, pfName, userId, lastName, firstName=u'', |     def createPersonForPrincipal(self, pfName, userId, lastName, firstName=u'', | ||||||
|                                  useExisting=False, **kw): |                                  useExisting=False, **kw): | ||||||
|         concepts = self.context.getConceptManager() |         concepts = self.context.getConceptManager() | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ from persistent.mapping import PersistentMapping | ||||||
| from zope import interface, component | from zope import interface, component | ||||||
| from zope.app.principalannotation import annotations | from zope.app.principalannotation import annotations | ||||||
| from zope.app.security.interfaces import IAuthentication, PrincipalLookupError | from zope.app.security.interfaces import IAuthentication, PrincipalLookupError | ||||||
|  | from zope.app.security.interfaces import IUnauthenticatedPrincipal | ||||||
| from zope.component import adapts | from zope.component import adapts | ||||||
| from zope.interface import implements | from zope.interface import implements | ||||||
| from zope.cachedescriptors.property import Lazy | from zope.cachedescriptors.property import Lazy | ||||||
|  | @ -57,11 +58,13 @@ PredicateInterfaceSourceList.predicateInterfaces += (IHasRole,) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def getPersonForUser(context, request=None, principal=None): | def getPersonForUser(context, request=None, principal=None): | ||||||
|  |     if context is None: | ||||||
|  |         return None | ||||||
|     if principal is None: |     if principal is None: | ||||||
|         if request is None: |         if request is not None: | ||||||
|             principal = getCurrentPrincipal() |  | ||||||
|         else: |  | ||||||
|             principal = getattr(request, 'principal', None) |             principal = getattr(request, 'principal', None) | ||||||
|  |         else: | ||||||
|  |             principal = getPrincipal(context) | ||||||
|     if principal is None: |     if principal is None: | ||||||
|         return None |         return None | ||||||
|     loops = baseObject(context).getLoopsRoot() |     loops = baseObject(context).getLoopsRoot() | ||||||
|  | @ -76,6 +79,15 @@ def getPersonForUser(context, request=None, principal=None): | ||||||
|     return pa.get(util.getUidForObject(loops)) |     return pa.get(util.getUidForObject(loops)) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def getPrincipal(context): | ||||||
|  |     principal = getCurrentPrincipal() | ||||||
|  |     if principal is not None: | ||||||
|  |         if IUnauthenticatedPrincipal.providedBy(principal): | ||||||
|  |             return None | ||||||
|  |         return principal | ||||||
|  |     return None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class Person(AdapterBase, BasePerson): | class Person(AdapterBase, BasePerson): | ||||||
|     """ typeInterface adapter for concepts of type 'person'. |     """ typeInterface adapter for concepts of type 'person'. | ||||||
|     """ |     """ | ||||||
|  | @ -95,9 +107,11 @@ class Person(AdapterBase, BasePerson): | ||||||
|                 return |                 return | ||||||
|             person = getPersonForUser(self.context, principal=principal) |             person = getPersonForUser(self.context, principal=principal) | ||||||
|             if person is not None and person != self.context: |             if person is not None and person != self.context: | ||||||
|                 raise ValueError( |                 name = getName(person) | ||||||
|                     'Error when creating user %s: There is already a person (%s) assigned to user %s.' |                 if name: | ||||||
|                     % (getName(self.context), getName(person), userId)) |                     raise ValueError( | ||||||
|  |                         'There is already a person (%s) assigned to user %s.' | ||||||
|  |                         % (getName(person), userId)) | ||||||
|             pa = annotations(principal) |             pa = annotations(principal) | ||||||
|             loopsId = util.getUidForObject(self.context.getLoopsRoot()) |             loopsId = util.getUidForObject(self.context.getLoopsRoot()) | ||||||
|             ann = pa.get(ANNOTATION_KEY) |             ann = pa.get(ANNOTATION_KEY) | ||||||
|  |  | ||||||
|  | @ -73,7 +73,7 @@ So we are now ready to query the favorites. | ||||||
| 
 | 
 | ||||||
|   >>> favs = list(favorites.query(userName=johnCId)) |   >>> favs = list(favorites.query(userName=johnCId)) | ||||||
|   >>> favs |   >>> favs | ||||||
|   [<Favorite ['27', 1, '33', '...']: {'type': 'favorite'}>] |   [<Favorite ['27', 1, '33', '...']: {'type': 'favorite', 'order': 100}>] | ||||||
| 
 | 
 | ||||||
|   >>> list(favAdapted.list(johnC)) |   >>> list(favAdapted.list(johnC)) | ||||||
|   ['27'] |   ['27'] | ||||||
|  |  | ||||||
|  | @ -57,15 +57,22 @@ class FavoriteView(NodeView): | ||||||
|     def listFavorites(self): |     def listFavorites(self): | ||||||
|         if self.favorites is None: |         if self.favorites is None: | ||||||
|             return |             return | ||||||
|         for uid in self.favorites.list(self.person): |         self.registerDojoDnd() | ||||||
|  |         form = self.request.form | ||||||
|  |         if 'favorites_change_order' in form: | ||||||
|  |             uids = form.get('favorite_uids') | ||||||
|  |             if uids: | ||||||
|  |                 self.favorites.reorder(uids) | ||||||
|  |         for trackUid, uid in self.favorites.listWithTracks(self.person): | ||||||
|             obj = util.getObjectForUid(uid) |             obj = util.getObjectForUid(uid) | ||||||
|             if obj is not None: |             if obj is not None: | ||||||
|                 adobj = adapted(obj) |                 adobj = adapted(obj) | ||||||
|                 yield dict(url=self.getUrlForTarget(obj), |                 yield dict(url=self.getUrlForTarget(obj), | ||||||
|                            uid=uid, |                            uid=uid, | ||||||
|                            title=adobj.favTitle, |                            title=obj.title, | ||||||
|                            description=adobj.description, |                            description=obj.description, | ||||||
|                            object=obj) |                            object=obj, | ||||||
|  |                            trackUid=trackUid) | ||||||
| 
 | 
 | ||||||
|     def add(self): |     def add(self): | ||||||
|         if self.favorites is None: |         if self.favorites is None: | ||||||
|  |  | ||||||
|  | @ -1,24 +1,42 @@ | ||||||
| <metal:actions define-macro="favorites_portlet" | <metal:actions define-macro="favorites_portlet" | ||||||
|                tal:define="view nocall:context/@@favorites_view; |                tal:define="view nocall:context/@@favorites_view; | ||||||
|                            targetUid view/targetUid"> |                            targetUid view/targetUid"> | ||||||
|     <div tal:repeat="item view/listFavorites"> |   <form method="post"> | ||||||
|       <span style="float:right" class="delete-item"> <a href="removeFavorite.html" |     <div dojoType="dojo.dnd.Source" withHandles="true" id="favorites_list"> | ||||||
|           tal:attributes="href |       <div class="dojoDndItem dojoDndHandle" style="padding: 0" | ||||||
|                 string:${view/virtualTargetUrl}/removeFavorite.html?id=${item/uid}; |            tal:repeat="item view/listFavorites"> | ||||||
|                           title string:Remove from favorites" |         <span style="float:right" class="delete-item"> <a | ||||||
|           i18n:attributes="title">X</a> </span> |             tal:attributes="href | ||||||
|       <a tal:attributes="href item/url; |                   string:${view/virtualTargetUrl}/removeFavorite.html?id=${item/uid}; | ||||||
|                          title item/description" |                             title string:Remove from favorites" | ||||||
|          tal:content="item/title">Some object</a> |             i18n:attributes="title">X</a> </span> | ||||||
|  |         <a tal:attributes="href item/url; | ||||||
|  |                            title item/description" | ||||||
|  |            tal:content="item/title">Some object</a> | ||||||
|  |         <input type="hidden" name="favorite_uids:list" | ||||||
|  |                tal:attributes="value item/trackUid" /> | ||||||
|  |       </div> | ||||||
|     </div> |     </div> | ||||||
|     <div id="addFavorite" class="action" |     <div> | ||||||
|          tal:condition="targetUid"> |       <input type="submit" style="display: none" | ||||||
|       <a i18n:translate="" |           name="favorites_change_order" id="favorites_change_order" | ||||||
|          tal:attributes="href |           value="Save Changes" | ||||||
|                 string:${view/virtualTargetUrl}/addFavorite.html?id=$targetUid; |           i18n:attributes="value" /> | ||||||
|                          title string:Add current object to favorites" |       <script language="javascript"> | ||||||
|          i18n:attributes="title">Add to Favorites</a> |       dojo.subscribe('/dnd/drop', function(data) { | ||||||
|  |           if (data.node.id == 'favorites_list') { | ||||||
|  |               dojo.byId('favorites_change_order').style.display = ''}}); | ||||||
|  |       </script> | ||||||
|     </div> |     </div> | ||||||
|  |   </form> | ||||||
|  |   <div id="addFavorite" class="action" | ||||||
|  |        tal:condition="targetUid"> | ||||||
|  |     <a i18n:translate="" | ||||||
|  |        tal:attributes="href | ||||||
|  |               string:${view/virtualTargetUrl}/addFavorite.html?id=$targetUid; | ||||||
|  |                        title string:Add current object to favorites" | ||||||
|  |        i18n:attributes="title">Add to Favorites</a> | ||||||
|  |   </div> | ||||||
| </metal:actions> | </metal:actions> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -41,12 +41,16 @@ class Favorites(object): | ||||||
|         for item in self.listTracks(person, sortKey, type): |         for item in self.listTracks(person, sortKey, type): | ||||||
|             yield item.taskId |             yield item.taskId | ||||||
| 
 | 
 | ||||||
|  |     def listWithTracks(self, person, sortKey=None, type='favorite'): | ||||||
|  |         for item in self.listTracks(person, sortKey, type): | ||||||
|  |             yield util.getUidForObject(item), item.taskId | ||||||
|  | 
 | ||||||
|     def listTracks(self, person, sortKey=None, type='favorite'): |     def listTracks(self, person, sortKey=None, type='favorite'): | ||||||
|         if person is None: |         if person is None: | ||||||
|             return |             return | ||||||
|         personUid = util.getUidForObject(person) |         personUid = util.getUidForObject(person) | ||||||
|         if sortKey is None: |         if sortKey is None: | ||||||
|             sortKey = lambda x: -x.timeStamp |             sortKey = lambda x: (x.data.get('order', 100), -x.timeStamp) | ||||||
|         for item in sorted(self.context.query(userName=personUid), key=sortKey): |         for item in sorted(self.context.query(userName=personUid), key=sortKey): | ||||||
|             if type is not None: |             if type is not None: | ||||||
|                 if item.type != type: |                 if item.type != type: | ||||||
|  | @ -59,7 +63,7 @@ class Favorites(object): | ||||||
|         uid = util.getUidForObject(obj) |         uid = util.getUidForObject(obj) | ||||||
|         personUid = util.getUidForObject(person) |         personUid = util.getUidForObject(person) | ||||||
|         if data is None: |         if data is None: | ||||||
|             data = {'type': 'favorite'} |             data = {'type': 'favorite', 'order': 100} | ||||||
|         if nodups: |         if nodups: | ||||||
|             for track in self.context.query(userName=personUid, taskId=uid): |             for track in self.context.query(userName=personUid, taskId=uid): | ||||||
|                 if track.type == data['type']:    # already present |                 if track.type == data['type']:    # already present | ||||||
|  | @ -78,6 +82,18 @@ class Favorites(object): | ||||||
|                 self.context.removeTrack(track) |                 self.context.removeTrack(track) | ||||||
|         return changed |         return changed | ||||||
| 
 | 
 | ||||||
|  |     def reorder(self, uids): | ||||||
|  |         offset = 0 | ||||||
|  |         for idx, uid in enumerate(uids): | ||||||
|  |             track = util.getObjectForUid(uid) | ||||||
|  |             if track is not None: | ||||||
|  |                 data = track.data | ||||||
|  |                 order = data.get('order', 100) | ||||||
|  |                 if order < idx or (order >= 100 and order < idx + 100): | ||||||
|  |                     offset = 100 | ||||||
|  |                 data['order'] = idx + offset | ||||||
|  |                 track.data = data | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class Favorite(Track): | class Favorite(Track): | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		
		Reference in a new issue