merge bbmaster + bbmaster2 to new branch 2master

This commit is contained in:
Helmut Merz 2020-03-03 15:22:13 +01:00
commit 3c8edc3e90
42 changed files with 362 additions and 142 deletions

3
.gitignore vendored
View file

@ -1,6 +1,9 @@
*.pyc
*.pyo
ajax/dojo/*
build/
dist/
*.egg-info
*.project
*.pydevproject
*.sublime-project

14
MANIFEST.in Normal file
View file

@ -0,0 +1,14 @@
global-include *.cfg
global-include *.css *.js
global-include *.gif *.jpg *.png
global-include *.html
global-include *.md *.txt
global-include *.mht
global-include *.mo *.po *.pot
global-include *.pt
global-include *.xml
global-include *.zcml
global-include mime.types
graft cybertools/integrator/tests/data
graft cybertools/text/testfiles

7
README.md Normal file
View file

@ -0,0 +1,7 @@
# Introduction
This is a set of utility libraries to be used mainly
with Zope 3 / bluebream and the web application platform
*loops*.
More information: see https://www.cyberconcepts.org.

View file

@ -1,3 +1,6 @@
"""
$Id$
"""
# package cybertools
# module aliases
import sys
import doctest
sys.modules['zope.testing.doctestunit'] = doctest

View file

@ -9,7 +9,7 @@
<div class="top span-6"
metal:define-slot="top">
<a href="#" name="top" metal:define-slot="logo"
tal:attributes="href string:${request/URL/1}"><img class="logo"
tal:attributes="href string:${view/requestUrl/1}"><img class="logo"
src="logo.gif" border="0" alt="Home"
tal:attributes="src string:${resourceBase}logo.gif" /></a>
<div metal:define-slot="top-actions">

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
# 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:

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2011 Helmut Merz helmutm@cy55.de
# Copyright (c) 2016 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
"""
Controller for views, templates, macros.
$Id$
"""
from zope import component

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

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

@ -7,7 +7,7 @@
<div id="global" metal:define-macro="global">
<div class="top" metal:define-slot="top">
<a href="#" name="top" metal:define-slot="logo"
tal:attributes="href string:${request/URL/1}"><img class="logo"
tal:attributes="href string:${view/requestUrl/1}"><img class="logo"
src="logo.gif" border="0" alt="Home"
tal:attributes="src string:${resourceBase}logo.gif" /></a>
<div metal:define-slot="top-actions">

View file

@ -35,7 +35,7 @@
<div id="global">
<div class="top" metal:define-slot="top">
<a href="#" name="top" metal:define-slot="logo"
tal:attributes="href string:${request/URL/1}"><img class="logo"
tal:attributes="href string:${view/requestUrl/1}"><img class="logo"
src="logo.gif" border="0" alt="Home"
tal:attributes="src string:${resourceBase}logo.gif" /></a>
</div>

View file

@ -12,7 +12,7 @@
region="top" splitter="false">
<div class="top" metal:define-slot="top">
<a href="#" name="top" metal:define-slot="logo"
tal:attributes="href string:${request/URL/1}"><img class="logo"
tal:attributes="href string:${view/requestUrl/1}"><img class="logo"
src="logo.gif" border="0" alt="Home"
tal:attributes="src string:${resourceBase}logo.gif" /></a>
</div>

View file

@ -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',)

View file

@ -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)

View file

@ -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.

View file

@ -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))

View file

@ -1,5 +1,5 @@
<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"
tal:define="body view/body;
layout view/context/template">
@ -20,7 +20,7 @@
</tal:js>
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico"
tal:attributes="href string:${view/page/resourceBase}${layout/favicon}" />
<base href="." tal:attributes="href request/URL">
<base href="." tal:attributes="href view/requestUrl">
</head>
<body tal:replace="structure body" />
</html>

View file

@ -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)

View file

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

View file

@ -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)

View file

@ -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]

View file

@ -144,7 +144,11 @@
<metal:upload define-macro="input_fileupload">
<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>

View file

@ -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:

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
# 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

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
# 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

View file

@ -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...}]

View file

@ -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.

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
# 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]

View file

@ -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

View file

@ -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)

View file

@ -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'),

View file

@ -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 <helmutm@cy55.de>\n"
"Language-Team: loops developers <helmutm@cy55.de>\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"

View file

@ -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():

View file

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

View file

@ -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="""

View file

@ -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]

View file

@ -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,

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
# 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.

View file

@ -7,7 +7,7 @@
<div id="global" metal:define-macro="global">
<div class="top" metal:define-slot="top">
<a href="#" name="top" metal:define-slot="logo"
tal:attributes="href string:${request/URL/1}"><img class="logo"
tal:attributes="href string:${view/requestUrl/1}"><img class="logo"
border="0" alt="Home"
tal:attributes="src string:${resourceBase}logo.png" /></a>
<div metal:define-slot="top-actions">

View file

@ -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