Surveys can now have required questions and comments can be turned on/off.
Storing comments is now working. However some issues arise when form errors occur, like missing context and the errors also trigger before a form has been submitted.
Reviewed by: Lennard de Rijk
--- a/app/soc/content/js/survey-edit-090627.js Fri Jul 03 14:19:23 2009 +0200
+++ b/app/soc/content/js/survey-edit-090627.js Fri Jul 03 14:35:03 2009 +0200
@@ -565,11 +565,43 @@
// create the HTML for the field
switch (button_id) {
case "short_answer":
- new_field = "<input type='text'/ class='short_answer'>";
+ new_field = ["<fieldset>\n",
+ '<label for="required_for_',
+ field_name, '">Required</label>',
+ '<select id="required_for_', field_name,
+ '" name="required_for_', field_name,
+ '"><option value="True" selected="selected">True',
+ '</option>', '<option value="False">False</option>',
+ '</select><br/>', '<label for="comment_for_',
+ field_name, '">Allow Comments</label>',
+ '<select id="comment_for_', field_name,
+ '" name="comment_for_', field_name, '">',
+ '<option value="True" selected="selected">',
+ 'True</option>', '<option value="False">',
+ 'False</option>', '</select><br/>',
+ "<input type='text' ",
+ "class='short_answer'>", "</fieldset>"
+ ].join("");
break;
case "long_answer":
- new_field = ["<textarea cols='40' rows='", MIN_ROWS,
- "' class='long_answer'/>"].join("");
+ field_count = survey_table.find('tr').length;
+ new_field_count = field_count + 1 + '__';
+ new_field = ['<fieldset>\n', '<label for="required_for_',
+ field_name, '">Required</label>',
+ '<select id="required_for_', field_name,
+ '" name="required_for_', field_name,
+ '"><option value="True" selected="selected">True',
+ '</option>', '<option value="False">False</option>',
+ '</select><br/>', '<label for="comment_for_',
+ field_name, '">Allow Comments</label>',
+ '<select id="comment_for_', field_name,
+ '" name="comment_for_', field_name, '">',
+ '<option value="True" selected="selected">',
+ 'True</option>', '<option value="False">',
+ 'False</option>', '</select><br/>',
+ "<textarea cols='40' rows='", MIN_ROWS,
+ "' class='long_answer'/>", '</fieldset>'
+ ].join("");
break;
case "selection":
new_field = ["<select><option></option>", default_option,
@@ -601,7 +633,18 @@
if (button_id === 'choice') {
var name = (field_name);
new_field = $([
- '<fieldset>\n <label for="render_for_', name,
+ '<fieldset>\n', '<label for="required_for_', name,
+ '">Required</label>',
+ '<select id="required_for_', name, '" name="required_for_',
+ name, '"><option value="True" selected="selected">True',
+ '</option>', '<option value="False">False</option>',
+ '</select><br/>', '<label for="comment_for_', name,
+ '">Allow Comments</label>', '<select id="comment_for_', name,
+ '" name="comment_for_', name, '">',
+ '<option value="True" selected="selected">True</option>',
+ '<option value="False">False</option>',
+ '</select><br/>',
+ '<label for="render_for_', name,
'">Render as</label>', '\n <select id="render_for_', name,
'" name="render_for_', name, '">', '\n <option',
'selected="selected" value="select">select</option>',
@@ -672,7 +715,7 @@
else {
new_field = $(new_field);
// maybe the name should be serialized in a more common format
- $(new_field).attr({
+ $(new_field).find('.long_answer, .short_answer').attr({
'id': 'id_' + formatted_name,
'name': formatted_name
});
--- a/app/soc/templates/soc/survey/universal_choice_editor.html Fri Jul 03 14:19:23 2009 +0200
+++ b/app/soc/templates/soc/survey/universal_choice_editor.html Fri Jul 03 14:35:03 2009 +0200
@@ -1,5 +1,23 @@
<fieldset>
+{# Drop-down setting whether question is required #}
+ <label for="required_for_{{ name }}">Required</label>
+ <select id="required_for_{{ name }}" name="required_for_{{ name }}">
+ <option value="True" {% if is_required %} selected='selected' {% endif %}
+ >True</option>
+ <option value="False" {% if not is_required %} selected='selected' {% endif %}
+ >False</option>
+ </select><br/>
+
+{# Drop-down setting whether question allows comments #}
+ <label for="comment_for_{{ name }}">Allow Comments</label>
+ <select id="comment_for_{{ name }}" name="comment_for_{{ name }}">
+ <option value="True" {% if has_comment %} selected='selected' {% endif %}
+ >True</option>
+ <option value="False" {% if not has_comment %} selected='selected' {% endif %}
+ >False</option>
+ </select><br/>
+
{# Question type drop-down #}
<label for="type_for_{{ name }}">Question Type</label>
<select id="type_for_{{ name }}" name="type_for_{{ name }}">
--- a/app/soc/views/helper/surveys.py Fri Jul 03 14:19:23 2009 +0200
+++ b/app/soc/views/helper/surveys.py Fri Jul 03 14:35:03 2009 +0200
@@ -46,6 +46,26 @@
from soc.models.survey import SurveyContent
+# TODO(ajaksu) add this to template
+REQUIRED_COMMENT_TPL = """
+ <label for="required_for_{{ name }}">Required</label>
+ <select id="required_for_{{ name }}" name="required_for_{{ name }}">
+ <option value="True" {% if is_required %} selected='selected' {% endif %}
+ >True</option>
+ <option value="False" {% if not is_required %} selected='selected'
+ {% endif %}>False</option>
+ </select><br/>
+
+ <label for="comment_for_{{ name }}">Allow Comments</label>
+ <select id="comment_for_{{ name }}" name="comment_for_{{ name }}">
+ <option value="True" {% if has_comment %} selected='selected' {% endif %}
+ >True</option>
+ <option value="False" {% if not has_comment %} selected='selected'
+ {% endif %}>False</option>
+ </select><br/>
+"""
+
+
class SurveyForm(djangoforms.ModelForm):
"""Main SurveyContent form.
@@ -161,12 +181,17 @@
# find correct field type
addField = self.fields_map[schema.getType(field)]
- addField(field, value, extra_attrs, schema, label=label, comment=comment)
+
+ # check if question is required, it's never required when editing
+ required = not self.editing and schema.getRequired(field)
+ kwargs = dict(label=label, req=required)
- # handle comments
- comment_name = COMMENT_PREFIX + field
- if comment_name in post_dict or hasattr(self.survey_record, comment_name):
- self.data[comment_name] = comment
+ # add new field
+ addField(field, value, extra_attrs, schema, **kwargs)
+
+ # handle comments if question allows them
+ if schema.getHasComment(field):
+ self.data[COMMENT_PREFIX + field] = comment
self.addCommentField(field, comment, extra_attrs, tip='Add a comment.')
return self.insertFields()
@@ -189,7 +214,7 @@
self.survey_fields[property])
return self.fields
- def addLongField(self, field, value, attrs, schema, req=False, label='',
+ def addLongField(self, field, value, attrs, schema, req=True, label='',
tip='', comment=''):
"""Add a long answer fields to this form.
@@ -204,7 +229,11 @@
comment: initial comment value for field
"""
- widget = widgets.Textarea(attrs=attrs)
+ # use a widget that allows setting required and comments
+ has_comment = schema.getHasComment(field)
+ is_required = schema.getRequired(field)
+ widget = LongTextarea(is_required, has_comment, attrs=attrs,
+ editing=self.editing)
if not tip:
tip = 'Please provide a long answer to this question.'
@@ -230,7 +259,12 @@
"""
attrs['class'] = "text_question"
- widget = widgets.TextInput(attrs=attrs)
+
+ # use a widget that allows setting required and comments
+ has_comment = schema.getHasComment(field)
+ is_required = schema.getRequired(field)
+ widget = ShortTextInput(is_required, has_comment, attrs=attrs,
+ editing=self.editing)
if not tip:
tip = 'Please provide a short answer to this question.'
@@ -298,8 +332,6 @@
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:
@@ -365,6 +397,18 @@
def getType(self, field):
return self.schema[field]["type"]
+ def getRequired(self, field):
+ """Check whether survey question is required.
+ """
+
+ return self.schema[field]["required"]
+
+ def getHasComment(self, field):
+ """Check whether survey question allows adding a comment.
+ """
+
+ return self.schema[field]["has_comment"]
+
def getRender(self, field):
return self.schema[field]["render"]
@@ -375,7 +419,9 @@
if editing:
kind = self.getType(field)
render = self.getRender(field)
- widget = UniversalChoiceEditor(kind, render)
+ is_required = self.getRequired(field)
+ has_comment = self.getHasComment(field)
+ widget = UniversalChoiceEditor(kind, render, is_required, has_comment)
else:
widget = WIDGETS[self.schema[field]['render']](attrs=attrs)
return widget
@@ -401,7 +447,15 @@
Allows adding and removing options, re-ordering and editing option text.
"""
- def __init__(self, kind, render, attrs=None, choices=()):
+ def __init__(self, kind, render, is_required, has_comment, attrs=None,
+ choices=()):
+ """
+ params:
+ kind: question kind (one of selection, pick_multi or pick_quant)
+ render: question widget (single_select, multi_checkbox or quant_radio)
+ is_required: bool, controls selection in the required_for field
+ has_comments: bool, controls selection in the has_comments field
+ """
self.attrs = attrs or {}
@@ -411,9 +465,14 @@
self.choices = list(choices)
self.kind = kind
self.render_as = render
+ self.is_required = is_required
+ self.has_comment = has_comment
+
def render(self, name, value, attrs=None, choices=()):
- """ renders UCE widget
+ """Render UCE widget.
+
+ Option reordering, editing, addition and deletion are added here.
"""
if value is None:
@@ -433,6 +492,12 @@
is_radio_buttons=selected * (self.render_as == 'quant_radio'),
)
+ # set required and has_comment selects
+ context.update(dict(
+ is_required = self.is_required,
+ has_comment = self.has_comment,
+ ))
+
str_value = forms.util.smart_unicode(value) # normalize to string.
chained_choices = enumerate(chain(self.choices, choices))
choices = {}
@@ -472,6 +537,86 @@
super(PickQuantField, self).__init__(*args, **kwargs)
+class LongTextarea(widgets.Textarea):
+ """Set whether long question is required or allows comments.
+ """
+
+ def __init__(self, is_required, has_comment, attrs=None, editing=False):
+ """Initialize widget and store editing mode.
+
+ params:
+ is_required: bool, controls selection in the 'required' extra field
+ has_comments: bool, controls selection in the 'has_comment' extra field
+ editing: bool, controls rendering as plain textarea or with extra fields
+ """
+
+ self.editing = editing
+ self.is_required = is_required
+ self.has_comment = has_comment
+
+ super(LongTextarea, self).__init__(attrs)
+
+ def render(self, name, value, attrs=None):
+ """Render plain textarea or widget with extra fields.
+
+ Extra fields are 'required' and 'has_comment'.
+ """
+
+ # plain text area
+ output = super(LongTextarea, self).render(name, value, attrs)
+
+ if self.editing:
+ # add 'required' and 'has_comment' fields
+ context = dict(name=name, is_required=self.is_required,
+ has_comment=self.has_comment)
+ template = loader.get_template_from_string(REQUIRED_COMMENT_TPL)
+ rendered = template.render(context=loader.Context(dict_=context))
+ output = rendered + output
+
+ output = '<fieldset>' + output + '</fieldset>'
+ return output
+
+
+class ShortTextInput(widgets.TextInput):
+ """Set whether short answer question is required or allows comments.
+ """
+
+ def __init__(self, is_required, has_comment, attrs=None, editing=False):
+ """Initialize widget and store editing mode.
+
+ params:
+ is_required: bool, controls selection in the 'required' extra field
+ has_comments: bool, controls selection in the 'has_comment' extra field
+ editing: bool, controls rendering as plain text input or with extra fields
+ """
+
+ self.editing = editing
+ self.is_required = is_required
+ self.has_comment = has_comment
+
+ super(ShortTextInput, self).__init__(attrs)
+
+ def render(self, name, value, attrs=None):
+ """Render plain text input or widget with extra fields.
+
+ Extra fields are 'required' and 'has_comment'.
+ """
+
+ # plain text area
+ output = super(ShortTextInput, self).render(name, value, attrs)
+
+ if self.editing:
+ # add 'required' and 'has_comment' fields
+ context = dict(name=name, is_required=self.is_required,
+ has_comment=self.has_comment)
+ template = loader.get_template_from_string(REQUIRED_COMMENT_TPL)
+ rendered = template.render(context=loader.Context(dict_=context))
+ output = rendered + output
+
+ output = '<fieldset>' + output + '</fieldset>'
+ return output
+
+
class PickOneSelect(forms.Select):
"""Stub for customizing the single choice select widget.
"""
@@ -664,8 +809,8 @@
response_dict[name] = value
# handle comments
- comment_name = COMMENT_PREFIX + name
- if comment_name in post_dict:
+ if schema.getHasComment(name):
+ comment_name = COMMENT_PREFIX + name
comment = post_dict.get(comment_name)
if comment:
response_dict[comment_name] = comment
--- a/app/soc/views/models/survey.py Fri Jul 03 14:19:23 2009 +0200
+++ b/app/soc/views/models/survey.py Fri Jul 03 14:35:03 2009 +0200
@@ -55,6 +55,9 @@
TEXT_TYPES = set(('long_answer', 'short_answer'))
PROPERTY_TYPES = tuple(CHOICE_TYPES) + tuple(TEXT_TYPES)
+# used in View.getSchemaOptions to map POST values
+BOOL = {'True': True, 'False': False}
+
_short_answer = ("Short Answer",
"Less than 40 characters. Rendered as a text input. "
"It's possible to add a free form question (Content) "
@@ -424,6 +427,14 @@
if question_for in POST:
schema[key]["question"] = POST[question_for]
+ # set wheter the question is required
+ required_for = 'required_for_' + key
+ schema[key]['required'] = BOOL[POST[required_for]]
+
+ # set wheter the question allows comments
+ comment_for = 'comment_for_' + key
+ schema[key]['has_comment'] = BOOL[POST[comment_for]]
+
def createGet(self, request, context, params, seed):
"""Pass the question types for the survey creation template.
"""