Surveys can now have required questions and comments can be turned on/off.
authorDaniel Diniz <ajaksu@gmail.com>
Fri, 03 Jul 2009 14:35:03 +0200
changeset 2502 2e096acc8720
parent 2501 d612b48e6e12
child 2503 c14a754b0644
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
app/soc/content/js/survey-edit-090627.js
app/soc/templates/soc/survey/universal_choice_editor.html
app/soc/views/helper/surveys.py
app/soc/views/models/survey.py
--- 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.
     """