""" Models to support Course Surveys feature """ import logging from lxml import etree from collections import OrderedDict from django.db import models from student.models import User from django.core.exceptions import ValidationError from model_utils.models import TimeStampedModel from survey.exceptions import SurveyFormNameAlreadyExists, SurveyFormNotFound from openedx.core.djangoapps.xmodule_django.models import CourseKeyField log = logging.getLogger("edx.survey") class SurveyForm(TimeStampedModel): """ Model to define a Survey Form that contains the HTML form data that is presented to the end user. A SurveyForm is not tied to a particular run of a course, to allow for sharing of Surveys across courses """ name = models.CharField(max_length=255, db_index=True, unique=True) form = models.TextField() def __unicode__(self): return self.name def save(self, *args, **kwargs): """ Override save method so we can validate that the form HTML is actually parseable """ self.validate_form_html(self.form) # now call the actual save method super(SurveyForm, self).save(*args, **kwargs) @classmethod def validate_form_html(cls, html): """ Makes sure that the html that is contained in the form field is valid """ try: fields = cls.get_field_names_from_html(html) except Exception as ex: log.exception("Cannot parse SurveyForm html: {}".format(ex)) raise ValidationError("Cannot parse SurveyForm as HTML: {}".format(ex)) if not len(fields): raise ValidationError("SurveyForms must contain at least one form input field") @classmethod def create(cls, name, form, update_if_exists=False): """ Helper class method to create a new Survey Form. update_if_exists=True means that if a form already exists with that name, then update it. Otherwise throw an SurveyFormAlreadyExists exception """ survey = cls.get(name, throw_if_not_found=False) if not survey: survey = SurveyForm(name=name, form=form) else: if update_if_exists: survey.form = form else: raise SurveyFormNameAlreadyExists() survey.save() return survey @classmethod def get(cls, name, throw_if_not_found=True): """ Helper class method to look up a Survey Form, throw FormItemNotFound if it does not exists in the database, unless throw_if_not_found=False then we return None """ survey = None exists = SurveyForm.objects.filter(name=name).exists() if exists: survey = SurveyForm.objects.get(name=name) elif throw_if_not_found: raise SurveyFormNotFound() return survey def get_answers(self, user=None, limit_num_users=10000): """ Returns all answers for all users for this Survey """ return SurveyAnswer.get_answers(self, user, limit_num_users=limit_num_users) def has_user_answered_survey(self, user): """ Returns whether a given user has supplied answers to this survey """ return SurveyAnswer.do_survey_answers_exist(self, user) def save_user_answers(self, user, answers, course_key): """ Store answers to the form for a given user. Answers is a dict of simple name/value pairs IMPORTANT: There is no validaton of form answers at this point. All data supplied to this method is presumed to be previously validated """ # first remove any answer the user might have done before self.clear_user_answers(user) SurveyAnswer.save_answers(self, user, answers, course_key) def clear_user_answers(self, user): """ Removes all answers that a user has submitted """ SurveyAnswer.objects.filter(form=self, user=user).delete() def get_field_names(self): """ Returns a list of defined field names for all answers in a survey. This can be helpful for reporting like features, i.e. adding headers to the reports This is taken from the set of fields inside the form. """ return SurveyForm.get_field_names_from_html(self.form) @classmethod def get_field_names_from_html(cls, html): """ Returns a list of defined field names from a block of HTML """ names = [] # make sure the form is wrap in some outer single element # otherwise lxml can't parse it # NOTE: This wrapping doesn't change the ability to query it tree = etree.fromstring(u'