From 9977e8b191cf235200aa89d33d13e1ef7543f0e9 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Tue, 17 Dec 2013 09:40:56 +0100 Subject: [PATCH 01/82] reduce standard width for text fields (backport from bbmaster branch) --- composer/schema/browser/schema_macros.pt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer/schema/browser/schema_macros.pt b/composer/schema/browser/schema_macros.pt index da3a145..03c712f 100755 --- a/composer/schema/browser/schema_macros.pt +++ b/composer/schema/browser/schema_macros.pt @@ -90,7 +90,7 @@ tal:define="width field/width|nothing" tal:attributes="name name; id name; style python: - ('width: %s;;' % (width and str(width)+'px' or '570px')) + + ('width: %s;;' % (width and str(width)+'px' or '555px')) + 'height: 1.5em;;'; value data/?name|string:; xxrequired field/required_js;" /> From d4d7a47b09bf7056f0d0820ae86199b79ac7fd94 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sun, 19 Jan 2014 10:47:21 +0100 Subject: [PATCH 02/82] do not ignore value if equal to default on saving, but only in check on empty row --- composer/schema/grid/field.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/composer/schema/grid/field.py b/composer/schema/grid/field.py index 32b101e..7b71014 100644 --- a/composer/schema/grid/field.py +++ b/composer/schema/grid/field.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2013 Helmut Merz helmutm@cy55.de +# Copyright (c) 2014 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -122,6 +122,8 @@ class GridFieldInstance(ListFieldInstance): def unmarshallRow(self, row, idx=None): item = {} cardinality = getattr(self.context, 'cardinality', None) + ignoreInCheckOnEmpty = list( + getattr(self.context, 'ignoreInCheckOnEmpty', [])) for fi in self.columnFieldInstances: if idx is not None: fi.index = idx @@ -133,10 +135,9 @@ class GridFieldInstance(ListFieldInstance): else: if fi.default is not None: if value == fi.default: - continue + ignoreInCheckOnEmpty.append(fi.name) if value: item[fi.name] = value - ignoreInCheckOnEmpty = getattr(self.context, 'ignoreInCheckOnEmpty', []) for k, v in item.items(): if k not in ignoreInCheckOnEmpty: #and v != '__no_change__': return item From 2bc3267e9be69ccd46458cd43e464fc7d165d54e Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Fri, 28 Feb 2014 17:09:42 +0100 Subject: [PATCH 03/82] add marker for possible future change: check for new index upon call to indeTrack() --- tracking/btree.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tracking/btree.py b/tracking/btree.py index f817535..e9ef1dc 100644 --- a/tracking/btree.py +++ b/tracking/btree.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2011 Helmut Merz helmutm@cy55.de +# Copyright (c) 2014 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,8 +18,6 @@ """ ZODB-/BTree-based implementation of user interaction tracking. - -$Id$ """ import time @@ -237,6 +235,7 @@ class TrackingStorage(BTreeContainer): self.unindexTrack(trackNum, track) def indexTrack(self, trackNum, track, idx=None): + #self.setupIndexes() if not trackNum: trackNum = int(track.__name__) data = track.indexdata From cb365aaea2d9e847c0a160efce63570f8aa2d87c Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Wed, 9 Apr 2014 13:09:15 +0200 Subject: [PATCH 04/82] always check for correct index setup upon indexing a track --- tracking/btree.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tracking/btree.py b/tracking/btree.py index e9ef1dc..bdde5d9 100644 --- a/tracking/btree.py +++ b/tracking/btree.py @@ -140,11 +140,12 @@ class TrackingStorage(BTreeContainer): def setupIndexes(self): changed = False - for idx in self.indexAttributes: + for idx in self.trackFactory.index_attributes: if idx not in self.indexes: self.indexes[idx] = FieldIndex() changed = True if changed: + self.indexAttributes = self.trackFactory.index_attributes self.reindexTracks() def idFromNum(self, num): @@ -235,7 +236,7 @@ class TrackingStorage(BTreeContainer): self.unindexTrack(trackNum, track) def indexTrack(self, trackNum, track, idx=None): - #self.setupIndexes() + self.setupIndexes() if not trackNum: trackNum = int(track.__name__) data = track.indexdata From cc696ee87d990084f673350638dc5da40bd47652 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Mon, 14 Apr 2014 19:04:09 +0200 Subject: [PATCH 05/82] handle not initialized stat, e.g. when a class with existing objects is derived from Stateful later --- stateful/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stateful/base.py b/stateful/base.py index 74272f4..49e8429 100644 --- a/stateful/base.py +++ b/stateful/base.py @@ -52,7 +52,7 @@ class Stateful(object): def getStateObject(self): states = self.getStatesDefinition().states - if self.state not in states: + if self.getState() not in states: self.state = self.getStatesDefinition().initialState return states[self.state] From d50b1ca9df1bbbdf61a5b63bd076f00c23e01ce2 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Tue, 29 Apr 2014 20:40:25 +0200 Subject: [PATCH 06/82] allow cancelling of finished work items --- organize/work.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/organize/work.py b/organize/work.py index 39b8fb7..0fe6102 100644 --- a/organize/work.py +++ b/organize/work.py @@ -59,7 +59,7 @@ def workItemStates(): 'move', 'cancel', 'modify'), color='lightgreen'), State('finished', 'finished', ('plan', 'accept', 'start', 'work', 'finish', - 'move', 'modify', 'close'), + 'move', 'modify', 'close', 'cancel'), color='green'), State('cancelled', 'cancelled', ('plan', 'accept', 'start', 'work', 'move', 'modify', 'close'), @@ -244,6 +244,9 @@ class WorkItem(Stateful, Track): elif self.state in ('planned', 'accepted', 'done'): self.state = self.state + '_x' self.reindex('state') + elif self.state in ('finished',) and action == 'cancel': + self.state = self.state + '_x' + self.reindex('state') new.doTransition(action) new.reindex() return new From cacca105ac57bf1d14beeae6151ccfd5dea61f39 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Fri, 2 May 2014 14:45:10 +0200 Subject: [PATCH 07/82] provide team evaluation of survey --- knowledge/survey/README.txt | 18 ++++++++++++++++-- knowledge/survey/questionnaire.py | 16 ++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/knowledge/survey/README.txt b/knowledge/survey/README.txt index 9315131..dd3e3b7 100644 --- a/knowledge/survey/README.txt +++ b/knowledge/survey/README.txt @@ -40,6 +40,10 @@ It's possible to leave some of the questions unanswered. >>> resp02 = Response(quest, 'john') >>> resp02.values = {qu01: 2, qu03: 4} + +Evaluation +========== + Now let's calculate the result for resp01. >>> res = resp01.getResult() @@ -55,8 +59,8 @@ Now let's calculate the result for resp01. fi03 4.0 fi01 2.4 -Grouped Feedback Items -====================== +Grouped feedback items +---------------------- >>> from cybertools.knowledge.survey.questionnaire import QuestionGroup >>> qugroup = QuestionGroup(quest) @@ -74,3 +78,13 @@ Grouped Feedback Items ... print fi.text, round(score, 2) fi03 0.75 +Team evaluation +--------------- + + >>> resp03 = Response(quest, 'mary') + >>> resp03.values = {qu01: 1, qu02: 2, qu03: 4} + + >>> res, ranks, averages = resp01.getTeamResult([resp01, resp03]) + >>> ranks, averages + ([2], [0.6666...]) + diff --git a/knowledge/survey/questionnaire.py b/knowledge/survey/questionnaire.py index b73e9e6..459bc8e 100644 --- a/knowledge/survey/questionnaire.py +++ b/knowledge/survey/questionnaire.py @@ -109,3 +109,19 @@ class Response(object): wScore = relScore * len(qugroup.feedbackItems) - 0.00001 result.append((qugroup, qugroup.feedbackItems[int(wScore)], relScore)) return result + + def getTeamResult(self, teamData): + mine = self.getGroupedResult() + all = [d.getGroupedResult() for d in teamData] + averages = [] + ranks = [] + for idx, qgdata in enumerate(mine): + total = 0.0 + pos = len(teamData) + for j, data in enumerate(all): + total += data[idx][2] + if qgdata[2] >= data[idx][2]: + pos = len(teamData) - j + ranks.append(pos) + averages.append(total / len(teamData)) + return mine, ranks, averages From 94463df4d57995a91add81f0aadd05411eb3e93b Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 3 May 2014 10:44:47 +0200 Subject: [PATCH 08/82] allow actions also in previous work items of a run --- organize/work.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/organize/work.py b/organize/work.py index 0fe6102..d380d46 100644 --- a/organize/work.py +++ b/organize/work.py @@ -225,8 +225,8 @@ class WorkItem(Stateful, Track): return list(getParent(self).query(runId=self.runId)) def doAction(self, action, userName, **kw): - if self != self.currentWorkItems[-1]: - raise ValueError("Actions are only allowed on the last item of a run.") + #if self != self.currentWorkItems[-1]: + # raise ValueError("Actions are only allowed on the last item of a run.") if action not in [t.name for t in self.getAvailableTransitions()]: raise ValueError("Action '%s' not allowed in state '%s'" % (action, self.state)) From d994b646e9e6bb9727917a986ab09e5b84f9abd9 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Thu, 8 May 2014 13:24:49 +0200 Subject: [PATCH 09/82] use precalculated scores per question group for team result --- knowledge/survey/README.txt | 10 +++++++--- knowledge/survey/questionnaire.py | 14 ++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/knowledge/survey/README.txt b/knowledge/survey/README.txt index dd3e3b7..921be1b 100644 --- a/knowledge/survey/README.txt +++ b/knowledge/survey/README.txt @@ -84,7 +84,11 @@ Team evaluation >>> resp03 = Response(quest, 'mary') >>> resp03.values = {qu01: 1, qu02: 2, qu03: 4} - >>> res, ranks, averages = resp01.getTeamResult([resp01, resp03]) - >>> ranks, averages - ([2], [0.6666...]) + >>> resp01.values[qugroup] = resp01.getGroupedResult()[0][2] + >>> resp03.values[qugroup] = resp03.getGroupedResult()[0][2] + + >>> ranks, averages = resp01.getTeamResult(resp03.getGroupedResult(), + ... [resp01, resp03]) + >>> ranks, averages + ([1], [0.6666...]) diff --git a/knowledge/survey/questionnaire.py b/knowledge/survey/questionnaire.py index 459bc8e..0a2d259 100644 --- a/knowledge/survey/questionnaire.py +++ b/knowledge/survey/questionnaire.py @@ -110,18 +110,16 @@ class Response(object): result.append((qugroup, qugroup.feedbackItems[int(wScore)], relScore)) return result - def getTeamResult(self, teamData): - mine = self.getGroupedResult() - all = [d.getGroupedResult() for d in teamData] + def getTeamResult(self, mine, teamData): averages = [] ranks = [] for idx, qgdata in enumerate(mine): total = 0.0 - pos = len(teamData) - for j, data in enumerate(all): - total += data[idx][2] - if qgdata[2] >= data[idx][2]: + values = sorted([data.values[qgdata[0]] for data in teamData]) + for j, value in enumerate(values): + total += value + if qgdata[2] >= value: pos = len(teamData) - j ranks.append(pos) averages.append(total / len(teamData)) - return mine, ranks, averages + return ranks, averages From dc03f4fd009e1d7097690e59aa2a5f19dc034898 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Mon, 12 May 2014 17:54:42 +0200 Subject: [PATCH 10/82] provide translations for address fields --- .../de/LC_MESSAGES/cybertools.organize.mo | Bin 1402 -> 1994 bytes .../de/LC_MESSAGES/cybertools.organize.po | 32 +++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/organize/locales/de/LC_MESSAGES/cybertools.organize.mo b/organize/locales/de/LC_MESSAGES/cybertools.organize.mo index 14b572521c0a5bb42d67be95e4b5511fe9813517..002cbd15ee838c0e5d50c3999db4c05d6bdaba5c 100644 GIT binary patch literal 1994 zcmaKsJ!~9B6vqcJ1dae9fPsWa56MA9c+Y1OOV;tB+!=ofpKakwiZp2M_TB9`d$Xt6 z**HE*XlM`=bVwj!QBcu91vD<{p+RX$k%lHzp#0zNUVIUV(cS(0X5PH7dCxabPkpYi z&f)tKzEck)wH+_XQ*x8IrG{cheBjExCBDv-lDv~F ziBn>fCAAcP@|N;G60XLae*?Q)FL5E#R5j(D`oqTVr*i*YY&V259;!iS`iU_J|*K6mChI8o zx(ADk)sWw=Y--!jbf1GZ>p`93BpId@@^y|AN$fvvv^H8R$0^KLYn9~$ZDHZ{V2I#7 z-|%z8SR4BWbtCOk_HSKouLeuUdJ>FG;{=QF9HLPPqU6?Hd(pkEHbQqyLRn_Fau~vpgAAQM`VUE^u zUicYr&ra?Yrb47!mcmYWKRz7i{O{8|x&G0Qu{V9D;$P9Y#uKJ?Fi|lZKhjaG?i20; D#5epB delta 542 zcmXxhu}eZh9Ki9P`qb1)dm$JU2<1>DSr#?g5)SGVfm1y~l(2@UMbKcQOG6yBG)8|x ztwBveQ-46THUteWK{WLJJ)S@A?sLC;9KXAJugTwd@58pPgqWmH(;w(F^h$t%c*1df zMqPiy5Pstr4*c&D6d55;VjMFV!&#(BoK|$!VC`l zJV6zGDI4`gmT(cP$ho{%0k5d4#WeJi`U1n4U=l0&Sqk-ZS~x-1n-4>8qX*H@-$Whn zNj104MQ|n1G=ITjs^WCI*_w0Mkg7}Tn3K?+`3Y^C18XN*%oo=3%j@?qRyq*gY&lkv9GknysVPTW|83JaVE_OC diff --git a/organize/locales/de/LC_MESSAGES/cybertools.organize.po b/organize/locales/de/LC_MESSAGES/cybertools.organize.po index 4744b23..501a5cc 100644 --- a/organize/locales/de/LC_MESSAGES/cybertools.organize.po +++ b/organize/locales/de/LC_MESSAGES/cybertools.organize.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: $Id$\n" "POT-Creation-Date: 2008-12-01 12:00 CET\n" -"PO-Revision-Date: 2013-05-17 12:00 CET\n" +"PO-Revision-Date: 2014-05-12 12:00 CET\n" "Last-Translator: Helmut Merz \n" "Language-Team: loops developers \n" "MIME-Version: 1.0\n" @@ -65,3 +65,33 @@ msgstr "Beginn" msgid "End date" msgstr "Ende" +msgid "Street, number" +msgstr "Straße, Hausnummer" + +msgid "Street and number" +msgstr "Straße und Hausnummer" + +msgid "ZIP code" +msgstr "Postleitzahl" + +msgid "ZIP code, postal code" +msgstr "Postleitzahl" + +msgid "City" +msgstr "Ort" + +msgid "Name of the city" +msgstr "Name der Stadt/des Orts" + +msgid "Country code" +msgstr "Ländercode" + +msgid "International two-letter country code" +msgstr "Aus zwei Buchstaben bestehender Ländercode" + +msgid "Additional lines" +msgstr "Zusätzliche Adresszeilen" + +msgid "Additional address lines" +msgstr "Zusätzliche Adresszeilen" + From b04daf0c51e794e23b3d91d5505e3a808935c6d3 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Thu, 22 May 2014 15:54:39 +0200 Subject: [PATCH 11/82] no automatic index set up: gives error on Zope 2 --- tracking/btree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracking/btree.py b/tracking/btree.py index bdde5d9..07191d7 100644 --- a/tracking/btree.py +++ b/tracking/btree.py @@ -236,7 +236,7 @@ class TrackingStorage(BTreeContainer): self.unindexTrack(trackNum, track) def indexTrack(self, trackNum, track, idx=None): - self.setupIndexes() + #self.setupIndexes() if not trackNum: trackNum = int(track.__name__) data = track.indexdata From 0f03b894f41099bb788a4345dd6afe9cf0f5edb1 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 24 May 2014 14:49:29 +0200 Subject: [PATCH 12/82] refactor result data structure, fix rank calculation, add team rank --- knowledge/survey/README.txt | 22 +++++++++++----------- knowledge/survey/questionnaire.py | 31 +++++++++++++++++++------------ 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/knowledge/survey/README.txt b/knowledge/survey/README.txt index 921be1b..3a95523 100644 --- a/knowledge/survey/README.txt +++ b/knowledge/survey/README.txt @@ -69,14 +69,14 @@ Grouped feedback items >>> qugroup.feedbackItems = [fi01, fi02, fi03] >>> res = resp01.getGroupedResult() - >>> for qugroup, fi, score in res: - ... print fi.text, round(score, 2) - fi02 0.58 + >>> for r in res: + ... print r['feedback'].text, round(r['score'], 2), r['rank'] + fi02 0.58 1 >>> res = resp02.getGroupedResult() - >>> for qugroup, fi, score in res: - ... print fi.text, round(score, 2) - fi03 0.75 + >>> for r in res: + ... print r['feedback'].text, round(r['score'], 2), r['rank'] + fi03 0.75 1 Team evaluation --------------- @@ -84,11 +84,11 @@ Team evaluation >>> resp03 = Response(quest, 'mary') >>> resp03.values = {qu01: 1, qu02: 2, qu03: 4} - >>> resp01.values[qugroup] = resp01.getGroupedResult()[0][2] - >>> resp03.values[qugroup] = resp03.getGroupedResult()[0][2] + >>> resp01.values[qugroup] = resp01.getGroupedResult()[0]['score'] + >>> resp03.values[qugroup] = resp03.getGroupedResult()[0]['score'] - >>> ranks, averages = resp01.getTeamResult(resp03.getGroupedResult(), + >>> teamData = resp01.getTeamResult(resp03.getGroupedResult(), ... [resp01, resp03]) - >>> ranks, averages - ([1], [0.6666...]) + >>> teamData + [{'average': 0.6666...}] diff --git a/knowledge/survey/questionnaire.py b/knowledge/survey/questionnaire.py index 0a2d259..5b698e4 100644 --- a/knowledge/survey/questionnaire.py +++ b/knowledge/survey/questionnaire.py @@ -107,19 +107,26 @@ class Response(object): if scoreMax > 0.0: relScore = score / scoreMax wScore = relScore * len(qugroup.feedbackItems) - 0.00001 - result.append((qugroup, qugroup.feedbackItems[int(wScore)], relScore)) + result.append(dict( + group=qugroup, + feedback=qugroup.feedbackItems[int(wScore)], + score=relScore)) + ranks = getRanks([r['score'] for r in result]) + for idx, r in enumerate(result): + r['rank'] = ranks[idx] return result def getTeamResult(self, mine, teamData): - averages = [] - ranks = [] + result = [] for idx, qgdata in enumerate(mine): - total = 0.0 - values = sorted([data.values[qgdata[0]] for data in teamData]) - for j, value in enumerate(values): - total += value - if qgdata[2] >= value: - pos = len(teamData) - j - ranks.append(pos) - averages.append(total / len(teamData)) - return ranks, averages + values = [data.values[qgdata['group']] for data in teamData] + avg = sum(values) / len(teamData) + result.append(dict(group=qgdata['group'], average=avg)) + ranks = getRanks([r['average'] for r in result]) + for idx, r in enumerate(result): + r['rank'] = ranks[idx] + return result + +def getRanks(values): + ordered = list(reversed(sorted(values))) + return [ordered.index(v) + 1 for v in values] From 358545833fb489ff6e1af83b00cc060c9aa5ba7f Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Thu, 29 May 2014 11:32:46 +0200 Subject: [PATCH 13/82] allow for non-numeric values which will be ignored in calculations --- knowledge/survey/questionnaire.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/knowledge/survey/questionnaire.py b/knowledge/survey/questionnaire.py index 5b698e4..a12d519 100644 --- a/knowledge/survey/questionnaire.py +++ b/knowledge/survey/questionnaire.py @@ -98,7 +98,7 @@ class Response(object): score = scoreMax = 0.0 for qu in qugroup.questions: value = self.values.get(qu) - if value is None: + if value is None or isinstance(value, basestring): continue if qu.revertAnswerOptions: value = qu.answerRange - value - 1 From b8009b51f85ccdeecfbb8f26c1538a28da2d50c7 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Thu, 29 May 2014 18:06:41 +0200 Subject: [PATCH 14/82] handle question groups without answers --- knowledge/survey/questionnaire.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/knowledge/survey/questionnaire.py b/knowledge/survey/questionnaire.py index a12d519..4581c93 100644 --- a/knowledge/survey/questionnaire.py +++ b/knowledge/survey/questionnaire.py @@ -119,8 +119,10 @@ class Response(object): def getTeamResult(self, mine, teamData): result = [] for idx, qgdata in enumerate(mine): - values = [data.values[qgdata['group']] for data in teamData] - avg = sum(values) / len(teamData) + values = [data.values.get(qgdata['group']) for data in teamData] + values = [v for v in values if v is not None] + #avg = sum(values) / len(teamData) + avg = sum(values) / len(values) result.append(dict(group=qgdata['group'], average=avg)) ranks = getRanks([r['average'] for r in result]) for idx, r in enumerate(result): From abae2c2bb3dc8b7f665e73087250876ead125661 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sun, 1 Jun 2014 11:06:46 +0200 Subject: [PATCH 15/82] put printer settings at end --- browser/blue/controller.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/browser/blue/controller.py b/browser/blue/controller.py index b678076..0df524a 100644 --- a/browser/blue/controller.py +++ b/browser/blue/controller.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2011 Helmut Merz helmutm@cy55.de +# Copyright (c) 2014 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -37,9 +37,9 @@ class Controller(BaseController): macros = self.macros presentationMode = self.request.get('liquid.viewmode') == 'presentation' params = [('blue/blue8.css', 'all', 20, False), - ('print.css', 'print', 25, False), ('blue/ie.css', 'all', 30, True), - ('custom.css', 'all', 100, False)] + ('custom.css', 'all', 100, False), + ('print.css', 'print', 200, False),] #if presentationMode: # params.append(('presentation.css', 'all', 30, False)) for id, media, prio, ie in params: From b1c8465219aab443b4eee7247f18faaf47d98820 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sun, 29 Jun 2014 10:38:58 +0200 Subject: [PATCH 16/82] avoid ForbiddenAttribute error on update via management interface --- tracking/interfaces.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tracking/interfaces.py b/tracking/interfaces.py index ab08571..8e37e14 100644 --- a/tracking/interfaces.py +++ b/tracking/interfaces.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 Helmut Merz helmutm@cy55.de +# Copyright (c) 2014 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,8 +18,6 @@ """ Interface definitions for tracking of user interactions. - -$Id$ """ from zope.interface import Interface, Attribute @@ -50,6 +48,12 @@ class ITrack(Interface): # """ Return the internal name (ID) of the track. # """ + def update(newData, overwrite=False): + """ Update the track with new data, by default creating a new + track. Overwrite the existing track if the corresponding + flag is set. + """ + class ITrackingStorage(Interface): """ A utility for storing user tracks. From 166c74ed319f03231fe1fe435dab24ddc83df56e Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Tue, 22 Jul 2014 14:30:01 +0200 Subject: [PATCH 17/82] hide time field for birthday --- organize/interfaces.py | 1 + 1 file changed, 1 insertion(+) diff --git a/organize/interfaces.py b/organize/interfaces.py index 27bccf5..5bc0574 100644 --- a/organize/interfaces.py +++ b/organize/interfaces.py @@ -85,6 +85,7 @@ class IPerson(Interface): description=_(u'The date of birth - should be a ' 'datetime.date object.'), required=False,) + birthDate.hideTime = True age = schema.Int( title=_(u'Age'), From 249d507a723782276428a8ed8c894b08fefc568e Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Tue, 22 Jul 2014 14:30:44 +0200 Subject: [PATCH 18/82] improve check-up work item type --- organize/work.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/organize/work.py b/organize/work.py index d380d46..eb49ca9 100644 --- a/organize/work.py +++ b/organize/work.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2013 Helmut Merz helmutm@cy55.de +# Copyright (c) 2014 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -156,9 +156,10 @@ workItemTypes = Jeep(( fields =('deadline',), indicator='work_deadline'), WorkItemType('checkup', u'Check-up', - actions=('plan', 'accept', 'finish', 'cancel', + actions=('plan', 'accept', 'start', 'finish', 'cancel', 'modify', 'delegate', 'close', 'reopen'), - fields =('deadline', 'start-end',), + #fields =('deadline', 'start-end',), + fields =('deadline', 'daterange',), indicator='work_checkup', delegatedState='closed', prefillDate=False), )) @@ -331,7 +332,7 @@ class WorkItem(Stateful, Track): self.reindex('userName') start = kw.get('start') or kw.get('deadline') # TODO: check OK? if start is not None: - self.timeStamp = start + self.timeStamp = start # TODO: better use end self.reindex('timeStamp') data = self.data for k, v in kw.items(): From 6e1333076b69a0fd8ea476924b4476dabd4a7413 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 20 Sep 2014 11:16:54 +0200 Subject: [PATCH 19/82] allow definition of field groups that may be used for controlling layout of records fields --- composer/schema/field.py | 9 ++++++++- composer/schema/grid/field.py | 25 ++++++++++++++++++++++++- composer/schema/schema.py | 4 +--- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/composer/schema/field.py b/composer/schema/field.py index 0eb88f2..3e8d63b 100644 --- a/composer/schema/field.py +++ b/composer/schema/field.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2013 Helmut Merz helmutm@cy55.de +# Copyright (c) 2014 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -41,6 +41,13 @@ from cybertools.composer.schema.schema import formErrors from cybertools.util.format import toStr, toUnicode +class FieldGroup(object): + + def __init__(self, name, label): + self.name = name + self.label = label + + class Field(Component): implements(IField) diff --git a/composer/schema/grid/field.py b/composer/schema/grid/field.py index 7b71014..a02f651 100644 --- a/composer/schema/grid/field.py +++ b/composer/schema/grid/field.py @@ -42,7 +42,30 @@ class GridFieldInstance(ListFieldInstance): @Lazy def columnTypes(self): - return [createField(t) for t in self.context.column_types] + fields = [createField(t) for t in self.context.column_types] + for f in fields: + f.linkedFields = [createField(sf) + for sf in getattr(f.baseField, 'linkedFields', [])] + return fields + + #@Lazy + def columnTypesForLayout(self): + result = [] + groups = {} + for f in self.columnTypes: + group = getattr(f.baseField, 'group', None) + if group is None: + result.append(dict(name=f.name, + label=(f.description or f.title), fields=[f])) + else: + g = groups.get(group.name) + if g is None: + g = dict(name=group.name, label=group.label, fields=[f]) + groups[group.name] = g + result.append(g) + else: + g['fields'].append(f) + return result @Lazy def columnFieldInstances(self): diff --git a/composer/schema/schema.py b/composer/schema/schema.py index 0834481..fb5ae88 100644 --- a/composer/schema/schema.py +++ b/composer/schema/schema.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2010 Helmut Merz helmutm@cy55.de +# Copyright (c) 2014 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,8 +19,6 @@ """ Basic classes for schemas, i.e. sets of fields that may be used for creating editing forms or display views for objects. - -$Id$ """ from zope.interface import implements From 72528a62fcac73457a55f54d189264eb077d87d3 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Wed, 8 Oct 2014 20:50:56 +0200 Subject: [PATCH 20/82] provide index-based access to fields in field group --- composer/schema/grid/field.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/composer/schema/grid/field.py b/composer/schema/grid/field.py index a02f651..1c4d272 100644 --- a/composer/schema/grid/field.py +++ b/composer/schema/grid/field.py @@ -52,19 +52,22 @@ class GridFieldInstance(ListFieldInstance): def columnTypesForLayout(self): result = [] groups = {} - for f in self.columnTypes: + for idx, f in enumerate(self.columnTypes): group = getattr(f.baseField, 'group', None) if group is None: - result.append(dict(name=f.name, - label=(f.description or f.title), fields=[f])) + result.append(dict(name=f.name, + label=(f.description or f.title), + fields=[f], indexes=[idx])) else: g = groups.get(group.name) if g is None: - g = dict(name=group.name, label=group.label, fields=[f]) + g = dict(name=group.name, label=group.label, + fields=[f], indexes=[idx]) groups[group.name] = g result.append(g) else: g['fields'].append(f) + g['indexes'].append(idx) return result @Lazy From b7efdf6a33bd053423822766b44125e21bc870b1 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Fri, 10 Oct 2014 08:55:45 +0200 Subject: [PATCH 21/82] provide sublabels for multi-field groups in records cells --- composer/schema/field.py | 3 ++- composer/schema/grid/field.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/composer/schema/field.py b/composer/schema/field.py index 3e8d63b..e4ae831 100644 --- a/composer/schema/field.py +++ b/composer/schema/field.py @@ -43,9 +43,10 @@ from cybertools.util.format import toStr, toUnicode class FieldGroup(object): - def __init__(self, name, label): + def __init__(self, name, label, sublabels=[]): self.name = name self.label = label + self.sublabels = sublabels class Field(Component): diff --git a/composer/schema/grid/field.py b/composer/schema/grid/field.py index 1c4d272..c03ac55 100644 --- a/composer/schema/grid/field.py +++ b/composer/schema/grid/field.py @@ -57,12 +57,12 @@ class GridFieldInstance(ListFieldInstance): if group is None: result.append(dict(name=f.name, label=(f.description or f.title), - fields=[f], indexes=[idx])) + fields=[f], indexes=[idx], group=None)) else: g = groups.get(group.name) if g is None: g = dict(name=group.name, label=group.label, - fields=[f], indexes=[idx]) + fields=[f], indexes=[idx], group=group) groups[group.name] = g result.append(g) else: From 7b3065439a37c4fd4cb360b722437219333cf8aa Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Wed, 29 Oct 2014 08:44:40 +0100 Subject: [PATCH 22/82] try to avoid any spurious Unicode items --- xedit/browser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/xedit/browser.py b/xedit/browser.py index 5ea67d4..00acae7 100644 --- a/xedit/browser.py +++ b/xedit/browser.py @@ -58,6 +58,7 @@ class ExternalEditorView(object): r.append('cookie:' + cookie) r.append('') r.append(fromUnicode(data)) + r = [str(item) for item in r] result = '\n'.join(r) self.setHeaders(len(result)) return result From 419e899aa4c6c52eefa220cefa99d0ecba51c54a Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Wed, 17 Dec 2014 11:21:43 +0100 Subject: [PATCH 23/82] avoid error when number of columns in table definition has been increased --- composer/schema/grid/field.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer/schema/grid/field.py b/composer/schema/grid/field.py index c03ac55..9d11bfa 100644 --- a/composer/schema/grid/field.py +++ b/composer/schema/grid/field.py @@ -242,7 +242,8 @@ class KeyTableFieldInstance(RecordsFieldInstance): for k, v in value.items(): row = [k] for idx, fi in enumerate(self.columnFieldInstances[1:]): - row.append(fi.display(v[idx])) + if idx < len(v): + row.append(fi.display(v[idx])) rows.append(row) return dict(headers=headers, rows=rows) From bfd628e434afa3a68a9cc186d74725c0d1485c1a Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Wed, 17 Dec 2014 11:24:07 +0100 Subject: [PATCH 24/82] avoid error when number of columns in table definition has been increased --- composer/schema/grid/field.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer/schema/grid/field.py b/composer/schema/grid/field.py index 9d11bfa..c7586aa 100644 --- a/composer/schema/grid/field.py +++ b/composer/schema/grid/field.py @@ -254,7 +254,8 @@ class KeyTableFieldInstance(RecordsFieldInstance): for k, v in value.items(): item = {self.keyName: k} for idx, name in enumerate(self.dataNames): - item[name] = v[idx] + if idx < len(v): + item[name] = v[idx] result.append(item) return result From 9c5a3ba263de5e6949dacc348f30d784d1424b8b Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Wed, 17 Dec 2014 11:26:35 +0100 Subject: [PATCH 25/82] avoid error when number of columns in table definition has been increased --- composer/schema/grid/grid_macros.pt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer/schema/grid/grid_macros.pt b/composer/schema/grid/grid_macros.pt index aaf16f7..5422b50 100755 --- a/composer/schema/grid/grid_macros.pt +++ b/composer/schema/grid/grid_macros.pt @@ -57,7 +57,7 @@ tal:attributes="class string:${column/baseField/cssClass|string:}"> From 3d8a162ae57d53d1ca4c03617d09b416ec02bd5b Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Fri, 9 Jan 2015 13:03:39 +0100 Subject: [PATCH 26/82] use simple HTML 5 doctype to make Google happy --- composer/layout/browser/standard.pt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer/layout/browser/standard.pt b/composer/layout/browser/standard.pt index dc36878..4bb846f 100644 --- a/composer/layout/browser/standard.pt +++ b/composer/layout/browser/standard.pt @@ -1,5 +1,5 @@ + tal:condition="view/update"> From 73839ea0157b0e558cbbd587c66b05110a39ae58 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Tue, 3 Mar 2015 18:16:36 +0100 Subject: [PATCH 27/82] when starting a work item stop others that are running; start default date/time is 'now' for start of work --- organize/work.py | 22 +++++++++++++++++++++- organize/work.txt | 4 ++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/organize/work.py b/organize/work.py index eb49ca9..6692db7 100644 --- a/organize/work.py +++ b/organize/work.py @@ -233,6 +233,9 @@ class WorkItem(Stateful, Track): (action, self.state)) if action in self.specialActions: return self.specialActions[action](self, userName, **kw) + return self.doStandardAction(action, userName, **kw) + + def doStandardAction(self, action, userName, **kw): if self.state == 'new': self.setData(**kw) self.doTransition(action) @@ -283,6 +286,22 @@ class WorkItem(Stateful, Track): delegated.data['target'] = new.name return new + def doStart(self, userName, **kw): + action = 'start' + # stop any running work item of user: + if self.state != 'running': + running = getParent(self).query( + party=userName, state='running') + for wi in running: + wi.doAction('work', userName, + end=(kw.get('start') or getTimeStamp())) + # standard creation of new work item: + if not kw.get('start'): + kw['start'] = getTimeStamp() + kw['end'] = None + kw['duration'] = kw['effort'] = 0 + return self.doStandardAction(action, userName, **kw) + def move(self, userName, **kw): xkw = dict(kw) for k in ('deadline', 'start', 'end'): @@ -319,7 +338,8 @@ class WorkItem(Stateful, Track): item.reindex('state') return new - specialActions = dict(modify=modify, delegate=delegate, move=move, + specialActions = dict(modify=modify, delegate=delegate, + start=doStart, move=move, close=close) def setData(self, ignoreParty=False, **kw): diff --git a/organize/work.txt b/organize/work.txt index ab1cc45..c276fc1 100644 --- a/organize/work.txt +++ b/organize/work.txt @@ -100,7 +100,7 @@ but may also be specified explicitly. >>> wi03 = wi02.doAction('start', 'jim', start=1229958000) >>> wi03 + {'duration': 0, 'start': 1229958000, 'created': ..., 'creator': 'jim'}> Stopping and finishing work --------------------------- @@ -113,7 +113,7 @@ as "running" will be replaced by a new one. >>> wi03 + {'duration': 0, 'start': 1229958000, 'created': ..., 'creator': 'jim'}> >>> wi04 From d05b9f7f70d476096f26172f6eb7efa831a10e24 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Tue, 10 Mar 2015 09:20:32 +0100 Subject: [PATCH 28/82] allow question groups without feedback element --- knowledge/survey/questionnaire.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/knowledge/survey/questionnaire.py b/knowledge/survey/questionnaire.py index 4581c93..bfaa45e 100644 --- a/knowledge/survey/questionnaire.py +++ b/knowledge/survey/questionnaire.py @@ -107,9 +107,13 @@ class Response(object): if scoreMax > 0.0: relScore = score / scoreMax wScore = relScore * len(qugroup.feedbackItems) - 0.00001 + if qugroup.feedbackItems: + feedback = qugroup.feedbackItems[int(wScore)] + else: + feedback = u'' result.append(dict( group=qugroup, - feedback=qugroup.feedbackItems[int(wScore)], + feedback=feedback, score=relScore)) ranks = getRanks([r['score'] for r in result]) for idx, r in enumerate(result): From c9490bb1e9085132a040ed1222714dbcb25a3079 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Tue, 10 Mar 2015 09:24:04 +0100 Subject: [PATCH 29/82] allow question groups without feedback element --- knowledge/survey/questionnaire.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/knowledge/survey/questionnaire.py b/knowledge/survey/questionnaire.py index bfaa45e..cd86eb2 100644 --- a/knowledge/survey/questionnaire.py +++ b/knowledge/survey/questionnaire.py @@ -110,7 +110,7 @@ class Response(object): if qugroup.feedbackItems: feedback = qugroup.feedbackItems[int(wScore)] else: - feedback = u'' + feedback = FeedbackItem() result.append(dict( group=qugroup, feedback=feedback, From ba529fad7be22436d46a6bb989c8df5dd63f6a30 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sun, 15 Mar 2015 10:22:38 +0100 Subject: [PATCH 30/82] 'move' action: correctly inactivate old 'done' or 'finished' items --- organize/work.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/organize/work.py b/organize/work.py index 6692db7..8a3c860 100644 --- a/organize/work.py +++ b/organize/work.py @@ -42,7 +42,7 @@ def workItemStates(): return StatesDefinition('workItemStates', State('new', 'new', ('plan', 'accept', 'start', 'work', 'finish', 'delegate', - 'cancel', 'reopen'), + 'cancel', 'reopen'), # 'move', # ? color='red'), State('planned', 'planned', ('plan', 'accept', 'start', 'work', 'finish', 'delegate', @@ -79,8 +79,12 @@ def workItemStates(): State('replaced', 'replaced', (), color='grey'), State('planned_x', 'planned', (), color='red'), State('accepted_x', 'accepted', (), color='yellow'), - State('done_x', 'done', (), color='lightgreen'), - State('finished_x', 'finished', (), color='green'), + State('done_x', 'done', + ('modify', 'move', 'cancel'), color='lightgreen'), + State('finished_x', 'finished', + ('modify','move', 'cancel'), color='green'), + #State('done_y', 'done', (), color='grey'), + #State('finished_y', 'finished', (), color='grey'), # transitions: Transition('plan', 'plan', 'planned'), Transition('accept', 'accept', 'accepted'), @@ -306,10 +310,14 @@ class WorkItem(Stateful, Track): xkw = dict(kw) for k in ('deadline', 'start', 'end'): xkw.pop(k, None) # do not change on source item - moved = self.createNew('move', userName, **xkw) - moved.userName = self.userName - moved.state = 'moved' - moved.reindex() + if self.state == 'new': # should this be possible? + moved = self + self.setData(kw) + if self.state in ('done', 'finished', 'running'): + moved = self # is this OK? or better new state ..._y? + else: + moved = self.createNew('move', userName, **xkw) + moved.userName = self.userName task = kw.pop('task', None) new = moved.createNew(None, userName, taskId=task, runId=0, **kw) new.userName = self.userName @@ -317,10 +325,15 @@ class WorkItem(Stateful, Track): new.state = self.state new.reindex() moved.data['target'] = new.name - if self.state in ('planned', 'accepted', 'delegated', 'moved', - 'done', 'finished'): + moved.state = 'moved' + moved.reindex() + if self.state in ('planned', 'accepted', 'delegated', 'moved'): + #'done', 'finished'): self.state = self.state + '_x' self.reindex('state') + #elif self.state in ('done', 'finished'): + # self.state = self.state + '_y' + # self.reindex('state') return new def close(self, userName, **kw): From 0aeb854014887c444222c6c2eec70237d6b1cc5a Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Mon, 16 Mar 2015 11:15:41 +0100 Subject: [PATCH 31/82] new fields: priority, activity --- organize/work.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/organize/work.py b/organize/work.py index 8a3c860..0d97aad 100644 --- a/organize/work.py +++ b/organize/work.py @@ -100,7 +100,8 @@ def workItemStates(): initialState='new') -fieldNames = ['title', 'description', 'deadline', 'start', 'end', +fieldNames = ['title', 'description', 'deadline', 'priority', 'activity', + 'start', 'end', 'duration', 'effort', 'comment', 'party'] # for use in editingRules @@ -142,7 +143,8 @@ class WorkItemType(object): self.title = title self.description = description self.actions = actions or list(editingRules) - self.fields = fields or ('deadline', 'start-end', 'duration-effort') + self.fields = fields or ('deadline', 'priority', 'activity', + 'start-end', 'duration-effort') self.indicator = indicator self.delegatedState = delegatedState self.prefillDate = prefillDate @@ -182,7 +184,7 @@ class WorkItem(Stateful, Track): statesDefinition = 'organize.workItemStates' initAttributes = set(['workItemType', 'party', 'title', 'description', - 'deadline', 'start', 'end', + 'deadline', 'priority', 'activity', 'start', 'end', 'duration', 'effort']) def __init__(self, taskId, runId, userName, data): From 3d3013b76c46e38537130915830ce72e3346e50b Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 21 Mar 2015 17:10:10 +0100 Subject: [PATCH 32/82] fix running work items: stop only if of type 'work' --- organize/work.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/organize/work.py b/organize/work.py index 0d97aad..b5afeac 100644 --- a/organize/work.py +++ b/organize/work.py @@ -295,12 +295,16 @@ class WorkItem(Stateful, Track): def doStart(self, userName, **kw): action = 'start' # stop any running work item of user: - if self.state != 'running': + # TODO: check: party query OK? + if (userName == self.userName and + self.workItemType == 'work' and + self.state != 'running'): running = getParent(self).query( party=userName, state='running') for wi in running: - wi.doAction('work', userName, - end=(kw.get('start') or getTimeStamp())) + if wi.workItemType == 'work': + wi.doAction('work', userName, + end=(kw.get('start') or getTimeStamp())) # standard creation of new work item: if not kw.get('start'): kw['start'] = getTimeStamp() From 13da77de0a62552a84ea83d7b6b71f2378fb2f90 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 21 Mar 2015 17:10:40 +0100 Subject: [PATCH 33/82] provide 'descending' flag for sorting --- composer/report/base.py | 1 + composer/report/result.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/composer/report/base.py b/composer/report/base.py index e591be2..ede04ea 100644 --- a/composer/report/base.py +++ b/composer/report/base.py @@ -83,6 +83,7 @@ class Report(Template): queryCriteria = None outputFields = () sortCriteria = () + sortDescending = False limits = None diff --git a/composer/report/result.py b/composer/report/result.py index f772532..bf259d4 100644 --- a/composer/report/result.py +++ b/composer/report/result.py @@ -81,12 +81,14 @@ class GroupHeaderRow(BaseRow): class ResultSet(object): def __init__(self, context, data, rowFactory=Row, - sortCriteria=None, queryCriteria=BaseQueryCriteria(), + sortCriteria=None, sortDescending=False, + queryCriteria=BaseQueryCriteria(), limits=None): self.context = context # the report or report instance self.data = data self.rowFactory = rowFactory self.sortCriteria = sortCriteria + self.sortDescending = sortDescending self.queryCriteria = queryCriteria self.limits = limits self.totals = BaseRow(None, self) @@ -95,7 +97,9 @@ class ResultSet(object): result = [self.rowFactory(item, self) for item in self.data] result = [row for row in result if self.queryCriteria.check(row)] if self.sortCriteria: - result.sort(key=lambda x: [f.getSortValue(x) for f in self.sortCriteria]) + result.sort(key=lambda x: + [f.getSortValue(x) for f in self.sortCriteria], + reverse=self.sortDescending) if self.limits: start, stop = self.limits result = result[start:stop] From 08880e3b91c0d9cbc3b71301a94d695df500c3ce Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Tue, 14 Apr 2015 16:37:29 +0200 Subject: [PATCH 34/82] put file name in title if title field is empty --- composer/schema/browser/schema_macros.pt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/composer/schema/browser/schema_macros.pt b/composer/schema/browser/schema_macros.pt index 03c712f..62756ee 100755 --- a/composer/schema/browser/schema_macros.pt +++ b/composer/schema/browser/schema_macros.pt @@ -144,7 +144,11 @@ + tal:attributes="name name;" + onchange="if (this.form.title.value == '') { + var value = this.value.split('\\'); + this.form.title.value = value[value.length-1]; + }" /> From 2c5274e54b6c7a2f03f57ce5c173053be8e43fe6 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Fri, 17 Apr 2015 09:14:19 +0200 Subject: [PATCH 35/82] work in progress: handling of question types --- knowledge/survey/questionnaire.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/knowledge/survey/questionnaire.py b/knowledge/survey/questionnaire.py index cd86eb2..5f8e994 100644 --- a/knowledge/survey/questionnaire.py +++ b/knowledge/survey/questionnaire.py @@ -58,6 +58,7 @@ class Question(object): self.feedbackItems = {} self.text = text self.revertAnswerOptions = False + self.questionType = 'value_selection' def getAnswerRange(self): return self._answerRange or self.questionnaire.defaultAnswerRange @@ -86,6 +87,8 @@ class Response(object): def getResult(self): result = {} for question, value in self.values.items(): + if question.questionType != 'value_selection': + continue for fi, rf in question.feedbackItems.items(): if question.revertAnswerOptions: value = question.answerRange - value - 1 @@ -97,6 +100,8 @@ class Response(object): for qugroup in self.questionnaire.questionGroups: score = scoreMax = 0.0 for qu in qugroup.questions: + if qu.questionType != 'value_selection': + continue value = self.values.get(qu) if value is None or isinstance(value, basestring): continue From abdf27372b9f58e0d4590e44a1a0e0e1ca6f768d Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sun, 19 Apr 2015 15:16:03 +0200 Subject: [PATCH 36/82] correctly handle legacy case: question type not set --- knowledge/survey/questionnaire.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/knowledge/survey/questionnaire.py b/knowledge/survey/questionnaire.py index 5f8e994..b4f2390 100644 --- a/knowledge/survey/questionnaire.py +++ b/knowledge/survey/questionnaire.py @@ -100,7 +100,7 @@ class Response(object): for qugroup in self.questionnaire.questionGroups: score = scoreMax = 0.0 for qu in qugroup.questions: - if qu.questionType != 'value_selection': + if qu.questionType not in (None, 'value_selection'): continue value = self.values.get(qu) if value is None or isinstance(value, basestring): From aa8814b29b455c35228f1fe9ba94f31e1f0f66b9 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Tue, 21 Apr 2015 10:04:52 +0200 Subject: [PATCH 37/82] give groups as parameters to team calculation to make evaluation independent of user data --- knowledge/survey/README.txt | 3 +-- knowledge/survey/questionnaire.py | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/knowledge/survey/README.txt b/knowledge/survey/README.txt index 3a95523..44da8f7 100644 --- a/knowledge/survey/README.txt +++ b/knowledge/survey/README.txt @@ -87,8 +87,7 @@ Team evaluation >>> resp01.values[qugroup] = resp01.getGroupedResult()[0]['score'] >>> resp03.values[qugroup] = resp03.getGroupedResult()[0]['score'] - >>> teamData = resp01.getTeamResult(resp03.getGroupedResult(), - ... [resp01, resp03]) + >>> teamData = resp01.getTeamResult([qugroup], [resp01, resp03]) >>> teamData [{'average': 0.6666...}] diff --git a/knowledge/survey/questionnaire.py b/knowledge/survey/questionnaire.py index b4f2390..3051efc 100644 --- a/knowledge/survey/questionnaire.py +++ b/knowledge/survey/questionnaire.py @@ -125,14 +125,14 @@ class Response(object): r['rank'] = ranks[idx] return result - def getTeamResult(self, mine, teamData): + def getTeamResult(self, groups, teamData): result = [] - for idx, qgdata in enumerate(mine): - values = [data.values.get(qgdata['group']) for data in teamData] + for idx, group in enumerate(groups): + values = [data.values.get(group) for data in teamData] values = [v for v in values if v is not None] #avg = sum(values) / len(teamData) avg = sum(values) / len(values) - result.append(dict(group=qgdata['group'], average=avg)) + result.append(dict(group=group, average=avg)) ranks = getRanks([r['average'] for r in result]) for idx, r in enumerate(result): r['rank'] = ranks[idx] From b5b2b0c7b43d5d89aff290a403d347bd2b460d69 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Tue, 21 Apr 2015 12:41:39 +0200 Subject: [PATCH 38/82] separate attribute for texts in response object --- knowledge/survey/interfaces.py | 3 ++- knowledge/survey/questionnaire.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/knowledge/survey/interfaces.py b/knowledge/survey/interfaces.py index 6bc8c0b..1ea9164 100644 --- a/knowledge/survey/interfaces.py +++ b/knowledge/survey/interfaces.py @@ -80,7 +80,8 @@ class IResponse(Interface): questionnaire = Attribute('The questionnaire this response belongs to.') party = Attribute('Some identification of the party that responded ' 'to this questionnaire.') - values = Attribute('A mapping associating response values with questions.') + values = Attribute('A mapping associating numeric response values with questions.') + texts = Attribute('A mapping associating text response values with questions.') def getResult(): """ Calculate the result for this response. diff --git a/knowledge/survey/questionnaire.py b/knowledge/survey/questionnaire.py index 3051efc..4f28d4d 100644 --- a/knowledge/survey/questionnaire.py +++ b/knowledge/survey/questionnaire.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2013 Helmut Merz helmutm@cy55.de +# Copyright (c) 2015 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -83,6 +83,7 @@ class Response(object): self.questionnaire = questionnaire self.party = party self.values = {} + self.texts = {} def getResult(self): result = {} From a83f80a1f9937553951b145db84f58b4fc9c0b67 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Wed, 6 May 2015 08:07:06 +0200 Subject: [PATCH 39/82] fix access from response to questionnaire to allow use of question groups in more than one questionnaire --- browser/icons/page_copy.png | Bin 0 -> 663 bytes browser/icons/page_delete.png | Bin 0 -> 740 bytes knowledge/survey/questionnaire.py | 17 ++++++++--------- 3 files changed, 8 insertions(+), 9 deletions(-) create mode 100644 browser/icons/page_copy.png create mode 100644 browser/icons/page_delete.png diff --git a/browser/icons/page_copy.png b/browser/icons/page_copy.png new file mode 100644 index 0000000000000000000000000000000000000000..195dc6d6c365d298e466026b37c1959d96119ea7 GIT binary patch literal 663 zcmV;I0%-k-P)^@R5;6Z z(>-WZK@^7J_sq=QY_e{46@P+~LNG}sRzZsxQHvCsN*h5ir6^j7pq-$xu$N#V1gx}9 zClV7;5)7zih-s3DB)G=7|99>ji@So7-P24n=VQ(@GctDX!^_@$bj%oviY6e4Dh;od zooe%Wvs8LEKQ&&bL&@bwi=STIAI@!-gB2jC5+?y?VR~VkrNxam-`6*8&po|RZ5LpS zNKdJ%c4bTX`XjKsnecf%W>1%6WT?pKNdLLq{=(f(Col?P1+oq@R>)W(n=x!|*BIIh z6DJGw_w`)u6yN|vAhMteYK5#b%r5^v+VCFl1IGssaclZZMS{vs-LJ2$)n7DAr6==K z<29#%AXsBsDoO}SBaXR#_Ap!JKx)(1)3O2pj0_dYWz5By*X74fRT01$Fk%P_RzOMDtV?GU{nsYq#K8iy zb6qzLYDj`_f5$BwC*WE(t0m#xYJ*=jC2|HQYHh=pf#QG7oowi`h!L!{DB$8|qY{~X zu8@sU1tWq;n$XThR0%;45mdqXM892|{CJ@0DS*}>?ami06Q_^tvM~Y3K(_-`#m!8f z8f!QIrH4y#61;0Ym0cCoLl8{IPombPHtnn7%SbTdI&G-d>ZQo!_wBMF9nzX!g8HVY xYTJPGciz9XMh3w2fmZ(7v{)r*QZD48?mrio{~IaoqP z|1Ep}yDQG09bP~E^Dk?@JiKQJ z6-pO(3~IOP)IYisL6D6;oAEd;E%zR}{U$rMRNuD6nQV7nesKS>)yLo7JuDCrD>Abi zbj3uW23?^GA}9jQ{M^8v?ejL?HaT7AX5WPZNkBmfN`w-jL?{tT7ykZt$%Yln?p_m~ z-?>&d(LD(jAd}h=LPltPQbO$*Wbyl@G-_k5jXbb#qffHY03>M1jfEqoPJQ6Mr=Byp=^jfzePZV1 zLjCmNi31hdIJHa%e;5g=1(`u3BRzfeExY%=VCu{loOr{`%2hUR*x>tL^W_TTaj);0 zpPR6CUD1+0>4TQ6zVfH3TQ;%l6#(_%yspK@3gcmG#Q4!WCPyLU93nMKk7E2pcA=l45({2jNho>sdF*A~bA zxX?-cp~y_z_kFf+yqu3m#QiB}03?Z&9vvR5TNgj<)($Vm)xq5G>|o2sFMag&6aNF+ WAT1?sQBYt20000 0.0: relScore = score / scoreMax wScore = relScore * len(qugroup.feedbackItems) - 0.00001 From 12a5b339ad5d425438c64ad3cd5d40a7bb0eee0b Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Tue, 19 May 2015 07:10:10 +0200 Subject: [PATCH 40/82] fixes for 'start' and 'move' transitions --- organize/work.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/organize/work.py b/organize/work.py index b5afeac..63f8979 100644 --- a/organize/work.py +++ b/organize/work.py @@ -41,7 +41,7 @@ _not_found = object() def workItemStates(): return StatesDefinition('workItemStates', State('new', 'new', - ('plan', 'accept', 'start', 'work', 'finish', 'delegate', + ('plan', 'accept', 'start', 'work', 'finish', 'delegate', 'cancel', 'reopen'), # 'move', # ? color='red'), State('planned', 'planned', @@ -297,12 +297,12 @@ class WorkItem(Stateful, Track): # stop any running work item of user: # TODO: check: party query OK? if (userName == self.userName and - self.workItemType == 'work' and + self.workItemType in (None, 'work') and self.state != 'running'): running = getParent(self).query( party=userName, state='running') for wi in running: - if wi.workItemType == 'work': + if wi.workItemType in 'work': wi.doAction('work', userName, end=(kw.get('start') or getTimeStamp())) # standard creation of new work item: @@ -328,7 +328,10 @@ class WorkItem(Stateful, Track): new = moved.createNew(None, userName, taskId=task, runId=0, **kw) new.userName = self.userName new.data['source'] = moved.name - new.state = self.state + if self.state == 'new': + new.state = 'planned' + else: + new.state = self.state new.reindex() moved.data['target'] = new.name moved.state = 'moved' From df415160155cbbcd5ec7483984372851dd04847f Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Fri, 12 Jun 2015 07:30:35 +0200 Subject: [PATCH 41/82] minor fixes --- setup.py | 2 +- util/html.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 5037004..2d59f2d 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ setup(name='cybertools', install_requires=[ # -*- Extra requirements: -*- 'lxml', - 'PIL', + #'PIL', 'zope.app.catalog', 'zope.app.file', 'zope.app.intid', diff --git a/util/html.py b/util/html.py index a8d9e4e..485e2b1 100644 --- a/util/html.py +++ b/util/html.py @@ -70,11 +70,11 @@ def sanitizeStyle(value, validStyles=validStyles): parts = item.split(':') if len(parts) == 2: k, v = parts - if checkStyle(k): + if checkStyle(k, validStyles): result.append(item.strip()) return '; '.join(result) -def checkStyle(k): +def checkStyle(k, validStyles=validStyles): k = k.strip().lower() if k in validStyles: return True From e32c0411d42c7debef04e9bbb72c4ba7b30d7da4 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 13 Jun 2015 11:32:04 +0200 Subject: [PATCH 42/82] change PIL import to work with Pillow --- media/piltransform.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/media/piltransform.py b/media/piltransform.py index a040103..af4ce5d 100644 --- a/media/piltransform.py +++ b/media/piltransform.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 Helmut Merz helmutm@cy55.de +# Copyright (c) 2015 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -26,7 +26,7 @@ $Id$ from logging import getLogger try: - import Image + from PIL import Image except ImportError: getLogger('Asset Manager').warn('Python Imaging Library could not be found.') From 620bf6fe25ee08e3d0a78e710529c9366c14b45c Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Fri, 19 Jun 2015 12:05:14 +0200 Subject: [PATCH 43/82] handle product options correctly --- commerce/interfaces.py | 5 +++-- commerce/order.py | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/commerce/interfaces.py b/commerce/interfaces.py index 8ced6c9..802b071 100644 --- a/commerce/interfaces.py +++ b/commerce/interfaces.py @@ -1,6 +1,6 @@ #-*- coding: UTF-8 -*- # -# Copyright (c) 2012 Helmut Merz helmutm@cy55.de +# Copyright (c) 2015 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -408,12 +408,13 @@ class IOrderItem(ITrack): shop = Attribute(u'The shop from which the product is ordered.') order = Attribute(u'The order this order item belongs to.') unitPrice = Attribute(u'The basic unit price for one of the product ' - u'items ordered.') + u'ites ordered.') fullPrice = Attribute(u'The full price for the quantity ordered.') quantityShipped = Attribute(u'The total quantity that has been shipped ' u'already.') shippingInfo = Attribute(u'A list of mappings, with fields like: ' u'shippingId, shippingDate, quantity, packageId') + options = Attribute(u'Product options associated with this order item.') def remove(): """ Remove the order item from the order or cart. diff --git a/commerce/order.py b/commerce/order.py index 66732e7..1c66618 100644 --- a/commerce/order.py +++ b/commerce/order.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2009 Helmut Merz helmutm@cy55.de +# Copyright (c) 2015 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,8 +18,6 @@ """ Order and order item classes. - -$Id$ """ from zope.app.intid.interfaces import IIntIds @@ -118,7 +116,10 @@ class OrderItems(object): def add(self, product, party, shop, order='???', run=0, **kw): kw['shop'] = self.getUid(shop) + options = kw.get('options', []) existing = self.getCart(party, order, shop, run, product=product) + existing = [item for item in existing + if (item.data.get('options') or []) == options] if existing: track = existing[-1] track.modify(track.quantity + kw.get('quantity', 1)) From 81d17e8966cab33d0e3eadebf54676633ce3c409 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 20 Jun 2015 09:29:42 +0200 Subject: [PATCH 44/82] better backword compatibility: ignore product options if not present --- commerce/order.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/commerce/order.py b/commerce/order.py index 1c66618..5e8b147 100644 --- a/commerce/order.py +++ b/commerce/order.py @@ -116,10 +116,11 @@ class OrderItems(object): def add(self, product, party, shop, order='???', run=0, **kw): kw['shop'] = self.getUid(shop) - options = kw.get('options', []) existing = self.getCart(party, order, shop, run, product=product) - existing = [item for item in existing - if (item.data.get('options') or []) == options] + options = kw.get('options') + if options is not None: + existing = [item for item in existing + if (item.data.get('options') or []) == options] if existing: track = existing[-1] track.modify(track.quantity + kw.get('quantity', 1)) From c5087b764bea48e0b59d098704d4077340e7d132 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Thu, 16 Jul 2015 15:55:07 +0200 Subject: [PATCH 45/82] add some (still commented-out) code as marker for future extension, e.g. for allowing delegation of running work items --- organize/work.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/organize/work.py b/organize/work.py index 63f8979..921ee7f 100644 --- a/organize/work.py +++ b/organize/work.py @@ -52,7 +52,7 @@ def workItemStates(): 'move', 'cancel', 'modify'), color='yellow'), State('running', 'running', - ('work', 'finish', 'move', 'cancel', 'modify'), + ('work', 'finish', 'move', 'cancel', 'modify'), # 'delegate', # ? color='orange'), State('done', 'done', ('plan', 'accept', 'start', 'work', 'finish', 'delegate', @@ -279,6 +279,9 @@ class WorkItem(Stateful, Track): if self.state in ('planned', 'accepted', 'delegated', 'moved', 'done'): self.state = self.state + '_x' self.reindex('state') + #elif self.state == 'running': + # self.doAction('work', userName, + # end=(kw.get('end') or getTimeStamp())) xkw = dict(kw) xkw.pop('party', None) delegated = self.createNew('delegate', userName, **xkw) From 6a356c6f9ca7a3b2141b2a2b341f1c8523a0671c Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Thu, 16 Jul 2015 20:00:09 +0200 Subject: [PATCH 46/82] allow selection of question groups via party (i.e. person) --- knowledge/survey/questionnaire.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/knowledge/survey/questionnaire.py b/knowledge/survey/questionnaire.py index 45efa0f..bfb78e2 100644 --- a/knowledge/survey/questionnaire.py +++ b/knowledge/survey/questionnaire.py @@ -36,6 +36,9 @@ class Questionnaire(object): self.responses = [] self.defaultAnswerRange = 5 + def getQuestionGroups(self, party): + return self.questionGroups + class QuestionGroup(object): @@ -95,7 +98,7 @@ class Response(object): def getGroupedResult(self): result = [] - for qugroup in self.questionnaire.questionGroups: + for qugroup in self.questionnaire.getQuestionGroups(self.party): score = scoreMax = 0.0 for qu in qugroup.questions: if qu.questionType not in (None, 'value_selection'): From 6b22eb85dd499818aecb51f1e9d131f52c33405c Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Wed, 29 Jul 2015 09:25:20 +0200 Subject: [PATCH 47/82] add zope.app.dublincore (deprecated) dependency to support old persistent data --- .gitignore | 2 ++ setup.py | 1 + 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 898d2c7..7ac9b1c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ *.pyc +*.pyo ajax/dojo/* +*.egg-info *.project *.pydevproject *.sublime-project diff --git a/setup.py b/setup.py index 2d59f2d..28c1539 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ setup(name='cybertools', 'lxml', #'PIL', 'zope.app.catalog', + 'zope.app.dublincore', 'zope.app.file', 'zope.app.intid', 'zope.app.preview', From bf290aea2f5e8655f0f4af3c64ad3816a8b9d85b Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Thu, 13 Aug 2015 16:07:14 +0200 Subject: [PATCH 48/82] avoid errors because of missing files during import --- media/piltransform.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/media/piltransform.py b/media/piltransform.py index a040103..df43893 100644 --- a/media/piltransform.py +++ b/media/piltransform.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 Helmut Merz helmutm@cy55.de +# Copyright (c) 2015 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,8 +20,6 @@ Views for displaying media assets. Authors: Johann Schimpf, Erich Seifert. - -$Id$ """ from logging import getLogger @@ -35,6 +33,8 @@ from zope.interface import implements from cybertools.media.interfaces import IMediaAsset, IFileTransform from cybertools.storage.filesystem import FileSystemStorage +logger = getLogger('cybertools.media.piltransform.PILTransform') + def mimetypeToPIL(mimetype): return mimetype.split("/",1)[-1] @@ -51,8 +51,7 @@ class PILTransform(object): try: self.im = Image.open(path) except IOError, e: - from logging import getLogger - getLogger('cybertools.media.piltransform.PILTransform').warn(e) + logger.warn(e) self.im = None def rotate(self, angle, resize): @@ -101,10 +100,17 @@ class PILTransform(object): ratio = float(ow) / float(oh) height = int(round(float(width) / ratio)) dims = (width, height) - self.im.thumbnail(dims, Image.ANTIALIAS) + try: + self.im.thumbnail(dims, Image.ANTIALIAS) + except IOError, e: + logger.warn(e) + def save(self, path, mimetype): if self.im is None: return format = mimetypeToPIL(mimetype) - self.im.save(path) + try: + self.im.save(path) + except IOError, e: + logger.warn(e) From c15039b9a7b2ed8bab50afc952a61b33d39a54a8 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Thu, 13 Aug 2015 16:09:33 +0200 Subject: [PATCH 49/82] avoid zero division error when data are missing --- knowledge/survey/questionnaire.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/knowledge/survey/questionnaire.py b/knowledge/survey/questionnaire.py index bfb78e2..6fb6146 100644 --- a/knowledge/survey/questionnaire.py +++ b/knowledge/survey/questionnaire.py @@ -134,6 +134,8 @@ class Response(object): values = [data.values.get(group) for data in teamData] values = [v for v in values if v is not None] #avg = sum(values) / len(teamData) + if not values: + continue avg = sum(values) / len(values) result.append(dict(group=group, average=avg)) ranks = getRanks([r['average'] for r in result]) From e5a09ed436d311e6b4f598c133a2e1967719cef0 Mon Sep 17 00:00:00 2001 From: hplattner Date: Tue, 29 Sep 2015 11:42:42 +0200 Subject: [PATCH 50/82] add fill option to media asset resize function --- media/asset.py | 13 +++++++------ media/piltransform.py | 12 ++++++++++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/media/asset.py b/media/asset.py index ed3e3db..5af5514 100644 --- a/media/asset.py +++ b/media/asset.py @@ -29,11 +29,9 @@ from logging import getLogger import mimetypes import os, re, sys -from zope import component from zope.interface import implements -from cybertools.media.interfaces import IMediaAsset, IFileTransform +from cybertools.media.interfaces import IMediaAsset from cybertools.media.piltransform import PILTransform -from cybertools.storage.filesystem import FileSystemStorage TRANSFORM_STATEMENT = re.compile(r"\s*(\+?)([\w]+[\w\d]*)\(([^\)]*)\)\s*") @@ -41,13 +39,16 @@ DEFAULT_FORMATS = { "image": "image/jpeg" } + def parseTransformStatements(txStr): """ Parse statements in transform chain strings.""" statements = TRANSFORM_STATEMENT.findall(txStr) return statements + def getMimeBasetype(mimetype): - return mimetype.split("/",1)[0] + return mimetype.split("/", 1)[0] + def getMimetypeExt(mimetype): exts = mimetypes.guess_all_extensions(mimetype) @@ -74,9 +75,9 @@ class MediaAssetFile(object): getLogger('cybertools.media.asset.MediaAssetFile').warn( 'Media asset directory %r not found.' % path) self.transform() - #return self.getOriginalData() + # return self.getOriginalData() f = open(path, 'rb') - data =f.read() + data = f.read() f.close() return data diff --git a/media/piltransform.py b/media/piltransform.py index a040103..fa0dd40 100644 --- a/media/piltransform.py +++ b/media/piltransform.py @@ -93,7 +93,7 @@ class PILTransform(object): box = (left, upper, right, lower) self.im = self.im.crop(box) - def resize(self, width, height=None): + def resize(self, width, height=None, fill=False): if self.im is None: return if not height: @@ -101,7 +101,15 @@ class PILTransform(object): ratio = float(ow) / float(oh) height = int(round(float(width) / ratio)) dims = (width, height) - self.im.thumbnail(dims, Image.ANTIALIAS) + if fill: + image = self.im + image.thumbnail(dims, Image.ANTIALIAS) + new = Image.new('RGBA', dims, (255, 255, 255, 0)) #with alpha + new.paste(image,((dims[0] - image.size[0]) / 2, + (dims[1] - image.size[1]) / 2)) + self.im = new + return new + return self.im.thumbnail(dims, Image.ANTIALIAS) def save(self, path, mimetype): if self.im is None: From 917c2ae579ce079e2f38f98f7df4b8009b684919 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 10 Oct 2015 11:27:41 +0200 Subject: [PATCH 51/82] revert change by hplattner from 2013 that was erroneously merged --- composer/schema/instance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer/schema/instance.py b/composer/schema/instance.py index f95cc62..11d2718 100644 --- a/composer/schema/instance.py +++ b/composer/schema/instance.py @@ -53,7 +53,7 @@ class Instance(BaseInstance): fi = f.getFieldInstance(self, context=kw.get('context'), request=kw.get('request')) name = f.name - value = getattr(self.context, name, fi.default) + value = getattr(self.context, name) or fi.default if mode in ('view', 'preview'): value = fi.display(value) else: From 14cfdb7624d36b17fbb33e8423c70db6bf4a7b8c Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 9 Jan 2016 12:01:09 +0100 Subject: [PATCH 52/82] allow setting of main template via controller (which is found via skin) --- browser/controller.py | 8 +++++--- browser/main.pt | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/browser/controller.py b/browser/controller.py index ce64f81..e9d4044 100644 --- a/browser/controller.py +++ b/browser/controller.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2011 Helmut Merz helmutm@cy55.de +# Copyright (c) 2016 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,8 +18,6 @@ """ Controller for views, templates, macros. - -$Id$ """ from zope import component @@ -77,6 +75,10 @@ class Controller(object): IMemberInfoProvider) return provider is not None and provider.data or None + def setMainPage(self): + # May be overridden by subclasse for setting special main index template + pass + def getTemplateMacros(self, name, default): template = self.templates.get(name) if template is None: diff --git a/browser/main.pt b/browser/main.pt index 66b10f6..9df670c 100644 --- a/browser/main.pt +++ b/browser/main.pt @@ -1,5 +1,5 @@ Date: Tue, 26 Jan 2016 13:30:02 +0100 Subject: [PATCH 53/82] provide optional export value for special formattings --- composer/report/field.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/composer/report/field.py b/composer/report/field.py index 9bb807c..c41208b 100644 --- a/composer/report/field.py +++ b/composer/report/field.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2014 Helmut Merz helmutm@cy55.de +# Copyright (c) 2016 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -126,6 +126,9 @@ class Field(Component): def getDisplayValue(self, row): return self.getValue(row) + def getExportValue(self, row, format=None, lang=None): + return self.getValue(row) + def getSortValue(self, row): # TODO: consider 'descending' flag return self.getValue(row) From 5ab63ee78caaa401dba92535cc4ac626c10b35b1 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 20 Feb 2016 21:29:19 +0100 Subject: [PATCH 54/82] remove redundant call to setConctroller() --- browser/view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/browser/view.py b/browser/view.py index c90d94e..07f59cd 100644 --- a/browser/view.py +++ b/browser/view.py @@ -87,8 +87,8 @@ class GenericView(object): cont = viewAnnotations.get('controller', None) if cont is None: cont = component.queryMultiAdapter((self, self.request), name='controller') - if cont is not None: - self.setController(cont) + #if cont is not None: + # self.setController(cont) return cont controller = property(getController, setController) From 65181d3098e38209b257072a79be08619944fc99 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 20 Feb 2016 21:30:21 +0100 Subject: [PATCH 55/82] update editing rules (still only informative) to changed list of work item states --- organize/work.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/organize/work.py b/organize/work.py index 921ee7f..6617e06 100644 --- a/organize/work.py +++ b/organize/work.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2014 Helmut Merz helmutm@cy55.de +# Copyright (c) 2016 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -111,21 +111,21 @@ fieldNames = ['title', 'description', 'deadline', 'priority', 'activity', # . default (may be empty) editingRules = dict( - plan = {'*': '+++.....+'}, - accept = {'*': '+++.....-', - 'planned': '+++++++.-', - 'accepted': '+++++++.-'}, - start = {'*': '+++./...-'}, - work = {'*': '+++.....-', - 'running': '++++....-'}, - finish = {'*': '+++.....-', - 'running': '++++....-'}, - cancel = {'*': '+++////./'}, - modify = {'*': '+++++++++'}, - delegate= {'*': '+++++++.+'}, - move = {'*': '+++++++.-'}, - close = {'*': '+++////./'}, - reopen = {'*': '+++////./'}, + plan = {'*': '+++++.....+'}, + accept = {'*': '+++++.....-', + 'planned': '+++++++++.-', + 'accepted': '+++++++++.-'}, + start = {'*': '+++++./...-'}, + work = {'*': '+++++.....-', + 'running': '++++++....-'}, + finish = {'*': '+++++.....-', + 'running': '++++++....-'}, + cancel = {'*': '+++++////./'}, + modify = {'*': '+++++++++++'}, + delegate= {'*': '+++++++++.+'}, + move = {'*': '+++++++++.-'}, + close = {'*': '+++++////./'}, + reopen = {'*': '+++++////./'}, ) From 2a2097fa0dc2d800c33d6b884b199561f1857238 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Mon, 25 Apr 2016 12:27:03 +0200 Subject: [PATCH 56/82] tracking: allow wildcard (*) query --- tracking/btree.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tracking/btree.py b/tracking/btree.py index 1faf1cc..3325325 100644 --- a/tracking/btree.py +++ b/tracking/btree.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2014 Helmut Merz helmutm@cy55.de +# Copyright (c) 2016 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -292,7 +292,11 @@ class TrackingStorage(BTreeContainer): value = [value] resultx = None for v in value: - resultx = self.union(resultx, self.indexes[idx].apply((v, v))) + v2 = v + if isinstance(v, basestring) and v.endswith('*'): + v = v[:-1] + v2 = v + 'z' + resultx = self.union(resultx, self.indexes[idx].apply((v, v2))) result = self.intersect(result, resultx) elif idx == 'timeFrom': result = self.intersect(result, From 8166a0e3c3aaf4cd78e70f6d68611054908a2a92 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Thu, 9 Jun 2016 09:03:03 +0200 Subject: [PATCH 57/82] add green/blue LED icon --- browser/icons/ledgreenblue.png | Bin 0 -> 1105 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 browser/icons/ledgreenblue.png diff --git a/browser/icons/ledgreenblue.png b/browser/icons/ledgreenblue.png new file mode 100644 index 0000000000000000000000000000000000000000..a94c48dddbeccf430caa883b9d8575b5e1b118d5 GIT binary patch literal 1105 zcmV-X1g`suP)lT=!;Lj_%9ei zFwrQ9(U>Ue6PMUXF=7-f35fzRZJ|JEhnaRdof~sK=WHLQDH_A0u4E-U`{DccO7?HT ze_Y~UHz}oy^^pV7H3OrYyp6*`2ytybH&ZIrPB1fb3}Zt;|4RpAi7lg>ljg7xi0Mpu zX1Q2D!OWp!RHqj2uiLHe?0=^>^WjfN5Tr$jY8rIg$sY)6LcGE z>}}EUk=@UFJzgKCYN9C`fefezbt>%&OU)&+wH%_a!UrFJ&Bzmbahxcv0O~$eTd>?v z@HVekez|=2xn`yI1=XoaNAOJQO>aoQbQ<0AqWE+L4$I;+=17#%-x;ihsiT6 zHg8e8onY%jkFCOsjZTlv338FRUA`VR$euc@0k|=Jw3VpvN9%XSGikQ3eUgGzAfiVA zsQESWjU|3tJkLaR0<%|V;_3vM%0U2{Ayk`iwE$CPaQl1cPPij^cvl0kyVZ@$ULD6a zxm5aaT3XP7P{0SJoiC2rKFnXTL=;u_TSCix)8^<`Eb0J%EM)*x-cqAw^Xya50W zK-j*pBL=Q%5wmTC+2oEo$s*GL1WJH31G=P(uGW!?L{XsacLa(APlrAm34JHJfOP0#c%>IxcBUqaz3<=mF>}zY0}yu~3>{ z6)Z0{P7c)vw})rfVCy=bZR15ESh|j`sW_I!-4pA0 z`tesurTfuV;EL!>VCs_7-~7y#iF0p}JvsZ&BDCLeC?ePO?mOuDF=Kqw%PWNzj7=*W2a$FZ?Dxc>g<_g{ADo*vfr9PCTS zM-l_}AOcaCZ_LfjFOSKE%n{h;liGW5Fy)T)_IiT|A+p)h+|5jRjNIuXWKZ7a|E#|N X=aru_4N!Gm00000NkvXXu0mjfGLZov literal 0 HcmV?d00001 From 8777d1ecdb3d5ea45368eb4e0405c8a9dcd29278 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Thu, 9 Jun 2016 09:03:44 +0200 Subject: [PATCH 58/82] provide 'view/requestUrl' method to be used instead of 'request/URL' --- browser/main.pt | 2 +- browser/view.py | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/browser/main.pt b/browser/main.pt index 9df670c..cd206fe 100644 --- a/browser/main.pt +++ b/browser/main.pt @@ -34,7 +34,7 @@ - + Date: Thu, 9 Jun 2016 10:32:04 +0200 Subject: [PATCH 59/82] use new 'view/requestUrl' to avoid '/@@index.html' in URLs --- browser/blue/body.pt | 2 +- browser/liquid/body.pt | 2 +- browser/liquid/view_macros.pt | 2 +- browser/loops/loginform.pt | 2 +- browser/mojo/body.pt | 2 +- composer/layout/browser/standard.pt | 2 +- view/browser/liquid/body.pt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/browser/blue/body.pt b/browser/blue/body.pt index 1389844..f7c5b67 100644 --- a/browser/blue/body.pt +++ b/browser/blue/body.pt @@ -9,7 +9,7 @@
diff --git a/browser/liquid/body.pt b/browser/liquid/body.pt index fcfd3c1..a0761bb 100644 --- a/browser/liquid/body.pt +++ b/browser/liquid/body.pt @@ -7,7 +7,7 @@
diff --git a/browser/liquid/view_macros.pt b/browser/liquid/view_macros.pt index 9d662a5..861dae3 100644 --- a/browser/liquid/view_macros.pt +++ b/browser/liquid/view_macros.pt @@ -35,7 +35,7 @@
diff --git a/browser/loops/loginform.pt b/browser/loops/loginform.pt index 93489b8..994a231 100644 --- a/browser/loops/loginform.pt +++ b/browser/loops/loginform.pt @@ -24,7 +24,7 @@ You are not authorized to perform this action. However, you may login as a different user who is authorized.

+ tal:attributes="action view/requestUrl">
diff --git a/composer/layout/browser/standard.pt b/composer/layout/browser/standard.pt index 4bb846f..f57115c 100644 --- a/composer/layout/browser/standard.pt +++ b/composer/layout/browser/standard.pt @@ -20,7 +20,7 @@ - + diff --git a/view/browser/liquid/body.pt b/view/browser/liquid/body.pt index f983f3d..2c1c5be 100644 --- a/view/browser/liquid/body.pt +++ b/view/browser/liquid/body.pt @@ -7,7 +7,7 @@
From 34d65623adaeae462185ce9a2aa5f2ab7c8f83a0 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Thu, 9 Jun 2016 13:43:11 +0200 Subject: [PATCH 60/82] revert change for request/URL --- browser/loops/loginform.pt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser/loops/loginform.pt b/browser/loops/loginform.pt index 994a231..93489b8 100644 --- a/browser/loops/loginform.pt +++ b/browser/loops/loginform.pt @@ -24,7 +24,7 @@ You are not authorized to perform this action. However, you may login as a different user who is authorized.

+ tal:attributes="action request/URL">
- + Date: Sun, 12 Jun 2016 09:04:30 +0200 Subject: [PATCH 62/82] fix URL clipping --- browser/view.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/browser/view.py b/browser/view.py index 8f70072..a44eef6 100644 --- a/browser/view.py +++ b/browser/view.py @@ -67,7 +67,10 @@ class BodyTemplateView(object): class URLGetter(BaseURLGetter): def __str__(self): - return self.__request.getURL().rstrip('/@@index.html') + url = self.__request.getURL() + if url.endswith('/@@index.html'): + url = url[-len('/@@index.html')] + return url class GenericView(object): From 81ae451969ef60904d3c11c3832fd7840bf83e61 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Mon, 13 Jun 2016 10:01:47 +0200 Subject: [PATCH 63/82] fix stripping of '/@@index.html' --- browser/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser/view.py b/browser/view.py index a44eef6..0ec312c 100644 --- a/browser/view.py +++ b/browser/view.py @@ -69,7 +69,7 @@ class URLGetter(BaseURLGetter): def __str__(self): url = self.__request.getURL() if url.endswith('/@@index.html'): - url = url[-len('/@@index.html')] + url = url[:-len('/@@index.html')] return url From 0d1a37b5d20e572597023dfb12d4c89e8d9ee7fa Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Fri, 17 Jun 2016 09:43:29 +0200 Subject: [PATCH 64/82] provide method for tweaking request/URL, esp remove trailing '/@@index.html' --- composer/layout/browser/view.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/composer/layout/browser/view.py b/composer/layout/browser/view.py index df09459..5339c4b 100644 --- a/composer/layout/browser/view.py +++ b/composer/layout/browser/view.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2011 Helmut Merz helmutm@cy55.de +# Copyright (c) 2016 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,8 +18,6 @@ """ Basic view classes for layout-based presentation. - -$Id$ """ from zope import component @@ -27,6 +25,7 @@ from zope.interface import Interface, implements from zope.cachedescriptors.property import Lazy from zope.app.pagetemplate import ViewPageTemplateFile from zope.app.security.interfaces import IUnauthenticatedPrincipal +from zope.publisher.http import URLGetter as BaseURLGetter from cybertools.composer.layout.base import Layout from cybertools.composer.layout.interfaces import ILayoutManager @@ -54,6 +53,15 @@ class CachableRenderer(object): return rendererTemplate(self.view, view=self.view, macro=self.renderer) +class URLGetter(BaseURLGetter): + + def __str__(self): + url = self.__request.getURL() + if url.endswith('/@@index.html'): + url = url[:-len('/@@index.html')] + return url + + class BaseView(object): template = ViewPageTemplateFile('base.pt') @@ -73,6 +81,10 @@ class BaseView(object): def __call__(self): return self.template(self) + @property + def requestUrl(self): + return URLGetter(self.request) + @Lazy def authenticated(self): return not IUnauthenticatedPrincipal.providedBy(self.request.principal) From ef05da60e1be9b9b8c4f7b2de0d41e55d0934888 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Tue, 28 Jun 2016 08:14:02 +0200 Subject: [PATCH 65/82] take URLGetter implementation from common location --- composer/layout/browser/view.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/composer/layout/browser/view.py b/composer/layout/browser/view.py index 5339c4b..9f30da2 100644 --- a/composer/layout/browser/view.py +++ b/composer/layout/browser/view.py @@ -27,6 +27,7 @@ from zope.app.pagetemplate import ViewPageTemplateFile from zope.app.security.interfaces import IUnauthenticatedPrincipal from zope.publisher.http import URLGetter as BaseURLGetter +from cybertools.browser.view import URLGetter from cybertools.composer.layout.base import Layout from cybertools.composer.layout.interfaces import ILayoutManager from cybertools.composer.layout.interfaces import ILayout, ILayoutInstance @@ -53,15 +54,6 @@ class CachableRenderer(object): return rendererTemplate(self.view, view=self.view, macro=self.renderer) -class URLGetter(BaseURLGetter): - - def __str__(self): - url = self.__request.getURL() - if url.endswith('/@@index.html'): - url = url[:-len('/@@index.html')] - return url - - class BaseView(object): template = ViewPageTemplateFile('base.pt') From d742b87c9c4da9c8b006aa6f8ee46049e9cda7c9 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Thu, 25 Aug 2016 16:41:40 +0200 Subject: [PATCH 66/82] allow schema processor plugin adapter --- composer/schema/factory.py | 10 +++++++--- composer/schema/interfaces.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/composer/schema/factory.py b/composer/schema/factory.py index 4cf907b..eb08da0 100644 --- a/composer/schema/factory.py +++ b/composer/schema/factory.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2009 Helmut Merz helmutm@cy55.de +# Copyright (c) 2016 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -26,7 +26,7 @@ from zope.interface import Interface from zope import schema from cybertools.composer.schema.field import Field -from cybertools.composer.schema.interfaces import ISchemaFactory +from cybertools.composer.schema.interfaces import ISchemaFactory, ISchemaProcessor from cybertools.composer.schema.schema import Schema @@ -70,6 +70,7 @@ class SchemaFactory(object): def __init__(self, context): self.context = context + self.schemaProcessor = ISchemaProcessor(self, None) def __call__(self, interface, **kw): fieldMapping = self.fieldMapping @@ -84,7 +85,10 @@ class SchemaFactory(object): field = interface[fname] info = fieldMapping.get(field.__class__) f = createField(field, info) - fields.append(f) + if self.schemaProcessor is not None: + f = self.schemaProcessor.process(f) + if f is not None: + fields.append(f) return Schema(name=interface.__name__, *fields, **kw) diff --git a/composer/schema/interfaces.py b/composer/schema/interfaces.py index 8cda454..095626e 100644 --- a/composer/schema/interfaces.py +++ b/composer/schema/interfaces.py @@ -73,6 +73,17 @@ class ISchemaFactory(Interface): """ +class ISchemaProcessor(Interface): + """ Interface for schema processor adapters. + """ + + def process(field): + """ Process field according to specific processor data and + return modified field. Return None if field should not + be included in schema. + """ + + class FieldType(SimpleTerm): hidden = False From 1cd5f597388259193a6bcfd0e9661bc06a37a2e3 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 27 Aug 2016 09:31:56 +0200 Subject: [PATCH 67/82] call schema processor with **kw to provide view/request --- composer/schema/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer/schema/factory.py b/composer/schema/factory.py index eb08da0..98db074 100644 --- a/composer/schema/factory.py +++ b/composer/schema/factory.py @@ -86,7 +86,7 @@ class SchemaFactory(object): info = fieldMapping.get(field.__class__) f = createField(field, info) if self.schemaProcessor is not None: - f = self.schemaProcessor.process(f) + f = self.schemaProcessor.process(f, **kw) if f is not None: fields.append(f) return Schema(name=interface.__name__, *fields, **kw) From 6cff2cc6df68685446232b4b280b368021f0f256 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 8 Oct 2016 09:37:47 +0200 Subject: [PATCH 68/82] update copyright info --- composer/schema/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer/schema/interfaces.py b/composer/schema/interfaces.py index 095626e..a9648fd 100644 --- a/composer/schema/interfaces.py +++ b/composer/schema/interfaces.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2013 Helmut Merz helmutm@cy55.de +# Copyright (c) 2016 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by From 4c18a9731ccef1032449dc0de175317244ebf342 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 8 Oct 2016 11:17:39 +0200 Subject: [PATCH 69/82] add 'visible' property to field definition --- composer/schema/field.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer/schema/field.py b/composer/schema/field.py index 8222e01..287d34e 100644 --- a/composer/schema/field.py +++ b/composer/schema/field.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2014 Helmut Merz helmutm@cy55.de +# Copyright (c) 2016 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -54,6 +54,7 @@ class Field(Component): implements(IField) + visible = True required = False readonly = False nostore = False From 5aa98691815650d56b04f4de9b3e820e4504a6ba Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 8 Oct 2016 14:05:19 +0200 Subject: [PATCH 70/82] add 'visible' property to IField interface --- composer/schema/interfaces.py | 1 + 1 file changed, 1 insertion(+) diff --git a/composer/schema/interfaces.py b/composer/schema/interfaces.py index a9648fd..ebc4825 100644 --- a/composer/schema/interfaces.py +++ b/composer/schema/interfaces.py @@ -215,6 +215,7 @@ class IField(IComponent): '(only for dropdown and other selection fields)'), required=False,) + visible = Attribute('Should the field be shown in display views?') fieldRenderer = Attribute('Name of a renderer (i.e. a ZPT macro or ' 'an adapter) that is responsible for rendering ' '(presenting) the field as a whole.') From 170e8f1d4eeb7724ed760038f5c6c4aed78ead75 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Fri, 23 Dec 2016 17:14:11 +0100 Subject: [PATCH 71/82] allow hiding of a field from schema-based forms --- composer/schema/factory.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/composer/schema/factory.py b/composer/schema/factory.py index 98db074..75778f8 100644 --- a/composer/schema/factory.py +++ b/composer/schema/factory.py @@ -83,6 +83,8 @@ class SchemaFactory(object): if include and fname not in include: continue field = interface[fname] + if getattr(field, 'hidden', False): + continue info = fieldMapping.get(field.__class__) f = createField(field, info) if self.schemaProcessor is not None: From 9205e8592e0c9b118374ecaad835847923ce798b Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Mon, 26 Dec 2016 10:42:27 +0100 Subject: [PATCH 72/82] remove deprecated import from zope.testing --- integrator/filesystem.txt | 2 +- integrator/tests/test_bscw.py | 4 +--- integrator/tests/test_filesystem.py | 4 +--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/integrator/filesystem.txt b/integrator/filesystem.txt index 401ae86..aa05f6a 100644 --- a/integrator/filesystem.txt +++ b/integrator/filesystem.txt @@ -84,7 +84,7 @@ A file object has additional attributes/methods. >>> file.contentType 'application/x-tar' >>> file.getSize() - 432L + 432 >>> logo = sub['loops_logo.png'] >>> logo.internalPath diff --git a/integrator/tests/test_bscw.py b/integrator/tests/test_bscw.py index 41bd51d..7ccdf41 100755 --- a/integrator/tests/test_bscw.py +++ b/integrator/tests/test_bscw.py @@ -1,8 +1,6 @@ -# $Id$ import os import unittest, doctest -from zope.testing.doctestunit import DocFileSuite from zope.interface.verify import verifyClass @@ -13,7 +11,7 @@ flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS def test_suite(): return unittest.TestSuite(( - DocFileSuite('../bscw.txt', optionflags=flags), + doctest.DocFileSuite('../bscw.txt', optionflags=flags), )) if __name__ == '__main__': diff --git a/integrator/tests/test_filesystem.py b/integrator/tests/test_filesystem.py index 8a2e6d0..23c7f03 100755 --- a/integrator/tests/test_filesystem.py +++ b/integrator/tests/test_filesystem.py @@ -1,8 +1,6 @@ -# $Id$ import os import unittest, doctest -from zope.testing.doctestunit import DocFileSuite from zope.interface.verify import verifyClass @@ -22,7 +20,7 @@ class Test(unittest.TestCase): def test_suite(): return unittest.TestSuite(( unittest.makeSuite(Test), - DocFileSuite('../filesystem.txt', optionflags=flags), + doctest.DocFileSuite('../filesystem.txt', optionflags=flags), )) if __name__ == '__main__': From 325f463ce87b903a5a0260c65495faf5d443259d Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Mon, 26 Dec 2016 11:26:10 +0100 Subject: [PATCH 73/82] remove deprecated import from zope.testing --- composer/layout/tests.py | 4 +--- composer/message/tests.py | 4 +--- composer/report/tests.py | 4 +--- composer/rule/tests.py | 4 +--- composer/schema/tests.py | 4 +--- composer/tests.py | 4 +--- 6 files changed, 6 insertions(+), 18 deletions(-) diff --git a/composer/layout/tests.py b/composer/layout/tests.py index 51fe062..e29defd 100755 --- a/composer/layout/tests.py +++ b/composer/layout/tests.py @@ -1,7 +1,5 @@ -# $Id$ import unittest, doctest -from zope.testing.doctestunit import DocFileSuite class Test(unittest.TestCase): @@ -15,7 +13,7 @@ def test_suite(): flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS return unittest.TestSuite(( unittest.makeSuite(Test), - DocFileSuite('README.txt', optionflags=flags), + doctest.DocFileSuite('README.txt', optionflags=flags), )) if __name__ == '__main__': diff --git a/composer/message/tests.py b/composer/message/tests.py index 9886053..44129f7 100755 --- a/composer/message/tests.py +++ b/composer/message/tests.py @@ -1,7 +1,5 @@ -# $Id$ import unittest, doctest -from zope.testing.doctestunit import DocFileSuite class Test(unittest.TestCase): @@ -15,7 +13,7 @@ def test_suite(): flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS return unittest.TestSuite(( unittest.makeSuite(Test), - DocFileSuite('README.txt', optionflags=flags), + doctest.DocFileSuite('README.txt', optionflags=flags), )) if __name__ == '__main__': diff --git a/composer/report/tests.py b/composer/report/tests.py index 9886053..44129f7 100755 --- a/composer/report/tests.py +++ b/composer/report/tests.py @@ -1,7 +1,5 @@ -# $Id$ import unittest, doctest -from zope.testing.doctestunit import DocFileSuite class Test(unittest.TestCase): @@ -15,7 +13,7 @@ def test_suite(): flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS return unittest.TestSuite(( unittest.makeSuite(Test), - DocFileSuite('README.txt', optionflags=flags), + doctest.DocFileSuite('README.txt', optionflags=flags), )) if __name__ == '__main__': diff --git a/composer/rule/tests.py b/composer/rule/tests.py index f6319e6..9f3069e 100755 --- a/composer/rule/tests.py +++ b/composer/rule/tests.py @@ -1,10 +1,8 @@ -# $Id$ import unittest, doctest from email import message_from_string from zope.interface import implements from zope.sendmail.interfaces import IMailDelivery -from zope.testing.doctestunit import DocFileSuite class TestMailer(object): @@ -31,7 +29,7 @@ def test_suite(): flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS return unittest.TestSuite(( unittest.makeSuite(Test), - DocFileSuite('README.txt', optionflags=flags), + doctest.DocFileSuite('README.txt', optionflags=flags), )) if __name__ == '__main__': diff --git a/composer/schema/tests.py b/composer/schema/tests.py index 2e7a216..68fdf26 100755 --- a/composer/schema/tests.py +++ b/composer/schema/tests.py @@ -1,7 +1,5 @@ -# $Id$ import unittest, doctest -from zope.testing.doctestunit import DocFileSuite class Test(unittest.TestCase): @@ -15,7 +13,7 @@ def test_suite(): flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS return unittest.TestSuite(( unittest.makeSuite(Test), - DocFileSuite('README.txt', optionflags=flags), + doctest.DocFileSuite('README.txt', optionflags=flags), )) if __name__ == '__main__': diff --git a/composer/tests.py b/composer/tests.py index ccfbd9a..674c681 100755 --- a/composer/tests.py +++ b/composer/tests.py @@ -1,7 +1,5 @@ -# $Id$ import unittest, doctest -from zope.testing.doctestunit import DocFileSuite class Test(unittest.TestCase): @@ -15,7 +13,7 @@ def test_suite(): flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS return unittest.TestSuite(( unittest.makeSuite(Test), - DocFileSuite('README.txt', optionflags=flags), + doctest.DocFileSuite('README.txt', optionflags=flags), )) if __name__ == '__main__': From c03e47ab3d5627d00ad1f723b833b79bcca27f74 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Mon, 26 Dec 2016 18:14:15 +0100 Subject: [PATCH 74/82] cleanup pagetemplate code with string: literals --- composer/schema/grid/grid_macros.pt | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/composer/schema/grid/grid_macros.pt b/composer/schema/grid/grid_macros.pt index 5422b50..c7f6afb 100755 --- a/composer/schema/grid/grid_macros.pt +++ b/composer/schema/grid/grid_macros.pt @@ -46,7 +46,7 @@ - Column Title @@ -54,31 +54,34 @@ + tal:attributes="class column/baseField/cssClass|string:"> + style string:width: $width" /> + tal:attributes="class column/baseField/cssClass|string:"> + style string:width: $width" /> + tal:attributes="class column/baseField/cssClass|string:"> + style string:width: $width;" /> From d385dc8dea56165f70c92dcd780c7dd7a4a4a790 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 31 Dec 2016 13:43:05 +0100 Subject: [PATCH 75/82] make package ready for distribution as an sdist egg --- MANIFEST.in | 8 ++++++++ browser/url.py | 4 ++-- setup.py | 16 ++++++---------- 3 files changed, 16 insertions(+), 12 deletions(-) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..74596dd --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,8 @@ +recursive-include . *.cfg +recursive-include . *.css *.js +recursive-include . *.gif *.jpg *.png +recursive-include . *.md *.txt +recursive-include . *.mo *.po *.pot +recursive-include . *.pt *.zpt +recursive-include . *.zcml +recursive-include . mime.types diff --git a/browser/url.py b/browser/url.py index 942c787..0af2542 100644 --- a/browser/url.py +++ b/browser/url.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2011 Helmut Merz helmutm@cy55.de +# Copyright (c) 2017 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -27,7 +27,7 @@ from zope.app.container.traversal import ItemTraverser from zope.interface import Interface, implements -TraversalRedirector(ItemTraverser): +class TraversalRedirector(ItemTraverser): port = 9083 names = ('ctt', 'sona',) diff --git a/setup.py b/setup.py index 28c1539..b1a66f4 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,18 @@ from setuptools import setup, find_packages import sys, os -version = '2.0' +version = '2.1.3' setup(name='cybertools', version=version, - description="cybertools", + description="cybertools: basic utilities for Zope3/bluebream/loops", long_description="""\ """, classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers keywords='', - author='Helmut Merz', - author_email='helmutm@cy55.de', - url='', + author='cyberconcepts.org team', + author_email='team@cyberconcepts.org', + url='https://www.cyberconcepts.org', license='GPL', packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), include_package_data=True, @@ -20,15 +20,11 @@ setup(name='cybertools', install_requires=[ # -*- Extra requirements: -*- 'lxml', - #'PIL', + #'Pillow', 'zope.app.catalog', - 'zope.app.dublincore', 'zope.app.file', 'zope.app.intid', - 'zope.app.preview', - 'zope.app.renderer', 'zope.app.session', - 'zope.dublincore', 'zope.sendmail', ], entry_points=""" From 82bc58329b59559b86dc842f9330b160d5cbd318 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sun, 1 Jan 2017 17:19:52 +0100 Subject: [PATCH 76/82] dirty fix: monkey patch in IntIds to avoid ForbiddenAttribute error --- __init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/__init__.py b/__init__.py index 38314f3..09b1274 100644 --- a/__init__.py +++ b/__init__.py @@ -1,3 +1,6 @@ -""" -$Id$ -""" +# package cybertools + +# module aliases +import sys +import doctest +sys.modules['zope.testing.doctestunit'] = doctest From 2e09bb373821a00660d08186ca4fb53b72c39c1d Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sun, 1 Jan 2017 17:21:32 +0100 Subject: [PATCH 77/82] minor package-level changes, add README file --- .gitignore | 2 ++ MANIFEST.in | 16 ++++++++-------- README.md | 7 +++++++ 3 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 README.md diff --git a/.gitignore b/.gitignore index 7ac9b1c..41ef9e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ *.pyc *.pyo ajax/dojo/* +build/ +dist/ *.egg-info *.project *.pydevproject diff --git a/MANIFEST.in b/MANIFEST.in index 74596dd..18baa42 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,8 @@ -recursive-include . *.cfg -recursive-include . *.css *.js -recursive-include . *.gif *.jpg *.png -recursive-include . *.md *.txt -recursive-include . *.mo *.po *.pot -recursive-include . *.pt *.zpt -recursive-include . *.zcml -recursive-include . mime.types +global-include *.cfg +global-include *.css *.js +global-include *.gif *.jpg *.png +global-include *.md *.txt +global-include *.mo *.po *.pot +global-include *.pt +global-include *.zcml +global-include mime.types diff --git a/README.md b/README.md new file mode 100644 index 0000000..1a6c5a6 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Introduction + +This is a set of utility libraries to be used mainly +with Zope 3 / bluebream and the web application platform +*loops*. + +More information: see https://www.cyberconcepts.org. From 3b53657c5d924547daef2b3e74608fb669336fad Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Mon, 2 Jan 2017 16:07:09 +0100 Subject: [PATCH 78/82] fix MANIFEST.in: include missing files/directories --- MANIFEST.in | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 18baa42..35ae83c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,14 @@ global-include *.cfg global-include *.css *.js global-include *.gif *.jpg *.png +global-include *.html global-include *.md *.txt +global-include *.mht global-include *.mo *.po *.pot global-include *.pt +global-include *.xml global-include *.zcml global-include mime.types + +graft cybertools/integrator/tests/data +graft cybertools/text/testfiles From bd3d7a0e2f0ad0f0313bb8e01e77d241e2d290d1 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Mon, 16 Jan 2017 15:12:50 +0100 Subject: [PATCH 79/82] fix action start: 'party' query only works in adapter --- organize/work.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/organize/work.py b/organize/work.py index 6617e06..5d141e5 100644 --- a/organize/work.py +++ b/organize/work.py @@ -302,7 +302,7 @@ class WorkItem(Stateful, Track): if (userName == self.userName and self.workItemType in (None, 'work') and self.state != 'running'): - running = getParent(self).query( + running = IWorkItems(getParent(self)).query( party=userName, state='running') for wi in running: if wi.workItemType in 'work': From 93249faf94f0ac4df821c461b95098ac1008f146 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Mon, 16 Jan 2017 15:23:48 +0100 Subject: [PATCH 80/82] fix check for workitem type --- organize/work.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/organize/work.py b/organize/work.py index 5d141e5..816ca73 100644 --- a/organize/work.py +++ b/organize/work.py @@ -305,7 +305,7 @@ class WorkItem(Stateful, Track): running = IWorkItems(getParent(self)).query( party=userName, state='running') for wi in running: - if wi.workItemType in 'work': + if wi.workItemType in (None, 'work'): wi.doAction('work', userName, end=(kw.get('start') or getTimeStamp())) # standard creation of new work item: From 9f1bbb5193cb07609c774d1288437d8579fb90b4 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Tue, 9 Jan 2018 07:55:06 +0100 Subject: [PATCH 81/82] fix moving of work items --- organize/work.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/organize/work.py b/organize/work.py index 816ca73..7725e88 100644 --- a/organize/work.py +++ b/organize/work.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2016 Helmut Merz helmutm@cy55.de +# Copyright (c) 2018 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 @@ -322,7 +322,7 @@ class WorkItem(Stateful, Track): if self.state == 'new': # should this be possible? moved = self self.setData(kw) - if self.state in ('done', 'finished', 'running'): + if self.state in ('done', 'finished', 'running', 'done_x', 'finished_x'): moved = self # is this OK? or better new state ..._y? else: moved = self.createNew('move', userName, **xkw) From b690273bf0e6baf918d78ce3bd8f4772f8c4c353 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 9 Mar 2019 13:24:14 +0100 Subject: [PATCH 82/82] update version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b1a66f4..5d4ae1c 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages import sys, os -version = '2.1.3' +version = '2.1.4' setup(name='cybertools', version=version,