Merge branch 'master' of ssh://git.cy55.de/home/git/cybertools

This commit is contained in:
Helmut Merz 2015-06-12 07:31:58 +02:00
commit 8824044726
23 changed files with 250 additions and 74 deletions

View file

@ -1,5 +1,5 @@
# #
# Copyright (c) 2011 Helmut Merz helmutm@cy55.de # Copyright (c) 2014 Helmut Merz helmutm@cy55.de
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -37,9 +37,9 @@ class Controller(BaseController):
macros = self.macros macros = self.macros
presentationMode = self.request.get('liquid.viewmode') == 'presentation' presentationMode = self.request.get('liquid.viewmode') == 'presentation'
params = [('blue/blue8.css', 'all', 20, False), params = [('blue/blue8.css', 'all', 20, False),
('print.css', 'print', 25, False),
('blue/ie.css', 'all', 30, True), ('blue/ie.css', 'all', 30, True),
('custom.css', 'all', 100, False)] ('custom.css', 'all', 100, False),
('print.css', 'print', 200, False),]
#if presentationMode: #if presentationMode:
# params.append(('presentation.css', 'all', 30, False)) # params.append(('presentation.css', 'all', 30, False))
for id, media, prio, ie in params: for id, media, prio, ie in params:

BIN
browser/icons/page_copy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B

View file

@ -1,5 +1,5 @@
<metal:page define-macro="page" <metal:page define-macro="page"
tal:condition="view/update"><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> tal:condition="view/update"><!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
tal:define="body view/body; tal:define="body view/body;
layout view/context/template"> layout view/context/template">

View file

@ -83,6 +83,7 @@ class Report(Template):
queryCriteria = None queryCriteria = None
outputFields = () outputFields = ()
sortCriteria = () sortCriteria = ()
sortDescending = False
limits = None limits = None

View file

@ -81,12 +81,14 @@ class GroupHeaderRow(BaseRow):
class ResultSet(object): class ResultSet(object):
def __init__(self, context, data, rowFactory=Row, def __init__(self, context, data, rowFactory=Row,
sortCriteria=None, queryCriteria=BaseQueryCriteria(), sortCriteria=None, sortDescending=False,
queryCriteria=BaseQueryCriteria(),
limits=None): limits=None):
self.context = context # the report or report instance self.context = context # the report or report instance
self.data = data self.data = data
self.rowFactory = rowFactory self.rowFactory = rowFactory
self.sortCriteria = sortCriteria self.sortCriteria = sortCriteria
self.sortDescending = sortDescending
self.queryCriteria = queryCriteria self.queryCriteria = queryCriteria
self.limits = limits self.limits = limits
self.totals = BaseRow(None, self) self.totals = BaseRow(None, self)
@ -95,7 +97,9 @@ class ResultSet(object):
result = [self.rowFactory(item, self) for item in self.data] result = [self.rowFactory(item, self) for item in self.data]
result = [row for row in result if self.queryCriteria.check(row)] result = [row for row in result if self.queryCriteria.check(row)]
if self.sortCriteria: 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: if self.limits:
start, stop = self.limits start, stop = self.limits
result = result[start:stop] result = result[start:stop]

View file

@ -90,7 +90,7 @@
tal:define="width field/width|nothing" tal:define="width field/width|nothing"
tal:attributes="name name; id name; tal:attributes="name name; id name;
style python: style python:
('width: %s;;' % (width and str(width)+'px' or '570px')) + ('width: %s;;' % (width and str(width)+'px' or '555px')) +
'height: 1.5em;;'; 'height: 1.5em;;';
value data/?name|string:; value data/?name|string:;
xxrequired field/required_js;" /> xxrequired field/required_js;" />
@ -144,7 +144,11 @@
<metal:upload define-macro="input_fileupload"> <metal:upload define-macro="input_fileupload">
<input type="file" name="field" <input type="file" name="field"
tal:attributes="name name;" /> tal:attributes="name name;"
onchange="if (this.form.title.value == '') {
var value = this.value.split('\\');
this.form.title.value = value[value.length-1];
}" />
</metal:upload> </metal:upload>

View file

@ -1,5 +1,5 @@
# #
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de # Copyright (c) 2014 Helmut Merz helmutm@cy55.de
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -41,6 +41,14 @@ from cybertools.composer.schema.schema import formErrors
from cybertools.util.format import toStr, toUnicode from cybertools.util.format import toStr, toUnicode
class FieldGroup(object):
def __init__(self, name, label, sublabels=[]):
self.name = name
self.label = label
self.sublabels = sublabels
class Field(Component): class Field(Component):
implements(IField) implements(IField)

View file

@ -1,5 +1,5 @@
# #
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de # Copyright (c) 2014 Helmut Merz helmutm@cy55.de
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -42,7 +42,33 @@ class GridFieldInstance(ListFieldInstance):
@Lazy @Lazy
def columnTypes(self): 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 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], 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], group=group)
groups[group.name] = g
result.append(g)
else:
g['fields'].append(f)
g['indexes'].append(idx)
return result
@Lazy @Lazy
def columnFieldInstances(self): def columnFieldInstances(self):
@ -122,6 +148,8 @@ class GridFieldInstance(ListFieldInstance):
def unmarshallRow(self, row, idx=None): def unmarshallRow(self, row, idx=None):
item = {} item = {}
cardinality = getattr(self.context, 'cardinality', None) cardinality = getattr(self.context, 'cardinality', None)
ignoreInCheckOnEmpty = list(
getattr(self.context, 'ignoreInCheckOnEmpty', []))
for fi in self.columnFieldInstances: for fi in self.columnFieldInstances:
if idx is not None: if idx is not None:
fi.index = idx fi.index = idx
@ -133,10 +161,9 @@ class GridFieldInstance(ListFieldInstance):
else: else:
if fi.default is not None: if fi.default is not None:
if value == fi.default: if value == fi.default:
continue ignoreInCheckOnEmpty.append(fi.name)
if value: if value:
item[fi.name] = value item[fi.name] = value
ignoreInCheckOnEmpty = getattr(self.context, 'ignoreInCheckOnEmpty', [])
for k, v in item.items(): for k, v in item.items():
if k not in ignoreInCheckOnEmpty: #and v != '__no_change__': if k not in ignoreInCheckOnEmpty: #and v != '__no_change__':
return item return item
@ -215,7 +242,8 @@ class KeyTableFieldInstance(RecordsFieldInstance):
for k, v in value.items(): for k, v in value.items():
row = [k] row = [k]
for idx, fi in enumerate(self.columnFieldInstances[1:]): 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) rows.append(row)
return dict(headers=headers, rows=rows) return dict(headers=headers, rows=rows)
@ -226,7 +254,8 @@ class KeyTableFieldInstance(RecordsFieldInstance):
for k, v in value.items(): for k, v in value.items():
item = {self.keyName: k} item = {self.keyName: k}
for idx, name in enumerate(self.dataNames): for idx, name in enumerate(self.dataNames):
item[name] = v[idx] if idx < len(v):
item[name] = v[idx]
result.append(item) result.append(item)
return result return result

View file

@ -57,7 +57,7 @@
tal:attributes="class string:${column/baseField/cssClass|string:}"> tal:attributes="class string:${column/baseField/cssClass|string:}">
<input type="text" style="width: auto" <input type="text" style="width: auto"
tal:define="cname column/name" tal:define="cname column/name"
tal:attributes="value row/?cname; tal:attributes="value row/?cname|nothing;
name string:$name.$cname:records; name string:$name.$cname:records;
style string:width: ${column/baseField/width|string:auto};" /></td> style string:width: ${column/baseField/width|string:auto};" /></td>
</tr> </tr>

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -19,8 +19,6 @@
""" """
Basic classes for schemas, i.e. sets of fields that may be used for creating Basic classes for schemas, i.e. sets of fields that may be used for creating
editing forms or display views for objects. editing forms or display views for objects.
$Id$
""" """
from zope.interface import implements from zope.interface import implements

View file

@ -40,6 +40,10 @@ It's possible to leave some of the questions unanswered.
>>> resp02 = Response(quest, 'john') >>> resp02 = Response(quest, 'john')
>>> resp02.values = {qu01: 2, qu03: 4} >>> resp02.values = {qu01: 2, qu03: 4}
Evaluation
==========
Now let's calculate the result for resp01. Now let's calculate the result for resp01.
>>> res = resp01.getResult() >>> res = resp01.getResult()
@ -55,8 +59,8 @@ Now let's calculate the result for resp01.
fi03 4.0 fi03 4.0
fi01 2.4 fi01 2.4
Grouped Feedback Items Grouped feedback items
====================== ----------------------
>>> from cybertools.knowledge.survey.questionnaire import QuestionGroup >>> from cybertools.knowledge.survey.questionnaire import QuestionGroup
>>> qugroup = QuestionGroup(quest) >>> qugroup = QuestionGroup(quest)
@ -65,12 +69,25 @@ Grouped Feedback Items
>>> qugroup.feedbackItems = [fi01, fi02, fi03] >>> qugroup.feedbackItems = [fi01, fi02, fi03]
>>> res = resp01.getGroupedResult() >>> res = resp01.getGroupedResult()
>>> for qugroup, fi, score in res: >>> for r in res:
... print fi.text, round(score, 2) ... print r['feedback'].text, round(r['score'], 2), r['rank']
fi02 0.58 fi02 0.58 1
>>> res = resp02.getGroupedResult() >>> res = resp02.getGroupedResult()
>>> for qugroup, fi, score in res: >>> for r in res:
... print fi.text, round(score, 2) ... print r['feedback'].text, round(r['score'], 2), r['rank']
fi03 0.75 fi03 0.75 1
Team evaluation
---------------
>>> resp03 = Response(quest, 'mary')
>>> resp03.values = {qu01: 1, qu02: 2, qu03: 4}
>>> resp01.values[qugroup] = resp01.getGroupedResult()[0]['score']
>>> resp03.values[qugroup] = resp03.getGroupedResult()[0]['score']
>>> teamData = resp01.getTeamResult([qugroup], [resp01, resp03])
>>> teamData
[{'average': 0.6666...}]

View file

@ -80,7 +80,8 @@ class IResponse(Interface):
questionnaire = Attribute('The questionnaire this response belongs to.') questionnaire = Attribute('The questionnaire this response belongs to.')
party = Attribute('Some identification of the party that responded ' party = Attribute('Some identification of the party that responded '
'to this questionnaire.') '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(): def getResult():
""" Calculate the result for this response. """ Calculate the result for this response.

View file

@ -1,5 +1,5 @@
# #
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de # Copyright (c) 2015 Helmut Merz helmutm@cy55.de
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -58,12 +58,8 @@ class Question(object):
self.feedbackItems = {} self.feedbackItems = {}
self.text = text self.text = text
self.revertAnswerOptions = False self.revertAnswerOptions = False
self.questionType = 'value_selection'
def getAnswerRange(self): self.answerRange = None
return self._answerRange or self.questionnaire.defaultAnswerRange
def setAnswerRange(self, value):
self._answerRange = value
answerRange = property(getAnswerRange, setAnswerRange)
class FeedbackItem(object): class FeedbackItem(object):
@ -82,13 +78,18 @@ class Response(object):
self.questionnaire = questionnaire self.questionnaire = questionnaire
self.party = party self.party = party
self.values = {} self.values = {}
self.texts = {}
def getResult(self): def getResult(self):
result = {} result = {}
for question, value in self.values.items(): for question, value in self.values.items():
if question.questionType != 'value_selection':
continue
for fi, rf in question.feedbackItems.items(): for fi, rf in question.feedbackItems.items():
if question.revertAnswerOptions: if question.revertAnswerOptions:
value = question.answerRange - value - 1 answerRange = (question.answerRange or
self.questionnaire.defaultAnswerRange)
value = answerRange - value - 1
result[fi] = result.get(fi, 0.0) + rf * value result[fi] = result.get(fi, 0.0) + rf * value
return sorted(result.items(), key=lambda x: -x[1]) return sorted(result.items(), key=lambda x: -x[1])
@ -97,15 +98,46 @@ class Response(object):
for qugroup in self.questionnaire.questionGroups: for qugroup in self.questionnaire.questionGroups:
score = scoreMax = 0.0 score = scoreMax = 0.0
for qu in qugroup.questions: for qu in qugroup.questions:
value = self.values.get(qu) if qu.questionType not in (None, 'value_selection'):
if value is None:
continue continue
value = self.values.get(qu)
if value is None or isinstance(value, basestring):
continue
answerRange = (qu.answerRange or
self.questionnaire.defaultAnswerRange)
if qu.revertAnswerOptions: if qu.revertAnswerOptions:
value = qu.answerRange - value - 1 value = answerRange - value - 1
score += value score += value
scoreMax += qu.answerRange - 1 scoreMax += answerRange - 1
if scoreMax > 0.0: if scoreMax > 0.0:
relScore = score / scoreMax relScore = score / scoreMax
wScore = relScore * len(qugroup.feedbackItems) - 0.00001 wScore = relScore * len(qugroup.feedbackItems) - 0.00001
result.append((qugroup, qugroup.feedbackItems[int(wScore)], relScore)) if qugroup.feedbackItems:
feedback = qugroup.feedbackItems[int(wScore)]
else:
feedback = FeedbackItem()
result.append(dict(
group=qugroup,
feedback=feedback,
score=relScore))
ranks = getRanks([r['score'] for r in result])
for idx, r in enumerate(result):
r['rank'] = ranks[idx]
return result return result
def getTeamResult(self, groups, teamData):
result = []
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=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]

View file

@ -85,6 +85,7 @@ class IPerson(Interface):
description=_(u'The date of birth - should be a ' description=_(u'The date of birth - should be a '
'datetime.date object.'), 'datetime.date object.'),
required=False,) required=False,)
birthDate.hideTime = True
age = schema.Int( age = schema.Int(
title=_(u'Age'), title=_(u'Age'),

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: $Id$\n" "Project-Id-Version: $Id$\n"
"POT-Creation-Date: 2008-12-01 12:00 CET\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 <helmutm@cy55.de>\n" "Last-Translator: Helmut Merz <helmutm@cy55.de>\n"
"Language-Team: loops developers <helmutm@cy55.de>\n" "Language-Team: loops developers <helmutm@cy55.de>\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -65,3 +65,33 @@ msgstr "Beginn"
msgid "End date" msgid "End date"
msgstr "Ende" 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"

View file

@ -1,5 +1,5 @@
# #
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de # Copyright (c) 2014 Helmut Merz helmutm@cy55.de
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -41,8 +41,8 @@ _not_found = object()
def workItemStates(): def workItemStates():
return StatesDefinition('workItemStates', return StatesDefinition('workItemStates',
State('new', 'new', State('new', 'new',
('plan', 'accept', 'start', 'work', 'finish', 'delegate', ('plan', 'accept', 'start', 'work', 'finish', 'delegate',
'cancel', 'reopen'), 'cancel', 'reopen'), # 'move', # ?
color='red'), color='red'),
State('planned', 'planned', State('planned', 'planned',
('plan', 'accept', 'start', 'work', 'finish', 'delegate', ('plan', 'accept', 'start', 'work', 'finish', 'delegate',
@ -59,7 +59,7 @@ def workItemStates():
'move', 'cancel', 'modify'), color='lightgreen'), 'move', 'cancel', 'modify'), color='lightgreen'),
State('finished', 'finished', State('finished', 'finished',
('plan', 'accept', 'start', 'work', 'finish', ('plan', 'accept', 'start', 'work', 'finish',
'move', 'modify', 'close'), 'move', 'modify', 'close', 'cancel'),
color='green'), color='green'),
State('cancelled', 'cancelled', State('cancelled', 'cancelled',
('plan', 'accept', 'start', 'work', 'move', 'modify', 'close'), ('plan', 'accept', 'start', 'work', 'move', 'modify', 'close'),
@ -79,8 +79,12 @@ def workItemStates():
State('replaced', 'replaced', (), color='grey'), State('replaced', 'replaced', (), color='grey'),
State('planned_x', 'planned', (), color='red'), State('planned_x', 'planned', (), color='red'),
State('accepted_x', 'accepted', (), color='yellow'), State('accepted_x', 'accepted', (), color='yellow'),
State('done_x', 'done', (), color='lightgreen'), State('done_x', 'done',
State('finished_x', 'finished', (), color='green'), ('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: # transitions:
Transition('plan', 'plan', 'planned'), Transition('plan', 'plan', 'planned'),
Transition('accept', 'accept', 'accepted'), Transition('accept', 'accept', 'accepted'),
@ -96,7 +100,8 @@ def workItemStates():
initialState='new') initialState='new')
fieldNames = ['title', 'description', 'deadline', 'start', 'end', fieldNames = ['title', 'description', 'deadline', 'priority', 'activity',
'start', 'end',
'duration', 'effort', 'duration', 'effort',
'comment', 'party'] # for use in editingRules 'comment', 'party'] # for use in editingRules
@ -138,7 +143,8 @@ class WorkItemType(object):
self.title = title self.title = title
self.description = description self.description = description
self.actions = actions or list(editingRules) 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.indicator = indicator
self.delegatedState = delegatedState self.delegatedState = delegatedState
self.prefillDate = prefillDate self.prefillDate = prefillDate
@ -156,9 +162,10 @@ workItemTypes = Jeep((
fields =('deadline',), fields =('deadline',),
indicator='work_deadline'), indicator='work_deadline'),
WorkItemType('checkup', u'Check-up', WorkItemType('checkup', u'Check-up',
actions=('plan', 'accept', 'finish', 'cancel', actions=('plan', 'accept', 'start', 'finish', 'cancel',
'modify', 'delegate', 'close', 'reopen'), 'modify', 'delegate', 'close', 'reopen'),
fields =('deadline', 'start-end',), #fields =('deadline', 'start-end',),
fields =('deadline', 'daterange',),
indicator='work_checkup', indicator='work_checkup',
delegatedState='closed', prefillDate=False), delegatedState='closed', prefillDate=False),
)) ))
@ -177,7 +184,7 @@ class WorkItem(Stateful, Track):
statesDefinition = 'organize.workItemStates' statesDefinition = 'organize.workItemStates'
initAttributes = set(['workItemType', 'party', 'title', 'description', initAttributes = set(['workItemType', 'party', 'title', 'description',
'deadline', 'start', 'end', 'deadline', 'priority', 'activity', 'start', 'end',
'duration', 'effort']) 'duration', 'effort'])
def __init__(self, taskId, runId, userName, data): def __init__(self, taskId, runId, userName, data):
@ -225,13 +232,16 @@ class WorkItem(Stateful, Track):
return list(getParent(self).query(runId=self.runId)) return list(getParent(self).query(runId=self.runId))
def doAction(self, action, userName, **kw): def doAction(self, action, userName, **kw):
if self != self.currentWorkItems[-1]: #if self != self.currentWorkItems[-1]:
raise ValueError("Actions are only allowed on the last item of a run.") # raise ValueError("Actions are only allowed on the last item of a run.")
if action not in [t.name for t in self.getAvailableTransitions()]: if action not in [t.name for t in self.getAvailableTransitions()]:
raise ValueError("Action '%s' not allowed in state '%s'" % raise ValueError("Action '%s' not allowed in state '%s'" %
(action, self.state)) (action, self.state))
if action in self.specialActions: if action in self.specialActions:
return self.specialActions[action](self, userName, **kw) return self.specialActions[action](self, userName, **kw)
return self.doStandardAction(action, userName, **kw)
def doStandardAction(self, action, userName, **kw):
if self.state == 'new': if self.state == 'new':
self.setData(**kw) self.setData(**kw)
self.doTransition(action) self.doTransition(action)
@ -244,6 +254,9 @@ class WorkItem(Stateful, Track):
elif self.state in ('planned', 'accepted', 'done'): elif self.state in ('planned', 'accepted', 'done'):
self.state = self.state + '_x' self.state = self.state + '_x'
self.reindex('state') self.reindex('state')
elif self.state in ('finished',) and action == 'cancel':
self.state = self.state + '_x'
self.reindex('state')
new.doTransition(action) new.doTransition(action)
new.reindex() new.reindex()
return new return new
@ -279,25 +292,57 @@ class WorkItem(Stateful, Track):
delegated.data['target'] = new.name delegated.data['target'] = new.name
return new return new
def doStart(self, userName, **kw):
action = 'start'
# stop any running work item of user:
# TODO: check: party query OK?
if (userName == self.userName 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 in '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()
kw['end'] = None
kw['duration'] = kw['effort'] = 0
return self.doStandardAction(action, userName, **kw)
def move(self, userName, **kw): def move(self, userName, **kw):
xkw = dict(kw) xkw = dict(kw)
for k in ('deadline', 'start', 'end'): for k in ('deadline', 'start', 'end'):
xkw.pop(k, None) # do not change on source item xkw.pop(k, None) # do not change on source item
moved = self.createNew('move', userName, **xkw) if self.state == 'new': # should this be possible?
moved.userName = self.userName moved = self
moved.state = 'moved' self.setData(kw)
moved.reindex() 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) task = kw.pop('task', None)
new = moved.createNew(None, userName, taskId=task, runId=0, **kw) new = moved.createNew(None, userName, taskId=task, runId=0, **kw)
new.userName = self.userName new.userName = self.userName
new.data['source'] = moved.name new.data['source'] = moved.name
new.state = self.state if self.state == 'new':
new.state = 'planned'
else:
new.state = self.state
new.reindex() new.reindex()
moved.data['target'] = new.name moved.data['target'] = new.name
if self.state in ('planned', 'accepted', 'delegated', 'moved', moved.state = 'moved'
'done', 'finished'): moved.reindex()
if self.state in ('planned', 'accepted', 'delegated', 'moved'):
#'done', 'finished'):
self.state = self.state + '_x' self.state = self.state + '_x'
self.reindex('state') self.reindex('state')
#elif self.state in ('done', 'finished'):
# self.state = self.state + '_y'
# self.reindex('state')
return new return new
def close(self, userName, **kw): def close(self, userName, **kw):
@ -315,7 +360,8 @@ class WorkItem(Stateful, Track):
item.reindex('state') item.reindex('state')
return new return new
specialActions = dict(modify=modify, delegate=delegate, move=move, specialActions = dict(modify=modify, delegate=delegate,
start=doStart, move=move,
close=close) close=close)
def setData(self, ignoreParty=False, **kw): def setData(self, ignoreParty=False, **kw):
@ -328,7 +374,7 @@ class WorkItem(Stateful, Track):
self.reindex('userName') self.reindex('userName')
start = kw.get('start') or kw.get('deadline') # TODO: check OK? start = kw.get('start') or kw.get('deadline') # TODO: check OK?
if start is not None: if start is not None:
self.timeStamp = start self.timeStamp = start # TODO: better use end
self.reindex('timeStamp') self.reindex('timeStamp')
data = self.data data = self.data
for k, v in kw.items(): for k, v in kw.items():

View file

@ -100,7 +100,7 @@ but may also be specified explicitly.
>>> wi03 = wi02.doAction('start', 'jim', start=1229958000) >>> wi03 = wi02.doAction('start', 'jim', start=1229958000)
>>> wi03 >>> wi03
<WorkItem ['001', 1, 'jim', '2008-12-22 16:00', 'running']: <WorkItem ['001', 1, 'jim', '2008-12-22 16:00', 'running']:
{'duration': 700, 'start': 1229958000, 'created': ..., 'creator': 'jim'}> {'duration': 0, 'start': 1229958000, 'created': ..., 'creator': 'jim'}>
Stopping and finishing work Stopping and finishing work
--------------------------- ---------------------------
@ -113,7 +113,7 @@ as "running" will be replaced by a new one.
>>> wi03 >>> wi03
<WorkItem ['001', 1, 'jim', '2008-12-22 16:00', 'replaced']: <WorkItem ['001', 1, 'jim', '2008-12-22 16:00', 'replaced']:
{'duration': 700, 'start': 1229958000, 'created': ..., 'creator': 'jim'}> {'duration': 0, 'start': 1229958000, 'created': ..., 'creator': 'jim'}>
>>> wi04 >>> wi04
<WorkItem ['001', 1, 'jim', '2008-12-22 16:00', 'done']: <WorkItem ['001', 1, 'jim', '2008-12-22 16:00', 'done']:
{'start': 1229958000, 'created': ..., 'end': 1229958300, 'creator': 'jim'}> {'start': 1229958000, 'created': ..., 'end': 1229958300, 'creator': 'jim'}>

View file

@ -52,7 +52,7 @@ class Stateful(object):
def getStateObject(self): def getStateObject(self):
states = self.getStatesDefinition().states states = self.getStatesDefinition().states
if self.state not in states: if self.getState() not in states:
self.state = self.getStatesDefinition().initialState self.state = self.getStatesDefinition().initialState
return states[self.state] return states[self.state]

View file

@ -1,5 +1,5 @@
# #
# Copyright (c) 2011 Helmut Merz helmutm@cy55.de # Copyright (c) 2014 Helmut Merz helmutm@cy55.de
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
ZODB-/BTree-based implementation of user interaction tracking. ZODB-/BTree-based implementation of user interaction tracking.
$Id$
""" """
import time import time
@ -142,11 +140,12 @@ class TrackingStorage(BTreeContainer):
def setupIndexes(self): def setupIndexes(self):
changed = False changed = False
for idx in self.indexAttributes: for idx in self.trackFactory.index_attributes:
if idx not in self.indexes: if idx not in self.indexes:
self.indexes[idx] = FieldIndex() self.indexes[idx] = FieldIndex()
changed = True changed = True
if changed: if changed:
self.indexAttributes = self.trackFactory.index_attributes
self.reindexTracks() self.reindexTracks()
def idFromNum(self, num): def idFromNum(self, num):
@ -237,6 +236,7 @@ class TrackingStorage(BTreeContainer):
self.unindexTrack(trackNum, track) self.unindexTrack(trackNum, track)
def indexTrack(self, trackNum, track, idx=None): def indexTrack(self, trackNum, track, idx=None):
#self.setupIndexes()
if not trackNum: if not trackNum:
trackNum = int(track.__name__) trackNum = int(track.__name__)
data = track.indexdata data = track.indexdata

View file

@ -1,5 +1,5 @@
# #
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de # Copyright (c) 2014 Helmut Merz helmutm@cy55.de
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Interface definitions for tracking of user interactions. Interface definitions for tracking of user interactions.
$Id$
""" """
from zope.interface import Interface, Attribute from zope.interface import Interface, Attribute
@ -50,6 +48,12 @@ class ITrack(Interface):
# """ Return the internal name (ID) of the track. # """ 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): class ITrackingStorage(Interface):
""" A utility for storing user tracks. """ A utility for storing user tracks.

View file

@ -58,6 +58,7 @@ class ExternalEditorView(object):
r.append('cookie:' + cookie) r.append('cookie:' + cookie)
r.append('') r.append('')
r.append(fromUnicode(data)) r.append(fromUnicode(data))
r = [str(item) for item in r]
result = '\n'.join(r) result = '\n'.join(r)
self.setHeaders(len(result)) self.setHeaders(len(result))
return result return result