merge branch master
This commit is contained in:
commit
f898a2b49e
24 changed files with 4978 additions and 36 deletions
|
@ -257,3 +257,16 @@ hr.space {
|
||||||
apply to column that should drop below previous ones. */
|
apply to column that should drop below previous ones. */
|
||||||
.clear { clear:both; }
|
.clear { clear:both; }
|
||||||
|
|
||||||
|
/* controlling vertical layout */
|
||||||
|
.vlast {margin-bottom:0;padding-bottom:0;}
|
||||||
|
.vspan-1, .vspan-2, .vspan-3, .vspan-4, .vspan-5, .vspan-6, .vspan-7, .vspan-8 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.vspan-1 {height: 105px;}
|
||||||
|
.vspan-2 {height: 230px;}
|
||||||
|
.vspan-3 {height: 355px;}
|
||||||
|
.vspan-4 {height: 480px;}
|
||||||
|
.vspan-5 {height: 605px;}
|
||||||
|
.vspan-6 {height: 730px;}
|
||||||
|
.vspan-7 {height: 855px;}
|
||||||
|
.vspan-8 {height: 980px;}
|
||||||
|
|
BIN
browser/icons/book_next.png
Normal file
BIN
browser/icons/book_next.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 702 B |
BIN
browser/icons/book_previous.png
Normal file
BIN
browser/icons/book_previous.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 680 B |
BIN
browser/icons/ledx.png
Executable file
BIN
browser/icons/ledx.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# Copyright (c) 2011 Helmut Merz helmutm@cy55.de
|
# Copyright (c) 2013 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
|
||||||
|
@ -368,3 +368,4 @@ class IFormManager(IClientManager):
|
||||||
description=_(u'Email address that will be used as sender '
|
description=_(u'Email address that will be used as sender '
|
||||||
'address of confirmation and feedback messages.'),
|
'address of confirmation and feedback messages.'),
|
||||||
required=False,)
|
required=False,)
|
||||||
|
|
||||||
|
|
|
@ -8,3 +8,52 @@ docgen - Document Generation from Result Sets and XML Structures
|
||||||
>>> from cybertools.docgen.base import WordDocument
|
>>> from cybertools.docgen.base import WordDocument
|
||||||
>>> doc = WordDocument(None, TestRequest)
|
>>> doc = WordDocument(None, TestRequest)
|
||||||
|
|
||||||
|
|
||||||
|
Working with MHT Files
|
||||||
|
======================
|
||||||
|
|
||||||
|
>>> import os
|
||||||
|
>>> basePath = os.path.join(os.path.dirname(__file__), 'testing')
|
||||||
|
|
||||||
|
>>> path = os.path.join(basePath, 'test_doc.mht')
|
||||||
|
>>> f = open(path, 'rt')
|
||||||
|
>>> data = f.read()
|
||||||
|
>>> f.close()
|
||||||
|
|
||||||
|
>>> xbody = '''<div class="WordSection1">
|
||||||
|
... <v:shape id="Grafik_x0020_2" o:spid="_x0000_i1025" type="#_x0000_t75"
|
||||||
|
... style="width:320pt;height:240pt;visibility:visible;mso-wrap-style:square">
|
||||||
|
... <v:imagedata src="FB-Besprechungsprotokoll-Dateien/image002.jpg" o:title=""/>
|
||||||
|
... </v:shape>
|
||||||
|
... </div>
|
||||||
|
... '''
|
||||||
|
|
||||||
|
>>> body = '''<div class="WordSection1">
|
||||||
|
... <img src="files/test_image.jpg" />
|
||||||
|
... </div>
|
||||||
|
... '''
|
||||||
|
|
||||||
|
>>> from cybertools.docgen.mht import MHTFile
|
||||||
|
>>> document = MHTFile(data, body)
|
||||||
|
|
||||||
|
>>> imageRefs = document.htmlDoc.getImageRefs()
|
||||||
|
>>> for path in imageRefs:
|
||||||
|
... imagePath = os.path.join(basePath, os.path.basename(path))
|
||||||
|
... f = open(imagePath, 'rt')
|
||||||
|
... imageData = f.read()
|
||||||
|
... f.close()
|
||||||
|
... document.addImage(imageData, path)
|
||||||
|
|
||||||
|
>>> document.insertBody()
|
||||||
|
|
||||||
|
>>> output = document.asString()
|
||||||
|
>>> len(data), len(output)
|
||||||
|
(294996, 336140)
|
||||||
|
|
||||||
|
>>> outPath = os.path.join(basePath, 'out_doc.mht')
|
||||||
|
>>> #f = open(outPath, 'wt')
|
||||||
|
>>> #f.write(document.asString())
|
||||||
|
>>> #f.close()
|
||||||
|
|
||||||
|
>>> #os.unlink(outPath)
|
||||||
|
|
||||||
|
|
0
docgen/document.mht
Executable file → Normal file
0
docgen/document.mht
Executable file → Normal file
130
docgen/mht.py
Normal file
130
docgen/mht.py
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2012 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
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Working with MHT Files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from cStringIO import StringIO
|
||||||
|
import email
|
||||||
|
from PIL import Image
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
|
||||||
|
from cybertools.text.lib.BeautifulSoup import BeautifulSoup, Tag
|
||||||
|
|
||||||
|
|
||||||
|
class MHTFile(object):
|
||||||
|
|
||||||
|
#encoding = 'UTF-8'
|
||||||
|
#encoding = 'ISO8859-15'
|
||||||
|
encoding = 'Windows-1252'
|
||||||
|
bodyMarker = 'lxdoc_body'
|
||||||
|
foldernameSuffix = 'Dateien'
|
||||||
|
indexes = dict(body=1, filelist=-2)
|
||||||
|
|
||||||
|
path = documentName = None
|
||||||
|
|
||||||
|
imageTemplate = ('\n'
|
||||||
|
'Content-Location: %(path)s/%(docname)s-%(suffix)s/%(imgname)s\n'
|
||||||
|
'Content-Transfer-Encoding: base64\n'
|
||||||
|
'Content-Type: %(ctype)s\n\n%(imgdata)s\n\n')
|
||||||
|
|
||||||
|
filelistItemTemplate = ' <o:File HRef=3D"%s"/>\n'
|
||||||
|
filelistPattern =' <o:File HRef=3D"filelist.xml"/>'
|
||||||
|
|
||||||
|
def __init__(self, data, body):
|
||||||
|
self.data = data
|
||||||
|
self.msg = email.message_from_string(data)
|
||||||
|
self.boundary = '--' + self.msg.get_boundary()
|
||||||
|
self.parts = data.split(self.boundary)
|
||||||
|
self.body = body
|
||||||
|
self.htmlDoc = HTMLDoc(body)
|
||||||
|
self.lastImageNum = 0
|
||||||
|
self.imageMappings = {}
|
||||||
|
for idx, part in enumerate(self.msg.walk()):
|
||||||
|
docPath = part['Content-Location']
|
||||||
|
contentType = part.get_content_type()
|
||||||
|
if idx == self.indexes['body']:
|
||||||
|
self.path, docname = os.path.split(docPath)
|
||||||
|
self.documentName, ext = os.path.splitext(docname)
|
||||||
|
if contentType.startswith('image/'):
|
||||||
|
self.lastImageNum += 1
|
||||||
|
#print '###', self.path, self.documentName, self.lastImageNum
|
||||||
|
|
||||||
|
def getImageRefs(self):
|
||||||
|
return self.htmlDoc.getImageRefs()
|
||||||
|
|
||||||
|
def addImage(self, imageData, path):
|
||||||
|
image = Image.open(StringIO(imageData))
|
||||||
|
width, height = image.size
|
||||||
|
contentType, enc = mimetypes.guess_type(path)
|
||||||
|
bp, ext = os.path.splitext(path)
|
||||||
|
self.lastImageNum += 1
|
||||||
|
name = 'image%03i%s' % (self.lastImageNum, ext)
|
||||||
|
self.imageMappings[path] = (name, width, height)
|
||||||
|
flpos = self.indexes['filelist']
|
||||||
|
vars = dict(path=self.path, docname=self.documentName,
|
||||||
|
suffix=self.foldernameSuffix,
|
||||||
|
imgname=name, ctype=contentType,
|
||||||
|
imgdata=base64.encodestring(imageData))
|
||||||
|
content = self. imageTemplate % vars
|
||||||
|
self.parts.insert(flpos, str(content))
|
||||||
|
filelistRep = (self.filelistItemTemplate % name) + self.filelistPattern
|
||||||
|
filelist = self.parts[flpos]
|
||||||
|
self.parts[flpos] = str(filelist.replace(self.filelistPattern, filelistRep))
|
||||||
|
|
||||||
|
|
||||||
|
def insertBody(self):
|
||||||
|
path = '-'.join((self.documentName, self.foldernameSuffix))
|
||||||
|
self.htmlDoc.updateImageRefs(self.imageMappings, path)
|
||||||
|
content = self.htmlDoc.doc.renderContents(self.encoding)
|
||||||
|
bodyIndex = self.indexes['body']
|
||||||
|
baseDocument = self.parts[bodyIndex]
|
||||||
|
self.parts[bodyIndex] = baseDocument.replace(self.bodyMarker,
|
||||||
|
self.quopri(content))
|
||||||
|
|
||||||
|
def asString(self):
|
||||||
|
return self.boundary.join(self.parts)
|
||||||
|
|
||||||
|
def quopri(self, s):
|
||||||
|
return s.replace('="', '=3D"')
|
||||||
|
|
||||||
|
|
||||||
|
class HTMLDoc(object):
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
self.data = data
|
||||||
|
self.doc = BeautifulSoup(data)
|
||||||
|
|
||||||
|
def getImageRefs(self):
|
||||||
|
return [img['src'] for img in self.doc('img')]
|
||||||
|
|
||||||
|
def updateImageRefs(self, mappings, path=''):
|
||||||
|
for img in self.doc('img'):
|
||||||
|
name, width, height = mappings[img['src']]
|
||||||
|
imgdata = Tag(self.doc, 'v:imagedata')
|
||||||
|
imgdata['src'] = '/'.join((path, name))
|
||||||
|
imgdata.isSelfClosing = True
|
||||||
|
img.append(imgdata)
|
||||||
|
del img['src']
|
||||||
|
img['style'] = 'width:%spt;height:%spt' % (width, height)
|
||||||
|
img.isSelfClosing = False
|
||||||
|
img.name='v:shape'
|
||||||
|
|
4399
docgen/testing/test_doc.mht
Normal file
4399
docgen/testing/test_doc.mht
Normal file
File diff suppressed because it is too large
Load diff
BIN
docgen/testing/test_image.jpg
Normal file
BIN
docgen/testing/test_image.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
# Copyright (c) 2012 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 @@
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Access to objects in the file system.
|
Access to objects in the file system.
|
||||||
|
|
||||||
$Id$
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os, stat
|
import os, stat
|
||||||
|
@ -40,7 +38,7 @@ class ReadContainer(ReadContainer):
|
||||||
|
|
||||||
factoryName = 'filesystem'
|
factoryName = 'filesystem'
|
||||||
|
|
||||||
@Lazy
|
@property
|
||||||
def filenames(self):
|
def filenames(self):
|
||||||
return os.listdir(self.address)
|
return os.listdir(self.address)
|
||||||
|
|
||||||
|
@ -86,7 +84,7 @@ class File(File):
|
||||||
data = None
|
data = None
|
||||||
|
|
||||||
def getData(self, num=-1):
|
def getData(self, num=-1):
|
||||||
f = open(self.address, 'r')
|
f = open(self.address, 'rb')
|
||||||
data = f.read(num)
|
data = f.read(num)
|
||||||
f.close()
|
f.close()
|
||||||
return data
|
return data
|
||||||
|
@ -115,10 +113,13 @@ class ContainerFactory(ContainerFactory):
|
||||||
|
|
||||||
class FileFactory(FileFactory):
|
class FileFactory(FileFactory):
|
||||||
|
|
||||||
|
proxyClass = File
|
||||||
|
imageClass = Image
|
||||||
|
|
||||||
def __call__(self, address, **kw):
|
def __call__(self, address, **kw):
|
||||||
contentType = kw.pop('contentType', None)
|
contentType = kw.pop('contentType', None)
|
||||||
width = height = 0
|
width = height = 0
|
||||||
obj = File(address, contentType, **kw)
|
obj = self.proxyClass(address, contentType, **kw)
|
||||||
if not contentType:
|
if not contentType:
|
||||||
data = obj.getData(50)
|
data = obj.getData(50)
|
||||||
contentType, width, height = getImageInfo(data)
|
contentType, width, height = getImageInfo(data)
|
||||||
|
@ -126,7 +127,7 @@ class FileFactory(FileFactory):
|
||||||
name = os.path.basename(address)
|
name = os.path.basename(address)
|
||||||
contentType, encoding = guess_content_type(name, data, '')
|
contentType, encoding = guess_content_type(name, data, '')
|
||||||
if contentType.startswith('image/'):
|
if contentType.startswith('image/'):
|
||||||
return Image(address, contentType=contentType,
|
return self.imageClass(address, contentType=contentType,
|
||||||
width=width, height=height, **kw)
|
width=width, height=height, **kw)
|
||||||
else:
|
else:
|
||||||
obj.contentType = contentType
|
obj.contentType = contentType
|
||||||
|
|
76
knowledge/survey/README.txt
Normal file
76
knowledge/survey/README.txt
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
==========================
|
||||||
|
Surveys and Questionnaires
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Let's first set up a questionaire.
|
||||||
|
|
||||||
|
>>> from cybertools.knowledge.survey.questionnaire import Questionnaire, Question
|
||||||
|
>>> quest = Questionnaire()
|
||||||
|
|
||||||
|
>>> qu01 = Question(quest)
|
||||||
|
>>> qu02 = Question(quest)
|
||||||
|
>>> qu03 = Question(quest)
|
||||||
|
>>> qu01.revertAnswerOptions = True
|
||||||
|
|
||||||
|
|
||||||
|
Question-related Feedback Items
|
||||||
|
===============================
|
||||||
|
|
||||||
|
We now assign result elements with the questions of this questionnaire.
|
||||||
|
|
||||||
|
>>> from cybertools.knowledge.survey.questionnaire import FeedbackItem
|
||||||
|
>>> fi01 = FeedbackItem('fi01')
|
||||||
|
>>> fi02 = FeedbackItem('fi02')
|
||||||
|
>>> fi03 = FeedbackItem('fi03')
|
||||||
|
|
||||||
|
>>> qu01.feedbackItems = {fi01: 0.8, fi03: 0.2}
|
||||||
|
>>> qu02.feedbackItems = {fi01: 0.3, fi02: 0.7, fi03: 0.1}
|
||||||
|
>>> qu03.feedbackItems = {fi01: 0.2, fi03: 0.9}
|
||||||
|
|
||||||
|
|
||||||
|
Responses
|
||||||
|
---------
|
||||||
|
|
||||||
|
>>> from cybertools.knowledge.survey.questionnaire import Response
|
||||||
|
>>> resp01 = Response(quest, 'john')
|
||||||
|
>>> resp01.values = {qu01: 2, qu02: 1, qu03: 4}
|
||||||
|
|
||||||
|
It's possible to leave some of the questions unanswered.
|
||||||
|
|
||||||
|
>>> resp02 = Response(quest, 'john')
|
||||||
|
>>> resp02.values = {qu01: 2, qu03: 4}
|
||||||
|
|
||||||
|
Now let's calculate the result for resp01.
|
||||||
|
|
||||||
|
>>> res = resp01.getResult()
|
||||||
|
>>> for fi, score in res:
|
||||||
|
... print fi.text, score
|
||||||
|
fi03 4.1
|
||||||
|
fi01 2.7
|
||||||
|
fi02 0.7
|
||||||
|
|
||||||
|
>>> res = resp02.getResult()
|
||||||
|
>>> for fi, score in res:
|
||||||
|
... print fi.text, score
|
||||||
|
fi03 4.0
|
||||||
|
fi01 2.4
|
||||||
|
|
||||||
|
Grouped Feedback Items
|
||||||
|
======================
|
||||||
|
|
||||||
|
>>> from cybertools.knowledge.survey.questionnaire import QuestionGroup
|
||||||
|
>>> qugroup = QuestionGroup(quest)
|
||||||
|
>>> quest.questionGroups.append(qugroup)
|
||||||
|
>>> qugroup.questions = [qu01, qu02, qu03]
|
||||||
|
>>> qugroup.feedbackItems = [fi01, fi02, fi03]
|
||||||
|
|
||||||
|
>>> res = resp01.getGroupedResult()
|
||||||
|
>>> for qugroup, fi, score in res:
|
||||||
|
... print fi.text, round(score, 2)
|
||||||
|
fi02 0.58
|
||||||
|
|
||||||
|
>>> res = resp02.getGroupedResult()
|
||||||
|
>>> for qugroup, fi, score in res:
|
||||||
|
... print fi.text, round(score, 2)
|
||||||
|
fi03 0.75
|
||||||
|
|
1
knowledge/survey/__init__.py
Normal file
1
knowledge/survey/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"package cybertools.composer.survey"
|
91
knowledge/survey/interfaces.py
Normal file
91
knowledge/survey/interfaces.py
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013 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
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Interfaces for surveys.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.interface import Interface, Attribute
|
||||||
|
from zope import schema
|
||||||
|
from zope.i18nmessageid import MessageFactory
|
||||||
|
|
||||||
|
_ = MessageFactory('cybertools.knowledge')
|
||||||
|
|
||||||
|
|
||||||
|
class IQuestionnaire(Interface):
|
||||||
|
""" A collection of questions for setting up a survey.
|
||||||
|
"""
|
||||||
|
|
||||||
|
questionGroups = Attribute('An ordered collection of question groups (optional).')
|
||||||
|
questions = Attribute('An ordered collection of questions.')
|
||||||
|
responses = Attribute('A set of responses.')
|
||||||
|
defaultAnswerRange = Attribute('The number of answer options to select from. '
|
||||||
|
'Default value used for questions that do not '
|
||||||
|
'explicitly provide the values attribute.')
|
||||||
|
|
||||||
|
|
||||||
|
class IQuestionGroup(Interface):
|
||||||
|
""" A group of questions within a questionnaire.
|
||||||
|
|
||||||
|
This may be used just for the presentation of questions or for
|
||||||
|
grouped feedback items.
|
||||||
|
"""
|
||||||
|
|
||||||
|
questionnaire = Attribute('The questionnaire this question belongs to.')
|
||||||
|
questions = Attribute('An ordered collection of questions.')
|
||||||
|
feedbackItems = Attribute('An ordered collection of feedback items.')
|
||||||
|
|
||||||
|
|
||||||
|
class IQuestion(Interface):
|
||||||
|
""" A single question within a questionnaire.
|
||||||
|
"""
|
||||||
|
|
||||||
|
text = Attribute('The question asked.')
|
||||||
|
questionnaire = Attribute('The questionnaire this question belongs to.')
|
||||||
|
answerRange = Attribute('The number of answer options to select from.')
|
||||||
|
feedbackItems = Attribute('A mapping with feedback items as keys and '
|
||||||
|
'corresponding relevance factors as values.')
|
||||||
|
revertAnswerOptions = Attribute('Revert the sequence of answer '
|
||||||
|
'options internally so that a high selection gets a low score.')
|
||||||
|
|
||||||
|
|
||||||
|
class IFeedbackItem(Interface):
|
||||||
|
""" Some text (e.g. a recommendation) or some other kind of information
|
||||||
|
that may be deduced from the responses to a questionnaire.
|
||||||
|
"""
|
||||||
|
|
||||||
|
text = Attribute('A text representing this feedback item.')
|
||||||
|
|
||||||
|
|
||||||
|
class IResponse(Interface):
|
||||||
|
""" A set of response values given to the questions of a questionnaire
|
||||||
|
by a single person or party.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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.')
|
||||||
|
|
||||||
|
def getResult():
|
||||||
|
""" Calculate the result for this response.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def getGroupedResult():
|
||||||
|
""" Calculate the result for a questionnaire with grouped feedback items.
|
||||||
|
"""
|
111
knowledge/survey/questionnaire.py
Normal file
111
knowledge/survey/questionnaire.py
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013 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
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Questionnaires, questions and other stuff needed for surveys.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.interface import implements
|
||||||
|
from cybertools.knowledge.survey.interfaces import IQuestionnaire
|
||||||
|
from cybertools.knowledge.survey.interfaces import IQuestionGroup, IQuestion
|
||||||
|
from cybertools.knowledge.survey.interfaces import IFeedbackItem, IResponse
|
||||||
|
|
||||||
|
|
||||||
|
class Questionnaire(object):
|
||||||
|
|
||||||
|
implements(IQuestionnaire)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.questionGroups = []
|
||||||
|
self.questions = []
|
||||||
|
self.responses = []
|
||||||
|
self.defaultAnswerRange = 5
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionGroup(object):
|
||||||
|
|
||||||
|
implements(IQuestionGroup)
|
||||||
|
|
||||||
|
def __init__(self, questionnaire):
|
||||||
|
self.questionnaire = questionnaire
|
||||||
|
self.questions = []
|
||||||
|
self.feedbackItems = []
|
||||||
|
|
||||||
|
|
||||||
|
class Question(object):
|
||||||
|
|
||||||
|
implements(IQuestion)
|
||||||
|
|
||||||
|
_answerRange = None
|
||||||
|
|
||||||
|
def __init__(self, questionnaire, text=u''):
|
||||||
|
self.questionnaire = questionnaire
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
class FeedbackItem(object):
|
||||||
|
|
||||||
|
implements(IFeedbackItem)
|
||||||
|
|
||||||
|
def __init__(self, text=u''):
|
||||||
|
self.text = text
|
||||||
|
|
||||||
|
|
||||||
|
class Response(object):
|
||||||
|
|
||||||
|
implements(IResponse)
|
||||||
|
|
||||||
|
def __init__(self, questionnaire, party):
|
||||||
|
self.questionnaire = questionnaire
|
||||||
|
self.party = party
|
||||||
|
self.values = {}
|
||||||
|
|
||||||
|
def getResult(self):
|
||||||
|
result = {}
|
||||||
|
for question, value in self.values.items():
|
||||||
|
for fi, rf in question.feedbackItems.items():
|
||||||
|
if question.revertAnswerOptions:
|
||||||
|
value = question.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:
|
||||||
|
score = scoreMax = 0.0
|
||||||
|
for qu in qugroup.questions:
|
||||||
|
value = self.values.get(qu)
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
if qu.revertAnswerOptions:
|
||||||
|
value = qu.answerRange - value - 1
|
||||||
|
score += value
|
||||||
|
scoreMax += qu.answerRange - 1
|
||||||
|
if scoreMax > 0.0:
|
||||||
|
relScore = score / scoreMax
|
||||||
|
wScore = relScore * len(qugroup.feedbackItems) - 0.00001
|
||||||
|
result.append((qugroup, qugroup.feedbackItems[int(wScore)], relScore))
|
||||||
|
return result
|
27
knowledge/survey/tests.py
Normal file
27
knowledge/survey/tests.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
#! /usr/bin/python
|
||||||
|
|
||||||
|
"""
|
||||||
|
Tests for the 'cybertools.knowledge.survey' package.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest, doctest
|
||||||
|
from zope.testing.doctestunit import DocFileSuite
|
||||||
|
from cybertools.knowledge.knowing import Knowing
|
||||||
|
|
||||||
|
|
||||||
|
class TestSurvey(unittest.TestCase):
|
||||||
|
"Basic tests for the survey package."
|
||||||
|
|
||||||
|
def testBasicStuff(self):
|
||||||
|
p = Knowing()
|
||||||
|
|
||||||
|
|
||||||
|
def test_suite():
|
||||||
|
flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
|
||||||
|
return unittest.TestSuite((
|
||||||
|
unittest.makeSuite(TestSurvey),
|
||||||
|
DocFileSuite('README.txt', optionflags=flags),
|
||||||
|
))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main(defaultTest='test_suite')
|
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# Copyright (c) 2012 Helmut Merz helmutm@cy55.de
|
# Copyright (c) 2013 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
|
||||||
|
@ -80,6 +80,7 @@ def workItemStates():
|
||||||
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', (), color='lightgreen'),
|
||||||
|
State('finished_x', 'finished', (), color='green'),
|
||||||
# transitions:
|
# transitions:
|
||||||
Transition('plan', 'plan', 'planned'),
|
Transition('plan', 'plan', 'planned'),
|
||||||
Transition('accept', 'accept', 'accepted'),
|
Transition('accept', 'accept', 'accepted'),
|
||||||
|
@ -131,13 +132,16 @@ class WorkItemType(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name, title, description=u'',
|
def __init__(self, name, title, description=u'',
|
||||||
actions=None, fields=None, indicator=None):
|
actions=None, fields=None, indicator=None,
|
||||||
|
delegatedState='delegated', prefillDate=True):
|
||||||
self.name = name
|
self.name = name
|
||||||
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', 'start-end', 'duration-effort')
|
||||||
self.indicator = indicator
|
self.indicator = indicator
|
||||||
|
self.delegatedState = delegatedState
|
||||||
|
self.prefillDate = prefillDate
|
||||||
|
|
||||||
workItemTypes = Jeep((
|
workItemTypes = Jeep((
|
||||||
WorkItemType('work', u'Unit of Work', indicator='work_work'),
|
WorkItemType('work', u'Unit of Work', indicator='work_work'),
|
||||||
|
@ -150,7 +154,13 @@ workItemTypes = Jeep((
|
||||||
actions=('plan', 'accept', 'finish', 'cancel',
|
actions=('plan', 'accept', 'finish', 'cancel',
|
||||||
'modify', 'delegate', 'move', 'close', 'reopen'),
|
'modify', 'delegate', 'move', 'close', 'reopen'),
|
||||||
fields =('deadline',),
|
fields =('deadline',),
|
||||||
indicator='work_deadline')
|
indicator='work_deadline'),
|
||||||
|
WorkItemType('checkup', u'Check-up',
|
||||||
|
actions=('plan', 'accept', 'finish', 'cancel',
|
||||||
|
'modify', 'delegate', 'close', 'reopen'),
|
||||||
|
fields =('deadline', 'start-end',),
|
||||||
|
indicator='work_checkup',
|
||||||
|
delegatedState='closed', prefillDate=False),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
@ -182,7 +192,7 @@ class WorkItem(Stateful, Track):
|
||||||
|
|
||||||
def getWorkItemType(self):
|
def getWorkItemType(self):
|
||||||
name = self.workItemType
|
name = self.workItemType
|
||||||
return name and workItemTypes[name] or None
|
return name and workItemTypes[name] or workItemTypes['work']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def party(self):
|
def party(self):
|
||||||
|
@ -259,7 +269,7 @@ class WorkItem(Stateful, Track):
|
||||||
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)
|
||||||
delegated.state = 'delegated'
|
delegated.state = self.getWorkItemType().delegatedState
|
||||||
delegated.reindex('state')
|
delegated.reindex('state')
|
||||||
new = delegated.createNew('plan', userName, runId=0, **kw)
|
new = delegated.createNew('plan', userName, runId=0, **kw)
|
||||||
new.data['source'] = delegated.name
|
new.data['source'] = delegated.name
|
||||||
|
@ -284,7 +294,8 @@ class WorkItem(Stateful, Track):
|
||||||
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', 'done'):
|
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')
|
||||||
return new
|
return new
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# Copyright (c) 2007 Helmut Merz helmutm@cy55.de
|
# Copyright (c) 2013 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 @@
|
||||||
|
|
||||||
"""
|
"""
|
||||||
State definition implementation.
|
State definition implementation.
|
||||||
|
|
||||||
$Id$
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from zope.interface import implements
|
from zope.interface import implements
|
||||||
|
|
|
@ -90,11 +90,14 @@ class Track(Persistent):
|
||||||
#def getName(self):
|
#def getName(self):
|
||||||
# return self.__name__
|
# return self.__name__
|
||||||
|
|
||||||
def update(self, newData):
|
def update(self, newData, overwrite=False):
|
||||||
if not newData:
|
if not newData:
|
||||||
return
|
return
|
||||||
self.timeStamp = getTimeStamp()
|
self.timeStamp = getTimeStamp()
|
||||||
getParent(self).indexTrack(0, self, 'timeStamp')
|
getParent(self).indexTrack(0, self, 'timeStamp')
|
||||||
|
if overwrite:
|
||||||
|
data = newData
|
||||||
|
else:
|
||||||
data = self.data
|
data = self.data
|
||||||
data.update(newData)
|
data.update(newData)
|
||||||
self.data = data # record change
|
self.data = data # record change
|
||||||
|
@ -196,7 +199,7 @@ class TrackingStorage(BTreeContainer):
|
||||||
return trackId, self.trackNum
|
return trackId, self.trackNum
|
||||||
|
|
||||||
def saveUserTrack(self, taskId, runId, userName, data, update=False,
|
def saveUserTrack(self, taskId, runId, userName, data, update=False,
|
||||||
timeStamp=None):
|
timeStamp=None, overwrite=False):
|
||||||
ts = timeStamp or getTimeStamp()
|
ts = timeStamp or getTimeStamp()
|
||||||
if not runId:
|
if not runId:
|
||||||
runId = self.currentRuns.get(taskId) or self.startRun(taskId)
|
runId = self.currentRuns.get(taskId) or self.startRun(taskId)
|
||||||
|
@ -208,7 +211,7 @@ class TrackingStorage(BTreeContainer):
|
||||||
if update:
|
if update:
|
||||||
track = self.getLastUserTrack(taskId, runId, userName)
|
track = self.getLastUserTrack(taskId, runId, userName)
|
||||||
if track is not None:
|
if track is not None:
|
||||||
return self.updateTrack(track, data)
|
return self.updateTrack(track, data, overwrite)
|
||||||
trackId, trackNum = self.generateTrackId()
|
trackId, trackNum = self.generateTrackId()
|
||||||
track = self.trackFactory(taskId, runId, userName, data)
|
track = self.trackFactory(taskId, runId, userName, data)
|
||||||
track.__parent__ = self
|
track.__parent__ = self
|
||||||
|
@ -219,10 +222,10 @@ class TrackingStorage(BTreeContainer):
|
||||||
self.indexTrack(trackNum, track)
|
self.indexTrack(trackNum, track)
|
||||||
return trackId
|
return trackId
|
||||||
|
|
||||||
def updateTrack(self, track, data):
|
def updateTrack(self, track, data, overwrite=False):
|
||||||
trackId = str(track.__name__)
|
trackId = str(track.__name__)
|
||||||
trackNum = int(trackId)
|
trackNum = int(trackId)
|
||||||
track.update(data)
|
track.update(data, overwrite)
|
||||||
self.indexTrack(trackNum, track)
|
self.indexTrack(trackNum, track)
|
||||||
return trackId
|
return trackId
|
||||||
|
|
||||||
|
|
18
util/date.py
18
util/date.py
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# Copyright (c) 2011 Helmut Merz helmutm@cy55.de
|
# Copyright (c) 2013 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
|
||||||
|
@ -22,6 +22,10 @@ Date and time utilities.
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
try:
|
||||||
|
import pytz
|
||||||
|
except ImportError:
|
||||||
|
pytz = None
|
||||||
|
|
||||||
|
|
||||||
def getTimeStamp():
|
def getTimeStamp():
|
||||||
|
@ -65,3 +69,15 @@ def year(d=None):
|
||||||
if d is None:
|
if d is None:
|
||||||
d = datetime.today()
|
d = datetime.today()
|
||||||
return d.year
|
return d.year
|
||||||
|
|
||||||
|
|
||||||
|
def toLocalTime(d):
|
||||||
|
if pytz is None or not d:
|
||||||
|
return d
|
||||||
|
cet = pytz.timezone('CET')
|
||||||
|
try:
|
||||||
|
if not isinstance(d, datetime):
|
||||||
|
d = datetime(d.year, d.month, d.day)
|
||||||
|
return d.astimezone(cet)
|
||||||
|
except ValueError:
|
||||||
|
return d
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# Copyright (c) 2007 Helmut Merz helmutm@cy55.de
|
# Copyright (c) 2013 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 @@
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Some simple standard formatting routines.
|
Some simple standard formatting routines.
|
||||||
|
|
||||||
$Id$
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from zope.i18n.locales import locales
|
from zope.i18n.locales import locales
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
Basic Formatting Functions
|
Basic Formatting Functions
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
$Id$
|
|
||||||
|
|
||||||
>>> from cybertools.util import format
|
>>> from cybertools.util import format
|
||||||
>>> from datetime import datetime
|
>>> from datetime import datetime
|
||||||
|
|
||||||
|
@ -15,3 +13,5 @@ $Id$
|
||||||
>>> format.formatDate(time, type='dateTime', variant='medium')
|
>>> format.formatDate(time, type='dateTime', variant='medium')
|
||||||
u'21.08.2006 17:37:13'
|
u'21.08.2006 17:37:13'
|
||||||
|
|
||||||
|
>>> format.formatNumber(17.2)
|
||||||
|
u'17,20'
|
||||||
|
|
20
util/html.py
20
util/html.py
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# Copyright (c) 2012 Helmut Merz helmutm@cy55.de
|
# Copyright (c) 2013 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,6 +20,8 @@
|
||||||
Strip HTML tags and other HTML-related utilities.
|
Strip HTML tags and other HTML-related utilities.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from cybertools.text.lib.BeautifulSoup import BeautifulSoup, Comment
|
from cybertools.text.lib.BeautifulSoup import BeautifulSoup, Comment
|
||||||
from cybertools.text.lib.BeautifulSoup import Declaration, NavigableString
|
from cybertools.text.lib.BeautifulSoup import Declaration, NavigableString
|
||||||
|
|
||||||
|
@ -32,9 +34,11 @@ validAttrs = ('align alt border cellpadding cellspacing class colspan '
|
||||||
validStyles = 'font-style font-weight'.split()
|
validStyles = 'font-style font-weight'.split()
|
||||||
validStyleParts = 'border padding'.split()
|
validStyleParts = 'border padding'.split()
|
||||||
|
|
||||||
|
escCommPattern = re.compile(r'<\!--\[if .*?\!\[endif\]-->', re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
def sanitize(value, validTags=validTags, validAttrs=validAttrs,
|
def sanitize(value, validTags=validTags, validAttrs=validAttrs,
|
||||||
validStyles=validStyles):
|
validStyles=validStyles, stripEscapedComments=True):
|
||||||
soup = BeautifulSoup(value)
|
soup = BeautifulSoup(value)
|
||||||
for comment in soup.findAll(text=lambda text: isinstance(text, Comment)):
|
for comment in soup.findAll(text=lambda text: isinstance(text, Comment)):
|
||||||
comment.extract()
|
comment.extract()
|
||||||
|
@ -51,7 +55,10 @@ def sanitize(value, validTags=validTags, validAttrs=validAttrs,
|
||||||
if val:
|
if val:
|
||||||
attrs.append((attr, val))
|
attrs.append((attr, val))
|
||||||
tag.attrs = attrs
|
tag.attrs = attrs
|
||||||
return soup.renderContents().decode('utf8')
|
result = soup.renderContents()
|
||||||
|
if stripEscapedComments:
|
||||||
|
result = escCommPattern.sub(u'', result)
|
||||||
|
return result.decode('utf8')
|
||||||
|
|
||||||
|
|
||||||
def sanitizeStyle(value, validStyles=validStyles):
|
def sanitizeStyle(value, validStyles=validStyles):
|
||||||
|
@ -76,6 +83,13 @@ def checkStyle(k):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def stripComments(value):
|
||||||
|
soup = BeautifulSoup(value)
|
||||||
|
for comment in soup.findAll(text=lambda text: isinstance(text, Comment)):
|
||||||
|
comment.extract()
|
||||||
|
return soup.renderContents().decode('utf8')
|
||||||
|
|
||||||
|
|
||||||
def stripAll(value):
|
def stripAll(value):
|
||||||
value = sanitize(value)
|
value = sanitize(value)
|
||||||
def collectText(tags):
|
def collectText(tags):
|
||||||
|
|
|
@ -2,9 +2,7 @@
|
||||||
Tweaking HTML text
|
Tweaking HTML text
|
||||||
==================
|
==================
|
||||||
|
|
||||||
$Id$
|
>>> from cybertools.util.html import sanitize, stripComments
|
||||||
|
|
||||||
>>> from cybertools.util.html import sanitize
|
|
||||||
|
|
||||||
>>> input = """<html>
|
>>> input = """<html>
|
||||||
... <p class="standard" style="font-size: 200%; font-weight: bold">
|
... <p class="standard" style="font-size: 200%; font-weight: bold">
|
||||||
|
@ -28,6 +26,11 @@ All comments are stripped from the HTML input.
|
||||||
>>> sanitize(input2)
|
>>> sanitize(input2)
|
||||||
u'\n<p>text</p>\n\n<p>text</p>'
|
u'\n<p>text</p>\n\n<p>text</p>'
|
||||||
|
|
||||||
|
It's also possible to remove only the comments from the HTML input.
|
||||||
|
|
||||||
|
>>> stripComments(input2)
|
||||||
|
u'<html>\n<p>text</p>\n\n<p>text</p></html>'
|
||||||
|
|
||||||
It is also possible to strip all HTML tags from the input string.
|
It is also possible to strip all HTML tags from the input string.
|
||||||
|
|
||||||
>>> from cybertools.util.html import stripAll
|
>>> from cybertools.util.html import stripAll
|
||||||
|
|
Loading…
Add table
Reference in a new issue