diff --git a/.gitignore b/.gitignore index 1c8fc89..41ef9e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ *.pyc *.pyo ajax/dojo/* +build/ +dist/ +*.egg-info *.project *.pydevproject *.sublime-project diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..35ae83c --- /dev/null +++ b/MANIFEST.in @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..1a6c5a6 --- /dev/null +++ b/README.md @@ -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. diff --git a/__init__.py b/__init__.py index 38314f3..09b1274 100644 --- a/__init__.py +++ b/__init__.py @@ -1,3 +1,6 @@ -""" -$Id$ -""" +# package cybertools + +# module aliases +import sys +import doctest +sys.modules['zope.testing.doctestunit'] = doctest diff --git a/browser/blue/body.pt b/browser/blue/body.pt index 1389844..f7c5b67 100644 --- a/browser/blue/body.pt +++ b/browser/blue/body.pt @@ -9,7 +9,7 @@
diff --git a/browser/blue/controller.py b/browser/blue/controller.py index b678076..0df524a 100644 --- a/browser/blue/controller.py +++ b/browser/blue/controller.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2011 Helmut Merz helmutm@cy55.de +# Copyright (c) 2014 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -37,9 +37,9 @@ class Controller(BaseController): macros = self.macros presentationMode = self.request.get('liquid.viewmode') == 'presentation' params = [('blue/blue8.css', 'all', 20, False), - ('print.css', 'print', 25, False), ('blue/ie.css', 'all', 30, True), - ('custom.css', 'all', 100, False)] + ('custom.css', 'all', 100, False), + ('print.css', 'print', 200, False),] #if presentationMode: # params.append(('presentation.css', 'all', 30, False)) for id, media, prio, ie in params: diff --git a/browser/controller.py b/browser/controller.py index 827c592..e9d4044 100644 --- a/browser/controller.py +++ b/browser/controller.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 @@ """ Controller for views, templates, macros. - -$Id$ """ from zope import component diff --git a/browser/icons/ledgreenblue.png b/browser/icons/ledgreenblue.png new file mode 100644 index 0000000..a94c48d Binary files /dev/null and b/browser/icons/ledgreenblue.png differ diff --git a/browser/icons/page_copy.png b/browser/icons/page_copy.png new file mode 100644 index 0000000..195dc6d Binary files /dev/null and b/browser/icons/page_copy.png differ diff --git a/browser/icons/page_delete.png b/browser/icons/page_delete.png new file mode 100644 index 0000000..3141467 Binary files /dev/null and b/browser/icons/page_delete.png differ diff --git a/browser/liquid/body.pt b/browser/liquid/body.pt index fcfd3c1..a0761bb 100644 --- a/browser/liquid/body.pt +++ b/browser/liquid/body.pt @@ -7,7 +7,7 @@
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