app/soc/views/models/survey.py
changeset 2435 dd16e9b3c2d0
child 2439 7fac0da44bbf
equal deleted inserted replaced
2434:f6d45459b6b4 2435:dd16e9b3c2d0
       
     1 #!/usr/bin/python2.5
       
     2 #
       
     3 # Copyright 2009 the Melange authors.
       
     4 #
       
     5 # Licensed under the Apache License, Version 2.0 (the "License");
       
     6 # you may not use this file except in compliance with the License.
       
     7 # You may obtain a copy of the License at
       
     8 #
       
     9 #   http://www.apache.org/licenses/LICENSE-2.0
       
    10 #
       
    11 # Unless required by applicable law or agreed to in writing, software
       
    12 # distributed under the License is distributed on an "AS IS" BASIS,
       
    13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
       
    14 # See the License for the specific language governing permissions and
       
    15 # limitations under the License.
       
    16 
       
    17 """Views for Surveys.
       
    18 """
       
    19 
       
    20 __authors__ = [
       
    21   '"Daniel Diniz" <ajaksu@gmail.com>',
       
    22   '"James Levy" <jamesalexanderlevy@gmail.com>',
       
    23   ]
       
    24 
       
    25 
       
    26 import csv
       
    27 import datetime
       
    28 import re
       
    29 import StringIO
       
    30 import string
       
    31 
       
    32 from django import forms
       
    33 from django import http
       
    34 from django.utils import simplejson
       
    35 
       
    36 from google.appengine.ext import db
       
    37 
       
    38 from soc.cache import home
       
    39 from soc.logic import cleaning
       
    40 from soc.logic import dicts
       
    41 from soc.logic.models.survey import GRADES
       
    42 from soc.logic.models.survey import logic as survey_logic
       
    43 from soc.logic.models.survey import results_logic
       
    44 from soc.logic.models.user import logic as user_logic
       
    45 from soc.models.survey import Survey
       
    46 from soc.models.survey_record import SurveyRecord
       
    47 from soc.models.user import User
       
    48 from soc.views import out_of_band
       
    49 from soc.views.helper import access
       
    50 from soc.views.helper import decorators
       
    51 from soc.views.helper import redirects
       
    52 from soc.views.helper import responses
       
    53 from soc.views.helper import surveys
       
    54 from soc.views.helper import widgets
       
    55 from soc.views.models import base
       
    56 
       
    57 
       
    58 CHOICE_TYPES = set(('selection', 'pick_multi', 'choice', 'pick_quant'))
       
    59 TEXT_TYPES = set(('long_answer', 'short_answer'))
       
    60 PROPERTY_TYPES = tuple(CHOICE_TYPES) + tuple(TEXT_TYPES)
       
    61 
       
    62 _short_answer = ("Short Answer",
       
    63                 "Less than 40 characters. Rendered as a text input. "
       
    64                 "It's possible to add a free form question (Content) "
       
    65                 "and a in-input propmt/example text.")
       
    66 _choice = ("Selection",
       
    67            "Can be set as a single choice (selection) or multiple choice "
       
    68            "(pick_multi) question. Rendered as a select (single choice) "
       
    69            "or a group of checkboxes (multiple choice). It's possible to "
       
    70            "add a free form question (Content) and as many free form options "
       
    71            "as wanted. Each option can be edited (double-click), deleted "
       
    72            "(click on (-) button) or reordered (drag and drop).")
       
    73 _long_answer = ("Long Answer",
       
    74                 "Unlimited length, auto-growing field. endered as a textarea. "
       
    75                  "It's possible to add a free form question (Content) and "
       
    76                  "an in-input prompt/example text.")
       
    77 QUESTION_TYPES = dict(short_answer=_short_answer, long_answer=_long_answer,
       
    78                       choice=_choice)
       
    79 
       
    80 # for to_csv and View.exportSerialized
       
    81 FIELDS = 'author modified_by'
       
    82 PLAIN = 'is_featured content created modified'
       
    83 
       
    84 
       
    85 class View(base.View):
       
    86   """View methods for the Survey model.
       
    87   """
       
    88 
       
    89   def __init__(self, params=None):
       
    90     """Defines the fields and methods required for the base View class
       
    91     to provide the user with list, public, create, edit and delete views.
       
    92 
       
    93     Params:
       
    94       params: a dict with params for this View
       
    95     """
       
    96 
       
    97     # TODO: read/write access needs to match survey
       
    98     # TODO: usage requirements
       
    99 
       
   100     rights = access.Checker(params)
       
   101     rights['any_access'] = ['allow']
       
   102     rights['show'] = ['checkIsSurveyReadable']
       
   103     rights['create'] = ['checkIsUser']
       
   104     rights['edit'] = ['checkIsSurveyWritable']
       
   105     rights['delete'] = ['checkIsSurveyWritable']
       
   106     rights['list'] = ['checkDocumentList']
       
   107     rights['pick'] = ['checkDocumentPick']
       
   108     rights['grade'] = ['checkIsSurveyGradable']
       
   109 
       
   110     new_params = {}
       
   111     new_params['logic'] = survey_logic
       
   112     new_params['rights'] = rights
       
   113 
       
   114     new_params['name'] = "Survey"
       
   115     new_params['pickable'] = True
       
   116 
       
   117     new_params['extra_django_patterns'] = [
       
   118         (r'^%(url_name)s/(?P<access_type>activate)/%(scope)s$',
       
   119          'soc.views.models.%(module_name)s.activate',
       
   120          'Activate grades for %(name)s'),
       
   121          (r'^%(url_name)s/(?P<access_type>json)/%(scope)s$',
       
   122          'soc.views.models.%(module_name)s.json',
       
   123          'Export %(name)s as JSON'),
       
   124         (r'^%(url_name)s/(?P<access_type>results)/%(scope)s$',
       
   125          'soc.views.models.%(module_name)s.results',
       
   126          'View survey results for %(name)s'),
       
   127         ]
       
   128 
       
   129     new_params['export_content_type'] = 'text/text'
       
   130     new_params['export_extension'] = '.csv'
       
   131     new_params['export_function'] = to_csv
       
   132     new_params['delete_redirect'] = '/'
       
   133     new_params['list_key_order'] = [
       
   134         'link_id', 'scope_path', 'name', 'short_name', 'title',
       
   135         'content', 'prefix','read_access','write_access']
       
   136 
       
   137     new_params['edit_template'] = 'soc/survey/edit.html'
       
   138     new_params['create_template'] = 'soc/survey/edit.html'
       
   139 
       
   140     # TODO which one of these are leftovers from Document?
       
   141     new_params['no_create_raw'] = True
       
   142     new_params['no_create_with_scope'] = True
       
   143     new_params['no_create_with_key_fields'] = True
       
   144     new_params['no_list_raw'] = True
       
   145     new_params['sans_link_id_create'] = True
       
   146     new_params['sans_link_id_list'] = True
       
   147 
       
   148     new_params['create_dynafields'] = [
       
   149         {'name': 'link_id',
       
   150          'base': forms.fields.CharField,
       
   151          'label': 'Survey Link ID',
       
   152          },
       
   153         ]
       
   154 
       
   155     # survey_html: save form content if POST fails, so fields remain in UI
       
   156     new_params['create_extra_dynaproperties'] = {
       
   157         #'survey_content': forms.fields.CharField(widget=surveys.EditSurvey(),
       
   158                                                  #required=False),
       
   159         'survey_html': forms.fields.CharField(widget=forms.HiddenInput,
       
   160                                               required=False),
       
   161         'scope_path': forms.fields.CharField(widget=forms.HiddenInput,
       
   162                                              required=True),
       
   163         'prefix': forms.fields.CharField(widget=widgets.ReadOnlyInput(),
       
   164                                         required=True),
       
   165         'clean_content': cleaning.clean_html_content('content'),
       
   166         'clean_link_id': cleaning.clean_link_id('link_id'),
       
   167         'clean_scope_path': cleaning.clean_scope_path('scope_path'),
       
   168         'clean': cleaning.validate_document_acl(self, True),
       
   169         }
       
   170 
       
   171     new_params['extra_dynaexclude'] = ['author', 'created', 'content',
       
   172                                        'home_for', 'modified_by', 'modified',
       
   173                                        'take_survey', 'survey_content']
       
   174 
       
   175     new_params['edit_extra_dynaproperties'] = {
       
   176         'doc_key_name': forms.fields.CharField(widget=forms.HiddenInput),
       
   177         'created_by': forms.fields.CharField(widget=widgets.ReadOnlyInput(),
       
   178                                              required=False),
       
   179         'last_modified_by': forms.fields.CharField(
       
   180                                 widget=widgets.ReadOnlyInput(), required=False),
       
   181         'clean': cleaning.validate_document_acl(self),
       
   182         }
       
   183 
       
   184     params = dicts.merge(params, new_params)
       
   185     super(View, self).__init__(params=params)
       
   186 
       
   187   def list(self, request, access_type, page_name=None, params=None,
       
   188            filter=None, order=None, **kwargs):
       
   189     """See base.View.list.
       
   190     """
       
   191 
       
   192     return super(View, self).list(request, access_type, page_name=page_name,
       
   193                                   params=params, filter=kwargs)
       
   194 
       
   195   def _public(self, request, entity, context):
       
   196     """Survey taking and result display handler.
       
   197 
       
   198     Args:
       
   199       request: the django request object
       
   200       entity: the entity to make public
       
   201       context: the context object
       
   202 
       
   203     -- Taking Survey Pages Are Not 'Public' --
       
   204 
       
   205     For surveys, the "public" page is actually the access-protected
       
   206     survey-taking page.
       
   207 
       
   208     -- SurveyProjectGroups --
       
   209 
       
   210     Each survey can be taken once per user per project.
       
   211 
       
   212     This means that MidtermGSOC2009 can be taken once for a student
       
   213     for a project, and once for a mentor for each project they are
       
   214     mentoring.
       
   215 
       
   216     The project selected while taking a survey determines how this_user
       
   217     SurveyRecord will be linked to other SurveyRecords.
       
   218 
       
   219     --- Deadlines ---
       
   220 
       
   221     A deadline can also be used as a conditional for updating values,
       
   222     we have a special read_only UI and a check on the POST handler for this.
       
   223     Passing read_only=True here allows one to fetch the read_only view.
       
   224     """
       
   225 
       
   226     # check ACL
       
   227     rights = self._params['rights']
       
   228     rights.checkIsSurveyReadable({'key_name': entity.key().name(),
       
   229                                   'prefix': entity.prefix,
       
   230                                   'scope_path': entity.scope_path,
       
   231                                   'link_id': entity.link_id,},
       
   232                                  'key_name')
       
   233 
       
   234     survey = entity
       
   235     user = user_logic.getForCurrentAccount()
       
   236 
       
   237     status = self.getStatus(request, context, user, survey)
       
   238     read_only, can_write, not_ready = status
       
   239 
       
   240     # If user can edit this survey and is requesting someone else's results,
       
   241     # in a read-only request, we fetch them.
       
   242     if can_write and read_only and 'user_results' in request.GET:
       
   243       user = user_logic.getFromKeyNameOr404(request.GET['user_results'])
       
   244 
       
   245     if not_ready and not can_write:
       
   246       context['notice'] = "No survey available."
       
   247       return False
       
   248     elif not_ready:
       
   249       return False
       
   250     else:
       
   251       # check for existing survey_record
       
   252       record_query = SurveyRecord.all(
       
   253       ).filter("user =", user
       
   254       ).filter("survey =", survey)
       
   255       # get project from GET arg
       
   256       if request._get.get('project'):
       
   257         import soc.models.student_project
       
   258         project = soc.models.student_project.StudentProject.get(
       
   259         request._get.get('project'))
       
   260         record_query = record_query.filter("project =", project)
       
   261       else:
       
   262         project = None
       
   263       survey_record = record_query.get()
       
   264 
       
   265       if len(request.POST) < 1 or read_only or not_ready:
       
   266          # not submitting completed survey OR we're ignoring late submission
       
   267         pass
       
   268       else:
       
   269         # save/update the submitted survey
       
   270         context['notice'] = "Survey Submission Saved"
       
   271         survey_record = survey_logic.updateSurveyRecord(user, survey,
       
   272         survey_record, request.POST)
       
   273     survey_content = survey.survey_content
       
   274 
       
   275     if not survey_record and read_only:
       
   276       # no recorded answers, we're either past deadline or want to see answers
       
   277       is_same_user = user.key() == user_logic.getForCurrentAccount().key()
       
   278 
       
   279       if not can_write or not is_same_user:
       
   280         # If user who can edit looks at her own taking page, show the default
       
   281         # form as readonly. Otherwise, below, show nothing.
       
   282         context["notice"] = "There are no records for this survey and user."
       
   283         return False
       
   284 
       
   285     survey_form = surveys.SurveyForm(survey_content=survey_content,
       
   286                                      this_user=user,
       
   287                                      project=project,
       
   288                                      survey_record=survey_record,
       
   289                                      read_only=read_only,
       
   290                                      editing=False)
       
   291     survey_form.getFields()
       
   292     if 'evaluation' in survey.taking_access:
       
   293       survey_form = surveys.getRoleSpecificFields(survey, user,
       
   294                                   project, survey_form, survey_record)
       
   295 
       
   296     # set help and status text
       
   297     self.setHelpStatus(context, read_only,
       
   298     survey_record, survey_form, survey)
       
   299 
       
   300     if not context['survey_form']:
       
   301       access_tpl = "Access Error: This Survey Is Limited To %s"
       
   302       context["notice"] = access_tpl % string.capwords(survey.taking_access)
       
   303 
       
   304     context['read_only'] = read_only
       
   305     context['project'] = project
       
   306     return True
       
   307 
       
   308   def getStatus(self, request, context, user, survey):
       
   309     """Determine if we're past deadline or before opening, check user rights.
       
   310     """
       
   311 
       
   312     read_only = (context.get("read_only", False) or
       
   313                  request.GET.get("read_only", False) or
       
   314                  request.POST.get("read_only", False)
       
   315                  )
       
   316     now = datetime.datetime.now()
       
   317 
       
   318     # check deadline, see check for opening below
       
   319     if survey.deadline and now > survey.deadline:
       
   320       # are we already passed the deadline?
       
   321       context["notice"] = "The Deadline For This Survey Has Passed"
       
   322       read_only = True
       
   323 
       
   324     # check if user can edit this survey
       
   325     params = dict(prefix=survey.prefix, scope_path=survey.scope_path)
       
   326     checker = access.rights_logic.Checker(survey.prefix)
       
   327     roles = checker.getMembership(survey.write_access)
       
   328     rights = self._params['rights']
       
   329     can_write = access.Checker.hasMembership(rights, roles, params)
       
   330 
       
   331 
       
   332     not_ready = False
       
   333     # check if we're past the opening date
       
   334     if survey.opening and now < survey.opening:
       
   335       not_ready = True
       
   336 
       
   337       # only users that can edit a survey should see it before opening
       
   338       if not can_write:
       
   339         context["notice"] = "There is no such survey available."
       
   340         return False
       
   341       else:
       
   342         context["notice"] = "This survey is not open for taking yet."
       
   343 
       
   344     return read_only, can_write, not_ready
       
   345 
       
   346   def setHelpStatus(self, context, read_only, survey_record, survey_form,
       
   347                     survey):
       
   348     """Set help_text and status for template use.
       
   349     """
       
   350 
       
   351     if not read_only:
       
   352       if not survey.deadline:
       
   353         deadline_text = ""
       
   354       else:
       
   355         deadline_text = " by " + str(
       
   356       survey.deadline.strftime("%A, %d. %B %Y %I:%M%p"))
       
   357 
       
   358       if survey_record:
       
   359         help_text = "Edit and re-submit this survey" + deadline_text + "."
       
   360         status = "edit"
       
   361       else:
       
   362         help_text = "Please complete this survey" + deadline_text + "."
       
   363         status = "create"
       
   364 
       
   365     else:
       
   366       help_text = "Read-only view."
       
   367       status = "view"
       
   368 
       
   369     survey_data = dict(survey_form=survey_form, status=status,
       
   370                                      help_text=help_text)
       
   371     context.update(survey_data)
       
   372 
       
   373   def _editContext(self, request, context):
       
   374     """Performs any required processing on the context for edit pages.
       
   375 
       
   376     Args:
       
   377       request: the django request object
       
   378       context: the context dictionary that will be used
       
   379 
       
   380       Adds list of SurveyRecord results as supplement to view.
       
   381 
       
   382       See surveys.SurveyResults for details.
       
   383     """
       
   384 
       
   385     if not getattr(self, '_entity', None):
       
   386       return
       
   387 
       
   388     results = surveys.SurveyResults()
       
   389 
       
   390     context['survey_records'] = results.render(self._entity, self._params,
       
   391                                                filter={})
       
   392 
       
   393     super(View, self)._editContext(request, context)
       
   394 
       
   395   def _editPost(self, request, entity, fields):
       
   396     """See base.View._editPost().
       
   397 
       
   398     Processes POST request items to add new dynamic field names,
       
   399     question types, and default prompt values to SurveyContent model.
       
   400     """
       
   401 
       
   402     user = user_logic.getForCurrentAccount()
       
   403     schema = {}
       
   404     survey_fields = {}
       
   405 
       
   406     if not entity:
       
   407       # new Survey
       
   408       if 'serialized' in request.POST:
       
   409         fields, schema, survey_fields = self.importSerialized(request, fields, user)
       
   410       fields['author'] = user
       
   411     else:
       
   412       fields['author'] = entity.author
       
   413       schema = self.loadSurveyContent(schema, survey_fields, entity)
       
   414 
       
   415     # remove deleted properties from the model
       
   416     self.deleteQuestions(schema, survey_fields, request.POST)
       
   417 
       
   418     # add new text questions and re-build choice questions
       
   419     self.getRequestQuestions(schema, survey_fields, request.POST)
       
   420 
       
   421     # get schema options for choice questions
       
   422     self.getSchemaOptions(schema, survey_fields, request.POST)
       
   423 
       
   424     survey_content = getattr(entity,'survey_content', None)
       
   425     # create or update a SurveyContent for this Survey
       
   426     survey_content = survey_logic.createSurvey(survey_fields, schema,
       
   427                                                 survey_content=survey_content)
       
   428 
       
   429     # save survey_content for existent survey or pass for creating a new one
       
   430     if entity:
       
   431       entity.modified_by = user
       
   432       entity.survey_content = survey_content
       
   433       db.put(entity)
       
   434     else:
       
   435       fields['survey_content'] = survey_content
       
   436 
       
   437     fields['modified_by'] = user
       
   438     super(View, self)._editPost(request, entity, fields)
       
   439 
       
   440   def loadSurveyContent(self, schema, survey_fields, entity):
       
   441     """Populate the schema dict and get text survey questions.
       
   442     """
       
   443 
       
   444     if hasattr(entity, 'survey_content'):
       
   445 
       
   446       # there is a SurveyContent already
       
   447       survey_content = entity.survey_content
       
   448       schema = eval(survey_content.schema)
       
   449 
       
   450       for question_name in survey_content.dynamic_properties():
       
   451 
       
   452         # get the current questions from the SurveyContent
       
   453         if question_name not in schema:
       
   454           continue
       
   455 
       
   456         if schema[question_name]['type'] not in CHOICE_TYPES:
       
   457           # Choice questions are always regenerated from request, see
       
   458           # self.get_request_questions()
       
   459           question = getattr(survey_content, question_name)
       
   460           survey_fields[question_name] = question
       
   461 
       
   462     return schema
       
   463 
       
   464   def deleteQuestions(self, schema, survey_fields, POST):
       
   465     """Process the list of questions to delete, from a hidden input.
       
   466     """
       
   467 
       
   468     deleted = POST.get('__deleted__', '')
       
   469 
       
   470     if deleted:
       
   471       deleted = deleted.split(',')
       
   472       for field in deleted:
       
   473 
       
   474         if field in schema:
       
   475           del schema[field]
       
   476 
       
   477         if field in survey_fields:
       
   478           del survey_fields[field]
       
   479 
       
   480   def getRequestQuestions(self, schema, survey_fields, POST):
       
   481     """Get fields from request.
       
   482 
       
   483     We use two field/question naming and processing schemes:
       
   484       - Choice questions consist of <input/>s with a common name, being rebuilt
       
   485         anew on every edit POST so we can gather ordering, text changes,
       
   486         deletions and additions.
       
   487       - Text questions only have special survey__* names on creation, afterwards
       
   488         they are loaded from the SurveyContent dynamic properties.
       
   489     """
       
   490 
       
   491     for key, value in POST.items():
       
   492 
       
   493       if key.startswith('id_'):
       
   494         # Choice question fields, they are always generated from POST contents,
       
   495         # as their 'content' is editable and they're reorderable. Also get
       
   496         # its field index for handling reordering fields later.
       
   497         name, number = key[3:].replace('__field', '').rsplit('_', 1)
       
   498 
       
   499         if name not in schema:
       
   500           if 'NEW_' + name in POST:
       
   501             # new Choice question, set generic type and get its index
       
   502             schema[name] = {'type': 'choice'}
       
   503             schema[name]['index'] = int(POST['index_for_' + name])
       
   504 
       
   505         if name in schema and schema[name]['type'] in CHOICE_TYPES:
       
   506           # build an index:content dictionary
       
   507           if name in survey_fields:
       
   508             if value not in survey_fields[name]:
       
   509               survey_fields[name][int(number)] = value
       
   510           else:
       
   511             survey_fields[name] = {int(number): value}
       
   512 
       
   513       elif key.startswith('survey__'): # new Text question
       
   514         # this is super ugly but unless data is serialized the regex is needed
       
   515         prefix = re.compile('survey__([0-9]{1,3})__')
       
   516         prefix_match = re.match(prefix, key)
       
   517 
       
   518         index = prefix_match.group(0).replace('survey', '').replace('__','')
       
   519         index = int(index)
       
   520 
       
   521         field_name = prefix.sub('', key)
       
   522         field = 'id_' + key
       
   523 
       
   524         for ptype in PROPERTY_TYPES:
       
   525           # should only match one
       
   526           if ptype + "__" in field_name:
       
   527             field_name = field_name.replace(ptype + "__", "")
       
   528             schema[field_name] = {}
       
   529             schema[field_name]["index"] = index
       
   530             schema[field_name]["type"] = ptype
       
   531 
       
   532         survey_fields[field_name] = value
       
   533 
       
   534   def getSchemaOptions(self, schema, survey_fields, POST):
       
   535     """Get question, type, rendering and option order for choice questions.
       
   536     """
       
   537 
       
   538     RENDER = {'checkboxes': 'multi_checkbox', 'select': 'single_select',
       
   539               'radio_buttons': 'quant_radio'}
       
   540 
       
   541     RENDER_TYPES = {'select': 'selection',
       
   542                     'checkboxes': 'pick_multi',
       
   543                     'radio_buttons': 'pick_quant' }
       
   544 
       
   545     for key in schema:
       
   546       if schema[key]['type'] in CHOICE_TYPES and key in survey_fields:
       
   547         render_for = 'render_for_' + key
       
   548         if render_for in POST:
       
   549           schema[key]['render'] = RENDER[POST[render_for]]
       
   550           schema[key]['type'] = RENDER_TYPES[POST[render_for]]
       
   551 
       
   552         # handle reordering fields
       
   553         ordered = False
       
   554         order = 'order_for_' + key
       
   555         if order in POST and isinstance(survey_fields[key], dict):
       
   556           order = POST[order]
       
   557 
       
   558           # 'order_for_name' is jquery serialized from a sortable, so it's in
       
   559           # a 'name[]=1&name[]=2&name[]=0' format ('id-li-' is set in our JS)
       
   560           order = order.replace('id-li-%s[]=' % key, '')
       
   561           order = order.split('&')
       
   562 
       
   563           if len(order) == len(survey_fields[key]) and order[0]:
       
   564             order = [int(number) for number in order]
       
   565 
       
   566             if set(order) == set(survey_fields[key]):
       
   567               survey_fields[key] = [survey_fields[key][i] for i in order]
       
   568               ordered = True
       
   569 
       
   570           if not ordered:
       
   571             # we don't have a good ordering to use
       
   572             ordered = sorted(survey_fields[key].items())
       
   573             survey_fields[key] = [value for index, value in ordered]
       
   574 
       
   575       # set 'question' entry (free text label for question) in schema
       
   576       question_for = 'NEW_' + key
       
   577       if question_for in POST:
       
   578         schema[key]["question"] = POST[question_for]
       
   579 
       
   580   def createGet(self, request, context, params, seed):
       
   581     """Pass the question types for the survey creation template.
       
   582     """
       
   583 
       
   584     context['question_types'] = QUESTION_TYPES
       
   585 
       
   586     # avoid spurious results from showing on creation
       
   587     context['new_survey'] = True
       
   588     return super(View, self).createGet(request, context, params, seed)
       
   589 
       
   590   def editGet(self, request, entity, context, params=None):
       
   591     """Process GET requests for the specified entity.
       
   592 
       
   593     Builds the SurveyForm that represents the Survey question contents.
       
   594     """
       
   595 
       
   596     # TODO(ajaksu) Move CHOOSE_A_PROJECT_FIELD and CHOOSE_A_GRADE_FIELD
       
   597     # to template.
       
   598 
       
   599     CHOOSE_A_PROJECT_FIELD = """<tr class="role-specific">
       
   600     <th><label>Choose Project:</label></th>
       
   601     <td>
       
   602       <select disabled="TRUE" id="id_survey__NA__selection__project"
       
   603         name="survey__1__selection__see">
       
   604           <option>Survey Taker's Projects For This Program</option></select>
       
   605      </td></tr>
       
   606      """
       
   607 
       
   608     CHOOSE_A_GRADE_FIELD = """<tr class="role-specific">
       
   609     <th><label>Assign Grade:</label></th>
       
   610     <td>
       
   611       <select disabled=TRUE id="id_survey__NA__selection__grade"
       
   612        name="survey__1__selection__see">
       
   613         <option>Pass/Fail</option>
       
   614       </select></td></tr>
       
   615     """
       
   616 
       
   617     self._entity = entity
       
   618     survey_content = entity.survey_content
       
   619     user = user_logic.getForCurrentAccount()
       
   620     # no project or survey_record needed for survey prototype
       
   621     project = None
       
   622     survey_record = None
       
   623 
       
   624 
       
   625     survey_form = surveys.SurveyForm(survey_content=survey_content,
       
   626                                      this_user=user, project=project, survey_record=survey_record,
       
   627                                      editing=True, read_only=False)
       
   628     survey_form.getFields()
       
   629 
       
   630 
       
   631     # activate grades flag -- TODO: Can't configure notice on edit page
       
   632     if request._get.get('activate'):
       
   633       context['notice'] = "Evaluation Grades Have Been Activated"
       
   634 
       
   635     local = dict(survey_form=survey_form, question_types=QUESTION_TYPES,
       
   636                 survey_h=entity.survey_content)
       
   637     context.update(local)
       
   638 
       
   639     params['edit_form'] = HelperForm(params['edit_form'])
       
   640     if entity.deadline and datetime.datetime.now() > entity.deadline:
       
   641       # are we already passed the deadline?
       
   642       context["passed_deadline"] = True
       
   643 
       
   644     return super(View, self).editGet(request, entity, context, params=params)
       
   645 
       
   646   def getMenusForScope(self, entity, params):
       
   647     """List featured surveys iff after the opening date and before deadline.
       
   648     """
       
   649 
       
   650     # only list surveys for registered users
       
   651     user = user_logic.getForCurrentAccount()
       
   652     if not user:
       
   653       return []
       
   654 
       
   655     filter = {
       
   656         'prefix' : params['url_name'],
       
   657         'scope_path': entity.key().id_or_name(),
       
   658         'is_featured': True,
       
   659         }
       
   660 
       
   661     entities = self._logic.getForFields(filter)
       
   662     submenus = []
       
   663     now = datetime.datetime.now()
       
   664 
       
   665     # cache ACL
       
   666     survey_rights = {}
       
   667 
       
   668     # add a link to all featured documents
       
   669     for entity in entities:
       
   670 
       
   671       # only list those surveys the user can read
       
   672       if entity.read_access not in survey_rights:
       
   673 
       
   674         params = dict(prefix=entity.prefix, scope_path=entity.scope_path,
       
   675                       link_id=entity.link_id, user=user)
       
   676 
       
   677         # TODO(ajaksu) use access.Checker.checkIsSurveyReadable
       
   678         checker = access.rights_logic.Checker(entity.prefix)
       
   679         roles = checker.getMembership(entity.read_access)
       
   680         rights = self._params['rights']
       
   681         can_read = access.Checker.hasMembership(rights, roles, params)
       
   682 
       
   683         # cache ACL for a given entity.read_access
       
   684         survey_rights[entity.read_access] = can_read
       
   685 
       
   686         if not can_read:
       
   687           continue
       
   688 
       
   689       elif not survey_rights[entity.read_access]:
       
   690         continue
       
   691 
       
   692       # omit if either before opening or after deadline
       
   693       if entity.opening and entity.opening > now:
       
   694         continue
       
   695 
       
   696       if entity.deadline and entity.deadline < now:
       
   697         continue
       
   698 
       
   699       #TODO only if a document is readable it might be added
       
   700       submenu = (redirects.getPublicRedirect(entity, self._params),
       
   701                  entity.short_name, 'show')
       
   702 
       
   703       submenus.append(submenu)
       
   704     return submenus
       
   705 
       
   706   def activate(self, request, **kwargs):
       
   707     """This is a hack to support the 'Enable grades' button.
       
   708     """
       
   709     self.activateGrades(request)
       
   710     redirect_path = request.path.replace('/activate/', '/edit/') + '?activate=1'
       
   711     return http.HttpResponseRedirect(redirect_path)
       
   712 
       
   713 
       
   714   def activateGrades(self, request, **kwargs):
       
   715     """Updates SurveyRecord's grades for a given Survey.
       
   716     """
       
   717     survey_key_name = survey_logic.getKeyNameFromPath(request.path)
       
   718     survey = Survey.get_by_key_name(survey_key_name)
       
   719     survey_logic.activateGrades(survey)
       
   720     return
       
   721 
       
   722   @decorators.merge_params
       
   723   @decorators.check_access
       
   724   def viewResults(self, request, access_type, page_name=None,
       
   725                   params=None, **kwargs):
       
   726     """View for SurveyRecord and SurveyRecordGroup.
       
   727     """
       
   728 
       
   729     entity, context = self.getContextEntity(request, page_name, params, kwargs)
       
   730 
       
   731     if context is None:
       
   732       # user cannot see this page, return error response
       
   733       return entity
       
   734 
       
   735     can_write = False
       
   736     rights = self._params['rights']
       
   737     try:
       
   738       rights.checkIsSurveyWritable({'key_name': entity.key().name(),
       
   739                                     'prefix': entity.prefix,
       
   740                                     'scope_path': entity.scope_path,
       
   741                                     'link_id': entity.link_id,},
       
   742                                    'key_name')
       
   743       can_write = True
       
   744     except out_of_band.AccessViolation:
       
   745       pass
       
   746 
       
   747     user = user_logic.getForCurrentAccount()
       
   748 
       
   749     filter = self._params.get('filter') or {}
       
   750 
       
   751     # if user can edit the survey, show everyone's results
       
   752     if can_write:
       
   753       filter['survey'] = entity
       
   754     else:
       
   755       filter.update({'user': user, 'survey': entity})
       
   756 
       
   757     limit = self._params.get('limit') or 1000
       
   758     offset = self._params.get('offset') or 0
       
   759     order = self._params.get('order') or []
       
   760     idx = self._params.get('idx') or 0
       
   761 
       
   762     records = results_logic.getForFields(filter=filter, limit=limit,
       
   763                                       offset=offset, order=order)
       
   764 
       
   765     updates = dicts.rename(params, params['list_params'])
       
   766     context.update(updates)
       
   767 
       
   768     context['results'] = records, records
       
   769     context['content'] = entity.survey_content
       
   770 
       
   771     template = 'soc/survey/results_page.html'
       
   772     return responses.respond(request, template, context=context)
       
   773 
       
   774   @decorators.merge_params
       
   775   @decorators.check_access
       
   776   def exportSerialized(self, request, access_type, page_name=None,
       
   777                        params=None, **kwargs):
       
   778 
       
   779     sur, context = self.getContextEntity(request, page_name, params, kwargs)
       
   780 
       
   781     if context is None:
       
   782       # user cannot see this page, return error response
       
   783       return sur
       
   784 
       
   785     json = sur.toDict()
       
   786     json.update(dict((f, str(getattr(sur, f))) for f in PLAIN.split()))
       
   787     static = ((f, str(getattr(sur, f).link_id)) for f in FIELDS.split())
       
   788     json.update(dict(static))
       
   789 
       
   790     dynamic = sur.survey_content.dynamic_properties()
       
   791     content = ((prop, getattr(sur.survey_content, prop)) for prop in dynamic)
       
   792     json['survey_content'] = dict(content)
       
   793 
       
   794     schema =  sur.survey_content.schema
       
   795     json['survey_content']['schema'] = eval(sur.survey_content.schema)
       
   796 
       
   797     data = simplejson.dumps(json, indent=2)
       
   798 
       
   799     return self.json(request, data=json)
       
   800 
       
   801   def importSerialized(self, request, fields, user):
       
   802     json = request.POST['serialized']
       
   803     json = simplejson.loads(json)['data']
       
   804     survey_content = json.pop('survey_content')
       
   805     schema = survey_content.pop('schema')
       
   806     del json['author']
       
   807     del json['created']
       
   808     del json['modified']
       
   809     #del json['is_featured']
       
   810     # keywords can't be unicode
       
   811     keywords = {}
       
   812     for key, val in json.items():
       
   813       keywords[str(key)] = val
       
   814     if 'is_featured' in keywords:
       
   815       keywords['is_featured'] = eval(keywords['is_featured'])
       
   816     return keywords, schema, survey_content
       
   817 
       
   818   def getContextEntity(self, request, page_name, params, kwargs):
       
   819     context = responses.getUniversalContext(request)
       
   820     responses.useJavaScript(context, params['js_uses_all'])
       
   821     context['page_name'] = page_name
       
   822     entity = None
       
   823 
       
   824     # TODO(ajaksu) there has to be a better way in this universe to get these
       
   825     kwargs['prefix'] = 'program'
       
   826     kwargs['link_id'] = request.path.split('/')[-1]
       
   827     kwargs['scope_path'] = '/'.join(request.path.split('/')[4:-1])
       
   828 
       
   829     entity = survey_logic.getFromKeyFieldsOr404(kwargs)
       
   830 
       
   831     if not self._public(request, entity, context):
       
   832       error = out_of_band.Error('')
       
   833       error = responses.errorResponse(
       
   834           error, request, template=params['error_public'], context=context)
       
   835       return error, None
       
   836 
       
   837     return entity, context
       
   838 
       
   839 class HelperForm(object):
       
   840   """Thin wrapper for adding values to params['edit_form'].fields.
       
   841   """
       
   842 
       
   843   def __init__(self, form=None):
       
   844     """Store the edit_form.
       
   845     """
       
   846 
       
   847     self.form = form
       
   848 
       
   849   def __call__(self, instance=None):
       
   850     """Transparently instantiate and add initial values to the edit_form.
       
   851     """
       
   852 
       
   853     form = self.form(instance=instance)
       
   854     form.fields['created_by'].initial = instance.author.name
       
   855     form.fields['last_modified_by'].initial = instance.modified_by.name
       
   856     form.fields['doc_key_name'].initial = instance.key().id_or_name()
       
   857     return form
       
   858 
       
   859 
       
   860 def _get_csv_header(sur):
       
   861   """CSV header helper, needs support for comment lines in CSV.
       
   862   """
       
   863 
       
   864   tpl = '# %s: %s\n'
       
   865 
       
   866   # add static properties
       
   867   fields = ['# Melange Survey export for \n#  %s\n#\n' % sur.title]
       
   868   fields += [tpl % (k,v) for k,v in sur.toDict().items()]
       
   869   fields += [tpl % (f, str(getattr(sur, f))) for f in PLAIN.split()]
       
   870   fields += [tpl % (f, str(getattr(sur, f).link_id)) for f in FIELDS.split()]
       
   871   fields.sort()
       
   872 
       
   873   # add dynamic properties
       
   874   fields += ['#\n#---\n#\n']
       
   875   dynamic = sur.survey_content.dynamic_properties()
       
   876   dynamic = [(prop, getattr(sur.survey_content, prop)) for prop in dynamic]
       
   877   fields += [tpl % (k,v) for k,v in sorted(dynamic)]
       
   878 
       
   879   # add schema
       
   880   fields += ['#\n#---\n#\n']
       
   881   schema =  sur.survey_content.schema
       
   882   indent = '},\n#' + ' ' * 9
       
   883   fields += [tpl % ('Schema', schema.replace('},', indent)) + '#\n']
       
   884 
       
   885   return ''.join(fields).replace('\n', '\r\n')
       
   886 
       
   887 
       
   888 def _get_records(recs, props):
       
   889   """Fetch properties from SurveyRecords for CSV export.
       
   890   """
       
   891 
       
   892   records = []
       
   893   props = props[1:]
       
   894   for rec in recs:
       
   895     values = tuple(getattr(rec, prop, None) for prop in props)
       
   896     leading = (rec.user.link_id,)
       
   897     records.append(leading + values)
       
   898   return records
       
   899 
       
   900 
       
   901 def to_csv(survey):
       
   902   """CSV exporter.
       
   903   """
       
   904 
       
   905   # get header and properties
       
   906   header = _get_csv_header(survey)
       
   907   leading = ['user', 'created', 'modified']
       
   908   properties = leading + survey.survey_content.orderedProperties()
       
   909 
       
   910   try:
       
   911     first = survey.survey_records.run().next()
       
   912   except StopIteration:
       
   913     # bail out early if survey_records.run() is empty
       
   914     return header, survey.link_id
       
   915 
       
   916   # generate results list
       
   917   recs = survey.survey_records.run()
       
   918   recs = _get_records(recs, properties)
       
   919 
       
   920   # write results to CSV
       
   921   output = StringIO.StringIO()
       
   922   writer = csv.writer(output)
       
   923   writer.writerow(properties)
       
   924   writer.writerows(recs)
       
   925 
       
   926   return header + output.getvalue(), survey.link_id
       
   927 
       
   928 
       
   929 view = View()
       
   930 
       
   931 admin = decorators.view(view.admin)
       
   932 create = decorators.view(view.create)
       
   933 edit = decorators.view(view.edit)
       
   934 delete = decorators.view(view.delete)
       
   935 list = decorators.view(view.list)
       
   936 public = decorators.view(view.public)
       
   937 export = decorators.view(view.export)
       
   938 pick = decorators.view(view.pick)
       
   939 activate = decorators.view(view.activate)
       
   940 results = decorators.view(view.viewResults)
       
   941 json = decorators.view(view.exportSerialized)