# HG changeset patch # User James Levy # Date 1246193191 -7200 # Node ID dd16e9b3c2d04aa76fb02c7ccafda07f0b3e0d19 # Parent f6d45459b6b4ea93014abb893da80fe6f6cafba4 Added View for Surveys. Patch by: James Levy, Daniel Diniz Reviewed by: to-be-reviewed diff -r f6d45459b6b4 -r dd16e9b3c2d0 app/soc/views/models/survey.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/soc/views/models/survey.py Sun Jun 28 14:46:31 2009 +0200 @@ -0,0 +1,941 @@ +#!/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. + +"""Views for Surveys. +""" + +__authors__ = [ + '"Daniel Diniz" ', + '"James Levy" ', + ] + + +import csv +import datetime +import re +import StringIO +import string + +from django import forms +from django import http +from django.utils import simplejson + +from google.appengine.ext import db + +from soc.cache import home +from soc.logic import cleaning +from soc.logic import dicts +from soc.logic.models.survey import GRADES +from soc.logic.models.survey import logic as survey_logic +from soc.logic.models.survey import results_logic +from soc.logic.models.user import logic as user_logic +from soc.models.survey import Survey +from soc.models.survey_record import SurveyRecord +from soc.models.user import User +from soc.views import out_of_band +from soc.views.helper import access +from soc.views.helper import decorators +from soc.views.helper import redirects +from soc.views.helper import responses +from soc.views.helper import surveys +from soc.views.helper import widgets +from soc.views.models import base + + +CHOICE_TYPES = set(('selection', 'pick_multi', 'choice', 'pick_quant')) +TEXT_TYPES = set(('long_answer', 'short_answer')) +PROPERTY_TYPES = tuple(CHOICE_TYPES) + tuple(TEXT_TYPES) + +_short_answer = ("Short Answer", + "Less than 40 characters. Rendered as a text input. " + "It's possible to add a free form question (Content) " + "and a in-input propmt/example text.") +_choice = ("Selection", + "Can be set as a single choice (selection) or multiple choice " + "(pick_multi) question. Rendered as a select (single choice) " + "or a group of checkboxes (multiple choice). It's possible to " + "add a free form question (Content) and as many free form options " + "as wanted. Each option can be edited (double-click), deleted " + "(click on (-) button) or reordered (drag and drop).") +_long_answer = ("Long Answer", + "Unlimited length, auto-growing field. endered as a textarea. " + "It's possible to add a free form question (Content) and " + "an in-input prompt/example text.") +QUESTION_TYPES = dict(short_answer=_short_answer, long_answer=_long_answer, + choice=_choice) + +# for to_csv and View.exportSerialized +FIELDS = 'author modified_by' +PLAIN = 'is_featured content created modified' + + +class View(base.View): + """View methods for the Survey model. + """ + + def __init__(self, params=None): + """Defines the fields and methods required for the base View class + to provide the user with list, public, create, edit and delete views. + + Params: + params: a dict with params for this View + """ + + # TODO: read/write access needs to match survey + # TODO: usage requirements + + rights = access.Checker(params) + rights['any_access'] = ['allow'] + rights['show'] = ['checkIsSurveyReadable'] + rights['create'] = ['checkIsUser'] + rights['edit'] = ['checkIsSurveyWritable'] + rights['delete'] = ['checkIsSurveyWritable'] + rights['list'] = ['checkDocumentList'] + rights['pick'] = ['checkDocumentPick'] + rights['grade'] = ['checkIsSurveyGradable'] + + new_params = {} + new_params['logic'] = survey_logic + new_params['rights'] = rights + + new_params['name'] = "Survey" + new_params['pickable'] = True + + new_params['extra_django_patterns'] = [ + (r'^%(url_name)s/(?Pactivate)/%(scope)s$', + 'soc.views.models.%(module_name)s.activate', + 'Activate grades for %(name)s'), + (r'^%(url_name)s/(?Pjson)/%(scope)s$', + 'soc.views.models.%(module_name)s.json', + 'Export %(name)s as JSON'), + (r'^%(url_name)s/(?Presults)/%(scope)s$', + 'soc.views.models.%(module_name)s.results', + 'View survey results for %(name)s'), + ] + + new_params['export_content_type'] = 'text/text' + new_params['export_extension'] = '.csv' + new_params['export_function'] = to_csv + new_params['delete_redirect'] = '/' + new_params['list_key_order'] = [ + 'link_id', 'scope_path', 'name', 'short_name', 'title', + 'content', 'prefix','read_access','write_access'] + + new_params['edit_template'] = 'soc/survey/edit.html' + new_params['create_template'] = 'soc/survey/edit.html' + + # TODO which one of these are leftovers from Document? + new_params['no_create_raw'] = True + new_params['no_create_with_scope'] = True + new_params['no_create_with_key_fields'] = True + new_params['no_list_raw'] = True + new_params['sans_link_id_create'] = True + new_params['sans_link_id_list'] = True + + new_params['create_dynafields'] = [ + {'name': 'link_id', + 'base': forms.fields.CharField, + 'label': 'Survey Link ID', + }, + ] + + # survey_html: save form content if POST fails, so fields remain in UI + new_params['create_extra_dynaproperties'] = { + #'survey_content': forms.fields.CharField(widget=surveys.EditSurvey(), + #required=False), + 'survey_html': forms.fields.CharField(widget=forms.HiddenInput, + required=False), + 'scope_path': forms.fields.CharField(widget=forms.HiddenInput, + required=True), + 'prefix': forms.fields.CharField(widget=widgets.ReadOnlyInput(), + required=True), + 'clean_content': cleaning.clean_html_content('content'), + 'clean_link_id': cleaning.clean_link_id('link_id'), + 'clean_scope_path': cleaning.clean_scope_path('scope_path'), + 'clean': cleaning.validate_document_acl(self, True), + } + + new_params['extra_dynaexclude'] = ['author', 'created', 'content', + 'home_for', 'modified_by', 'modified', + 'take_survey', 'survey_content'] + + new_params['edit_extra_dynaproperties'] = { + 'doc_key_name': forms.fields.CharField(widget=forms.HiddenInput), + 'created_by': forms.fields.CharField(widget=widgets.ReadOnlyInput(), + required=False), + 'last_modified_by': forms.fields.CharField( + widget=widgets.ReadOnlyInput(), required=False), + 'clean': cleaning.validate_document_acl(self), + } + + params = dicts.merge(params, new_params) + super(View, self).__init__(params=params) + + def list(self, request, access_type, page_name=None, params=None, + filter=None, order=None, **kwargs): + """See base.View.list. + """ + + return super(View, self).list(request, access_type, page_name=page_name, + params=params, filter=kwargs) + + def _public(self, request, entity, context): + """Survey taking and result display handler. + + Args: + request: the django request object + entity: the entity to make public + context: the context object + + -- Taking Survey Pages Are Not 'Public' -- + + For surveys, the "public" page is actually the access-protected + survey-taking page. + + -- SurveyProjectGroups -- + + Each survey can be taken once per user per project. + + This means that MidtermGSOC2009 can be taken once for a student + for a project, and once for a mentor for each project they are + mentoring. + + The project selected while taking a survey determines how this_user + SurveyRecord will be linked to other SurveyRecords. + + --- Deadlines --- + + A deadline can also be used as a conditional for updating values, + we have a special read_only UI and a check on the POST handler for this. + Passing read_only=True here allows one to fetch the read_only view. + """ + + # check ACL + rights = self._params['rights'] + rights.checkIsSurveyReadable({'key_name': entity.key().name(), + 'prefix': entity.prefix, + 'scope_path': entity.scope_path, + 'link_id': entity.link_id,}, + 'key_name') + + survey = entity + user = user_logic.getForCurrentAccount() + + status = self.getStatus(request, context, user, survey) + read_only, can_write, not_ready = status + + # If user can edit this survey and is requesting someone else's results, + # in a read-only request, we fetch them. + if can_write and read_only and 'user_results' in request.GET: + user = user_logic.getFromKeyNameOr404(request.GET['user_results']) + + if not_ready and not can_write: + context['notice'] = "No survey available." + return False + elif not_ready: + return False + else: + # check for existing survey_record + record_query = SurveyRecord.all( + ).filter("user =", user + ).filter("survey =", survey) + # get project from GET arg + if request._get.get('project'): + import soc.models.student_project + project = soc.models.student_project.StudentProject.get( + request._get.get('project')) + record_query = record_query.filter("project =", project) + else: + project = None + survey_record = record_query.get() + + if len(request.POST) < 1 or read_only or not_ready: + # not submitting completed survey OR we're ignoring late submission + pass + else: + # save/update the submitted survey + context['notice'] = "Survey Submission Saved" + survey_record = survey_logic.updateSurveyRecord(user, survey, + survey_record, request.POST) + survey_content = survey.survey_content + + if not survey_record and read_only: + # no recorded answers, we're either past deadline or want to see answers + is_same_user = user.key() == user_logic.getForCurrentAccount().key() + + if not can_write or not is_same_user: + # If user who can edit looks at her own taking page, show the default + # form as readonly. Otherwise, below, show nothing. + context["notice"] = "There are no records for this survey and user." + return False + + survey_form = surveys.SurveyForm(survey_content=survey_content, + this_user=user, + project=project, + survey_record=survey_record, + read_only=read_only, + editing=False) + survey_form.getFields() + if 'evaluation' in survey.taking_access: + survey_form = surveys.getRoleSpecificFields(survey, user, + project, survey_form, survey_record) + + # set help and status text + self.setHelpStatus(context, read_only, + survey_record, survey_form, survey) + + if not context['survey_form']: + access_tpl = "Access Error: This Survey Is Limited To %s" + context["notice"] = access_tpl % string.capwords(survey.taking_access) + + context['read_only'] = read_only + context['project'] = project + return True + + def getStatus(self, request, context, user, survey): + """Determine if we're past deadline or before opening, check user rights. + """ + + read_only = (context.get("read_only", False) or + request.GET.get("read_only", False) or + request.POST.get("read_only", False) + ) + now = datetime.datetime.now() + + # check deadline, see check for opening below + if survey.deadline and now > survey.deadline: + # are we already passed the deadline? + context["notice"] = "The Deadline For This Survey Has Passed" + read_only = True + + # check if user can edit this survey + params = dict(prefix=survey.prefix, scope_path=survey.scope_path) + checker = access.rights_logic.Checker(survey.prefix) + roles = checker.getMembership(survey.write_access) + rights = self._params['rights'] + can_write = access.Checker.hasMembership(rights, roles, params) + + + not_ready = False + # check if we're past the opening date + if survey.opening and now < survey.opening: + not_ready = True + + # only users that can edit a survey should see it before opening + if not can_write: + context["notice"] = "There is no such survey available." + return False + else: + context["notice"] = "This survey is not open for taking yet." + + return read_only, can_write, not_ready + + def setHelpStatus(self, context, read_only, survey_record, survey_form, + survey): + """Set help_text and status for template use. + """ + + if not read_only: + if not survey.deadline: + deadline_text = "" + else: + deadline_text = " by " + str( + survey.deadline.strftime("%A, %d. %B %Y %I:%M%p")) + + if survey_record: + help_text = "Edit and re-submit this survey" + deadline_text + "." + status = "edit" + else: + help_text = "Please complete this survey" + deadline_text + "." + status = "create" + + else: + help_text = "Read-only view." + status = "view" + + survey_data = dict(survey_form=survey_form, status=status, + help_text=help_text) + context.update(survey_data) + + def _editContext(self, request, context): + """Performs any required processing on the context for edit pages. + + Args: + request: the django request object + context: the context dictionary that will be used + + Adds list of SurveyRecord results as supplement to view. + + See surveys.SurveyResults for details. + """ + + if not getattr(self, '_entity', None): + return + + results = surveys.SurveyResults() + + context['survey_records'] = results.render(self._entity, self._params, + filter={}) + + super(View, self)._editContext(request, context) + + def _editPost(self, request, entity, fields): + """See base.View._editPost(). + + Processes POST request items to add new dynamic field names, + question types, and default prompt values to SurveyContent model. + """ + + user = user_logic.getForCurrentAccount() + schema = {} + survey_fields = {} + + if not entity: + # new Survey + if 'serialized' in request.POST: + fields, schema, survey_fields = self.importSerialized(request, fields, user) + fields['author'] = user + else: + fields['author'] = entity.author + schema = self.loadSurveyContent(schema, survey_fields, entity) + + # remove deleted properties from the model + self.deleteQuestions(schema, survey_fields, request.POST) + + # add new text questions and re-build choice questions + self.getRequestQuestions(schema, survey_fields, request.POST) + + # get schema options for choice questions + self.getSchemaOptions(schema, survey_fields, request.POST) + + survey_content = getattr(entity,'survey_content', None) + # create or update a SurveyContent for this Survey + survey_content = survey_logic.createSurvey(survey_fields, schema, + survey_content=survey_content) + + # save survey_content for existent survey or pass for creating a new one + if entity: + entity.modified_by = user + entity.survey_content = survey_content + db.put(entity) + else: + fields['survey_content'] = survey_content + + fields['modified_by'] = user + super(View, self)._editPost(request, entity, fields) + + def loadSurveyContent(self, schema, survey_fields, entity): + """Populate the schema dict and get text survey questions. + """ + + if hasattr(entity, 'survey_content'): + + # there is a SurveyContent already + survey_content = entity.survey_content + schema = eval(survey_content.schema) + + for question_name in survey_content.dynamic_properties(): + + # get the current questions from the SurveyContent + if question_name not in schema: + continue + + if schema[question_name]['type'] not in CHOICE_TYPES: + # Choice questions are always regenerated from request, see + # self.get_request_questions() + question = getattr(survey_content, question_name) + survey_fields[question_name] = question + + return schema + + def deleteQuestions(self, schema, survey_fields, POST): + """Process the list of questions to delete, from a hidden input. + """ + + deleted = POST.get('__deleted__', '') + + if deleted: + deleted = deleted.split(',') + for field in deleted: + + if field in schema: + del schema[field] + + if field in survey_fields: + del survey_fields[field] + + def getRequestQuestions(self, schema, survey_fields, POST): + """Get fields from request. + + We use two field/question naming and processing schemes: + - Choice questions consist of s with a common name, being rebuilt + anew on every edit POST so we can gather ordering, text changes, + deletions and additions. + - Text questions only have special survey__* names on creation, afterwards + they are loaded from the SurveyContent dynamic properties. + """ + + for key, value in POST.items(): + + if key.startswith('id_'): + # Choice question fields, they are always generated from POST contents, + # as their 'content' is editable and they're reorderable. Also get + # its field index for handling reordering fields later. + name, number = key[3:].replace('__field', '').rsplit('_', 1) + + if name not in schema: + if 'NEW_' + name in POST: + # new Choice question, set generic type and get its index + schema[name] = {'type': 'choice'} + schema[name]['index'] = int(POST['index_for_' + name]) + + if name in schema and schema[name]['type'] in CHOICE_TYPES: + # build an index:content dictionary + if name in survey_fields: + if value not in survey_fields[name]: + survey_fields[name][int(number)] = value + else: + survey_fields[name] = {int(number): value} + + elif key.startswith('survey__'): # new Text question + # this is super ugly but unless data is serialized the regex is needed + prefix = re.compile('survey__([0-9]{1,3})__') + prefix_match = re.match(prefix, key) + + index = prefix_match.group(0).replace('survey', '').replace('__','') + index = int(index) + + field_name = prefix.sub('', key) + field = 'id_' + key + + for ptype in PROPERTY_TYPES: + # should only match one + if ptype + "__" in field_name: + field_name = field_name.replace(ptype + "__", "") + schema[field_name] = {} + schema[field_name]["index"] = index + schema[field_name]["type"] = ptype + + survey_fields[field_name] = value + + def getSchemaOptions(self, schema, survey_fields, POST): + """Get question, type, rendering and option order for choice questions. + """ + + RENDER = {'checkboxes': 'multi_checkbox', 'select': 'single_select', + 'radio_buttons': 'quant_radio'} + + RENDER_TYPES = {'select': 'selection', + 'checkboxes': 'pick_multi', + 'radio_buttons': 'pick_quant' } + + for key in schema: + if schema[key]['type'] in CHOICE_TYPES and key in survey_fields: + render_for = 'render_for_' + key + if render_for in POST: + schema[key]['render'] = RENDER[POST[render_for]] + schema[key]['type'] = RENDER_TYPES[POST[render_for]] + + # handle reordering fields + ordered = False + order = 'order_for_' + key + if order in POST and isinstance(survey_fields[key], dict): + order = POST[order] + + # 'order_for_name' is jquery serialized from a sortable, so it's in + # a 'name[]=1&name[]=2&name[]=0' format ('id-li-' is set in our JS) + order = order.replace('id-li-%s[]=' % key, '') + order = order.split('&') + + if len(order) == len(survey_fields[key]) and order[0]: + order = [int(number) for number in order] + + if set(order) == set(survey_fields[key]): + survey_fields[key] = [survey_fields[key][i] for i in order] + ordered = True + + if not ordered: + # we don't have a good ordering to use + ordered = sorted(survey_fields[key].items()) + survey_fields[key] = [value for index, value in ordered] + + # set 'question' entry (free text label for question) in schema + question_for = 'NEW_' + key + if question_for in POST: + schema[key]["question"] = POST[question_for] + + def createGet(self, request, context, params, seed): + """Pass the question types for the survey creation template. + """ + + context['question_types'] = QUESTION_TYPES + + # avoid spurious results from showing on creation + context['new_survey'] = True + return super(View, self).createGet(request, context, params, seed) + + def editGet(self, request, entity, context, params=None): + """Process GET requests for the specified entity. + + Builds the SurveyForm that represents the Survey question contents. + """ + + # TODO(ajaksu) Move CHOOSE_A_PROJECT_FIELD and CHOOSE_A_GRADE_FIELD + # to template. + + CHOOSE_A_PROJECT_FIELD = """ + + + + + """ + + CHOOSE_A_GRADE_FIELD = """ + + + + """ + + self._entity = entity + survey_content = entity.survey_content + user = user_logic.getForCurrentAccount() + # no project or survey_record needed for survey prototype + project = None + survey_record = None + + + survey_form = surveys.SurveyForm(survey_content=survey_content, + this_user=user, project=project, survey_record=survey_record, + editing=True, read_only=False) + survey_form.getFields() + + + # activate grades flag -- TODO: Can't configure notice on edit page + if request._get.get('activate'): + context['notice'] = "Evaluation Grades Have Been Activated" + + local = dict(survey_form=survey_form, question_types=QUESTION_TYPES, + survey_h=entity.survey_content) + context.update(local) + + params['edit_form'] = HelperForm(params['edit_form']) + if entity.deadline and datetime.datetime.now() > entity.deadline: + # are we already passed the deadline? + context["passed_deadline"] = True + + return super(View, self).editGet(request, entity, context, params=params) + + def getMenusForScope(self, entity, params): + """List featured surveys iff after the opening date and before deadline. + """ + + # only list surveys for registered users + user = user_logic.getForCurrentAccount() + if not user: + return [] + + filter = { + 'prefix' : params['url_name'], + 'scope_path': entity.key().id_or_name(), + 'is_featured': True, + } + + entities = self._logic.getForFields(filter) + submenus = [] + now = datetime.datetime.now() + + # cache ACL + survey_rights = {} + + # add a link to all featured documents + for entity in entities: + + # only list those surveys the user can read + if entity.read_access not in survey_rights: + + params = dict(prefix=entity.prefix, scope_path=entity.scope_path, + link_id=entity.link_id, user=user) + + # TODO(ajaksu) use access.Checker.checkIsSurveyReadable + checker = access.rights_logic.Checker(entity.prefix) + roles = checker.getMembership(entity.read_access) + rights = self._params['rights'] + can_read = access.Checker.hasMembership(rights, roles, params) + + # cache ACL for a given entity.read_access + survey_rights[entity.read_access] = can_read + + if not can_read: + continue + + elif not survey_rights[entity.read_access]: + continue + + # omit if either before opening or after deadline + if entity.opening and entity.opening > now: + continue + + if entity.deadline and entity.deadline < now: + continue + + #TODO only if a document is readable it might be added + submenu = (redirects.getPublicRedirect(entity, self._params), + entity.short_name, 'show') + + submenus.append(submenu) + return submenus + + def activate(self, request, **kwargs): + """This is a hack to support the 'Enable grades' button. + """ + self.activateGrades(request) + redirect_path = request.path.replace('/activate/', '/edit/') + '?activate=1' + return http.HttpResponseRedirect(redirect_path) + + + def activateGrades(self, request, **kwargs): + """Updates SurveyRecord's grades for a given Survey. + """ + survey_key_name = survey_logic.getKeyNameFromPath(request.path) + survey = Survey.get_by_key_name(survey_key_name) + survey_logic.activateGrades(survey) + return + + @decorators.merge_params + @decorators.check_access + def viewResults(self, request, access_type, page_name=None, + params=None, **kwargs): + """View for SurveyRecord and SurveyRecordGroup. + """ + + entity, context = self.getContextEntity(request, page_name, params, kwargs) + + if context is None: + # user cannot see this page, return error response + return entity + + can_write = False + rights = self._params['rights'] + try: + rights.checkIsSurveyWritable({'key_name': entity.key().name(), + 'prefix': entity.prefix, + 'scope_path': entity.scope_path, + 'link_id': entity.link_id,}, + 'key_name') + can_write = True + except out_of_band.AccessViolation: + pass + + user = user_logic.getForCurrentAccount() + + filter = self._params.get('filter') or {} + + # if user can edit the survey, show everyone's results + if can_write: + filter['survey'] = entity + else: + filter.update({'user': user, 'survey': entity}) + + limit = self._params.get('limit') or 1000 + offset = self._params.get('offset') or 0 + order = self._params.get('order') or [] + idx = self._params.get('idx') or 0 + + records = results_logic.getForFields(filter=filter, limit=limit, + offset=offset, order=order) + + updates = dicts.rename(params, params['list_params']) + context.update(updates) + + context['results'] = records, records + context['content'] = entity.survey_content + + template = 'soc/survey/results_page.html' + return responses.respond(request, template, context=context) + + @decorators.merge_params + @decorators.check_access + def exportSerialized(self, request, access_type, page_name=None, + params=None, **kwargs): + + sur, context = self.getContextEntity(request, page_name, params, kwargs) + + if context is None: + # user cannot see this page, return error response + return sur + + json = sur.toDict() + json.update(dict((f, str(getattr(sur, f))) for f in PLAIN.split())) + static = ((f, str(getattr(sur, f).link_id)) for f in FIELDS.split()) + json.update(dict(static)) + + dynamic = sur.survey_content.dynamic_properties() + content = ((prop, getattr(sur.survey_content, prop)) for prop in dynamic) + json['survey_content'] = dict(content) + + schema = sur.survey_content.schema + json['survey_content']['schema'] = eval(sur.survey_content.schema) + + data = simplejson.dumps(json, indent=2) + + return self.json(request, data=json) + + def importSerialized(self, request, fields, user): + json = request.POST['serialized'] + json = simplejson.loads(json)['data'] + survey_content = json.pop('survey_content') + schema = survey_content.pop('schema') + del json['author'] + del json['created'] + del json['modified'] + #del json['is_featured'] + # keywords can't be unicode + keywords = {} + for key, val in json.items(): + keywords[str(key)] = val + if 'is_featured' in keywords: + keywords['is_featured'] = eval(keywords['is_featured']) + return keywords, schema, survey_content + + def getContextEntity(self, request, page_name, params, kwargs): + context = responses.getUniversalContext(request) + responses.useJavaScript(context, params['js_uses_all']) + context['page_name'] = page_name + entity = None + + # TODO(ajaksu) there has to be a better way in this universe to get these + kwargs['prefix'] = 'program' + kwargs['link_id'] = request.path.split('/')[-1] + kwargs['scope_path'] = '/'.join(request.path.split('/')[4:-1]) + + entity = survey_logic.getFromKeyFieldsOr404(kwargs) + + if not self._public(request, entity, context): + error = out_of_band.Error('') + error = responses.errorResponse( + error, request, template=params['error_public'], context=context) + return error, None + + return entity, context + +class HelperForm(object): + """Thin wrapper for adding values to params['edit_form'].fields. + """ + + def __init__(self, form=None): + """Store the edit_form. + """ + + self.form = form + + def __call__(self, instance=None): + """Transparently instantiate and add initial values to the edit_form. + """ + + form = self.form(instance=instance) + form.fields['created_by'].initial = instance.author.name + form.fields['last_modified_by'].initial = instance.modified_by.name + form.fields['doc_key_name'].initial = instance.key().id_or_name() + return form + + +def _get_csv_header(sur): + """CSV header helper, needs support for comment lines in CSV. + """ + + tpl = '# %s: %s\n' + + # add static properties + fields = ['# Melange Survey export for \n# %s\n#\n' % sur.title] + fields += [tpl % (k,v) for k,v in sur.toDict().items()] + fields += [tpl % (f, str(getattr(sur, f))) for f in PLAIN.split()] + fields += [tpl % (f, str(getattr(sur, f).link_id)) for f in FIELDS.split()] + fields.sort() + + # add dynamic properties + fields += ['#\n#---\n#\n'] + dynamic = sur.survey_content.dynamic_properties() + dynamic = [(prop, getattr(sur.survey_content, prop)) for prop in dynamic] + fields += [tpl % (k,v) for k,v in sorted(dynamic)] + + # add schema + fields += ['#\n#---\n#\n'] + schema = sur.survey_content.schema + indent = '},\n#' + ' ' * 9 + fields += [tpl % ('Schema', schema.replace('},', indent)) + '#\n'] + + return ''.join(fields).replace('\n', '\r\n') + + +def _get_records(recs, props): + """Fetch properties from SurveyRecords for CSV export. + """ + + records = [] + props = props[1:] + for rec in recs: + values = tuple(getattr(rec, prop, None) for prop in props) + leading = (rec.user.link_id,) + records.append(leading + values) + return records + + +def to_csv(survey): + """CSV exporter. + """ + + # get header and properties + header = _get_csv_header(survey) + leading = ['user', 'created', 'modified'] + properties = leading + survey.survey_content.orderedProperties() + + try: + first = survey.survey_records.run().next() + except StopIteration: + # bail out early if survey_records.run() is empty + return header, survey.link_id + + # generate results list + recs = survey.survey_records.run() + recs = _get_records(recs, properties) + + # write results to CSV + output = StringIO.StringIO() + writer = csv.writer(output) + writer.writerow(properties) + writer.writerows(recs) + + return header + output.getvalue(), survey.link_id + + +view = View() + +admin = decorators.view(view.admin) +create = decorators.view(view.create) +edit = decorators.view(view.edit) +delete = decorators.view(view.delete) +list = decorators.view(view.list) +public = decorators.view(view.public) +export = decorators.view(view.export) +pick = decorators.view(view.pick) +activate = decorators.view(view.activate) +results = decorators.view(view.viewResults) +json = decorators.view(view.exportSerialized)