# HG changeset patch # User James Levy # Date 1246192341 -7200 # Node ID 636dfd5381c243951ad40a771333cca6f6bf2b28 # Parent 800a020c9bcfa82d78b98048274e63d65541221c Added Survey Views Helper for rendering several widgets. Patch by: James Levy, Daniel Diniz, Lennard de Rijk Reviewed by: Lennard de Rijk diff -r 800a020c9bcf -r 636dfd5381c2 app/soc/views/helper/surveys.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/soc/views/helper/surveys.py Sun Jun 28 14:32:21 2009 +0200 @@ -0,0 +1,670 @@ +#!/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" ', + '"James Levy" ', + '"Lennard de Rijk" ', + ] + + +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'
\n
    ' % 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'
  • ' % cb_label) + + output.append(u'
\n
') + 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'
\n%s\n
' + % 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 "

No Survey Results Have Been Submitted

" + + 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