def update_or_create_answer(datapoint, question, sample, created_at,
                            collected_by=None):
    answer = None
    created = False
    measured = datapoint.get('measured', None)
    metric = datapoint.get('metric', question.default_metric)
    unit = datapoint.get('unit', metric.unit)
    try:
        with transaction.atomic():
            if unit.system in Unit.NUMERICAL_SYSTEMS:
                try:
                    try:
                        measured = str(int(measured))
                    except ValueError:
                        measured = '{:.0f}'.format(decimal.Decimal(measured))
                    answer, created = Answer.objects.update_or_create(
                        sample=sample, question=question,
                        metric=metric, defaults={
                            'measured': int(measured),
                            'unit': unit,
                            'created_at': created_at,
                            'collected_by': collected_by})
                except (ValueError, decimal.InvalidOperation, DataError) as err:
                    # We cannot convert to integer (ex: "12.8kW/h")
                    # or the value exceeds 32-bit representation.
                    # XXX We store as a text value so it is not lost.
                    LOGGER.warning(
                        "\"%(measured)s\": %(err)s for '%(metric)s'" % {
                        'measured': measured.replace('"', '\\"'),
                        'err': str(err).strip(),
                        'metric': metric.title})
                    unit = Unit.objects.get(slug='freetext')

            if unit.system not in Unit.NUMERICAL_SYSTEMS:
                if unit.system == Unit.SYSTEM_ENUMERATED:
                    try:
                        measured = Choice.objects.get(
                            unit=unit, text=measured).pk
                    except Choice.DoesNotExist:
                        choices = Choice.objects.filter(unit=unit)
                        raise ValidationError("'%s' is not a valid choice."\
                            " Expected one of %s." % (measured,
                            [choice.get('text', "")
                             for choice in six.itervalues(choices)]))
                else:
                    choice_rank = Choice.objects.filter(
                        unit=unit).aggregate(Max('rank')).get(
                            'rank__max', 0)
                    choice_rank = choice_rank + 1 if choice_rank else 1
                    choice = Choice.objects.create(
                        text=measured,
                        unit=unit,
                        rank=choice_rank)
                    measured = choice.pk
                answer, created = Answer.objects.update_or_create(
                    sample=sample, question=question,
                    metric=metric, defaults={
                        'measured': measured,
                        'unit': unit,
                        'created_at': created_at,
                        'collected_by': collected_by})
    except DataError as err:
        LOGGER.exception(err)
        raise ValidationError(
            "\"%(measured)s\": %(err)s for '%(metric)s'" % {
                'measured': measured.replace('"', '\\"'),
                'err': str(err).strip(),
                'metric': metric.title})

    return answer, created def is_authenticated(request): class AnswerForm(forms.ModelForm):
    """
    Form used to submit an Answer to a Question as part of Sample to a Campaign.
    """
    class Meta:
        model = Answer
        fields = []

    def __init__(self, *args, **kwargs):
        super(AnswerForm, self).__init__(*args, **kwargs)
        if self.instance.question:
            question = self.instance.question
        elif 'question' in kwargs.get('initial', {}):
            question = kwargs['initial']['question']
        required = True
        if self.instance.sample and self.instance.sample.campaign:
            campaign_attrs = EnumeratedQuestions.objects.filter(
                campaign=self.instance.sample.campaign,
                question=question).first()
            if campaign_attrs:
                required = campaign_attrs.required
        fields = _create_field(question.question_type, question.text,
            required=required, choices=question.choices)
        self.fields['text'] = fields[0]

    def save(self, commit=True):
        # We same in the view.
        pass class SampleCreateForm(forms.ModelForm):

    class Meta:
        model = Sample
        fields = []

    def __init__(self, *args, **kwargs):
        super(SampleCreateForm, self).__init__(*args, **kwargs)
        for idx, question in enumerate(self.initial.get('questions', [])):
            key = 'question-%d' % (idx + 1)
            required = True
            campaign_attrs = EnumeratedQuestions.objects.filter(
                campaign=self.instance.campaign,
                question=question).first()
            if campaign_attrs:
                required = campaign_attrs.required
            fields = _create_field(question.question_type, question.text,
                required=required, choices=question.choices)
            self.fields[key] = fields[0]
            if fields[1]:
                self.fields[key.replace('question-', 'other-')] = fields[1]

    def clean(self):
        super(SampleCreateForm, self).clean()
        items = six.iteritems(self.cleaned_data)
        for key, value in items:
            if key.startswith('other-'):
                if value:
                    self.cleaned_data[key.replace(
                        'other-', 'question-')] = value
                del self.cleaned_data[key]
        return self.cleaned_data

    def save(self, commit=True):
        if 'account' in self.initial:
            self.instance.account = self.initial['account']
        if 'campaign' in self.initial:
            self.instance.campaign = self.initial['campaign']
        self.instance.slug = slugify(uuid.uuid4().hex)
        return super(SampleCreateForm, self).save(commit) class SampleUpdateForm(forms.ModelForm):
    """
    Auto-generated ``Form`` from a list of ``Question`` in a ``Campaign``.
    """

    class Meta:
        model = Sample
        fields = []

    def __init__(self, *args, **kwargs):
        super(SampleUpdateForm, self).__init__(*args, **kwargs)
        for idx, answer in enumerate(self.instance.get_answers_by_rank()):
            question = answer.question
            required = True
            rank = idx
            campaign_attrs = EnumeratedQuestions.objects.filter(
                campaign=self.instance.campaign,
                question=question).first()
            if campaign_attrs:
                required = campaign_attrs.required
                rank = campaign_attrs.rank
            fields = _create_field(question.question_type, question.text,
                required=required, choices=question.choices)
            # XXX set value.
            self.fields['question-%d' % rank] = fields[0]
            if fields[1]:
                self.fields['other-%d' % rank] = fields[1] class AccountMixin(object):
    """
    Mixin to use in views that will retrieve an account object (out of
    ``account_queryset``) associated to a slug parameter (``account_url_kwarg``)
    in the URL.
    If either ``account_url_kwarg`` is ``None`` or absent from the URL pattern,
    ``account`` will default to the ``request.user`` when the account model is
    compatible with the `User` model, else ``account`` will be ``None``.
    """
    account_queryset = get_account_model().objects.all()
    account_lookup_field = settings.ACCOUNT_LOOKUP_FIELD
    account_url_kwarg = 'organization'

    @property
    def account(self):
        if not hasattr(self, '_account'):
            if (self.account_url_kwarg is not None
                and self.account_url_kwarg in self.kwargs):
                if self.account_queryset is None:
                    raise ImproperlyConfigured(
                        "%(cls)s.account_queryset is None. Define "
                        "%(cls)s.account_queryset." % {
                            'cls': self.__class__.__name__
                        }
                    )
                if self.account_lookup_field is None:
                    raise ImproperlyConfigured(
                        "%(cls)s.account_lookup_field is None. Define "
                        "%(cls)s.account_lookup_field as the field used "
                        "to retrieve accounts in the database." % {
                            'cls': self.__class__.__name__
                        }
                    )
                kwargs = {'%s__exact' % self.account_lookup_field:
                    self.kwargs.get(self.account_url_kwarg)}
                try:
                    self._account = self.account_queryset.filter(**kwargs).get()
                except self.account_queryset.model.DoesNotExist:
                    #pylint: disable=protected-access
                    raise Http404(_("No %(verbose_name)s found matching"\
                        "the query") % {'verbose_name':
                        self.account_queryset.model._meta.verbose_name})
            else:
                if (isinstance(get_account_model(), get_user_model()) and
                    is_authenticated(self.request)):
                    self._account = self.request.user
                self._account = None
        return self._account

    def get_context_data(self, **kwargs):
        context = super(AccountMixin, self).get_context_data(**kwargs)
        context.update({'account': self.account})
        return context

    def get_reverse_kwargs(self):
        """
        List of kwargs taken from the url that needs to be passed through
        to ``get_success_url``.
        """
        return [self.account_url_kwarg]

    def get_url_kwargs(self):
        kwargs = {}
        for url_kwarg in self.get_reverse_kwargs():
            url_kwarg_val = self.kwargs.get(url_kwarg, None)
            if url_kwarg_val:
                kwargs.update({url_kwarg: url_kwarg_val})
        return kwargs class SampleMixin(AccountMixin):
    """
    Returns a ``Sample`` to a ``Campaign``.
    """

    # We have to use a special url_kwarg here because 'sample'
    # interfers with the way rest_framework handles **kwargs.
    sample_url_kwarg = 'sample'
    campaign_url_kwarg = 'campaign'
    path_url_kwarg = 'path'

    def get_reverse_kwargs(self):
        """
        List of kwargs taken from the url that needs to be passed through
        to ``get_success_url``.
        """
        return super(SampleMixin, self).get_reverse_kwargs() + [
            self.sample_url_kwarg, self.campaign_url_kwarg, self.path_url_kwarg]

    @property
    def path(self):
        if not hasattr(self, '_path'):
            self._path = self.kwargs.get(self.path_url_kwarg, '')
            if self._path and not self._path.startswith('/'):
                self._path = '/%s' % self._path
        return self._path

    @property
    def sample(self):
        if not hasattr(self, '_sample'):
            self._sample = self.get_sample()
        return self._sample

    def get_sample(self, url_kwarg=None):
        """
        Returns the ``Sample`` object associated with this URL.
        """
        if not url_kwarg:
            url_kwarg = self.sample_url_kwarg
        sample = None
        sample_slug = self.kwargs.get(url_kwarg)
        if sample_slug:
            # We have an id for the sample, let's get it and check
            # the user has rights to it.
            try:
                sample = Sample.objects.filter(slug=sample_slug).select_related(
                    'campaign').get()
            except Sample.DoesNotExist:
                raise Http404("Cannot find Sample(slug='%s')" % sample_slug)
        else:
            # Well no id, let's see if we can find a sample from
            # a campaign slug and a account
            campaign_slug = self.kwargs.get(self.campaign_url_kwarg)
            if campaign_slug:
                try:
                    sample = Sample.objects.filter(account=self.account,
                        campaign__slug=campaign_slug).select_related(
                        'campaign').get()
                except Sample.DoesNotExist:
                    raise Http404("Cannot find Sample(account__slug='%s',"\
                        " campaign__slug='%s')" % (self.account, campaign_slug))
        return sample class Answer(models.Model):
    """
    An Answer to a Question as part of Sample to a Campaign.
    """
    objects = AnswerManager()

    created_at = models.DateTimeField(auto_now_add=True)
    question = models.ForeignKey(settings.QUESTION_MODEL,
        on_delete=models.PROTECT)
    metric = models.ForeignKey(Metric, on_delete=models.PROTECT)
    unit = models.ForeignKey(Unit, on_delete=models.PROTECT, null=True)
    measured = models.IntegerField(null=True)
    denominator = models.IntegerField(null=True, default=1)
    collected_by = models.ForeignKey(settings.AUTH_USER_MODEL,
        null=True, on_delete=models.PROTECT)
    # XXX Optional fields when the answer is part of a campaign.
    sample = models.ForeignKey(Sample, on_delete=models.CASCADE,
        related_name='answers')

    class Meta:
        unique_together = ('sample', 'question', 'metric')

    def __str__(self):
        return '%s-%d' % (self.sample.slug, self.rank)

    @property
    def as_text_value(self):
        if self.unit.system in Unit.NUMERICAL_SYSTEMS:
            return self.measured
        return Choice.objects.get(pk=self.measured).text

    @property
    def rank(self):
        if not hasattr(self, '_rank'):
            self._rank = EnumeratedQuestions.objects.get(
                campaign=self.sample.campaign, question=self.question).rank
        return self._rank

    def get_multiple_choices(self):
        text = Choice.objects.get(pk=self.measured).text
        return text.replace('[', '').replace(']', '').replace(
            'u\'', '').replace('\'', '').split(', ') class Campaign(SlugTitleMixin, models.Model):

    slug = models.SlugField(unique=True)
    created_at = models.DateTimeField(null=True)
    title = models.CharField(max_length=150,
        help_text=_("Enter a campaign title."))
    description = models.TextField(null=True, blank=True,
        help_text=_("This description will be displayed to interviewees."))
    account = models.ForeignKey(settings.BELONGS_MODEL,
        on_delete=models.PROTECT, null=True)
    active = models.BooleanField(default=False)
    quizz_mode = models.BooleanField(default=False,
        help_text=_("If checked, correct answser are required"))
    defaults_single_page = models.BooleanField(default=False,
        help_text=_("If checked, will display all questions on a single page,"\
" else there will be one question per page."))
    one_response_only = models.BooleanField(default=False,
        help_text=_("Only allows to answer campaign once."))
    questions = models.ManyToManyField(settings.QUESTION_MODEL,
        through='survey.EnumeratedQuestions', related_name='campaigns')
    extra = get_extra_field_class()(null=True)

    def __str__(self):
        return str(self.slug)

    def has_questions(self):
        return self.questions.exists() class Choice(models.Model):
    """
    Choice for a multiple choice question.
    """
    unit = models.ForeignKey(Unit, on_delete=models.PROTECT)
    rank = models.IntegerField(
        help_text=_("used to order choice when presenting a question"))
    text = models.TextField()
    descr = models.TextField()

    class Meta:
        unique_together = ('unit', 'rank')

    def __str__(self):
        return str(self.text) class EnumeratedQuestions(models.Model):

    campaign = models.ForeignKey(Campaign, on_delete=models.CASCADE)
    question = models.ForeignKey(settings.QUESTION_MODEL,
        on_delete=models.CASCADE)
    rank = models.IntegerField(
        help_text=_("used to order questions when presenting a campaign."))
    required = models.BooleanField(default=True,
        help_text=_("If checked, an answer is required"))

    class Meta:
        unique_together = ('campaign', 'rank')

    def __str__(self):
        return str(self.question.path) class Sample(models.Model):
    """
    Sample to a Campaign. A Sample is composed of multiple Answers
    to Questions.
    """
    objects = SampleManager()

    slug = models.SlugField(unique=True,
        help_text="Unique identifier for the sample. It can be used in a URL.")
    created_at = models.DateTimeField(auto_now_add=True,
        help_text="Date/time of creation (in ISO format)")
    campaign = models.ForeignKey(Campaign, null=True, on_delete=models.PROTECT)
    account = models.ForeignKey(settings.ACCOUNT_MODEL,
        null=True, on_delete=models.PROTECT, related_name='samples')
    time_spent = models.DurationField(default=datetime.timedelta,
        help_text="Total recorded time to complete the campaign")
    is_frozen = models.BooleanField(default=False,
        help_text="When True, answers to that sample cannot be updated.")
    extra = get_extra_field_class()(null=True)

    def __str__(self):
        return str(self.slug)

    def save(self, force_insert=False, force_update=False,
             using=None, update_fields=None):
        if not self.slug:
            self.slug = slugify(uuid.uuid4().hex)
        return super(Sample, self).save(
            force_insert=force_insert, force_update=force_update,
            using=using, update_fields=update_fields)

    def get_answers_by_rank(self):
        # We attempt to get Django ORM to generate SQL equivalent to:
        # ```
        # SELECT survey_answer.* FROM survey_answer
        # INNER JOIN survey_sample
        # ON survey_answer.sample_id = survey_sample.id
        # INNER JOIN survey_enumeratedquestions
        # ON survey_answer.question_id = survey_enumeratedquestions.question_id
        # ON survey_sample.campaign_id = survey_enumeratedquestions.campaign_id
        # WHERE survey_answer.sample_id = %(sample_id)d
        # ORDER BY survey_enumeratedquestions.rank
        # ```
        queryset = Answer.objects.filter(
            sample=self,
            question__campaigns__in=[self.campaign.pk]).order_by(
            'question__enumeratedquestions__rank').annotate(
                rank=models.F('question__enumeratedquestions__rank'))
        return queryset def datetime_or_now(dtime_at=None):
    if not dtime_at:
        return datetime.datetime.utcnow().replace(tzinfo=utc)
    if isinstance(dtime_at, six.string_types):
        dtime_at = parse_datetime(dtime_at)
    if dtime_at.tzinfo is None:
        dtime_at = dtime_at.replace(tzinfo=utc)
    return dtime_at def get_question_model():
    """
    Returns the ``Question`` model that is active in this project.
    """
    try:
        return django_apps.get_model(settings.QUESTION_MODEL)
    except ValueError:
        raise ImproperlyConfigured(
            "QUESTION_MODEL must be of the form 'app_label.model_name'")
    except LookupError:
        raise ImproperlyConfigured("QUESTION_MODEL refers to model '%s'"\
" that has not been installed" % settings.QUESTION_MODEL)
import logging
from django.core.exceptions import NON_FIELD_ERRORS
from django.db import transaction
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.views.generic import (CreateView, RedirectView, TemplateView,
    UpdateView)
from rest_framework.exceptions import ValidationError
from ..api.sample import update_or_create_answer
from ..compat import is_authenticated, reverse
from ..forms import AnswerForm, SampleCreateForm, SampleUpdateForm
from ..mixins import AccountMixin, SampleMixin
from ..models import Answer, Campaign, Choice, EnumeratedQuestions, Sample
from ..utils import datetime_or_now, get_question_model
# Copyright (c) 2020, DjaoDjin inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.





LOGGER = logging.getLogger(__name__)


class AnswerUpdateView(SampleMixin, UpdateView):
    """
    Update an ``Answer``.
    """

    model = Answer
    form_class = AnswerForm
    lookup_rank_kwarg = 'rank'
    next_step_url = 'survey_answer_update'
    complete_url = 'survey_sample_results'

    @property
    def rank(self):
        return int(self.kwargs.get(self.lookup_rank_kwarg))

    @property
    def question(self):
        if not hasattr(self, '_question'):
            if self.sample:
                self._question = get_object_or_404(
                    get_question_model().objects.all(),
                    enumeratedquestions__campaign=self.sample.campaign,
                    enumeratedquestions__rank=self.rank)
            else:
                self._question = None  # API docs get here.
        return self._question

    def dispatch(self, request, *args, **kwargs):
        """
        Shows the Question or redirects to the complete URL if the ``Sample``
        instance is frozen.
        """
        # Implementation Note:
        #    The "is_frozen" test is done in dispatch because we want
        #    to prevent updates on any kind of requests.
        self.object = self.get_object()
        if not self.object or self.object.sample.is_frozen:
            return redirect(reverse(self.complete_url,
                kwargs=self.get_url_kwargs()))
        return super(AnswerUpdateView, self).dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super(AnswerUpdateView, self).get_context_data(**kwargs)
        context.update(self.get_url_kwargs())
        return context

    def get_next_answer(self):
        return self.sample.get_answers_by_rank().filter(measured=None).first()

    def get_object(self, queryset=None):
        if self.rank:
            return get_object_or_404(Answer,
                sample=self.sample, question=self.question)
        return self.get_next_answer()

    def get_success_url(self):
        kwargs = self.get_url_kwargs()
        next_answer = self.get_next_answer()
        if not next_answer:

            return reverse(self.complete_url,