|
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) |