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 *.pyc
*.pyo *.pyo
ajax/dojo/* ajax/dojo/*
build/
dist/
*.egg-info
*.project *.project
*.pydevproject *.pydevproject
*.sublime-project *.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 @@
""" # package cybertools
$Id$
""" # module aliases
import sys
import doctest
sys.modules['zope.testing.doctestunit'] = doctest

View file

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

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 # 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 @@
""" """
Controller for views, templates, macros. Controller for views, templates, macros.
$Id$
""" """
from zope import component 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 id="global" metal:define-macro="global">
<div class="top" metal:define-slot="top"> <div class="top" metal:define-slot="top">
<a href="#" name="top" metal:define-slot="logo" <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" src="logo.gif" border="0" alt="Home"
tal:attributes="src string:${resourceBase}logo.gif" /></a> tal:attributes="src string:${resourceBase}logo.gif" /></a>
<div metal:define-slot="top-actions"> <div metal:define-slot="top-actions">

View file

@ -35,7 +35,7 @@
<div id="global"> <div id="global">
<div class="top" metal:define-slot="top"> <div class="top" metal:define-slot="top">
<a href="#" name="top" metal:define-slot="logo" <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" src="logo.gif" border="0" alt="Home"
tal:attributes="src string:${resourceBase}logo.gif" /></a> tal:attributes="src string:${resourceBase}logo.gif" /></a>
</div> </div>

View file

@ -12,7 +12,7 @@
region="top" splitter="false"> region="top" splitter="false">
<div class="top" metal:define-slot="top"> <div class="top" metal:define-slot="top">
<a href="#" name="top" metal:define-slot="logo" <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" src="logo.gif" border="0" alt="Home"
tal:attributes="src string:${resourceBase}logo.gif" /></a> tal:attributes="src string:${resourceBase}logo.gif" /></a>
</div> </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 # 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
@ -27,7 +27,7 @@ from zope.app.container.traversal import ItemTraverser
from zope.interface import Interface, implements from zope.interface import Interface, implements
TraversalRedirector(ItemTraverser): class TraversalRedirector(ItemTraverser):
port = 9083 port = 9083
names = ('ctt', 'sona',) 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 # 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
@ -27,6 +27,7 @@ from zope.interface import Interface, implements
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope import component from zope import component
from zope.event import notify from zope.event import notify
from zope.publisher.http import URLGetter as BaseURLGetter
from zope.publisher.interfaces.browser import IBrowserSkinType from zope.publisher.interfaces.browser import IBrowserSkinType
from cybertools.browser.renderer import CachableRenderer from cybertools.browser.renderer import CachableRenderer
@ -63,6 +64,15 @@ class BodyTemplateView(object):
bodyTemplate = UnboundTemplateFile('liquid/body.pt') 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): class GenericView(object):
index = mainTemplate index = mainTemplate
@ -103,6 +113,10 @@ class GenericView(object):
# this is useful for a top-level page only # this is useful for a top-level page only
return self.index(*args, **kw) return self.index(*args, **kw)
@property
def requestUrl(self):
return URLGetter(self.request)
@Lazy @Lazy
def isAuthenticated(self): def isAuthenticated(self):
return not IUnauthenticatedPrincipal.providedBy(self.request.principal) return not IUnauthenticatedPrincipal.providedBy(self.request.principal)

View file

@ -1,6 +1,6 @@
#-*- coding: UTF-8 -*- #-*- 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 # 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
@ -408,12 +408,13 @@ class IOrderItem(ITrack):
shop = Attribute(u'The shop from which the product is ordered.') shop = Attribute(u'The shop from which the product is ordered.')
order = Attribute(u'The order this order item belongs to.') order = Attribute(u'The order this order item belongs to.')
unitPrice = Attribute(u'The basic unit price for one of the product ' 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.') fullPrice = Attribute(u'The full price for the quantity ordered.')
quantityShipped = Attribute(u'The total quantity that has been shipped ' quantityShipped = Attribute(u'The total quantity that has been shipped '
u'already.') u'already.')
shippingInfo = Attribute(u'A list of mappings, with fields like: ' shippingInfo = Attribute(u'A list of mappings, with fields like: '
u'shippingId, shippingDate, quantity, packageId') u'shippingId, shippingDate, quantity, packageId')
options = Attribute(u'Product options associated with this order item.')
def remove(): def remove():
""" Remove the order item from the order or cart. """ 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 # 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 @@
""" """
Order and order item classes. Order and order item classes.
$Id$
""" """
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
@ -119,6 +117,10 @@ class OrderItems(object):
def add(self, product, party, shop, order='???', run=0, **kw): def add(self, product, party, shop, order='???', run=0, **kw):
kw['shop'] = self.getUid(shop) kw['shop'] = self.getUid(shop)
existing = self.getCart(party, order, shop, run, product=product) 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: if existing:
track = existing[-1] track = existing[-1]
track.modify(track.quantity + kw.get('quantity', 1)) track.modify(track.quantity + kw.get('quantity', 1))

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">
@ -20,7 +20,7 @@
</tal:js> </tal:js>
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" <link rel="shortcut icon" type="image/x-icon" href="favicon.ico"
tal:attributes="href string:${view/page/resourceBase}${layout/favicon}" /> tal:attributes="href string:${view/page/resourceBase}${layout/favicon}" />
<base href="." tal:attributes="href request/URL"> <base href="." tal:attributes="href view/requestUrl">
</head> </head>
<body tal:replace="structure body" /> <body tal:replace="structure body" />
</html> </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 # 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 @@
""" """
Basic view classes for layout-based presentation. Basic view classes for layout-based presentation.
$Id$
""" """
from zope import component from zope import component
@ -27,7 +25,9 @@ from zope.interface import Interface, implements
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope.app.pagetemplate import ViewPageTemplateFile from zope.app.pagetemplate import ViewPageTemplateFile
from zope.app.security.interfaces import IUnauthenticatedPrincipal 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.base import Layout
from cybertools.composer.layout.interfaces import ILayoutManager from cybertools.composer.layout.interfaces import ILayoutManager
from cybertools.composer.layout.interfaces import ILayout, ILayoutInstance from cybertools.composer.layout.interfaces import ILayout, ILayoutInstance
@ -73,6 +73,10 @@ class BaseView(object):
def __call__(self): def __call__(self):
return self.template(self) return self.template(self)
@property
def requestUrl(self):
return URLGetter(self.request)
@Lazy @Lazy
def authenticated(self): def authenticated(self):
return not IUnauthenticatedPrincipal.providedBy(self.request.principal) return not IUnauthenticatedPrincipal.providedBy(self.request.principal)

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

@ -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 # 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
@ -129,6 +129,9 @@ class Field(Component):
def getDisplayValue(self, row): def getDisplayValue(self, row):
return self.getValue(row) return self.getValue(row)
def getExportValue(self, row, format=None, lang=None):
return self.getValue(row)
def getSortValue(self, row): def getSortValue(self, row):
# TODO: consider 'descending' flag # TODO: consider 'descending' flag
return self.getValue(row) return self.getValue(row)

View file

@ -123,13 +123,15 @@ class ResultSet(object):
def __init__(self, context, data, def __init__(self, context, data,
rowFactory=Row, headerRowFactory=GroupHeaderRow, rowFactory=Row, headerRowFactory=GroupHeaderRow,
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.headerRowFactory = headerRowFactory self.headerRowFactory = headerRowFactory
self.sortCriteria = sortCriteria self.sortCriteria = sortCriteria
self.sortDescending = sortDescending
self.queryCriteria = queryCriteria self.queryCriteria = queryCriteria
self.limits = limits self.limits = limits
self.totals = TotalsRow(None, self) self.totals = TotalsRow(None, self)
@ -204,7 +206,8 @@ class ResultSet(object):
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: 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: if self.groupColumns:
res = [] res = []
groupValues = [None for f in self.groupColumns] groupValues = [None for f in self.groupColumns]

View file

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

@ -85,6 +85,8 @@ class SchemaFactory(object):
field = interface[fname] field = interface[fname]
if getattr(field, 'suppress', False): if getattr(field, 'suppress', False):
continue continue
if getattr(field, 'hidden', False):
continue
info = fieldMapping.get(field.__class__) info = fieldMapping.get(field.__class__)
f = createField(field, info) f = createField(field, info)
if self.schemaProcessor is not None: 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 # 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

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
@ -36,6 +36,9 @@ class Questionnaire(object):
self.responses = [] self.responses = []
self.defaultAnswerRange = 5 self.defaultAnswerRange = 5
def getQuestionGroups(self, party):
return self.questionGroups
class QuestionGroup(object): class QuestionGroup(object):
@ -58,12 +61,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,30 +81,68 @@ 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])
def getGroupedResult(self): def getGroupedResult(self):
result = [] result = []
for qugroup in self.questionnaire.questionGroups: for qugroup in self.questionnaire.getQuestionGroups(self.party):
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)
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 mimetypes
import os, re, sys import os, re, sys
from zope import component
from zope.interface import implements 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.media.piltransform import PILTransform
from cybertools.storage.filesystem import FileSystemStorage
TRANSFORM_STATEMENT = re.compile(r"\s*(\+?)([\w]+[\w\d]*)\(([^\)]*)\)\s*") TRANSFORM_STATEMENT = re.compile(r"\s*(\+?)([\w]+[\w\d]*)\(([^\)]*)\)\s*")
@ -41,14 +39,17 @@ DEFAULT_FORMATS = {
"image": "image/jpeg" "image": "image/jpeg"
} }
def parseTransformStatements(txStr): def parseTransformStatements(txStr):
""" Parse statements in transform chain strings.""" """ Parse statements in transform chain strings."""
statements = TRANSFORM_STATEMENT.findall(txStr) statements = TRANSFORM_STATEMENT.findall(txStr)
return statements return statements
def getMimeBasetype(mimetype): def getMimeBasetype(mimetype):
return mimetype.split("/", 1)[0] return mimetype.split("/", 1)[0]
def getMimetypeExt(mimetype): def getMimetypeExt(mimetype):
exts = mimetypes.guess_all_extensions(mimetype) exts = mimetypes.guess_all_extensions(mimetype)
return exts and exts[-1] or "" return exts and exts[-1] or ""

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 # 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
@ -20,8 +20,6 @@
Views for displaying media assets. Views for displaying media assets.
Authors: Johann Schimpf, Erich Seifert. Authors: Johann Schimpf, Erich Seifert.
$Id$
""" """
from logging import getLogger from logging import getLogger
@ -39,6 +37,8 @@ from zope.interface import implements
from cybertools.media.interfaces import IMediaAsset, IFileTransform from cybertools.media.interfaces import IMediaAsset, IFileTransform
from cybertools.storage.filesystem import FileSystemStorage from cybertools.storage.filesystem import FileSystemStorage
logger = getLogger('cybertools.media.piltransform.PILTransform')
def mimetypeToPIL(mimetype): def mimetypeToPIL(mimetype):
return mimetype.split("/",1)[-1] return mimetype.split("/",1)[-1]
@ -55,8 +55,7 @@ class PILTransform(object):
try: try:
self.im = Image.open(path) self.im = Image.open(path)
except IOError, e: except IOError, e:
from logging import getLogger logger.warn(e)
getLogger('cybertools.media.piltransform.PILTransform').warn(e)
self.im = None self.im = None
def rotate(self, angle, resize): def rotate(self, angle, resize):
@ -97,7 +96,7 @@ class PILTransform(object):
box = (left, upper, right, lower) box = (left, upper, right, lower)
self.im = self.im.crop(box) 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: if self.im is None:
return return
if not height: if not height:
@ -105,10 +104,21 @@ class PILTransform(object):
ratio = float(ow) / float(oh) ratio = float(ow) / float(oh)
height = int(round(float(width) / ratio)) height = int(round(float(width) / ratio))
dims = (width, height) 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): def save(self, path, mimetype):
if self.im is None: if self.im is None:
return return
format = mimetypeToPIL(mimetype) format = mimetypeToPIL(mimetype)
try:
self.im.save(path) 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 ' 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"
@ -66,17 +66,31 @@ msgid "End date"
msgstr "Ende" msgstr "Ende"
msgid "Street, number" msgid "Street, number"
msgstr "Straße, Nr." msgstr "Straße, Hausnummer"
msgid "City" msgid "Street and number"
msgstr "Stadt" msgstr "Straße und Hausnummer"
msgid "ZIP code" 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" msgid "Additional lines"
msgstr "Zusätzliche Zeilen" msgstr "Zusätzliche Adresszeilen"
msgid "Additional address lines" 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 # 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,7 @@ 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',
@ -52,14 +52,14 @@ def workItemStates():
'move', 'cancel', 'modify'), 'move', 'cancel', 'modify'),
color='yellow'), color='yellow'),
State('running', 'running', State('running', 'running',
('work', 'finish', 'move', 'cancel', 'modify'), ('work', 'finish', 'move', 'cancel', 'modify'), # 'delegate', # ?
color='orange'), color='orange'),
State('done', 'done', State('done', 'done',
('plan', 'accept', 'start', 'work', 'finish', 'delegate', ('plan', 'accept', 'start', 'work', 'finish', 'delegate',
'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
@ -106,21 +111,21 @@ fieldNames = ['title', 'description', 'deadline', 'start', 'end',
# . default (may be empty) # . default (may be empty)
editingRules = dict( editingRules = dict(
plan = {'*': '+++.....+'}, plan = {'*': '+++++.....+'},
accept = {'*': '+++.....-', accept = {'*': '+++++.....-',
'planned': '+++++++.-', 'planned': '+++++++++.-',
'accepted': '+++++++.-'}, 'accepted': '+++++++++.-'},
start = {'*': '+++./...-'}, start = {'*': '+++++./...-'},
work = {'*': '+++.....-', work = {'*': '+++++.....-',
'running': '++++....-'}, 'running': '++++++....-'},
finish = {'*': '+++.....-', finish = {'*': '+++++.....-',
'running': '++++....-'}, 'running': '++++++....-'},
cancel = {'*': '+++////./'}, cancel = {'*': '+++++////./'},
modify = {'*': '+++++++++'}, modify = {'*': '+++++++++++'},
delegate= {'*': '+++++++.+'}, delegate= {'*': '+++++++++.+'},
move = {'*': '+++++++.-'}, move = {'*': '+++++++++.-'},
close = {'*': '+++////./'}, close = {'*': '+++++////./'},
reopen = {'*': '+++////./'}, reopen = {'*': '+++++////./'},
) )
@ -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
@ -266,6 +279,9 @@ class WorkItem(Stateful, Track):
if self.state in ('planned', 'accepted', 'delegated', 'moved', 'done'): if self.state in ('planned', 'accepted', 'delegated', 'moved', 'done'):
self.state = self.state + '_x' self.state = self.state + '_x'
self.reindex('state') self.reindex('state')
#elif self.state == 'running':
# self.doAction('work', userName,
# end=(kw.get('end') or getTimeStamp()))
xkw = dict(kw) xkw = dict(kw)
xkw.pop('party', None) xkw.pop('party', None)
delegated = self.createNew('delegate', userName, **xkw) delegated = self.createNew('delegate', userName, **xkw)
@ -279,25 +295,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 = 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): 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
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 = self.createNew('move', userName, **xkw)
moved.userName = self.userName moved.userName = self.userName
moved.state = 'moved'
moved.reindex()
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
if self.state == 'new':
new.state = 'planned'
else:
new.state = self.state 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 +363,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 +377,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

@ -1,18 +1,18 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
import sys, os import sys, os
version = '2.0' version = '2.1.4'
setup(name='cybertools', setup(name='cybertools',
version=version, version=version,
description="cybertools", description="cybertools: basic utilities for Zope3/bluebream/loops",
long_description="""\ long_description="""\
""", """,
classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
keywords='', keywords='',
author='Helmut Merz', author='cyberconcepts.org team',
author_email='helmutm@cy55.de', author_email='team@cyberconcepts.org',
url='', url='https://www.cyberconcepts.org',
license='GPL', license='GPL',
packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
include_package_data=True, include_package_data=True,
@ -20,14 +20,11 @@ setup(name='cybertools',
install_requires=[ install_requires=[
# -*- Extra requirements: -*- # -*- Extra requirements: -*-
'lxml', 'lxml',
#'PIL', #'Pillow',
'zope.app.catalog', 'zope.app.catalog',
'zope.app.file', 'zope.app.file',
'zope.app.intid', 'zope.app.intid',
'zope.app.preview',
'zope.app.renderer',
'zope.app.session', 'zope.app.session',
'zope.dublincore',
'zope.sendmail', 'zope.sendmail',
], ],
entry_points=""" entry_points="""

View file

@ -50,7 +50,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) 2016 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
@ -292,7 +292,11 @@ class TrackingStorage(BTreeContainer):
value = [value] value = [value]
resultx = None resultx = None
for v in value: 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) result = self.intersect(result, resultx)
elif idx == 'timeFrom': elif idx == 'timeFrom':
result = self.intersect(result, 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 # 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

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

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