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/mojo/body.pt b/browser/mojo/body.pt
index a76a3d0..b4eec06 100644
--- a/browser/mojo/body.pt
+++ b/browser/mojo/body.pt
@@ -12,7 +12,7 @@
region="top" splitter="false">
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/browser/view.py b/browser/view.py
index 07f59cd..0ec312c 100644
--- a/browser/view.py
+++ b/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
@@ -27,6 +27,7 @@ from zope.interface import Interface, implements
from zope.cachedescriptors.property import Lazy
from zope import component
from zope.event import notify
+from zope.publisher.http import URLGetter as BaseURLGetter
from zope.publisher.interfaces.browser import IBrowserSkinType
from cybertools.browser.renderer import CachableRenderer
@@ -63,6 +64,15 @@ class BodyTemplateView(object):
bodyTemplate = UnboundTemplateFile('liquid/body.pt')
+class URLGetter(BaseURLGetter):
+
+ def __str__(self):
+ url = self.__request.getURL()
+ if url.endswith('/@@index.html'):
+ url = url[:-len('/@@index.html')]
+ return url
+
+
class GenericView(object):
index = mainTemplate
@@ -103,6 +113,10 @@ class GenericView(object):
# this is useful for a top-level page only
return self.index(*args, **kw)
+ @property
+ def requestUrl(self):
+ return URLGetter(self.request)
+
@Lazy
def isAuthenticated(self):
return not IUnauthenticatedPrincipal.providedBy(self.request.principal)
diff --git a/commerce/interfaces.py b/commerce/interfaces.py
index 824d989..5a704b0 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 05b3289..14e180e 100644
--- a/commerce/order.py
+++ b/commerce/order.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2011 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.cachedescriptors.property import Lazy
@@ -119,6 +117,10 @@ class OrderItems(object):
def add(self, product, party, shop, order='???', run=0, **kw):
kw['shop'] = self.getUid(shop)
existing = self.getCart(party, order, shop, run, product=product)
+ 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))
diff --git a/composer/layout/browser/standard.pt b/composer/layout/browser/standard.pt
index dc36878..f57115c 100644
--- a/composer/layout/browser/standard.pt
+++ b/composer/layout/browser/standard.pt
@@ -1,5 +1,5 @@
+ tal:condition="view/update">
@@ -20,7 +20,7 @@
-
+
diff --git a/composer/layout/browser/view.py b/composer/layout/browser/view.py
index df09459..9f30da2 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,7 +25,9 @@ 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.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
@@ -73,6 +73,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)
diff --git a/composer/report/base.py b/composer/report/base.py
index 5b63a09..b3b34d3 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/field.py b/composer/report/field.py
index 526c740..b57d580 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
@@ -129,6 +129,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)
diff --git a/composer/report/result.py b/composer/report/result.py
index 857fdd6..ca63d4d 100755
--- a/composer/report/result.py
+++ b/composer/report/result.py
@@ -123,13 +123,15 @@ class ResultSet(object):
def __init__(self, context, data,
rowFactory=Row, headerRowFactory=GroupHeaderRow,
- 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.headerRowFactory = headerRowFactory
self.sortCriteria = sortCriteria
+ self.sortDescending = sortDescending
self.queryCriteria = queryCriteria
self.limits = limits
self.totals = TotalsRow(None, self)
@@ -204,7 +206,8 @@ class ResultSet(object):
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])
+ [f.getSortValue(x) for f in self.sortCriteria],
+ reverse=self.sortDescending)
if self.groupColumns:
res = []
groupValues = [None for f in self.groupColumns]
diff --git a/composer/schema/browser/schema_macros.pt b/composer/schema/browser/schema_macros.pt
index 4804e3c..6171ff5 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];
+ }" />
diff --git a/composer/schema/factory.py b/composer/schema/factory.py
index c95bd36..53d56b4 100644
--- a/composer/schema/factory.py
+++ b/composer/schema/factory.py
@@ -85,6 +85,8 @@ class SchemaFactory(object):
field = interface[fname]
if getattr(field, 'suppress', False):
continue
+ if getattr(field, 'hidden', False):
+ continue
info = fieldMapping.get(field.__class__)
f = createField(field, info)
if self.schemaProcessor is not None:
diff --git a/composer/schema/grid/field.py b/composer/schema/grid/field.py
index 80a6ed8..be1c980 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
@@ -42,7 +42,33 @@ 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 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
def columnFieldInstances(self):
@@ -122,6 +148,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 +161,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
diff --git a/composer/schema/schema.py b/composer/schema/schema.py
index c8bd8eb..92f50e0 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
diff --git a/knowledge/survey/README.txt b/knowledge/survey/README.txt
index 9315131..44da8f7 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)
@@ -65,12 +69,25 @@ 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
+---------------
+
+ >>> 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...}]
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 b73e9e6..6fb6146 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
@@ -36,6 +36,9 @@ class Questionnaire(object):
self.responses = []
self.defaultAnswerRange = 5
+ def getQuestionGroups(self, party):
+ return self.questionGroups
+
class QuestionGroup(object):
@@ -58,12 +61,8 @@ class Question(object):
self.feedbackItems = {}
self.text = text
self.revertAnswerOptions = False
-
- def getAnswerRange(self):
- return self._answerRange or self.questionnaire.defaultAnswerRange
- def setAnswerRange(self, value):
- self._answerRange = value
- answerRange = property(getAnswerRange, setAnswerRange)
+ self.questionType = 'value_selection'
+ self.answerRange = None
class FeedbackItem(object):
@@ -82,30 +81,68 @@ class Response(object):
self.questionnaire = questionnaire
self.party = party
self.values = {}
+ self.texts = {}
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
+ answerRange = (question.answerRange or
+ self.questionnaire.defaultAnswerRange)
+ value = answerRange - value - 1
result[fi] = result.get(fi, 0.0) + rf * value
return sorted(result.items(), key=lambda x: -x[1])
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:
- value = self.values.get(qu)
- if value is None:
+ if qu.questionType not in (None, 'value_selection'):
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:
- value = qu.answerRange - value - 1
+ value = answerRange - value - 1
score += value
- scoreMax += qu.answerRange - 1
+ scoreMax += answerRange - 1
if scoreMax > 0.0:
relScore = score / scoreMax
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
+
+ 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)
+ if not values:
+ continue
+ 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]
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 ec17a3f..1696912 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
@@ -33,12 +31,14 @@ except:
except:
getLogger('Asset Manager').warn('Python Imaging Library '
'could not be found.')
-
+
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]
@@ -55,8 +55,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):
@@ -97,7 +96,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:
@@ -105,10 +104,21 @@ 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:
return
format = mimetypeToPIL(mimetype)
- self.im.save(path)
+ try:
+ self.im.save(path)
+ except IOError, e:
+ logger.warn(e)
diff --git a/organize/interfaces.py b/organize/interfaces.py
index b880f77..3575a6f 100644
--- a/organize/interfaces.py
+++ b/organize/interfaces.py
@@ -86,6 +86,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'),
diff --git a/organize/locales/de/LC_MESSAGES/cybertools.organize.mo b/organize/locales/de/LC_MESSAGES/cybertools.organize.mo
index 2826550..002cbd1 100644
Binary files a/organize/locales/de/LC_MESSAGES/cybertools.organize.mo and b/organize/locales/de/LC_MESSAGES/cybertools.organize.mo differ
diff --git a/organize/locales/de/LC_MESSAGES/cybertools.organize.po b/organize/locales/de/LC_MESSAGES/cybertools.organize.po
index 1db0f8f..ed16268 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"
@@ -66,17 +66,31 @@ msgid "End date"
msgstr "Ende"
msgid "Street, number"
-msgstr "Straße, Nr."
+msgstr "Straße, Hausnummer"
-msgid "City"
-msgstr "Stadt"
+msgid "Street and number"
+msgstr "Straße und Hausnummer"
msgid "ZIP code"
-msgstr "PLZ"
+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 Zeilen"
+msgstr "Zusätzliche Adresszeilen"
msgid "Additional address lines"
-msgstr "Zusätzliche Zeilen in Anschrift"
-
+msgstr "Zusätzliche Adresszeilen"
diff --git a/organize/work.py b/organize/work.py
index 39b8fb7..7725e88 100644
--- a/organize/work.py
+++ b/organize/work.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2013 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
@@ -41,8 +41,8 @@ _not_found = object()
def workItemStates():
return StatesDefinition('workItemStates',
State('new', 'new',
- ('plan', 'accept', 'start', 'work', 'finish', 'delegate',
- 'cancel', 'reopen'),
+ ('plan', 'accept', 'start', 'work', 'finish', 'delegate',
+ 'cancel', 'reopen'), # 'move', # ?
color='red'),
State('planned', 'planned',
('plan', 'accept', 'start', 'work', 'finish', 'delegate',
@@ -52,14 +52,14 @@ 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',
'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'),
@@ -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'),
@@ -96,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
@@ -106,21 +111,21 @@ fieldNames = ['title', 'description', 'deadline', 'start', 'end',
# . 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 = {'*': '+++++////./'},
)
@@ -138,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
@@ -156,9 +162,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),
))
@@ -177,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):
@@ -225,13 +232,16 @@ 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))
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)
@@ -244,6 +254,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
@@ -266,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)
@@ -279,25 +295,57 @@ 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:
+ # TODO: check: party query OK?
+ if (userName == self.userName and
+ self.workItemType in (None, 'work') and
+ self.state != 'running'):
+ running = IWorkItems(getParent(self)).query(
+ party=userName, state='running')
+ for wi in running:
+ if wi.workItemType in (None, '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):
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', 'done_x', 'finished_x'):
+ 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
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
- 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):
@@ -315,7 +363,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):
@@ -328,7 +377,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():
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
diff --git a/setup.py b/setup.py
index 2d59f2d..5d4ae1c 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.4'
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,14 +20,11 @@ setup(name='cybertools',
install_requires=[
# -*- Extra requirements: -*-
'lxml',
- #'PIL',
+ #'Pillow',
'zope.app.catalog',
'zope.app.file',
'zope.app.intid',
- 'zope.app.preview',
- 'zope.app.renderer',
'zope.app.session',
- 'zope.dublincore',
'zope.sendmail',
],
entry_points="""
diff --git a/stateful/base.py b/stateful/base.py
index 67cee8d..dd9b4f1 100644
--- a/stateful/base.py
+++ b/stateful/base.py
@@ -50,7 +50,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]
diff --git a/tracking/btree.py b/tracking/btree.py
index 132fc06..3325325 100644
--- a/tracking/btree.py
+++ b/tracking/btree.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 @@
"""
ZODB-/BTree-based implementation of user interaction tracking.
-
-$Id$
"""
import time
@@ -142,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):
@@ -237,6 +236,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
@@ -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,
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.
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 @@
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