From 9977e8b191cf235200aa89d33d13e1ef7543f0e9 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Tue, 17 Dec 2013 09:40:56 +0100 Subject: [PATCH 01/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] '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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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/40] 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'