Added Survey Views Helper for rendering several widgets.
Patch by: James Levy, Daniel Diniz, Lennard de Rijk
Reviewed by: Lennard de Rijk
#!/usr/bin/python2.5
#
# Copyright 2009 the Melange authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Custom widgets used for Survey form fields, plus the SurveyContent form.
"""
__authors__ = [
'"Daniel Diniz" <ajaksu@gmail.com>',
'"James Levy" <jamesalexanderlevy@gmail.com>',
'"Lennard de Rijk" <ljvderijk@gmail.com>',
]
from itertools import chain
import datetime
from google.appengine.ext.db import djangoforms
from django import forms
from django.forms import widgets
from django.forms.fields import CharField
from django.template import loader
from django.utils.encoding import force_unicode
from django.utils.html import escape
from django.utils.safestring import mark_safe
from soc.logic import dicts
from soc.logic.lists import Lists
from soc.logic.models.survey import logic as survey_logic
from soc.logic.models.survey import results_logic
from soc.models.survey import SurveyContent
class SurveyForm(djangoforms.ModelForm):
"""Main SurveyContent form.
This class is used to produce survey forms for several circumstances:
- Admin creating survey from scratch
- Admin updating existing survey
- User taking survey
- User updating already taken survey
Using dynamic properties of the survey model (if passed as an arg) the
survey form is dynamically formed.
"""
def __init__(self, *args, **kwargs):
"""Store special kwargs as attributes.
read_only: controls whether the survey taking UI allows data entry.
editing: controls whether to show the edit or show form.
"""
self.kwargs = kwargs
self.survey_content = self.kwargs.get('survey_content', None)
self.this_user = self.kwargs.get('this_user', None)
self.project = self.kwargs.get('project', None)
self.survey_record = self.kwargs.get('survey_record', None)
del self.kwargs['survey_content']
del self.kwargs['this_user']
del self.kwargs['project']
del self.kwargs['survey_record']
self.read_only = self.kwargs.get('read_only', None)
if 'read_only' in self.kwargs:
del self.kwargs['read_only']
self.editing = self.kwargs.get('editing', None)
if 'editing' in self.kwargs:
del self.kwargs['editing']
super(SurveyForm, self).__init__(*args, **self.kwargs)
def getFields(self):
"""Build the SurveyContent (questions) form fields.
Populates self.survey_fields, which will be ordered in self.insert_fields.
"""
if not self.survey_content:
return
self.survey_fields = {}
schema = eval(self.survey_content.schema)
has_record = (not self.editing) and self.survey_record
extra_attrs = {}
# figure out whether we want a read-only view
if not self.editing:
# only survey taking can be read-only
read_only = self.read_only
if not read_only:
deadline = self.survey_content.survey_parent.get().deadline
read_only = deadline and (datetime.datetime.now() > deadline)
else:
extra_attrs['disabled'] = 'disabled'
# add unordered fields to self.survey_fields
for field in self.survey_content.dynamic_properties():
# a comment made by the user
comment = ''
if has_record and hasattr(self.survey_record, field):
# previously entered value
value = getattr(self.survey_record, field)
if hasattr(self.survey_record, 'comment_for_' + field):
comment = getattr(self.survey_record, 'comment_for_' + field)
else:
# use prompts set by survey creator
value = getattr(self.survey_content, field)
if field not in schema:
logging.error('field %s not found in schema %s' %
(field, str(schema) ) )
continue
elif 'question' in schema[field]:
label = schema[field].get('question', None) or field
else:
label = field
# dispatch to field-specific methods
if schema[field]["type"] == "long_answer":
self.addLongField(field, value, extra_attrs, label=label,
comment=comment)
elif schema[field]["type"] == "short_answer":
self.addShortField(field, value, extra_attrs, label=label,
comment=comment)
elif schema[field]["type"] == "selection":
self.addSingleField(field, value, extra_attrs, schema, label=label,
comment=comment)
elif schema[field]["type"] == "pick_multi":
self.addMultiField(field, value, extra_attrs, schema, label=label,
comment=comment)
elif schema[field]["type"] == "pick_quant":
self.addQuantField(field, value, extra_attrs, schema, label=label,
comment=comment)
return self.insertFields()
def insertFields(self):
"""Add ordered fields to self.fields.
"""
survey_order = self.survey_content.getSurveyOrder()
# first, insert dynamic survey fields
for position, property in survey_order.items():
position = position * 2
self.fields.insert(position, property, self.survey_fields[property])
if not self.editing:
property = 'comment_for_' + property
self.fields.insert(position - 1, property,
self.survey_fields[property])
return self.fields
def addLongField(self, field, value, attrs, req=False, label='', tip='',
comment=''):
"""Add a long answer fields to this form.
params:
field: the current field
value: the initial value for this field
attrs: additional attributes for field
req: required bool
label: label for field
tip: tooltip text for field
comment: initial comment value for field
"""
widget = widgets.Textarea(attrs=attrs)
if not tip:
tip = 'Please provide a long answer to this question.'
question = CharField(help_text=tip, required=req, label=label,
widget=widget, initial=value)
self.survey_fields[field] = question
if not self.editing:
widget = widgets.Textarea(attrs=attrs)
comment = CharField(help_text=tip, required=False, label='Comments',
widget=widget, initial=comment)
self.survey_fields['comment_for_' + field] = comment
def addShortField(self, field, value, attrs, req=False, label='', tip='',
comment=''):
"""Add a short answer fields to this form.
params:
field: the current field
value: the initial value for this field
attrs: additional attributes for field
req: required bool
label: label for field
tip: tooltip text for field
comment: initial comment value for field
"""
attrs['class'] = "text_question"
widget = widgets.TextInput(attrs=attrs)
if not tip:
tip = 'Please provide a short answer to this question.'
question = CharField(help_text=tip, required=req, label=label,
widget=widget, max_length=140, initial=value)
self.survey_fields[field] = question
if not self.editing:
widget = widgets.Textarea(attrs=attrs)
comment = CharField(help_text=tip, required=False, label='Comments',
widget=widget, initial=comment)
self.survey_fields['comment_for_' + field] = comment
def addSingleField(self, field, value, attrs, schema, req=False, label='',
tip='', comment=''):
"""Add a selection field to this form.
params:
field: the current field
value: the initial value for this field
attrs: additional attributes for field
schema: schema for survey
req: required bool
label: label for field
tip: tooltip text for field
comment: initial comment value for field
"""
if self.editing:
kind = schema[field]["type"]
render = schema[field]["render"]
widget = UniversalChoiceEditor(kind, render)
else:
widget = WIDGETS[schema[field]['render']](attrs=attrs)
these_choices = []
# add all properties, but select chosen one
options = getattr(self.survey_content, field)
has_record = not self.editing and self.survey_record
if has_record and hasattr(self.survey_record, field):
these_choices.append((value, value))
if value in options:
options.remove(value)
for option in options:
these_choices.append((option, option))
if not tip:
tip = 'Please select an answer this question.'
question = PickOneField(help_text=tip, required=req, label=label,
choices=tuple(these_choices), widget=widget)
self.survey_fields[field] = question
if not self.editing:
widget = widgets.Textarea(attrs=attrs)
comment = CharField(help_text=tip, required=False, label='Comments',
widget=widget, initial=comment)
self.survey_fields['comment_for_' + field] = comment
def addMultiField(self, field, value, attrs, schema, req=False, label='',
tip='', comment=''):
"""Add a pick_multi field to this form.
params:
field: the current field
value: the initial value for this field
attrs: additional attributes for field
schema: schema for survey
req: required bool
label: label for field
tip: tooltip text for field
comment: initial comment value for field
"""
if self.editing:
kind = schema[field]["type"]
render = schema[field]["render"]
widget = UniversalChoiceEditor(kind, render)
else:
widget = WIDGETS[schema[field]['render']](attrs=attrs)
# TODO(ajaksu) need to allow checking checkboxes by default
if self.survey_record and isinstance(value, basestring):
# pass value as 'initial' so MultipleChoiceField renders checked boxes
value = value.split(',')
else:
value = None
these_choices = [(v,v) for v in getattr(self.survey_content, field)]
if not tip:
tip = 'Please select one or more of these choices.'
question = PickManyField(help_text=tip, required=req, label=label,
choices=tuple(these_choices), widget=widget,
initial=value)
self.survey_fields[field] = question
if not self.editing:
widget = widgets.Textarea(attrs=attrs)
comment = CharField(help_text=tip, required=False, label='Comments',
widget=widget, initial=comment)
self.survey_fields['comment_for_' + field] = comment
def addQuantField(self, field, value, attrs, schema, req=False, label='',
tip='', comment=''):
"""Add a pick_quant field to this form.
params:
field: the current field
value: the initial value for this field
attrs: additional attributes for field
schema: schema for survey
req: required bool
label: label for field
tip: tooltip text for field
comment: initial comment value for field
"""
if self.editing:
kind = schema[field]["type"]
render = schema[field]["render"]
widget = UniversalChoiceEditor(kind, render)
else:
widget = WIDGETS[schema[field]['render']](attrs=attrs)
if self.survey_record:
value = value
else:
value = None
these_choices = [(v,v) for v in getattr(self.survey_content, field)]
if not tip:
tip = 'Please select one of these choices.'
question = PickQuantField(help_text=tip, required=req, label=label,
choices=tuple(these_choices), widget=widget,
initial=value)
self.survey_fields[field] = question
if not self.editing:
widget = widgets.Textarea(attrs=attrs)
comment = CharField(help_text=tip, required=False, label='Comments',
widget=widget, initial=comment)
self.survey_fields['comment_for_' + field] = comment
class Meta(object):
model = SurveyContent
exclude = ['schema']
class UniversalChoiceEditor(widgets.Widget):
"""Edit interface for choice questions.
Allows adding and removing options, re-ordering and editing option text.
"""
def __init__(self, kind, render, attrs=None, choices=()):
self.attrs = attrs or {}
# Choices can be any iterable, but we may need to render this widget
# multiple times. Thus, collapse it into a list so it can be consumed
# more than once.
self.choices = list(choices)
self.kind = kind
self.render_as = render
def render(self, name, value, attrs=None, choices=()):
""" renders UCE widget
"""
if value is None:
value = ''
final_attrs = self.build_attrs(attrs, name=name)
# find out which options should be selected in type and render drop-downs.
selected = 'selected="selected"'
context = dict(
name=name,
is_selection=selected * (self.kind == 'selection'),
is_pick_multi=selected * (self.kind == 'pick_multi'),
is_pick_quant=selected * (self.kind == 'pick_quant'),
is_select=selected * (self.render_as == 'single_select'),
is_checkboxes=selected * (self.render_as == 'multi_checkbox'),
is_radio_buttons=selected * (self.render_as == 'quant_radio'),
)
str_value = forms.util.smart_unicode(value) # normalize to string.
chained_choices = enumerate(chain(self.choices, choices))
choices = {}
for i, (option_value, option_label) in chained_choices:
option_value = escape(forms.util.smart_unicode(option_value))
choices[i] = option_value
context['choices'] = choices
template = 'soc/survey/universal_choice_editor.html'
return loader.render_to_string(template, context)
class PickOneField(forms.ChoiceField):
"""Stub for customizing the single choice field.
"""
#TODO(james): Ensure that more than one option cannot be selected
def __init__(self, *args, **kwargs):
super(PickOneField, self).__init__(*args, **kwargs)
class PickManyField(forms.MultipleChoiceField):
"""Stub for customizing the multiple choice field.
"""
def __init__(self, *args, **kwargs):
super(PickManyField, self).__init__(*args, **kwargs)
class PickQuantField(forms.MultipleChoiceField):
"""Stub for customizing the multiple choice field.
"""
#TODO(james): Ensure that more than one quant cannot be selected
def __init__(self, *args, **kwargs):
super(PickQuantField, self).__init__(*args, **kwargs)
class PickOneSelect(forms.Select):
"""Stub for customizing the single choice select widget.
"""
def __init__(self, *args, **kwargs):
super(PickOneSelect, self).__init__(*args, **kwargs)
class PickManyCheckbox(forms.CheckboxSelectMultiple):
"""Customized multiple choice checkbox widget.
"""
def __init__(self, *args, **kwargs):
super(PickManyCheckbox, self).__init__(*args, **kwargs)
def render(self, name, value, attrs=None, choices=()):
"""Render checkboxes as list items grouped in a fieldset.
This is the pick_multi widget for survey taking
"""
if value is None:
value = []
has_id = attrs and attrs.has_key('id')
final_attrs = self.build_attrs(attrs, name=name)
# normalize to strings.
str_values = set([forms.util.smart_unicode(v) for v in value])
is_checked = lambda value: value in str_values
smart_unicode = forms.util.smart_unicode
# set container fieldset and list
output = [u'<fieldset id="id_%s">\n <ul class="pick_multi">' % name]
# add numbered checkboxes wrapped in list items
chained_choices = enumerate(chain(self.choices, choices))
for i, (option_value, option_label) in chained_choices:
option_label = escape(smart_unicode(option_label))
# If an ID attribute was given, add a numeric index as a suffix,
# so that the checkboxes don't all have the same ID attribute.
if has_id:
final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i))
cb = widgets.CheckboxInput(final_attrs, check_test=is_checked)
rendered_cb = cb.render(name, option_value)
cb_label = (rendered_cb, option_label)
output.append(u' <li><label>%s %s</label></li>' % cb_label)
output.append(u' </ul>\n</fieldset>')
return u'\n'.join(output)
def id_for_label(self, id_):
# see the comment for RadioSelect.id_for_label()
if id_:
id_ += '_fieldset'
return id_
id_for_label = classmethod(id_for_label)
class PickQuantRadioRenderer(widgets.RadioFieldRenderer):
"""Used by PickQuantRadio to enable customization of radio widgets.
"""
def __init__(self, *args, **kwargs):
super(PickQuantRadioRenderer, self).__init__(*args, **kwargs)
def render(self):
"""Outputs set of radio fields in a div.
"""
return mark_safe(u'<div class="quant_radio">\n%s\n</div>'
% u'\n'.join([u'%s' % force_unicode(w) for w in self]))
class PickQuantRadio(forms.RadioSelect):
"""TODO(James,Ajaksu) Fix Docstring
"""
renderer = PickQuantRadioRenderer
def __init__(self, *args, **kwargs):
super(PickQuantRadio, self).__init__(*args, **kwargs)
# in the future, we'll have more widget types here
WIDGETS = {'multi_checkbox': PickManyCheckbox,
'single_select': PickOneSelect,
'quant_radio': PickQuantRadio}
class SurveyResults(widgets.Widget):
"""Render List of Survey Results For Given Survey.
"""
def render(self, survey, params, filter=filter, limit=1000, offset=0,
order=[], idx=0, context={}):
""" renders list of survey results
params:
survey: current survey
params: dict of params for rendering list
filter: filter for list results
limit: limit for list results
offset: offset for list results
order: order for list results
idx: index for list results
context: context dict for template
"""
logic = results_logic
filter = {'survey': survey}
data = logic.getForFields(filter=filter, limit=limit, offset=offset,
order=order)
params['name'] = "Survey Results"
content = {
'idx': idx,
'data': data,
'logic': logic,
'limit': limit,
}
updates = dicts.rename(params, params['list_params'])
content.update(updates)
contents = [content]
if len(content) == 1:
content = content[0]
key_order = content.get('key_order')
context['list'] = Lists(contents)
# TODO(ajaksu) is this the best way to build the results list?
for list_ in context['list']._contents:
if len(list_['data']) < 1:
return "<p>No Survey Results Have Been Submitted</p>"
list_['row'] = 'soc/survey/list/results_row.html'
list_['heading'] = 'soc/survey/list/results_heading.html'
list_['description'] = 'Survey Results:'
context['properties'] = survey.survey_content.orderedProperties()
context['entity_type'] = "Survey Results"
context['entity_type_plural'] = "Results"
context['no_lists_msg'] = "No Survey Results"
path = (survey.entity_type().lower(), survey.prefix,
survey.scope_path, survey.link_id)
context['grade_action'] = "/%s/grade/%s/%s/%s" % path
markup = loader.render_to_string('soc/survey/results.html',
dictionary=context).strip('\n')
return markup
def getRoleSpecificFields(survey, user, this_project, survey_form,
survey_record):
"""For evaluations, mentors get required Project and Grade fields, and
students get a required Project field.
Because we need to get a list of the user's projects, we call the
logic getProjects method, which doubles as an access check.
(No projects means that the survey cannot be taken.)
params:
survey: the survey being taken
user: the survey-taking user
this_project: either an already-selected project, or None
survey_form: the surveyForm widget for this survey
survey_record: an existing survey record for a user-project-survey combo,
or None
"""
from django import forms
field_count = len(eval(survey.survey_content.schema).items())
these_projects = survey_logic.getProjects(survey, user)
if not these_projects: return False # no projects found
project_pairs = []
#insert a select field with options for each project
for project in these_projects:
project_pairs.append((project.key(), project.title))
if project_pairs:
project_tuples = tuple(project_pairs)
# add select field containing list of projects
projectField = forms.fields.ChoiceField(
choices=project_tuples,
required=True,
widget=forms.Select())
projectField.choices.insert(0, (None, "Choose a Project") )
# if editing an existing survey
if not this_project and survey_record:
this_project = survey_record.project
if this_project:
for tup in project_tuples:
if tup[1] == this_project.title:
if survey_record: project_name = tup[1] + " (Saved)"
else: project_name = tup[1]
projectField.choices.remove(tup)
projectField.choices.insert(0, (tup[0], project_name) )
break
survey_form.fields.insert(0, 'project', projectField )
if survey.taking_access == "mentor evaluation":
# If this is a mentor, add a field
# determining if student passes or fails.
# Activate grades handler should determine whether new status
# is midterm_passed, final_passed, etc.
grade_choices = (('pass', 'Pass'), ('fail', 'Fail'))
grade_vals = { 'pass': True, 'fail': False }
gradeField = forms.fields.ChoiceField(choices=grade_choices,
required=True,
widget=forms.Select())
gradeField.choices.insert(0, (None, "Choose a Grade") )
if survey_record:
for g in grade_choices:
if grade_vals[g[0]] == survey_record.grade:
gradeField.choices.insert(0, (g[0],g[1] + " (Saved)") )
gradeField.choices.remove(g)
break;
gradeField.show_hidden_initial = True
survey_form.fields.insert(field_count + 1, 'grade', gradeField)
return survey_form