PloneSurvey/ 0000755 0000765 0000120 00000000000 11150233745 015110 5 ustar michaeldavis admin 0000000 0000000 PloneSurvey/__init__.py 0000644 0000765 0000120 00000001606 10704113530 017215 0 ustar michaeldavis admin 0000000 0000000 import os, os.path
from Globals import package_home
from Products.Archetypes.public import process_types, listTypes
from Products.CMFCore import utils
from Products.CMFCore.DirectoryView import registerDirectory
from Products.CMFPlone.interfaces import IPloneSiteRoot
from Products.GenericSetup import EXTENSION, profile_registry
from config import SKINS_DIR, GLOBALS, PROJECTNAME
from config import ADD_CONTENT_PERMISSION
registerDirectory(SKINS_DIR, GLOBALS)
def initialize(context):
import Products.PloneSurvey.content
content_types, constructors, ftis = process_types(
listTypes(PROJECTNAME),
PROJECTNAME)
utils.ContentInit(
PROJECTNAME + ' Content',
content_types = content_types,
permission = ADD_CONTENT_PERMISSION,
extra_constructors = constructors,
fti = ftis,
).initialize(context)
PloneSurvey/browser/ 0000755 0000765 0000120 00000000000 11150233744 016572 5 ustar michaeldavis admin 0000000 0000000 PloneSurvey/browser/__init__.py 0000644 0000765 0000120 00000000023 10546475144 020710 0 ustar michaeldavis admin 0000000 0000000
# browser package
PloneSurvey/browser/configure.zcml 0000644 0000765 0000120 00000000271 10546475144 021454 0 ustar michaeldavis admin 0000000 0000000
PloneSurvey/config.py 0000644 0000765 0000120 00000010162 10704166730 016732 0 ustar michaeldavis admin 0000000 0000000 from Products.CMFCore import permissions
from Products.Archetypes.utils import DisplayList
from Products.Archetypes.utils import IntDisplayList
from Products.validation import validation
ADD_CONTENT_PERMISSION = permissions.AddPortalContent
PROJECTNAME = "PloneSurvey"
SKINS_DIR = 'skins'
GLOBALS = globals()
try:
from reportlab.lib import colors
except ImportError:
HAS_REPORTLAB = False
else:
HAS_REPORTLAB = True
SURVEY_STATUS = DisplayList((
('open', 'Open', 'label_survey_open'),
('closed', 'Closed', 'label_survey_closed'),
))
NOTIFICATION_METHOD = DisplayList((
('', 'No emails', 'label_no_emails'),
('each_submission', 'Email on each submission', 'label_all_emails'),
))
TEXT_INPUT_TYPE = DisplayList((
('text', 'Text Field', 'label_text_field'),
('area', 'Text Area', 'label_text_area'),
))
SELECT_INPUT_TYPE = DisplayList((
('radio', 'Radio Buttons', 'label_radio_buttons'),
('selectionBox', 'Selection Box', 'label_selection_box'),
('multipleSelect', 'Multiple Selection Box', 'label_multiple_selection_box'),
('checkbox', 'Check Boxes', 'label_check_boxes'),
))
INPUT_TYPE = DisplayList((
('radio', 'Radio Buttons', 'label_radio_buttons'),
('selectionBox', 'Selection Box', 'label_selection_box'),
('text', 'Text Field', 'label_text_field'),
('area', 'Text Area', 'label_text_area'),
('multipleSelect', 'Multiple Selection Box', 'label_multiple_selection_box'),
('checkbox', 'Check Boxes', 'label_check_boxes'),
))
COMMENT_TYPE = DisplayList((
('', 'None', 'label_no_comment_field'),
('text', 'Text Field', 'label_text_field'),
('area', 'Text Area', 'label_text_area'),
))
TWO_D_INPUT_TYPE = DisplayList((
('radio', 'Radio Buttons', 'label_radio_buttons'),
('selectionBox', 'Selection Box', 'label_selection_box'),
))
LIKERT_OPTIONS = IntDisplayList((
(0, 'Use the options below', 'XXX'),
(1, '("Very Good", "Good", "OK Only", "Poor", "Very Poor")', 'XXX'),
(2, '("Very Useful", "Useful", "Quite Useful", "A little Useful", "Not Useful")', 'XXX'),
(3, '("Agree Strongly", "Agree", "Neutral", "Disagree", "Disagree Strongly")', 'XXX'),
))
LIKERT_OPTIONS_MAP = {
1 : IntDisplayList((
(5, 'Very Good', 'XXX'),
(4, 'Good', 'XXX'),
(3, 'OK Only', 'XXX'),
(2, 'Poor', 'XXX'),
(1, 'Very Poor', 'XXX'),
)),
2 : IntDisplayList((
(5, 'Very Useful', 'XXX'),
(4, 'Useful', 'XXX'),
(3, 'Quite Useful', 'XXX'),
(2, 'A little Useful', 'XXX'),
(1, 'Not Useful', 'XXX'),
)),
3 : IntDisplayList((
(5, 'Agree Strongly', 'XXX'),
(4, 'Agree', 'XXX'),
(3, 'Neutral', 'XXX'),
(2, 'Disagree', 'XXX'),
(1, 'Disagree Strongly', 'XXX'),
)),
}
BARCHART_COLORS = ['barchart_blue.gif',
'barchart_green.gif',
'barchart_red.gif',
'barchart_yellow.gif',
'barchart_cyan.gif',
'barchart_magneta.gif']
VALIDATORS = validation.keys()
# remove non useful validators
if 'isEmpty' in VALIDATORS:
VALIDATORS.remove('isEmpty')
if 'isValidId' in VALIDATORS:
VALIDATORS.remove('isValidId')
if 'checkImageMaxSize' in VALIDATORS:
VALIDATORS.remove('checkImageMaxSize')
if 'checkNewsImageMaxSize' in VALIDATORS:
VALIDATORS.remove('checkNewsImageMaxSize')
if 'isMaxSize' in VALIDATORS:
VALIDATORS.remove('isMaxSize')
if 'isTAL' in VALIDATORS:
VALIDATORS.remove('isTAL')
if 'checkFileMaxSize' in VALIDATORS:
VALIDATORS.remove('checkFileMaxSize')
if 'isNonEmptyFile' in VALIDATORS:
VALIDATORS.remove('isNonEmptyFile')
if 'isEmptyNoError' in VALIDATORS:
VALIDATORS.remove('isEmptyNoError')
if 'isTidyHtml' in VALIDATORS:
VALIDATORS.remove('isTidyHtml')
if 'isUnixLikeName' in VALIDATORS:
VALIDATORS.remove('isUnixLikeName')
if 'isTidyHtmlWithCleanup' in VALIDATORS:
VALIDATORS.remove('isTidyHtmlWithCleanup')
if 'inNumericRange' in VALIDATORS:
VALIDATORS.remove('inNumericRange')
if 'isPrintable' in VALIDATORS:
VALIDATORS.remove('isPrintable')
TEXT_VALIDATORS = VALIDATORS
PloneSurvey/configure.zcml 0000644 0000765 0000120 00000000476 10704113530 017760 0 ustar michaeldavis admin 0000000 0000000
PloneSurvey/content/ 0000755 0000765 0000120 00000000000 11150233745 016562 5 ustar michaeldavis admin 0000000 0000000 PloneSurvey/content/__init__.py 0000644 0000765 0000120 00000000307 10704674344 020703 0 ustar michaeldavis admin 0000000 0000000 import Survey
import SubSurvey
import SurveyMatrix
import SurveyMatrixQuestion
import SurveySelectQuestion
import SurveyTextQuestion
#import SurveyTwoDimensional
#import SurveyTwoDimensionalQuestion
PloneSurvey/content/BaseQuestion.py 0000644 0000765 0000120 00000025411 10704674344 021551 0 ustar michaeldavis admin 0000000 0000000 from Globals import InitializeClass
from AccessControl import ClassSecurityInfo
from AccessControl import Unauthorized
from BTrees.OOBTree import OOBTree
from persistent.mapping import PersistentMapping
from Products.Archetypes.atapi import *
from Products.ATContentTypes.content.schemata import ATContentTypeSchema
from Products.ATContentTypes.content.base import ATCTContent
from Products.CMFCore.utils import getToolByName
from Products.PloneSurvey import permissions
from Products.PloneSurvey.config import COMMENT_TYPE
BaseQuestionSchema = ATContentTypeSchema.copy() + Schema((
BooleanField('required',
searchable=0,
required=0,
default=1,
widget=BooleanWidget(
label="Required",
label_msgid="label_required",
description="Select if this question is required, meaning participant must give a response.",
description_msgid="help_required",
i18n_domain="plonesurvey",
),
),
StringField('commentType',
schemata="Comment Field",
searchable=0,
required=0,
vocabulary=COMMENT_TYPE,
widget=SelectionWidget(
label="Comment Type",
label_msgid="label_comment_type",
description="Select what type of comment box you would like.",
description_msgid="help_label_comment_type",
format="select",
i18n_domain="plonesurvey",
)
),
StringField('commentLabel',
schemata="Comment Field",
searchable=0,
required=0,
default="Comment - mandatory if \"no\"",
widget=StringWidget(
label="Comment label",
label_msgid="label_comment_label",
description="The comment label.",
description_msgid="help_comment_label",
i18n_domain="plonesurvey",
)
),
## LinesField('dimensions',
## searchable=0,
## required=0,
## default=[],
## multiValued=1,
## vocabulary='getDimensionsVocab',
## widget=MultiSelectionWidget(
## format='checkbox',
## label="Dimensions",
## label_msgid="label_dimensions",
## description="""Specify the dimensions which apply to this question.""",
## description_msgid="help_dimensions",
## i18n_domain="plonesurvey",),
## index='FieldIndex'
## ),
))
BaseQuestionSchema["title"].widget.label = "Question"
BaseQuestionSchema["description"].widget.label = "Description"
BaseQuestionSchema["description"].widget.label_msgid = "label_question_description"
BaseQuestionSchema["description"].widget.description = "Add a long description of the question here, to clarify any details."
BaseQuestionSchema["description"].widget.description_msgid = "help_question_description"
BaseQuestionSchema["description"].widget.i18n_domain = "plonesurvey"
class BaseQuestion(ATCTContent):
"""Base class for survey questions"""
immediate_view = "base_edit"
global_allow = 0
filter_content_types = 1
allowed_content_types = ()
include_default_actions = 1
_at_rename_after_creation = True
def __init__(self, oid, **kwargs):
self.reset()
BaseContent.__init__(self, oid, **kwargs)
security = ClassSecurityInfo()
security.declareProtected(permissions.ModifyPortalContent, 'reset')
def reset(self):
"""Remove answers for all users."""
self.answers = OOBTree()
security.declareProtected(permissions.ModifyPortalContent, 'resetForUser')
def resetForUser(self, userid):
"""Remove answer for a single user"""
if self.answers.has_key(userid):
del self.answers[userid]
security.declareProtected(permissions.View, 'addAnswer')
def addAnswer(self, value, comments=""):
"""Add an answer and optional comments for a user.
This method protects _addAnswer from anonymous users specifying a
userid when they vote, and thus apparently voting as another user
of their choice.
"""
# Get hold of the parent survey
survey = None
ob = self
while survey is None:
ob = ob.aq_parent
if ob.meta_type == 'Survey':
survey = ob
elif getattr(ob, '_isPortalRoot', False):
raise Exception("Could not find a parent Survey.")
portal_membership = getToolByName(self, 'portal_membership')
if portal_membership.isAnonymousUser() and not survey.getAllowAnonymous():
raise Unauthorized, ("This survey is not available to anonymous users.")
# Use the survey to get hold of the appropriate userid
userid = survey.getSurveyId()
# Call the real method for storing the answer for this user.
return self._addAnswer(userid, value, comments)
def _addAnswer(self, userid, value, comments=""):
"""Add an answer and optional comments for a user."""
# We don't let users over-write answers that they've already made.
# Their first answer must be explicitly 'reset' before another
# answer can be supplied.
# XXX this causes problem when survey fails validation
# will also cause problem with save function
## if self.answers.has_key(userid):
## # XXX Should this get raised? If so, a more appropriate
## # exception is probably in order.
## msg = "User '%s' has already answered this question. Reset the original response to supply a new answer."
## raise Exception(msg % userid)
## else:
self.answers[userid] = PersistentMapping(value=value,
comments=comments)
if not isinstance(self.answers, (PersistentMapping, OOBTree)):
# It must be a standard dictionary from an old install, so
# we need to inform the ZODB about the change manually.
self.answers._p_changed = 1
security.declareProtected(permissions.View, 'getAnswerFor')
def getAnswerFor(self, userid):
"""Get a specific user's answer"""
answer = self.answers.get(userid, {}).get('value', None)
if self.getInputType() in ['multipleSelect', 'checkbox']:
if type(answer) == 'NoneType':
return []
return answer
security.declareProtected(permissions.View, 'getCommentsFor')
def getCommentsFor(self, userid):
"""Get a specific user's comments"""
return self.answers.get(userid, {}).get('comments', None)
security.declareProtected(permissions.View, 'getComments')
def getComments(self):
"""Return a userid, comments mapping"""
mlist = []
for k, v in self.answers.items():
mapping = {}
mapping['userid'] = k
mapping['comments'] = v.get('comments', '')
mlist.append(mapping)
return mlist
security.declareProtected(permissions.View, 'getAnswerOptionsWeights')
def getAnswerOptionsWeights(self):
"""
The accessor ensures that the number of answerOptionsWeights matches
the number of answerOptions. This accessor will be redundant when
answer options become objects.
"""
# Sanitize weights
weights = []
fld = self.getField('answerOptionsWeights')
if fld is not None:
for w in fld.get(self):
try:
i = int(w)
weights.append(i)
except:
weights.append(0)
target_len = len(self.getAnswerOptions())
len_weights = len(weights)
if len_weights > target_len:
return weights[:target_len]
elif len_weights < target_len:
# Pad with zero
weights.extend([0 for i in range(0,target_len - len_weights)])
return weights
return weights
def validate_answerOptionsWeights(self, value):
# Length of value must match length of answerOptions.
# Each element must be a valid integer.
request = self.REQUEST
if request.has_key('answerOptions'):
target_len = len(request.get('answerOptions', []))
else:
target_len = len(self.getAnswerOptions())
if len(value) != target_len:
return "Please enter %s integer values" % target_len
for v in value:
try:
i = int(v)
except:
return "%s is not a valid integer" % v
security.declareProtected(permissions.View, 'getAnswerOptionsAsObjects')
def getAnswerOptionsAsObjects(self):
"""
Assemble answerOptions and answerOptionsWeights into a list
of objects. When answers become objects we can adjust this
method and leave calling code intact.
"""
if not hasattr(self, 'getAnswerOptions'):
return []
class AnswerOptionHelper(BaseObject):
def __init__(self, answeroption, weight, parent):
self.answeroption = answeroption
self.weight = weight
self.parent = parent
def __call__(self):
return self.answeroption
def getWeight(self):
return self.weight
def aq_parent(self):
return self.parent
ret = []
n = 0
weights = self.getAnswerOptionsWeights()
for ao in self.getAnswerOptions():
# Index and conversion errors should not be present thanks to
# validators.
ob = AnswerOptionHelper(ao, int(weights[n]), self)
ret.append(ob)
n += 1
return ret
security.declareProtected(permissions.View, 'getNumberOfRespondents')
def getNumberOfRespondents(self):
return len(self.answers.keys())
security.declareProtected(permissions.View, 'getWeightFor')
def getWeightFor(self, answerOption):
return self.getAnswerOptionsWeights()[list(self.getAnswerOptions()).index(answerOption)]
security.declareProtected(permissions.View, 'getDimensionsVocab')
def getDimensionsVocab(self):
"""Return dimensions of parent"""
parent = self.aq_parent
while parent and (parent.portal_type != 'Survey'):
parent = parent.aq_parent
if parent.portal_type == 'Survey':
return parent.getDimensions()
return []
security.declareProtected(permissions.View, 'getMaxWeight')
def getMaxWeight(self):
"""
If in future we want max weight to be user settable then
calling code already use this method as an 'accessor'
"""
return max([int(w) for w in self.getAnswerOptionsWeights()])
InitializeClass(BaseQuestion)
PloneSurvey/content/configure.zcml 0000644 0000765 0000120 00000001134 10554104110 021417 0 ustar michaeldavis admin 0000000 0000000
PloneSurvey/content/SubSurvey.py 0000644 0000765 0000120 00000015717 10704146727 021125 0 ustar michaeldavis admin 0000000 0000000 import string
from AccessControl import ClassSecurityInfo
from Products.Archetypes.atapi import *
from Products.ATContentTypes.content.schemata import ATContentTypeSchema
from Products.ATContentTypes.content.schemata import finalizeATCTSchema
from Products.ATContentTypes.content.base import ATCTOrderedFolder
from Products.CMFCore.utils import getToolByName
from Products.PloneSurvey import permissions
schema = ATContentTypeSchema.copy() + Schema((
StringField('requiredQuestion',
schemata="Branching",
searchable=0,
required=0,
vocabulary='getValidationQuestions',
widget=SelectionWidget(
format="radio",
label="Conditional Question",
label_msgid="label_previous_question",
description="""The conditional question determines whether to display this Sub Survey.
Select 'None' to display the Sub Survey unconditionally.""",
description_msgid="help_previous_question",
i18n_domain="plonesurvey",
),
),
StringField('requiredAnswer',
schemata="Branching",
searchable=0,
required=0,
vocabulary='getQuestions',
widget=StringWidget(
label="Required Answer",
label_msgid="label_previous_question_answer",
description="""Enter a required answer to the conditional question above to determine
whether this Sub Survey is displayed.""",
description_msgid="help_previous_question_answer",
i18n_domain="plonesurvey",
),
),
BooleanField('requiredAnswerYesNo',
schemata="Branching",
searchable=0,
required=0,
default=1,
widget=BooleanWidget(
label="Use Required Answer?",
label_msgid="label_previous_question_answer_yes_no",
description="Check this box if the required answer should be selected for this Sub Survey to be displayed.",
description_msgid="help_previous_question_answer_yes_no",
i18n_domain="plonesurvey",
),
),
))
finalizeATCTSchema(schema, moveDiscussion=False)
schema["description"].widget.label = "Survey description"
schema["description"].widget.label_msgid = "label_description"
schema["description"].widget.description = "Add a short description of the survey here."
schema["description"].widget.description_msgid = "help_description"
schema["description"].widget.i18n_domain = "plonesurvey"
del schema["relatedItems"]
class SubSurvey(ATCTOrderedFolder):
"""A sub page within a survey"""
schema = schema
_at_rename_after_creation = True
security = ClassSecurityInfo()
security.declarePublic('canSetDefaultPage')
def canSetDefaultPage(self):
"""Doesn't make sense for surveys to allow alternate views"""
return False
security.declarePublic('canConstrainTypes')
def canConstrainTypes(self):
"""Should not be able to add non survey types"""
return False
security.declareProtected(permissions.View, 'isMultipage')
def isMultipage(self):
"""Return true if there is more than one page in the survey"""
return True
security.declareProtected(permissions.ModifyPortalContent, 'getValidationQuestions')
def getValidationQuestions(self):
"""Return the questions for the validation field"""
portal_catalog = getToolByName(self, 'portal_catalog')
questions = [('', 'None')]
path = string.join(self.aq_parent.getPhysicalPath(), '/')
results = portal_catalog.searchResults(portal_type = ['Survey Select Question',],
path = path)
for result in results:
object = result.getObject()
questions.append((object.getId(), object.Title() + ', ' + str(object.getQuestionOptions())))
vocab_list = DisplayList((questions))
return questions
security.declareProtected(permissions.View, 'getBranchingCondition')
def getBranchingCondition(self):
"""Return the title of the branching question"""
branchings = ''
required_question = self.getRequiredQuestion()
branch_question = self[required_question]
branchings = branch_question.Title()+':'+self.getRequiredAnswer()
return branchings
security.declareProtected(permissions.View, 'getQuestions')
def getQuestions(self):
"""Return the questions for this part of the survey"""
questions = self.getFolderContents(
contentFilter={'portal_type':[
'Survey Matrix',
'Survey Select Question',
'Survey Text Question',
'Survey Two Dimensional',
]}, full_objects=True)
return questions
security.declareProtected(permissions.View, 'checkCompleted')
def checkCompleted(self):
"""Return true if this page is completed"""
# XXX
return True
security.declareProtected(permissions.View, 'getNextPage')
def getNextPage(self):
"""Return the next page of the survey"""
parent = self.aq_parent
userid = self.getSurveyId()
pages = parent.getFolderContents(contentFilter={'portal_type':'Sub Survey',}, full_objects=True)
num_pages = len(pages)
for i in range(num_pages):
if pages[i].getId() == self.getId():
current_page = i
while 1==1:
try:
next_page = pages[current_page+1]
except IndexError:
# no next page, so survey finished
parent.setCompletedForUser()
return parent.exitSurvey()
if next_page.getRequiredQuestion():
if not self.getRequiredQuestion():
question = self[next_page.getRequiredQuestion()]
if next_page.getRequiredAnswerYesNo():
if question.getAnswerFor(userid) == next_page.getRequiredAnswer():
return next_page()
else:
if question.getAnswerFor(userid) != next_page.getRequiredAnswer():
return next_page()
else:
if self.getRequiredQuestion() != next_page.getRequiredQuestion():
try:
question = self[next_page.getRequiredQuestion()]
except KeyError:
next_page = pages[current_page+2]
return next_page()
if next_page.getRequiredAnswerYesNo():
if question.getAnswerFor(userid) == next_page.getRequiredAnswer():
return next_page()
else:
if question.getAnswerFor(userid) != next_page.getRequiredAnswer():
return next_page()
else:
return next_page()
current_page += 1
registerType(SubSurvey)
PloneSurvey/content/Survey.py 0000644 0000765 0000120 00000071345 10704674344 020453 0 ustar michaeldavis admin 0000000 0000000 import string
from DateTime import DateTime
from ZODB.POSException import ConflictError
#from zope.interface import implements
from AccessControl import ClassSecurityInfo
from Products.Archetypes.atapi import *
from Products.ATContentTypes.content.schemata import ATContentTypeSchema
from Products.ATContentTypes.content.schemata import finalizeATCTSchema
from Products.ATContentTypes.content.base import ATCTOrderedFolder
from Products.ATContentTypes.lib.constraintypes import ConstrainTypesMixinSchema
# needed for getAnonymousID
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone import PloneMessageFactory
from Products.PluggableAuthService.PluggableAuthService import addPluggableAuthService
from Products.PlonePAS.Extensions.Install import *
from cStringIO import StringIO
from Products.PloneSurvey import permissions
from Products.PloneSurvey.config import SURVEY_STATUS, NOTIFICATION_METHOD, BARCHART_COLORS
#from Products.PloneSurvey.interfaces import ISurvey
schema = ATContentTypeSchema.copy() + ConstrainTypesMixinSchema + Schema((
TextField('body',
searchable = 1,
required=0,
schemata="Introduction",
default_content_type = 'text/html',
default_output_type = 'text/html',
allowable_content_types=('text/plain',
'text/structured',
'text/html',
),
widget = RichWidget(description = "Enter an introduction for the survey.",
label = "Introduction",
label_msgid = 'label_introduction',
description_msgid = 'help_introduction',
rows = 5,
i18n_domain="plonesurvey",
),
),
## LinesField('dimensions',
## searchable=0,
## required=0,
## default=[],
## widget=LinesWidget(
## label="Dimensions",
## label_msgid="label_dimensions",
## description="""Questions can be associated with one or more dimensions.
## Press enter to seperate the options.""",
## description_msgid="help_dimensions",
## i18n_domain="plonesurvey",),
## ),
TextField('thankYouMessage',
required=0,
searchable=0,
default_method="translateThankYouMessage",
widget=TextAreaWidget(
label="'Thank you' message text",
label_msgid="label_thank",
description="""This is the message that will be displayed to the
user when they complete the survey.""",
description_msgid="help_thankyou",
i18n_domain="plonesurvey",
),
),
TextField('savedMessage',
required=0,
searchable=0,
default_method="translateSavedMessage",
widget=TextAreaWidget(
label="'Saved' message test",
label_msgid="label_saved_text",
description="""This is the message that will be displayed to the user
when they save the survey, but don't submit it.""",
description_msgid="help_saved_text",
i18n_domain="plonesurvey",
),
),
StringField('exitUrl',
required=0,
searchable=0,
widget=StringWidget(
label="Exit URL",
label_msgid="label_exit_url",
description="""This is the URL that the user will be directed to on completion of the survey.
Use "http://site.to.go.to/page" or "route/to/page" for this portal""",
description_msgid="help_exit_url",
i18n_domain="plonesurvey",
),
),
BooleanField('confidential',
searchable=0,
required=0,
widget=BooleanWidget(
label="Confidential",
label_msgid="XXX",
description="""Prevent respondents usernames from appearing in results""",
description_msgid="XXX",
i18n_domain="plonesurvey",
),
),
BooleanField('allowAnonymous',
searchable=0,
required=0,
widget=BooleanWidget(
label="Allow Anonymous",
label_msgid="label_allow_anonymous",
i18n_domain="plonesurvey",
),
),
BooleanField('allowSave',
searchable=0,
required=0,
widget=BooleanWidget(
label="Allow Save Functionality",
label_msgid="label_allow_save",
description="Allow logged in users to save survey for finishing later.",
description_msgid="help_allow_save",
i18n_domain="plonesurvey",
),
),
StringField('surveyNotificationEmail',
required=0,
searchable=0,
widget=StringWidget(
label="Survey Notification Email Address",
label_msgid="label_survey_notification_email",
description="Enter an email address to receive notifications of survey completions.",
description_msgid="help_survey_notification_email",
i18n_domain="plonesurvey",
),
),
StringField('surveyNotificationMethod',
required=0,
searchable=0,
vocabulary=NOTIFICATION_METHOD,
widget=SelectionWidget(
label="Survey Notification Method",
label_msgid="label_survey_notification_method",
description="Select a method to receive notification emails.",
description_msgid="help_survey_notification_method",
i18n_domain="plonesurvey",
),
),
StringField('completedFor',
searchable=0,
required=0,
default=[],
widget=StringWidget(visible=0,),
),
))
finalizeATCTSchema(schema, moveDiscussion=False)
schema["description"].widget.label = "Survey description"
schema["description"].widget.label_msgid = "label_description"
schema["description"].widget.description = "Add a short description of the survey here."
schema["description"].widget.description_msgid = "help_description"
schema["description"].widget.i18n_domain = "plonesurvey"
del schema["relatedItems"]
# Dumb class to work around bug in _getPropertyProviderForUser which
# causes it to always operate on portal.acl_users
class BasicPropertySheet:
def __init__(self, sheet):
self._properties = dict(sheet.propertyItems())
def propertyItems(self):
return self._properties.items()
def setProperty(self, id, value):
self._properties[id] = value
class Survey(ATCTOrderedFolder):
"""You can add questions to surveys"""
schema = schema
_at_rename_after_creation = True
#implements(ISurvey)
security = ClassSecurityInfo()
def at_post_create_script(self):
# Create PAS acl_users else login_form does not work
# Re-use code in PlonePAS install
addPluggableAuthService(self)
io = StringIO()
challenge_chooser_setup(self, io)
registerPluginTypes(self.acl_users)
setupPlugins(self, io)
# Recreate mutable_properties but specify fields
uf = self.acl_users
pas = uf.manage_addProduct['PluggableAuthService']
plone_pas = uf.manage_addProduct['PlonePAS']
plone_pas.manage_delObjects('mutable_properties')
plone_pas.manage_addZODBMutablePropertyProvider('mutable_properties',
fullname='', key='')
activatePluginInterfaces(self, 'mutable_properties', io)
security.declarePublic('canSetDefaultPage')
def canSetDefaultPage(self):
"""Doesn't make sense for surveys to allow alternate views"""
return False
security.declarePublic('canConstrainTypes')
def canConstrainTypes(self):
"""Should not be able to add non survey types"""
return False
security.declareProtected(permissions.View, 'isMultipage')
def isMultipage(self):
"""Return true if there is more than one page in the survey"""
if self.getFolderContents(contentFilter={'portal_type':'Sub Survey',}):
return True
security.declareProtected(permissions.View, 'getQuestions')
def getQuestions(self):
"""Return the questions for this part of the survey"""
questions = self.getFolderContents(
contentFilter={'portal_type':[
'Survey Matrix',
'Survey Select Question',
'Survey Text Question',
'Survey Two Dimensional',
]},
full_objects=True)
return questions
security.declareProtected(permissions.View, 'getAllQuestions')
def getAllQuestions(self):
"""Return all the questions in the survey"""
portal_catalog = getToolByName(self, 'portal_catalog')
questions = []
path = string.join(self.getPhysicalPath(), '/')
results = portal_catalog.searchResults(portal_type = ['Survey Matrix Question',
'Survey Select Question',
'Survey Text Question',
'Survey Two Dimensional',],
path = path,
order = 'getObjPositionInParent')
for result in results:
questions.append(result.getObject())
return questions
security.declareProtected(permissions.View, 'getAllQuestionsInOrder')
def getAllQuestionsInOrder(self, include_sub_survey=False):
"""Return all the questions in the survey"""
questions = []
objects = self.getFolderContents(
contentFilter={'portal_type':[
'Sub Survey',
'Survey Matrix',
'Survey Select Question',
'Survey Text Question',
'Survey Two Dimensional',
]},
full_objects=True)
for object in objects:
if object.portal_type == 'Sub Survey':
if include_sub_survey:
questions.append(object)
sub_survey_objects = object.getFolderContents(
contentFilter={'portal_type':[
'Survey Matrix',
'Survey Select Question',
'Survey Text Question',
'Survey Two Dimensional',
]},
full_objects=True)
for sub_survey_object in sub_survey_objects:
questions.append(sub_survey_object)
if sub_survey_object.portal_type == 'Survey Matrix':
survey_matrix_objects = sub_survey_object.getFolderContents(
contentFilter={'portal_type' : 'Survey Matrix Question'},
full_objects=True)
for survey_matrix_object in survey_matrix_objects:
questions.append(survey_matrix_object)
elif sub_survey_object.portal_type == 'Survey Two Dimensional':
survey_2d_objects = sub_survey_object.getFolderContents(
contentFilter={'portal_type' : 'Survey 2-Dimensional Question'},
full_objects=True)
for survey_2d_object in survey_2d_objects:
questions.append(survey_2d_object)
elif object.portal_type == 'Survey Two Dimensional':
questions.append(object)
survey_2d_objects = object.getFolderContents(
contentFilter={'portal_type' : 'Survey 2-Dimensional Question'},
full_objects=True)
for survey_2d_object in survey_2d_objects:
questions.append(survey_2d_object)
# XXX should check if comment is present
elif object.portal_type == 'Survey Matrix':
questions.append(object)
survey_matrix_objects = object.getFolderContents(
contentFilter={'portal_type' : 'Survey Matrix Question'},
full_objects=True)
for survey_matrix_object in survey_matrix_objects:
questions.append(survey_matrix_object)
# XXX should check if comment is present
else:
questions.append(object)
return questions
security.declareProtected(permissions.View, 'getNextPage')
def getNextPage(self):
"""Return the next page of the survey"""
pages = self.getFolderContents(contentFilter={'portal_type':'Sub Survey',}, full_objects=True)
current_page = -1
userid = self.getSurveyId()
while 1==1:
try:
next_page = pages[current_page+1]
except IndexError:
# no next page, so survey finished
self.setCompletedForUser()
return self.exitSurvey()
if next_page.getRequiredQuestion():
question = next_page[next_page.getRequiredQuestion()]
if next_page.getRequiredAnswerYesNo():
if question.getAnswerFor(userid) == next_page.getRequiredAnswer():
return next_page()
else:
if question.getAnswerFor(userid) != next_page.getRequiredAnswer():
return next_page()
else:
return next_page()
current_page += 1
security.declareProtected(permissions.View, 'exitSurvey')
def exitSurvey(self):
"""Return the defined exit url"""
exit_url = self.getExitUrl()
if exit_url[:4] != 'http':
self.plone_utils.addPortalMessage(self.getThankYouMessage())
exit_url = self.portal_url() + '/' + exit_url
return self.REQUEST.RESPONSE.redirect(exit_url)
security.declareProtected(permissions.View, 'saveSurvey')
def saveSurvey(self):
"""Return the defined exit url"""
exit_url = self.getExitUrl()
if exit_url[:4] != 'http':
self.plone_utils.addPortalMessage(self.getSavedMessage())
exit_url = self.portal_url() + '/' + exit_url
return self.REQUEST.RESPONSE.redirect(exit_url)
security.declareProtected(permissions.View, 'setCompletedForUser')
def setCompletedForUser(self):
"""Set completed for a user"""
userid = self.getSurveyId()
completed = self.getCompletedFor()
completed.append(userid)
self.setCompletedFor(completed)
# Scramble respondent's password because we don't want him back
acl_users = self.get_acl_users()
user = acl_users.getUserById(userid)
if user is not None:
portal_registration = getToolByName(self, 'portal_registration')
pw = portal_registration.generatePassword()
self.acl_users.userFolderEditUser(userid, pw, user.getRoles(), user.getDomains(), key=pw)
# Set key
props = acl_users.mutable_properties.getPropertiesForUser(user)
props = BasicPropertySheet(props)
props.setProperty('key', pw)
acl_users.mutable_properties.setPropertiesForUser(user, props)
if self.getSurveyNotificationMethod() == 'each_submission':
self.send_email(userid)
security.declareProtected(permissions.View, 'checkCompletedFor')
def checkCompletedFor(self, user_id):
"""Check whether a user has completed the survey"""
completed = self.getCompletedFor()
if user_id in completed:
return True
return False
security.declareProtected(permissions.View, 'getSurveyId')
def getSurveyId(self):
"""Return the userid for the survey"""
portal_membership = getToolByName(self, 'portal_membership')
if not portal_membership.isAnonymousUser():
return portal_membership.getAuthenticatedMember().getId()
request = self.REQUEST
response = request.RESPONSE
survey_cookie = self.getId()
if self.getAllowAnonymous() and request.has_key(survey_cookie):
return request.get(survey_cookie, "Anonymous")
survey_id = self.getAnonymousId()
#expires = (DateTime() + 365).toZone('GMT').rfc822() # cookie expires in 1 year (365 days)
response.setCookie(survey_cookie, survey_id, path='/')
return survey_id
security.declareProtected(permissions.View, 'getAnonymousId')
def getAnonymousId(self):
"""returns the id to use for an anonymous user"""
portal_membership = getToolByName(self, 'portal_membership')
if portal_membership.isAnonymousUser() and self.getAllowAnonymous():
if not hasattr(self, 'survey_id_no'):
self.survey_id_no = 0
self.survey_id_no += 1
#return 'anonymous' + str(self.survey_id_no)
return 'Anonymous' + '@' + str(DateTime())
elif portal_membership.isAnonymousUser():
return self.REQUEST.RESPONSE.redirect(self.portal_url()+'/login_form?came_from='+self.absolute_url())
return portal_membership.getAuthenticatedMember().getId()
security.declareProtected(permissions.ModifyPortalContent, 'getRespondents')
def getRespondents(self):
"""Return a list of respondents"""
questions = self.getAllQuestionsInOrder()
users = {}
for question in questions:
for user in question.answers.keys():
users[user] = 1
return users.keys()
security.declareProtected(permissions.ModifyPortalContent, 'getRespondentFullName')
def getRespondentFullName(self, userid):
"""get user. used by results spreadsheets to show fullname"""
portal_membership = getToolByName(self, 'portal_membership')
member = portal_membership.getMemberById(userid)
if member is None:
return
full_name = member.getProperty('fullname')
if full_name:
return full_name
return member.id
security.declareProtected(permissions.ModifyPortalContent, 'getRespondents')
def getAnswersByUser(self, userid):
"""Return a set of answers by user id"""
questions = self.getAllQuestionsInOrder()
answers = {}
for question in questions:
answer = question.getAnswerFor(userid)
answers[question.getId()] = answer
return answers
security.declareProtected(permissions.View, 'getQuestionsCount')
def getQuestionsCount(self):
"""Return a count of questions asked"""
return len(self.questions)
security.declareProtected(permissions.View, 'getSurveyColors')
def getSurveyColors(self, num_options):
"""Return the colors for the barchart"""
colors = BARCHART_COLORS
num_colors = len(colors)
while num_colors < num_options:
colors = colors + colors
num_colors = len(colors)
return colors
security.declareProtected(permissions.View, 'buildSpreadsheetUrl')
def buildSpreadsheetUrl(self):
"""Create a filename for the spreadsheets"""
date = DateTime().strftime("%Y-%m-%d")
id = self.getId()
id = "%s-%s" % (id, date)
url = "%s.csv" % id
return url
security.declareProtected(permissions.ResetOwnResponses,
'resetForAuthenticatedUser')
def resetForAuthenticatedUser(self):
mtool = getToolByName(self, 'portal_membership')
member = mtool.getAuthenticatedMember()
user_id = member.getMemberId()
return self.resetForUser(user_id)
security.declareProtected(permissions.ModifyPortalContent,
'resetForUser')
def resetForUser(self, userid):
"""Remove answer for a single user"""
completed = self.getCompletedFor()
if userid in completed:
completed.remove(userid)
self.setCompletedFor(completed)
questions = self.getAllQuestionsInOrder()
for question in questions:
question.resetForUser(userid)
security.declareProtected(permissions.View, 'send_email')
def send_email(self, userid):
""" Send email to nominated address """
properties = self.portal_properties.site_properties
mTo = self.getSurveyNotificationEmail()
mFrom = properties.email_from_address
mSubj = '[%s] New survey submitted' % self.Title()
message = []
message.append('Survey %s.' % self.Title())
message.append('has been completed by user: %s.' % userid)
message.append(self.absolute_url() + '/survey_view_results')
mMsg = '\n\n'.join(message)
try:
self.MailHost.send(mMsg, mTo, mFrom, mSubj)
except ConflictError:
raise
except:
# XXX too many things can go wrong
pass
security.declarePublic('translateThankYouMessage')
def translateThankYouMessage(self):
""" """
return self.translate(msgid="text_default_thank_you",
default="Thank you for completing the survey.",
domain="plonesurvey")
security.declarePublic('translateSavedMessage')
def translateSavedMessage(self):
""" """
return self.translate(msgid="text_default_saved_message",
default="You have saved the survey.\nDon't forget to come back and finish it.",
domain="plonesurvey")
security.declareProtected(permissions.ModifyPortalContent, 'deleteAuthenticatedRespondent')
def deleteAuthenticatedRespondent(self, email, REQUEST=None):
"""Delete authenticated respondent"""
# xxx: delete answers by this user as well?
self.get_acl_users().userFolderDelUsers([email])
if REQUEST is not None:
pu = getToolByName(self, 'plone_utils')
pu.addPortalMessage("Respondent %s deleted" % email)
REQUEST.RESPONSE.redirect(REQUEST.HTTP_REFERER)
security.declareProtected(permissions.ModifyPortalContent, 'addAuthenticatedRespondent')
def addAuthenticatedRespondent(self, emailaddress, **kw):
acl_users = self.get_acl_users()
portal_registration = getToolByName(self, 'portal_registration')
# Create user
password = portal_registration.generatePassword()
acl_users.userFolderAddUser(emailaddress, password, roles=['Member'], domains=[],
groups=())
# Set user properties
user = acl_users.getUserById(emailaddress)
props = acl_users.mutable_properties.getPropertiesForUser(user)
props = BasicPropertySheet(props)
for k,v in kw.items():
props.setProperty(k, v)
props.setProperty('key', password)
acl_users.mutable_properties.setPropertiesForUser(user, props)
security.declareProtected(permissions.ModifyPortalContent, 'getAuthenticatedRespondent')
def getAuthenticatedRespondent(self, emailaddress):
"""
Return dictionary with respondent details. This method is needed because
getProperty is hosed on the user object.
"""
di = {'emailaddress':emailaddress, 'id':emailaddress}
acl_users = self.get_acl_users()
user = acl_users.getUserById(emailaddress)
props = user.getPropertysheet('mutable_properties')
for k,v in props.propertyItems():
di[k] = v
return di
security.declareProtected(permissions.ModifyPortalContent, 'getAuthenticatedRespondents')
def getAuthenticatedRespondents(self):
return [self.getAuthenticatedRespondent(id) for id in self.get_acl_users().getUserNames()]
def get_acl_users(self):
"""Fetch acl_users. Create if it does not yet exist."""
if not 'acl_users' in self.objectIds():
self.at_post_create_script()
return self.acl_users
security.declareProtected(permissions.ModifyPortalContent, 'buildSpreadsheet2')
def buildSpreadsheet2(self):
"""Build spreadsheet 2."""
lines = []
header = '"user",'
for question in self.getAllQuestionsInOrder():
header += '"' + question.Title() + '",'
# if question.getCommentType():
# header += '"' + question.getCommentLabel() + '",'
header += '"completed"'
lines.append(header)
for user in self.getRespondents():
line = []
if self.getRespondentFullName(user):
line.append('"' + self.getRespondentFullName(user) + '"')
else:
line.append('"Anonymous"')
for question in self.getAllQuestionsInOrder():
answer = ""
# handle there being no answer (e.g branched question)
if question.getAnswerFor(user):
answer = question.getAnswerFor(user)
if not (isinstance(answer, str) or isinstance(answer, int)):
answers = ''
for value in answer:
if answers != '':
answers = answers + ', ' + value
else:
answers = value
answer = answers
try:
answer = answer.replace('"',"'")
except:
answer = ""
line.append('"' + str(answer) + '"')
# if question.getCommentType():
# if question.getCommentsFor(user):
# line.append('"' + test(question.getCommentsFor(user), question.getCommentsFor(user).replace('"',"'"), "Blank") + '"')
if self.checkCompletedFor(user):
line.append('"Completed"')
else:
line.append('"Not Completed"')
line = ','.join(line)
lines.append(line)
return lines
security.declareProtected(permissions.ModifyPortalContent, 'buildSpreadsheet3')
def buildSpreadsheet3(self):
"""Build spreadsheet 3."""
lines = []
header = '"user",'
for question in self.get_all_questions_in_order_filtered(ignore_meta_types=['SurveyTwoDimensional','SurveyMatrix']):
header += '"' + question.Title() + '",'
# if question.getCommentType():
# header += '"' + question.getCommentLabel() + '",'
header += '"completed"'
lines.append(header)
for user in self.getRespondents():
line = []
if self.getRespondentFullName(user):
line.append(self.getRespondentFullName(user))
else:
line.append('"Anonymous"')
for question in self.get_all_questions_in_order_filtered(ignore_meta_types=['SurveyTwoDimensional','SurveyMatrix']):
answer = ""
if question.getInputType() in ['text', 'area']:
if question.getAnswerFor(user):
answer = '"' + question.getAnswerFor(user).replace('"',"'") + '"'
else:
answer = ""
elif question.getInputType() in ['checkbox', 'multipleSelect']:
options = question.getQuestionOptions()
answerList = question.getAnswerFor(user)
if answerList and not isinstance(answerList, str):
for option in options:
if answerList.count(option) > 0:
answer += '1;'
else:
answer += '0;'
answer = '"' + answer[0:len(answer)-1] + '"'
elif answerList:
answer = '"' + answerList + '"'
else:
answer = ''
else:
options = question.getQuestionOptions()
answerLabel = question.getAnswerFor(user)
answer = str(len(options))
i = 0
while i < len(options):
if options[i] == answerLabel:
answer = str(i)
break
i = i + 1
line.append(answer)
# if question.getCommentType():
# line.append('"' + test(question.getCommentsFor(user), question.getCommentsFor(user).replace('"',"'"), "Blank") + '"')
if self.checkCompletedFor(user):
line.append('"Completed"')
else:
line.append('"Not Completed"')
line = ','.join(line)
lines.append(line)
return lines
registerType(Survey)
PloneSurvey/content/SurveyMatrix.py 0000644 0000765 0000120 00000011520 10704674344 021625 0 ustar michaeldavis admin 0000000 0000000 import string
from AccessControl import ClassSecurityInfo
from Products.Archetypes.atapi import *
from Products.ATContentTypes.content.base import ATCTOrderedFolder
from Products.ATContentTypes.content.schemata import finalizeATCTSchema
from Products.CMFCore.utils import getToolByName
from Products.PloneSurvey import permissions
from Products.PloneSurvey.config import SELECT_INPUT_TYPE, LIKERT_OPTIONS, LIKERT_OPTIONS_MAP
from Products.PloneSurvey.content.BaseQuestion import BaseQuestion, BaseQuestionSchema
MainSchema = BaseQuestionSchema.copy()
del MainSchema['required']
##del MainSchema['dimensions']
schema = MainSchema + Schema((
IntegerField('likertOptions',
searchable=0,
required=0,
vocabulary=LIKERT_OPTIONS,
widget=SelectionWidget(
label="Likert Options",
label_msgid="XXX",
description="Select a Likert scale to use for options, or use the box below.",
description_msgid="XXX",
i18n_domain="plonesurvey",
),
),
LinesField('answerOptions',
searchable=0,
required=1,
default=("Yes", "No"),
widget=LinesWidget(
label="Answer options",
label_msgid="label_answer_options",
description="""Enter the options you want to be available to the user here.
Press enter to seperate the options.""",
description_msgid="help_answer_options",
i18n_domain="plonesurvey",
),
),
StringField('nullValue',
searchable=0,
required=0,
widget=StringWidget(
label="Null Value",
label_msgid="XXX",
description="""Leave this blank to make the question required, or
enter a value for no response, eg Not applicable""",
description_msgid="XXX",
i18n_domain="plonesurvey",
),
),
## LinesField('answerOptionsWeights',
## searchable=0,
## required=0,
## default=("1", "-1"),
## widget=LinesWidget(
## label="Answer option weights",
## label_msgid="label_answer_options_weights",
## description="""Enter the weight for each answer option.
## Press enter to seperate the weights.""",
## description_msgid="help_answer_options_weights",
## i18n_domain="plonesurvey",
## ),
## ),
StringField('inputType',
searchable=0,
required=0,
vocabulary=SELECT_INPUT_TYPE,
widget=SelectionWidget(
label="Input Type",
label_msgid="label_input_type",
description="Please select what type of input you would like to use for this question.",
description_msgid="help_input_type",
i18n_domain="plonesurvey",
),
),
))
finalizeATCTSchema(schema, moveDiscussion=False)
del schema["relatedItems"]
class SurveyMatrix(ATCTOrderedFolder, BaseQuestion):
"""A matrix of questions within a survey"""
schema = schema
_at_rename_after_creation = True
security = ClassSecurityInfo()
# A matrix doesn't have answers of its own, but it needs to have an
# 'answers' attribute so that it plays properly with getAnswerFor etc.
answers = {}
security.declarePublic('canSetDefaultPage')
def canSetDefaultPage(self):
"""Doesn't make sense for surveys to allow alternate views"""
return False
security.declarePublic('canConstrainTypes')
def canConstrainTypes(self):
"""Should not be able to add non survey types"""
return False
security.declareProtected(permissions.View, 'getRequired')
def getRequired(self):
"""Return 1 or 0 depending on if a null value exists"""
if self.getNullValue():
return 0
else:
return 1
security.declareProtected(permissions.View, 'getQuestionOptions')
def getQuestionOptions(self):
"""Return the options for this question"""
if self.getLikertOptions():
if self.getNullValue():
vocab = LIKERT_OPTIONS_MAP[self.getLikertOptions()]
options = DisplayList()
options.add(0, self.getNullValue())
for item in vocab:
options.add(item, vocab.getValue(item))
return options
return LIKERT_OPTIONS_MAP[self.getLikertOptions()]
return self.getAnswerOptions()
security.declareProtected(permissions.View, 'getQuestions')
def getQuestions(self):
"""Return the questions for this part of the survey"""
questions = self.getFolderContents(
contentFilter={'portal_type':[
'Survey Matrix Question',
]},
full_objects=True)
return questions
registerType(SurveyMatrix)
PloneSurvey/content/SurveyMatrixQuestion.py 0000644 0000765 0000120 00000006674 10704350172 023361 0 ustar michaeldavis admin 0000000 0000000 import string
from AccessControl import ClassSecurityInfo
from Products.Archetypes.atapi import *
from Products.ATContentTypes.content.schemata import finalizeATCTSchema
from Products.CMFCore.utils import getToolByName
from Products.PloneSurvey import permissions
from Products.PloneSurvey.config import SELECT_INPUT_TYPE, BARCHART_COLORS
from Products.PloneSurvey.content.BaseQuestion import BaseQuestion, BaseQuestionSchema
MainSchema = BaseQuestionSchema.copy()
del MainSchema['commentType']
del MainSchema['commentLabel']
del MainSchema['required']
schema = MainSchema
finalizeATCTSchema(schema, moveDiscussion=False)
del schema["relatedItems"]
class SurveyMatrixQuestion(BaseQuestion):
"""A question in a matrix within a survey"""
schema = schema
_at_rename_after_creation = True
security = ClassSecurityInfo()
security.declareProtected(permissions.View, 'getRequired')
def getRequired(self):
"""Return 1 or 0 depending on if a null value exists"""
return self.aq_parent.getRequired()
security.declareProtected(permissions.View, 'getAggregateAnswers')
def getAggregateAnswers(self):
"""Return a mapping of aggregrate answer values,
suitable for a histogram"""
if self.getInputType() in ['area', 'text']:
return {}
aggregate_answers = {}
options = self.getQuestionOptions()
for option in options:
aggregate_answers[option] = 0
for k, answer in self.answers.items():
if answer['value']:
if isinstance(answer['value'], str) or isinstance(answer['value'], int):
try:
aggregate_answers[answer['value']] += 1
except KeyError:
aggregate_answers[answer['value']] = 1
else:
for value in answer['value']:
try:
aggregate_answers[value] += 1
except KeyError:
aggregate_answers[value] = 1
return aggregate_answers
security.declareProtected(permissions.View, 'getPercentageAnswers')
def getPercentageAnswers(self):
"""Return a mapping of aggregrate answer values,
suitable for a barchart"""
max = 0
aggregate_answers = self.getAggregateAnswers()
for k,v in aggregate_answers.items():
if v > max:
max = v
pct_aggregate_answers = {}
for k,v in aggregate_answers.items():
if v == 0:
value = 0
else:
value = v/float(max)
pct_aggregate_answers[k] = int(value * 100)
return pct_aggregate_answers
security.declareProtected(permissions.View, 'getPercentages')
def getPercentages(self):
"""Return a mapping of percentages for each answer"""
total = 0
aggregate_answers = self.getAggregateAnswers()
for k,v in aggregate_answers.items():
total = v + total
pct_answers = {}
for k,v in aggregate_answers.items():
if v == 0:
value = 0
else:
value = v/float(total)
pct_answers[k] = int(value * 100)
return pct_answers
security.declareProtected(permissions.View, 'getAnswerOptionsWeights')
def getAnswerOptionsWeights(self):
return self.aq_parent.getAnswerOptionsWeights()
registerType(SurveyMatrixQuestion)
PloneSurvey/content/SurveySelectQuestion.py 0000644 0000765 0000120 00000014432 10704674344 023335 0 ustar michaeldavis admin 0000000 0000000 from AccessControl import ClassSecurityInfo
from Products.Archetypes.atapi import *
from Products.ATContentTypes.content.schemata import finalizeATCTSchema
from Products.PloneSurvey import permissions
from Products.PloneSurvey.config import SELECT_INPUT_TYPE, LIKERT_OPTIONS, LIKERT_OPTIONS_MAP
from Products.PloneSurvey.content.BaseQuestion import BaseQuestion, BaseQuestionSchema
MainSchema = BaseQuestionSchema.copy()
del MainSchema['required']
schema = MainSchema + Schema((
IntegerField('likertOptions',
searchable=0,
required=0,
vocabulary=LIKERT_OPTIONS,
widget=SelectionWidget(
label="Likert Options",
label_msgid="XXX",
description="Select a Likert scale to use for options, or use the box below.",
description_msgid="XXX",
i18n_domain="plonesurvey",
),
),
LinesField('answerOptions',
searchable=0,
required=0,
default=("Yes", "No"),
widget=LinesWidget(
label="Answer options",
label_msgid="label_answer_options",
description="""Enter the options you want to be available to the user here.
Press enter to seperate the options.""",
description_msgid="help_answer_options",
i18n_domain="plonesurvey",
),
),
StringField('nullValue',
searchable=0,
required=0,
widget=StringWidget(
label="Null Value",
label_msgid="XXX",
description="""Leave this blank to make the question required, or
enter a value for no response, eg Not applicable.
If this is a multiple select or checkbox field,
enter some random text, which will not appear in the survey,
to make this question not required.""",
description_msgid="XXX",
i18n_domain="plonesurvey",
),
),
## LinesField('answerOptionsWeights',
## searchable=0,
## required=0,
## default=("1", "-1"),
## widget=LinesWidget(
## label="Answer option weights",
## label_msgid="label_answer_options_weights",
## description="""Enter the weight for each answer option.
## Press enter to seperate the weights.""",
## description_msgid="help_answer_options_weights",
## i18n_domain="plonesurvey",
## ),
## ),
StringField('inputType',
searchable=0,
required=0,
vocabulary=SELECT_INPUT_TYPE,
widget=SelectionWidget(
label="Input Type",
label_msgid="label_input_type",
description="Please select what type of input you would like to use for this question.",
description_msgid="help_input_type",
i18n_domain="plonesurvey",
),
),
))
finalizeATCTSchema(schema, moveDiscussion=False)
del schema["relatedItems"]
class SurveySelectQuestion(BaseQuestion):
"""A question with select vocab within a survey"""
schema = schema
_at_rename_after_creation = True
security = ClassSecurityInfo()
security.declareProtected(permissions.View, 'getRequired')
def getRequired(self):
"""Return 1 or 0 depending on if a null value exists"""
if self.getNullValue():
return 0
else:
return 1
security.declareProtected(permissions.View, 'getQuestionOptions')
def getQuestionOptions(self):
"""Return the options for this question"""
if self.getLikertOptions():
if self.getNullValue():
vocab = LIKERT_OPTIONS_MAP[self.getLikertOptions()]
options = DisplayList()
options.add(0, self.getNullValue())
for item in vocab:
options.add(item, vocab.getValue(item))
return options
return LIKERT_OPTIONS_MAP[self.getLikertOptions()]
return self.getAnswerOptions()
security.declareProtected(permissions.View, 'getAggregateAnswers')
def getAggregateAnswers(self):
"""Return a mapping of aggregrate answer values,
suitable for a histogram"""
if self.getInputType() in ['area', 'text']:
return {}
aggregate_answers = {}
options = self.getQuestionOptions()
for option in options:
aggregate_answers[option] = 0
for k, answer in self.answers.items():
if answer['value']:
if isinstance(answer['value'], str) or isinstance(answer['value'], int):
try:
aggregate_answers[answer['value']] += 1
except KeyError:
aggregate_answers[answer['value']] = 1
else:
for value in answer['value']:
try:
aggregate_answers[value] += 1
except KeyError:
aggregate_answers[value] = 1
return aggregate_answers
security.declareProtected(permissions.View, 'getPercentageAnswers')
def getPercentageAnswers(self):
"""Return a mapping of aggregrate answer values,
suitable for a barchart"""
max = 0
aggregate_answers = self.getAggregateAnswers()
for k,v in aggregate_answers.items():
if v > max:
max = v
pct_aggregate_answers = {}
for k,v in aggregate_answers.items():
if v == 0:
value = 0
else:
value = v/float(max)
pct_aggregate_answers[k] = int(value * 100)
return pct_aggregate_answers
security.declareProtected(permissions.View, 'getPercentages')
def getPercentages(self):
"""Return a mapping of percentages for each answer"""
total = 0
aggregate_answers = self.getAggregateAnswers()
for k,v in aggregate_answers.items():
total = v + total
pct_answers = {}
for k,v in aggregate_answers.items():
if v == 0:
value = 0
else:
value = v/float(total)
pct_answers[k] = int(value * 100)
return pct_answers
registerType(SurveySelectQuestion)
PloneSurvey/content/SurveyTextQuestion.py 0000644 0000765 0000120 00000007035 10704146727 023042 0 ustar michaeldavis admin 0000000 0000000 from AccessControl import ClassSecurityInfo
#from zope.interface import implements
from Products.Archetypes.atapi import *
from Products.ATContentTypes.content.schemata import finalizeATCTSchema
from Products.validation import validation
from Products.PloneSurvey import permissions
from Products.PloneSurvey.config import TEXT_INPUT_TYPE, TEXT_VALIDATORS
from Products.PloneSurvey.content.BaseQuestion import BaseQuestion, BaseQuestionSchema
#from Products.PloneSurvey.interfaces import ISurveyTextQuestion
MainSchema = BaseQuestionSchema.copy()
del MainSchema['commentType']
del MainSchema['commentLabel']
schema = MainSchema + Schema((
StringField('inputType',
searchable=0,
required=0,
vocabulary=TEXT_INPUT_TYPE,
default='text',
widget=SelectionWidget(
label="Input Type",
label_msgid="label_input_type",
description="Please select what type of input you would like to use for this question.",
description_msgid="help_input_type",
i18n_domain="plonesurvey",
),
),
IntegerField('maxLength',
searchable=0,
required=0,
default=4000,
widget=StringWidget(
label="Maximum length of characters",
label_msgid="XXX",
description="Enter the maximum number of characters a user can enter for this question",
description_msgid="XXX",
i18n_domain="plonesurvey",
),
),
IntegerField('cols',
searchable=0,
required=0,
default=20,
widget=StringWidget(
label="Cols (width in characters)",
label_msgid="label_text_cols",
description="Enter a number of columns for this field (width of the field in the characters)",
description_msgid="help_text_cols",
i18n_domain="plonesurvey",
),
),
IntegerField('rows',
searchable=0,
required=0,
default=6,
widget=StringWidget(
label="Rows (number of lines)",
label_msgid="label_text_rows",
description="Enter a number of rows for this field. This value is applicable only in the Text Area input type",
description_msgid="help_text_rows",
i18n_domain="plonesurvey",
),
),
StringField('validation',
searchable=0,
required=0,
vocabulary='getValidators',
widget=SelectionWidget(
label="Validation",
label_msgid="label_validation",
description="Select a validation for this question",
description_msgid="help_validation",
i18n_domain="plonesurvey",
),
),
))
finalizeATCTSchema(schema, moveDiscussion=False)
del schema["relatedItems"]
class SurveyTextQuestion(BaseQuestion):
"""A textual question within a survey"""
schema = schema
_at_rename_after_creation = True
#implements(ISurveyTextQuestion)
security = ClassSecurityInfo()
security.declareProtected(permissions.View, 'getValidators')
def getValidators(self):
"""Return a list of validators"""
validator_list = ['None', ]
validator_list.extend(TEXT_VALIDATORS)
return validator_list
security.declareProtected(permissions.View, 'getValidators')
def validateQuestion(self, value):
"""Return a list of validators"""
validator = self.getValidation()
v = validation.validatorFor(validator)
return v(value)
registerType(SurveyTextQuestion)
PloneSurvey/content/SurveyTwoDimensional.py 0000644 0000765 0000120 00000005432 10704647431 023317 0 ustar michaeldavis admin 0000000 0000000 import string
from AccessControl import ClassSecurityInfo
from Products.Archetypes.atapi import *
from Products.ATContentTypes.content.schemata import finalizeATCTSchema
from Products.CMFCore.utils import getToolByName
from Products.PloneSurvey import permissions
from Products.PloneSurvey.config import TWO_D_INPUT_TYPE
from Products.PloneSurvey.content.BaseQuestion import BaseQuestion, BaseQuestionSchema
from Products.PloneSurvey.content.SurveyTwoDimensionalQuestion import SurveyTwoDimensionalQuestion
MainSchema = BaseQuestionSchema.copy()
schema = MainSchema + Schema((
StringField('inputType',
searchable=0,
required=0,
vocabulary=TWO_D_INPUT_TYPE,
default='radio',
widget=SelectionWidget(
label="Input Type",
label_msgid="label_input_type",
description="Please select what type of input you would like to use for this question.",
description_msgid="help_input_type",
i18n_domain="plonesurvey",
),
),
))
finalizeATCTSchema(schema, moveDiscussion=False)
del schema["relatedItems"]
class SurveyTwoDimensional(OrderedBaseFolder, BaseQuestion):
"""A two-dimensional question within a survey"""
schema = schema
_at_rename_after_creation = True
security = ClassSecurityInfo()
# A 2D doesn't have answers of its own, but it needs to have an
# 'answers' attribute so that it plays properly with getAnswerFor etc.
answers = {}
security.declareProtected(permissions.View, 'getAbstract')
def getAbstract(self, **kw):
return self.Description()
security.declareProtected(permissions.ModifyPortalContent, 'setAbstract')
def setAbstract(self, val, **kw):
self.setDescription(val)
security.declareProtected(permissions.View, 'getQuestions')
def getQuestions(self):
"""Return the questions for this part of the survey"""
questions = self.getFolderContents(
contentFilter={'portal_type':[
'Survey 2-Dimensional Question',
]},
full_objects=True)
return questions
def at_post_create_script(self):
"""Create two Survey Select Questions"""
# We can't use invokeFactory here because allowed_content_types is empty
id1 = self.getId()+'-dimension-one'
question = SurveyTwoDimensionalQuestion(id1)
self._setObject(id1, question)
question = self._getOb(id1)
question.edit(title=self.title_or_id() + ' Dimension One')
id2 = self.getId()+'-dimension-two'
question = SurveyTwoDimensionalQuestion(id2)
self._setObject(id2, question)
question = self._getOb(id2)
question.edit(title=self.title_or_id() + ' Dimension Two')
registerType(SurveyTwoDimensional)
PloneSurvey/content/SurveyTwoDimensionalQuestion.py 0000644 0000765 0000120 00000010323 10704674344 025045 0 ustar michaeldavis admin 0000000 0000000 from AccessControl import ClassSecurityInfo
from Products.Archetypes.atapi import *
from Products.ATContentTypes.content.schemata import finalizeATCTSchema
from Products.PloneSurvey import permissions
from Products.PloneSurvey.config import TWO_D_INPUT_TYPE
from Products.PloneSurvey.content.BaseQuestion import BaseQuestion, BaseQuestionSchema
MainSchema = BaseQuestionSchema.copy()
del MainSchema['required']
##MainSchema['dimensions'].widget.visible = {'view':'visible', 'edit':'invisible'}
schema = MainSchema + Schema((
LinesField('answerOptions',
searchable=0,
required=0,
default=("Yes", "No"),
widget=LinesWidget(
label="Answer options",
label_msgid="label_answer_options",
description="""Enter the options you want to be available to the user here.
Press enter to seperate the options.""",
description_msgid="help_answer_options",
i18n_domain="plonesurvey",
),
),
## LinesField('answerOptionsWeights',
## searchable=0,
## required=0,
## default=("1", "-1"),
## widget=LinesWidget(
## label="Answer option weights",
## label_msgid="label_answer_options_weights",
## description="""Enter the weight for each answer option.
## Press enter to seperate the weights.""",
## description_msgid="help_answer_options_weights",
## i18n_domain="plonesurvey",
## ),
## ),
))
finalizeATCTSchema(schema, moveDiscussion=False)
del schema["relatedItems"]
class SurveyTwoDimensionalQuestion(BaseQuestion):
"""A two-dimensional question with a weighted vocab within a survey"""
schema = schema
_at_rename_after_creation = True
security = ClassSecurityInfo()
security.declareProtected(permissions.View, 'getRequired')
def getRequired(self):
"""Use parent's value"""
return self.aq_parent.getRequired()
security.declareProtected(permissions.View, 'getInputType')
def getInputType(self):
"""Use parent's value"""
return self.aq_parent.getInputType()
security.declareProtected(permissions.View, 'getDimensions')
def getDimensions(self):
"""Use parent's value"""
return self.aq_parent.getDimensions()
security.declareProtected(permissions.View, 'getQuestionOptions')
def getQuestionOptions(self):
"""Return the options for this question"""
return self.getAnswerOptions()
security.declareProtected(permissions.View, 'getAggregateAnswers')
def getAggregateAnswers(self):
"""Return a mapping of aggregrate answer values,
suitable for a histogram"""
if self.getInputType() in ['area', 'text']:
return {}
aggregate_answers = {}
options = self.getAnswerOptions()
for option in options:
aggregate_answers[option] = 0
for k, answer in self.answers.items():
if answer['value']:
if isinstance(answer['value'], str):
try:
aggregate_answers[answer['value']] += 1
except KeyError:
aggregate_answers[answer['value']] = 1
else:
for value in answer['value']:
try:
aggregate_answers[value] += 1
except KeyError:
aggregate_answers[value] = 1
return aggregate_answers
security.declareProtected(permissions.View, 'getPercentageAnswers')
def getPercentageAnswers(self):
"""Return a mapping of aggregrate answer values,
suitable for a barchart"""
max = 0
aggregate_answers = self.getAggregateAnswers()
for k,v in aggregate_answers.items():
if v > max:
max = v
pct_aggregate_answers = {}
for k,v in aggregate_answers.items():
if v == 0:
value = 0
else:
value = v/float(max)
pct_aggregate_answers[k] = int(value * 100)
return pct_aggregate_answers
registerType(SurveyTwoDimensionalQuestion)
PloneSurvey/credits.txt 0000644 0000765 0000120 00000002720 10704626405 017312 0 ustar michaeldavis admin 0000000 0000000 Credits
Original CMFQuestions Development:
Main development --
Seb Bacon, "Jamkit":http://www.jamkit.com
Sponsored by --
Defense Academy UK
UI tweaks --
Alexander Limi, "Plone Solutions":http://www.plonesolutions.com
Enhancements --
Adam Ullman, "aullman@users.sourceforge.net":mailto:aullman@users.sourceforge.net
PloneSurvey Development:
Migration to Archetypes --
Michael Davis, "Cranfield University":mailto:m.r.davis@cranfield.ac.uk
Product rename, bug testing --
Wendy Chin, "Cranfield University":mailto:w.chin@cranfield.ac.uk
bug fixes, i18n, small improvements, usability
Radim Novotny, "CoreNet Solutions":mailto:radim.novotny@corenet.cz
testing, bug fixes, small improvements --
Jin Tan, "Cranfield University":mailto:J.Tan@cranfield.ac.uk
Large scale testing, minor bug fixes leading to 1.1 release --
Nick Davis, "University of Leicester":mailto:nd51@leicester.ac.uk
ReportLab integration, two dimensional questions, answer weighting, authenticated respondents, survey dimensions
Hedley Roos
i18n testing
Paul Roeland
Translations
Czech Translation
Radim Novotny, "CoreNet Solutions":mailto:radim.novotny@corenet.cz
German translation
Eggert Ehmke, Sven Deichmann
Polish translation and i18n fixs
Piotr Furman - webservice.pl
French translation
Marc Van Coillie
Brasilian Portuguese translation
Lu�s Fl�vio Rocha
Italian translation
Massimo Azzolini
Dutch translation
Pander
PloneSurvey/Extensions/ 0000755 0000765 0000120 00000000000 11150233744 017246 5 ustar michaeldavis admin 0000000 0000000 PloneSurvey/Extensions/extractI18Nmsgids.py 0000644 0000765 0000120 00000003674 10450304362 023110 0 ustar michaeldavis admin 0000000 0000000 # This method is used only for generating msgids for manual.pot
# Should NOT be used by users, only by developers with i18n knowledge.
def extractI18Nmsgids(self):
from Products.PloneSurvey.content.Survey import Survey
from Products.PloneSurvey.content.SubSurvey import SubSurvey
from Products.PloneSurvey.content.SurveyLikertQuestion import SurveyLikertQuestion
from Products.PloneSurvey.content.SurveyMatrix import SurveyMatrix
from Products.PloneSurvey.content.SurveyMatrixQuestion import SurveyMatrixQuestion
from Products.PloneSurvey.content.SurveySelectQuestion import SurveySelectQuestion
from Products.PloneSurvey.content.SurveyTextQuestion import SurveyTextQuestion
from Products.PloneSurvey.content.SurveyQuestion import SurveyQuestion
types = [Survey, SubSurvey, SurveyLikertQuestion, SurveyMatrix, SurveyMatrixQuestion,
SurveySelectQuestion, SurveyTextQuestion, SurveyQuestion]
i18ndata = {}
for t in types:
if hasattr(t, 'schema'):
schema = t.schema
for field in schema.keys():
w = schema[field].widget
if hasattr(w, 'i18n_domain'):
if w.i18n_domain == 'plonesurvey':
msgid = w.label_msgid
default = w.label
if msgid and msgid not in i18ndata.keys():
i18ndata[msgid] = default
if hasattr(w, 'description_msgid'):
msgid = w.description_msgid
default = w.description
if msgid and msgid not in i18ndata.keys():
i18ndata[msgid] = default
for msg, default in i18ndata.items():
print "#."
print "# Default: %s" % default
print 'msgid "%s"' % msg
print 'msgstr ""'
print ''
PloneSurvey/Extensions/get_2d_chart.py 0000644 0000765 0000120 00000021366 10704071235 022154 0 ustar michaeldavis admin 0000000 0000000 '''
Call this method on a SurveyTwoDimensional instance. Draws a two
dimensional graph with a point for each answer.
'''
from reportlab.graphics.charts.lineplots import LinePlot
from reportlab.graphics.charts.axes import YValueAxis
from reportlab.graphics.shapes import Drawing, Group, String, Rect
from reportlab.lib import colors
from reportlab.graphics.widgets.markers import makeMarker
from reportlab.graphics.widgets.grids import DoubleGrid, Grid
from reportlab.graphics.charts.legends import Legend, LineLegend
from reportlab.graphics.charts.textlabels import Label
from reportlab.graphics.widgetbase import Widget
class StringWidget(Widget):
"""
This class fits into the Legend plugin architecture.
This class is extensible, so we can add coloured text
in future.
"""
def __init__(self, msg, fontName='Helvetica', fontSize=12):
self.msg = str(msg)
self.fontName = fontName
self.fontSize = fontSize
self.x = 0
self.y = 0
self.width = 0
self.height = 0
def draw(self):
g = Group()
# 0.718 is for Helvetica. xxx: needs work for other fonts
g.add(String(self.x, self.y+1-0.718, self.msg, fontSize=self.fontSize, fontName=self.fontName))
return g
def setupGrid(lineplot):
"""This code from ReportLab lineplots.py"""
sel = lineplot
xva, yva = sel.xValueAxis, sel.yValueAxis
if xva: xva.joinAxis = yva
if yva: yva.joinAxis = xva
yva.setPosition(sel.x, sel.y, sel.height)
yva.configure(sel.data)
# if zero is in chart, put x axis there, otherwise
# use bottom.
xAxisCrossesAt = yva.scale(0)
if ((xAxisCrossesAt > sel.y + sel.height) or (xAxisCrossesAt < sel.y)):
y = sel.y
else:
y = xAxisCrossesAt
xva.setPosition(sel.x, y, sel.width)
xva.configure(sel.data)
back = sel.background
if isinstance(back, Grid):
if back.orientation == 'vertical' and xva._tickValues:
xpos = map(xva.scale, [xva._valueMin] + xva._tickValues)
steps = []
for i in range(len(xpos)-1):
steps.append(xpos[i+1] - xpos[i])
back.deltaSteps = steps
elif back.orientation == 'horizontal' and yva._tickValues:
ypos = map(yva.scale, [yva._valueMin] + yva._tickValues)
steps = []
for i in range(len(ypos)-1):
steps.append(ypos[i+1] - ypos[i])
back.deltaSteps = steps
elif isinstance(back, DoubleGrid):
# Ideally, these lines would not be needed...
back.grid0.x = sel.x
back.grid0.y = sel.y
back.grid0.width = sel.width
back.grid0.height = sel.height
back.grid1.x = sel.x
back.grid1.y = sel.y
back.grid1.width = sel.width
back.grid1.height = sel.height
# some room left for optimization...
if back.grid0.orientation == 'vertical' and xva._tickValues:
xpos = map(xva.scale, [xva._valueMin] + xva._tickValues)
steps = []
for i in range(len(xpos)-1):
steps.append(xpos[i+1] - xpos[i])
back.grid0.deltaSteps = steps
elif back.grid0.orientation == 'horizontal' and yva._tickValues:
ypos = map(yva.scale, [yva._valueMin] + yva._tickValues)
steps = []
for i in range(len(ypos)-1):
steps.append(ypos[i+1] - ypos[i])
back.grid0.deltaSteps = steps
if back.grid1.orientation == 'vertical' and xva._tickValues:
xpos = map(xva.scale, [xva._valueMin] + xva._tickValues)
steps = []
for i in range(len(xpos)-1):
steps.append(xpos[i+1] - xpos[i])
back.grid1.deltaSteps = steps
elif back.grid1.orientation == 'horizontal' and yva._tickValues:
ypos = map(yva.scale, [yva._valueMin] + yva._tickValues)
steps = []
for i in range(len(ypos)-1):
steps.append(ypos[i+1] - ypos[i])
back.grid1.deltaSteps = steps
def run(self):
def weight_sort(a, b):
return cmp(a.getWeight(), b.getWeight())
drawing = Drawing(600, 300)
lc = LinePlot()
# Determine axis dimensions and create data set
maxval = 0
minval = 0
dimension_one_values = []
dimension_two_values = []
dimension_one_answeroptions_as_objects = []
dimension_two_answeroptions_as_objects = []
counter = 0
for question in self.getQuestions():
weights = [int(weight) for weight in question.getAnswerOptionsWeights()]
answeroptions = list(question.getAnswerOptions())
# This is used by the legend. Sort on weight.
if counter == 0:
dimension_one_answeroptions_as_objects = question.getAnswerOptionsAsObjects()
dimension_one_answeroptions_as_objects.sort(weight_sort)
else:
dimension_two_answeroptions_as_objects = question.getAnswerOptionsAsObjects()
dimension_two_answeroptions_as_objects.sort(weight_sort)
# Minmax
lmin = min(weights)
lmax = max(weights)
if lmin < minval:
minval = lmin
if lmax > maxval:
maxval = lmax
# Data
for user, answer in question.answers.items():
value = answer.get('value', None)
weight = None
if value is not None:
# Lookup the integer weight of this answer
if value in answeroptions:
index = answeroptions.index(value)
weight = weights[index]
# Always add to the list. ReportLab deals with None.
if counter == 0:
dimension_one_values.append(weight)
else:
dimension_two_values.append(weight)
counter += 1
# Set minmax
absmax = max(abs(minval), abs(maxval)) * 1.1
lc.xValueAxis.valueMin = -absmax
lc.xValueAxis.valueMax = absmax
lc.yValueAxis.valueMin = -absmax
lc.yValueAxis.valueMax = absmax
# Zip to create data
data = [zip(dimension_one_values, dimension_two_values)]
if not len(data[0]):
return
lc.x = 0
lc.y = 0
# Misc setup
lc.height = 300
lc.width = 300
lc.data = data
lc.joinedLines = 0
lc.fillColor = None
lc.lines[0].strokeColor = colors.red
lc.lines[0].symbol = makeMarker('FilledCircle')
# Add a grid
grid = DoubleGrid()
lc.background = grid
setupGrid(lc)
lc.background = None
# Finetune the grid
grid.grid0.strokeWidth = 0.2
grid.grid1.strokeWidth = 0.2
# Add to drawing else it overwrites the center Y axis
drawing.add(grid)
# Add a Y axis to pass through the origin
yaxis = YValueAxis()
yaxis.setPosition(lc.width/2, 0, lc.height)
yaxis.configure([(0,-absmax),(0,absmax)])
yaxis.strokeColor = colors.blue
drawing.add(yaxis)
# Color X-Axis
lc.xValueAxis.strokeColor = colors.green
drawing.add(lc)
# Legend for Dimension One
drawing.add(String(lc.width+20, lc.height-12, 'Dimension One (X-Axis):',
fontName='Helvetica', fontSize=12, fillColor=colors.green))
legend = Legend()
legend.alignment = 'right'
legend.x = lc.width + 20
legend.y = lc.height - 20
legend.fontName = 'Helvetica'
legend.fontSize = 12
legend.columnMaximum = 7
items = []
for ob in dimension_one_answeroptions_as_objects:
items.append( ( StringWidget(ob.getWeight()), ob() ) )
legend.colorNamePairs = items
drawing.add(legend, 'legend1')
# Legend for Dimension Two
drawing.add(String(lc.width+20, lc.height/2-12, 'Dimension Two (Y-Axis):',
fontName='Helvetica', fontSize=12, fillColor=colors.blue))
legend = Legend()
legend.alignment = 'right'
legend.x = lc.width + 20
legend.y = lc.height/2 - 20
legend.fontName = 'Helvetica'
legend.fontSize = 12
legend.columnMaximum = 7
items = []
for ob in dimension_two_answeroptions_as_objects:
items.append( ( StringWidget(ob.getWeight()), ob() ) )
legend.colorNamePairs = items
drawing.add(legend, 'legend2')
# Write out
data = drawing.asString('png')
request = self.REQUEST
response = request.RESPONSE
response.setHeader('Content-Type', 'image/png')
response.setHeader('Content-Disposition','inline; filename=%s.png' % self.getId())
response.setHeader('Content-Length', len(data))
response.setHeader('Cache-Control', 's-maxage=0')
return data
PloneSurvey/Extensions/Install.py 0000644 0000765 0000120 00000002720 10704125375 021233 0 ustar michaeldavis admin 0000000 0000000 from StringIO import StringIO
from Products.CMFCore.utils import getToolByName
from Products.Archetypes.public import listTypes
from Products.Archetypes.Extensions.utils import installTypes, install_subskin
from Products.PloneSurvey.config import PROJECTNAME, GLOBALS, HAS_REPORTLAB
def install(self):
out = StringIO()
portal_setup = getToolByName(self, 'portal_setup')
portal_setup.setImportContext('profile-Products.PloneSurvey:default')
portal_setup.runAllImportSteps()
# Create External methods
if HAS_REPORTLAB:
if not hasattr(self, 'get_2d_chart'):
em = self.manage_addProduct['ExternalMethod']
em.manage_addExternalMethod('get_2d_chart', 'get_2d_chart', 'PloneSurvey.get_2d_chart', 'run')
if not hasattr(self, 'results_questions'):
em = self.manage_addProduct['ExternalMethod']
em.manage_addExternalMethod('results_questions', 'results_questions', 'PloneSurvey.results_questions', 'run')
if not hasattr(self, 'results_dimensions'):
em = self.manage_addProduct['ExternalMethod']
em.manage_addExternalMethod('results_dimensions', 'results_dimensions', 'PloneSurvey.results_dimensions', 'run')
#self.manage_permission(perms.VIEW_SURVEY_RESULTS_PERMISSION,
# ('Manager', 'Owner'),
# acquire=1)
print >> out, "Successfully installed %s." % PROJECTNAME
return out.getvalue()
PloneSurvey/Extensions/results_dimensions.py 0000644 0000765 0000120 00000003745 10704071235 023561 0 ustar michaeldavis admin 0000000 0000000 '''
Call this method on a Survey instance.
Draws a (stacked) barchart with average scores per dimension.
'''
from reportlab.graphics.charts.barcharts import VerticalBarChart
from reportlab.graphics.shapes import Drawing, Group, String, Rect
from reportlab.lib import colors
from reportlab.graphics.widgets.markers import makeMarker
from reportlab.graphics.charts.legends import Legend, LineLegend
from reportlab.graphics.charts.textlabels import Label
def run(self, include_sub_survey=False, dimensions=[]):
# Build data set
averages = []
category_names = []
for dimension in self.getDimensions():
averages.append(self.get_average_score_for_dimension(
include_sub_survey=include_sub_survey,
dimension=dimension))
category_names.append(dimension)
drawing = Drawing(600, 300)
bc = VerticalBarChart()
bc.x = 20
bc.y = 20
bc.height = 260
bc.width = 580
bc.data = [averages]
bc.categoryAxis.categoryNames = category_names
bc.categoryAxis.labels.fontName = 'Helvetica'
bc.categoryAxis.labels.fontSize = 10
bc.categoryAxis.labels.textAnchor = 'middle'
bc.categoryAxis.visibleTicks = 0
bc.valueAxis.valueMax = 100.0
bc.valueAxis.valueMin = min(averages, 0)
bc.valueAxis.labels.fontName = 'Helvetica'
bc.valueAxis.labels.fontSize = 10
bc.valueAxis.labels.textAnchor = 'middle'
bc.barLabelFormat = '%.0f'
bc.barLabels.dy = 8
bc.barLabels.fontName = 'Helvetica'
bc.barLabels.fontSize = 10
bc.barWidth = len(averages)
bc.fillColor = None
drawing.add(bc)
# Write out
data = drawing.asString('png')
request = self.REQUEST
response = request.RESPONSE
response.setHeader('Content-Type', 'image/png')
response.setHeader('Content-Disposition','inline; filename=%s.png' % self.getId())
response.setHeader('Content-Length', len(data))
response.setHeader('Cache-Control', 's-maxage=0')
return data
PloneSurvey/Extensions/results_questions.py 0000644 0000765 0000120 00000004303 10704071235 023432 0 ustar michaeldavis admin 0000000 0000000 '''
Call this method on a Survey instance.
Draws a (stacked) barchart with average scores.
'''
from reportlab.graphics.charts.barcharts import VerticalBarChart
from reportlab.graphics.shapes import Drawing, Group, String, Rect
from reportlab.lib import colors
from reportlab.graphics.widgets.markers import makeMarker
from reportlab.graphics.charts.legends import Legend, LineLegend
from reportlab.graphics.charts.textlabels import Label
def run(self, include_sub_survey=False, dimensions=[]):
# Build data set
averages = []
category_names = []
# Iterate over questions in survey
counter = 1
for question in self.get_all_questions_in_order_filtered(
include_sub_survey=include_sub_survey,
dimensions=dimensions,
ignore_meta_types=('SurveyMatrix', 'SurveyTwoDimensional'),
ignore_input_types=('text', 'area')):
averages.append(question.get_question_stats()['weighted_average_percentage'])
category_names.append(str(counter))
counter += 1
drawing = Drawing(600, 300)
bc = VerticalBarChart()
bc.x = 20
bc.y = 20
bc.height = 260
bc.width = 580
bc.data = [averages]
bc.categoryAxis.categoryNames = category_names
bc.categoryAxis.labels.fontName = 'Helvetica'
bc.categoryAxis.labels.fontSize = 10
bc.categoryAxis.labels.textAnchor = 'middle'
bc.categoryAxis.visibleTicks = 0
bc.valueAxis.valueMax = 100.0
bc.valueAxis.valueMin = min(averages, 0)
bc.valueAxis.labels.fontName = 'Helvetica'
bc.valueAxis.labels.fontSize = 10
bc.valueAxis.labels.textAnchor = 'middle'
bc.barLabelFormat = '%.0f'
bc.barLabels.dy = 8
bc.barLabels.fontName = 'Helvetica'
bc.barLabels.fontSize = 10
bc.barWidth = len(averages)
bc.fillColor = None
drawing.add(bc)
# Write out
data = drawing.asString('png')
request = self.REQUEST
response = request.RESPONSE
response.setHeader('Content-Type', 'image/png')
response.setHeader('Content-Disposition','inline; filename=%s.png' % self.getId())
response.setHeader('Content-Length', len(data))
response.setHeader('Cache-Control', 's-maxage=0')
return data
PloneSurvey/HISTORY.txt 0000644 0000765 0000120 00000010514 10704674516 017024 0 ustar michaeldavis admin 0000000 0000000 1.2.0 - SVN/unreleased
==================
* Bug fixes, minor improvements etc during Naples Sprint
[Michael Davis, Nick Davis, Paul Roeland]
* ReportLab integration, two dimensional questions, answer weighting, authenticated respondents, survey dimensions
[Hedley Roos]
* Italian translation
[Massimo Azzolini]
* Dutch translation
[Pander]
* Brasilian Portuguese translation
[Lu�s Fl�vio Rocha]
* Update to German translation
[Sven Deichmann]
* Add Likert scale functionality to types
[Michael Davis]
* Sub class types from ATContentTypes
[Michael Davis]
* Implement generic setup
[Michael Davis]
* Remove backward compatibility with 1.0
[Michael Davis]
1.1.0 - 21/12/06
==================
* Fix spreadsheet bugs (see resolved issues in tracker)
[Michael Davis,Nick Davis]
* Remove sub survey from navigation portlet
[Michael Davis]
* Deprecate Survey Likert Question
[Michael Davis]
* Add French translation from Marc Van Coillie
[Michael Davis]
* Add max length for text questions
[Michael Davis]
* Add Polish translation and some i18n fixes
[Piotr Furman]
* Add save functionality
[Michael Davis]
* Convert answers to OOBTree
[Michael Davis]
* Tidy overview template, and add functionality to it
[Michael Davis]
* On the overview template, add links to edit function
[Jin Tan]
* fixed the overview information: sub survey
[Jin Tan]
* Add German po file from Eggert Ehmke
[Jin Tan]
* fixed the overview information: sub survey and branching
[Jin Tan]
* Add overview for user function
[Jin Tan]
* Add method to return questions in correct order
[Jin Tan]
* Remove required field from Survey Matrix and use BaseQuestion abstract
[Jin Tan]
* Don't validate non required fields with no value
[Jin Tan]
* Move getColors to survey root
[davismr]
* Add css file to portal_css
[davismr]
* Add test framework and some basic tests
[davismr]
* Radio buttons and Check boxes are using