64 {% endif %}>False</option> |
66 {% endif %}>False</option> |
65 </select><br/> |
67 </select><br/> |
66 """ |
68 """ |
67 |
69 |
68 |
70 |
69 class SurveyForm(djangoforms.ModelForm): |
71 class SurveyTakeForm(djangoforms.ModelForm): |
70 """Main SurveyContent form. |
72 """SurveyContent form for recording survey answers. |
71 |
73 |
72 This class is used to produce survey forms for several circumstances: |
74 This class is used to produce survey forms for survey taking: |
73 - Admin creating survey from scratch |
|
74 - Admin updating existing survey |
|
75 - User taking survey |
75 - User taking survey |
76 - User updating already taken survey |
76 - User updating already taken survey |
77 |
77 |
78 Using dynamic properties of the survey model (if passed as an arg) the |
78 Using dynamic properties of the survey model (if passed as an arg) the |
79 survey form is dynamically formed. |
79 survey form is dynamically formed. |
80 """ |
80 """ |
81 |
81 |
82 def __init__(self, *args, **kwargs): |
82 def __init__(self, *args, **kwargs): |
83 """Store special kwargs as attributes. |
83 """Store special kwargs as attributes. |
84 |
84 |
|
85 survey_content: a SuveryContent entity. |
|
86 survey_logic: instance of SurveyLogic. |
|
87 survey_record: a SurveyRecord entity. |
85 read_only: controls whether the survey taking UI allows data entry. |
88 read_only: controls whether the survey taking UI allows data entry. |
86 editing: controls whether to show the edit or show form. |
|
87 """ |
89 """ |
88 |
90 |
89 self.kwargs = kwargs |
91 self.kwargs = kwargs |
|
92 |
90 self.survey_content = self.kwargs.pop('survey_content', None) |
93 self.survey_content = self.kwargs.pop('survey_content', None) |
91 self.this_user = self.kwargs.pop('this_user', None) |
|
92 self.project = self.kwargs.pop('project', None) |
|
93 self.survey_logic = self.kwargs.pop('survey_logic', None) |
94 self.survey_logic = self.kwargs.pop('survey_logic', None) |
94 self.survey_record = self.kwargs.pop('survey_record', None) |
95 self.survey_record = self.kwargs.pop('survey_record', None) |
95 |
|
96 self.read_only = self.kwargs.pop('read_only', None) |
96 self.read_only = self.kwargs.pop('read_only', None) |
97 self.editing = self.kwargs.pop('editing', None) |
|
98 |
97 |
99 self.fields_map = dict( |
98 self.fields_map = dict( |
100 long_answer=self.addLongField, |
99 long_answer=self.addLongField, |
101 short_answer=self.addShortField, |
100 short_answer=self.addShortField, |
102 selection=self.addSingleField, |
101 selection=self.addSingleField, |
103 pick_multi=self.addMultiField, |
102 pick_multi=self.addMultiField, |
104 pick_quant=self.addQuantField, |
103 pick_quant=self.addQuantField, |
105 ) |
104 ) |
106 |
105 |
107 self.kwargs['data'] = {} |
106 self.kwargs['data'] = {} |
108 super(SurveyForm, self).__init__(*args, **self.kwargs) |
107 super(SurveyTakeForm, self).__init__(*args, **self.kwargs) |
109 |
108 |
110 def getFields(self, post_dict=None): |
109 def getFields(self, post_dict=None): |
111 """Build the SurveyContent (questions) form fields. |
110 """Build the SurveyContent (questions) form fields. |
112 |
111 |
113 params: |
112 params: |
210 |
207 |
211 # first, insert dynamic survey fields |
208 # first, insert dynamic survey fields |
212 for position, property in survey_order.items(): |
209 for position, property in survey_order.items(): |
213 position = position * 2 |
210 position = position * 2 |
214 self.fields.insert(position, property, self.survey_fields[property]) |
211 self.fields.insert(position, property, self.survey_fields[property]) |
215 if not self.editing: |
212 |
216 # add comment if field has one and this isn't an edit view |
213 # add comment if field has one and this isn't an edit view |
217 property = COMMENT_PREFIX + property |
214 property = COMMENT_PREFIX + property |
218 if property in self.survey_fields: |
215 if property in self.survey_fields: |
219 self.fields.insert(position - 1, property, |
216 self.fields.insert(position - 1, property, |
220 self.survey_fields[property]) |
217 self.survey_fields[property]) |
221 return self.fields |
218 return self.fields |
222 |
219 |
223 def addLongField(self, field, value, attrs, schema, req=True, label='', |
220 def addLongField(self, field, value, attrs, schema, req=True, label='', |
224 tip='', comment=''): |
221 tip='', comment=''): |
225 """Add a long answer fields to this form. |
222 """Add a long answer fields to this form. |
380 choices=tuple(these_choices), widget=widget, |
370 choices=tuple(these_choices), widget=widget, |
381 initial=value) |
371 initial=value) |
382 self.survey_fields[field] = question |
372 self.survey_fields[field] = question |
383 |
373 |
384 def addCommentField(self, field, comment, attrs, tip): |
374 def addCommentField(self, field, comment, attrs, tip): |
385 if not self.editing: |
375 widget = widgets.Textarea(attrs=attrs) |
386 widget = widgets.Textarea(attrs=attrs) |
376 comment_field = CharField(help_text=tip, required=False, label='Comments', |
387 comment_field = CharField(help_text=tip, required=False, label='Comments', |
377 widget=widget, initial=comment) |
388 widget=widget, initial=comment) |
378 self.survey_fields[COMMENT_PREFIX + field] = comment_field |
389 self.survey_fields[COMMENT_PREFIX + field] = comment_field |
379 |
390 |
380 |
391 class Meta(object): |
381 class Meta(object): |
392 model = SurveyContent |
382 model = SurveyContent |
393 exclude = ['schema'] |
383 exclude = ['schema'] |
394 |
384 |
395 |
385 |
|
386 class SurveyEditForm(djangoforms.ModelForm): |
|
387 """SurveyContent form for editing a survey. |
|
388 |
|
389 This class is used to produce survey forms for several circumstances: |
|
390 - Admin creating survey from scratch |
|
391 - Admin updating existing survey |
|
392 |
|
393 Using dynamic properties of the survey model (if passed as an arg) the |
|
394 survey form is dynamically formed. |
|
395 """ |
|
396 |
|
397 def __init__(self, *args, **kwargs): |
|
398 """Store special kwargs as attributes. |
|
399 |
|
400 survey_content: a SurveyContent entity. |
|
401 survey_logic: an instance of SurveyLogic. |
|
402 """ |
|
403 |
|
404 self.kwargs = kwargs |
|
405 self.survey_content = self.kwargs.pop('survey_content', None) |
|
406 self.survey_logic = self.kwargs.pop('survey_logic', None) |
|
407 |
|
408 super(SurveyEditForm, self).__init__(*args, **self.kwargs) |
|
409 |
|
410 def getFields(self): |
|
411 """Build the SurveyContent (questions) form fields. |
|
412 |
|
413 params: |
|
414 post_dict: dict with POST data that will be used for validation |
|
415 |
|
416 Populates self.survey_fields, which will be ordered in self.insert_fields. |
|
417 Also populates self.data, which will be used in form validation. |
|
418 """ |
|
419 |
|
420 if not self.survey_content: |
|
421 return |
|
422 |
|
423 self.survey_fields = {} |
|
424 schema = SurveyContentSchema(self.survey_content.schema) |
|
425 extra_attrs = {} |
|
426 |
|
427 # add unordered fields to self.survey_fields |
|
428 for field in self.survey_content.dynamic_properties(): |
|
429 |
|
430 # use prompts set by survey creator |
|
431 value = getattr(self.survey_content, field) |
|
432 from_content = True |
|
433 |
|
434 label = schema.getLabel(field) |
|
435 if label is None: |
|
436 continue |
|
437 |
|
438 tip = 'Please provide an answer to this question.' |
|
439 kwargs = schema.getEditFieldArgs(field, value, tip, label) |
|
440 |
|
441 kwargs['widget'] = schema.getEditWidget(field, extra_attrs) |
|
442 |
|
443 |
|
444 # add new field |
|
445 self.survey_fields[field] = schema.getEditField(field)(**kwargs) |
|
446 |
|
447 # TODO(ajaksu): find a new way to keep fields in order |
|
448 return self.insertFields() |
|
449 |
|
450 def insertFields(self): |
|
451 """Add ordered fields to self.fields. |
|
452 """ |
|
453 |
|
454 survey_order = self.survey_content.getSurveyOrder() |
|
455 |
|
456 # insert dynamic survey fields |
|
457 for position, property in survey_order.items(): |
|
458 self.fields.insert(position, property, self.survey_fields[property]) |
|
459 |
|
460 return self.fields |
|
461 |
|
462 class Meta(object): |
|
463 model = SurveyContent |
|
464 exclude = ['schema'] |
|
465 |
|
466 |
396 class SurveyContentSchema(object): |
467 class SurveyContentSchema(object): |
397 """Abstract question metadata handling. |
468 """Abstract question metadata handling. |
398 """ |
469 """ |
399 |
470 |
400 def __init__(self, schema): |
471 def __init__(self, schema): |
|
472 """Set the dictionary that this class encapsulates. |
|
473 |
|
474 Args: |
|
475 schema: schema as stored in SurveyConent entity |
|
476 """ |
|
477 |
401 self.schema = eval(schema) |
478 self.schema = eval(schema) |
402 |
479 |
403 def getType(self, field): |
480 def getType(self, field): |
|
481 """Fetch question type for field e.g. short_answer, pick_multi, etc. |
|
482 |
|
483 Args: |
|
484 field: name of the field to get the type from |
|
485 """ |
|
486 |
404 return self.schema[field]["type"] |
487 return self.schema[field]["type"] |
405 |
488 |
406 def getRequired(self, field): |
489 def getRequired(self, field): |
407 """Check whether survey question is required. |
490 """Check whether survey question is required. |
|
491 |
|
492 Args: |
|
493 field: name of the field to check the required property for |
408 """ |
494 """ |
409 |
495 |
410 return self.schema[field]["required"] |
496 return self.schema[field]["required"] |
411 |
497 |
412 def getHasComment(self, field): |
498 def getHasComment(self, field): |
413 """Check whether survey question allows adding a comment. |
499 """Check whether survey question allows adding a comment. |
|
500 |
|
501 Args: |
|
502 field: name of the field to get the hasComment property for |
414 """ |
503 """ |
415 |
504 |
416 return self.schema[field]["has_comment"] |
505 return self.schema[field]["has_comment"] |
417 |
506 |
418 def getRender(self, field): |
507 def getRender(self, field): |
|
508 """Get rendering options for choice questions. |
|
509 |
|
510 Args: |
|
511 field: name of the field to get the rendering option for |
|
512 """ |
|
513 |
419 return self.schema[field]["render"] |
514 return self.schema[field]["render"] |
420 |
515 |
421 def getWidget(self, field, editing, attrs): |
516 def getEditField(self, field): |
422 """Get survey editing or taking widget for choice questions. |
517 """For a given question kind, get the correct edit view field. |
423 """ |
518 """ |
424 |
519 |
425 if editing: |
520 kind = self.getType(field) |
426 kind = self.getType(field) |
521 if kind in CHOICE_TYPES: |
|
522 Field = PickOneField |
|
523 else: |
|
524 Field = CharField |
|
525 |
|
526 return Field |
|
527 |
|
528 def getEditFieldArgs(self, field, value, tip, label): |
|
529 """Build edit view field arguments. |
|
530 |
|
531 params: |
|
532 field: field name |
|
533 value: field value (text for text questions, list for choice questions) |
|
534 tipe: help text, to be used in a tooltip |
|
535 label: the field's question (or identifier if question is missing) |
|
536 """ |
|
537 |
|
538 kind = self.getType(field) |
|
539 |
|
540 kwargs = dict(help_text=tip, required=False, label=label) |
|
541 |
|
542 if kind in CHOICE_TYPES: |
|
543 kwargs['choices'] = tuple([(val, val) for val in value]) |
|
544 else: |
|
545 kwargs['initial'] = value |
|
546 |
|
547 return kwargs |
|
548 |
|
549 def getEditWidget(self, field, attrs): |
|
550 """Get survey editing widget for questions. |
|
551 """ |
|
552 |
|
553 kind = self.getType(field) |
|
554 is_required = self.getRequired(field) |
|
555 has_comment = self.getHasComment(field) |
|
556 |
|
557 if kind in CHOICE_TYPES: |
|
558 widget = UniversalChoiceEditor |
427 render = self.getRender(field) |
559 render = self.getRender(field) |
428 is_required = self.getRequired(field) |
560 args = kind, render, is_required, has_comment |
429 has_comment = self.getHasComment(field) |
|
430 widget = UniversalChoiceEditor(kind, render, is_required, has_comment) |
|
431 else: |
561 else: |
432 widget = WIDGETS[self.schema[field]['render']](attrs=attrs) |
562 args = is_required, has_comment |
433 return widget |
563 if kind == 'long_answer': |
|
564 attrs['class'] = "text_question" |
|
565 widget = LongTextarea |
|
566 elif kind == 'short_answer': |
|
567 widget = ShortTextInput |
|
568 |
|
569 kwargs = dict(attrs=attrs) |
|
570 |
|
571 return widget(*args, **kwargs) |
434 |
572 |
435 def getLabel(self, field): |
573 def getLabel(self, field): |
436 """Fetch the free text 'question' or use field name as label. |
574 """Fetch the free text 'question' or use field name as label. |
437 """ |
575 """ |
438 |
576 |
569 """ |
706 """ |
570 |
707 |
571 # plain text area |
708 # plain text area |
572 output = super(LongTextarea, self).render(name, value, attrs) |
709 output = super(LongTextarea, self).render(name, value, attrs) |
573 |
710 |
574 if self.editing: |
711 # add 'required' and 'has_comment' fields |
575 # add 'required' and 'has_comment' fields |
712 context = dict(name=name, is_required=self.is_required, |
576 context = dict(name=name, is_required=self.is_required, |
713 has_comment=self.has_comment) |
577 has_comment=self.has_comment) |
714 template = loader.get_template_from_string(REQUIRED_COMMENT_TPL) |
578 template = loader.get_template_from_string(REQUIRED_COMMENT_TPL) |
715 rendered = template.render(context=loader.Context(dict_=context)) |
579 rendered = template.render(context=loader.Context(dict_=context)) |
716 output = rendered + output |
580 output = rendered + output |
717 |
581 |
718 output = '<fieldset>' + output + '</fieldset>' |
582 output = '<fieldset>' + output + '</fieldset>' |
|
583 return output |
719 return output |
584 |
720 |
585 |
721 |
586 class ShortTextInput(widgets.TextInput): |
722 class ShortTextInput(widgets.TextInput): |
587 """Set whether short answer question is required or allows comments. |
723 """Set whether short answer question is required or allows comments. |
588 """ |
724 """ |
589 |
725 |
590 def __init__(self, is_required, has_comment, attrs=None, editing=False): |
726 def __init__(self, is_required, has_comment, attrs=None): |
591 """Initialize widget and store editing mode. |
727 """Initialize widget and store editing mode. |
592 |
728 |
593 params: |
729 params: |
594 is_required: bool, controls selection in the 'required' extra field |
730 is_required: bool, controls selection in the 'required' extra field |
595 has_comments: bool, controls selection in the 'has_comment' extra field |
731 has_comments: bool, controls selection in the 'has_comment' extra field |
596 editing: bool, controls rendering as plain text input or with extra fields |
732 editing: bool, controls rendering as plain text input or with extra fields |
597 """ |
733 """ |
598 |
734 |
599 self.editing = editing |
|
600 self.is_required = is_required |
735 self.is_required = is_required |
601 self.has_comment = has_comment |
736 self.has_comment = has_comment |
602 |
737 |
603 super(ShortTextInput, self).__init__(attrs) |
738 super(ShortTextInput, self).__init__(attrs) |
604 |
739 |
778 markup = loader.render_to_string('soc/survey/results.html', |
912 markup = loader.render_to_string('soc/survey/results.html', |
779 dictionary=context).strip('\n') |
913 dictionary=context).strip('\n') |
780 return markup |
914 return markup |
781 |
915 |
782 |
916 |
783 def getRoleSpecificFields(survey, user, this_project, survey_form, |
|
784 survey_record): |
|
785 """For evaluations, mentors get required Project and Grade fields, and |
|
786 students get a required Project field. |
|
787 |
|
788 Because we need to get a list of the user's projects, we call the |
|
789 logic getProjects method, which doubles as an access check. |
|
790 (No projects means that the survey cannot be taken.) |
|
791 |
|
792 params: |
|
793 survey: the survey being taken |
|
794 user: the survey-taking user |
|
795 this_project: either an already-selected project, or None |
|
796 survey_form: the surveyForm widget for this survey |
|
797 survey_record: an existing survey record for a user-project-survey combo, |
|
798 or None |
|
799 """ |
|
800 |
|
801 field_count = len(eval(survey.survey_content.schema).items()) |
|
802 these_projects = survey_logic.getProjects(survey, user) |
|
803 if not these_projects: |
|
804 return False # no projects found |
|
805 |
|
806 project_pairs = [] |
|
807 #insert a select field with options for each project |
|
808 for project in these_projects: |
|
809 project_pairs.append((project.key(), project.title)) |
|
810 if project_pairs: |
|
811 project_tuples = tuple(project_pairs) |
|
812 # add select field containing list of projects |
|
813 projectField = forms.fields.ChoiceField( |
|
814 choices=project_tuples, |
|
815 required=True, |
|
816 widget=forms.Select()) |
|
817 projectField.choices.insert(0, (None, "Choose a Project") ) |
|
818 # if editing an existing survey |
|
819 if not this_project and survey_record: |
|
820 this_project = survey_record.project |
|
821 if this_project: |
|
822 for tup in project_tuples: |
|
823 if tup[1] == this_project.title: |
|
824 if survey_record: project_name = tup[1] + " (Saved)" |
|
825 else: project_name = tup[1] |
|
826 projectField.choices.remove(tup) |
|
827 projectField.choices.insert(0, (tup[0], project_name) ) |
|
828 break |
|
829 survey_form.fields.insert(0, 'project', projectField ) |
|
830 |
|
831 if survey.taking_access == "mentor evaluation": |
|
832 # If this is a mentor, add a field |
|
833 # determining if student passes or fails. |
|
834 # Activate grades handler should determine whether new status |
|
835 # is midterm_passed, final_passed, etc. |
|
836 grade_choices = (('pass', 'Pass'), ('fail', 'Fail')) |
|
837 grade_vals = { 'pass': True, 'fail': False } |
|
838 gradeField = forms.fields.ChoiceField(choices=grade_choices, |
|
839 required=True, |
|
840 widget=forms.Select()) |
|
841 |
|
842 gradeField.choices.insert(0, (None, "Choose a Grade") ) |
|
843 if survey_record: |
|
844 for g in grade_choices: |
|
845 if grade_vals[g[0]] == survey_record.grade: |
|
846 gradeField.choices.insert(0, (g[0],g[1] + " (Saved)") ) |
|
847 gradeField.choices.remove(g) |
|
848 break; |
|
849 gradeField.show_hidden_initial = True |
|
850 |
|
851 survey_form.fields.insert(field_count + 1, 'grade', gradeField) |
|
852 |
|
853 return survey_form |
|
854 |
|
855 |
|
856 class HelperForm(object): |
917 class HelperForm(object): |
857 """Thin wrapper for adding values to params['edit_form'].fields. |
918 """Thin wrapper for adding values to params['edit_form'].fields. |
858 """ |
919 """ |
859 |
920 |
860 def __init__(self, form=None): |
921 def __init__(self, form=None): |