Search code examples
djangodjango-formsdjango-viewsdjango-testing

POST to TestUpdateView updates existing object and creates new object


I have a test for an UpdateView that is:

  • Updating the existing object
  • Then trying to create a new blank object at the same time

This behavior does not happen when I run through the view in the browser, so it is something to do with how the test is running.

  • POST to the update URL
  • Request runs through the UpdateView
  • Form_valid() runs and hits instance.save()
  • Then something is causing a second object to be created right after it saves the original object

Any ideas?

The Test

class TestReviewUpdateView(TestCase):

    def setUp(self):
        self.review = ReviewFactory()
        self.submission = self.review.submission
        self.factory = RequestFactory()
        self.kwargs = {'submission_pk': self.submission.pk}

    def test_form_valid_with_object(self):
        self.request = self.factory.post(reverse(
            'submissions:review_update', kwargs=self.kwargs))

        # Create user
        self.request.user = StaffFactory()

        view = views.ReviewUpdateView()
        view.request = self.request
        view.object = self.review
        kwargs = {
            'scores': self.review.get_list_of_scores()
        }
        form = forms.ReviewForm(**kwargs)
        form.cleaned_data = {'axe_0': '4', 'axe_1': '4', 'axe_2': '4'}
        response = view.form_valid(form)
        assert response.status_code == 302

The View

class ReviewUpdateView(
    BaseReviewForm,
    UpdateView
):
    """ A view for updating reviews. """

    def dispatch(self, request, *args, **kwargs):
        self.submission = self.get_submission()
        self.conference = self.submission.conference
        return super().dispatch(request, *args, **kwargs)

def get_submission(self):
    return Submission.upcoming_objects.get_queryset(self.request.user).get(
            pk=self.kwargs['submission_pk'])

    def get_object(self):
        return self.model.objects.get(
            submission=self.submission,
            user=self.request.user)

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs.update({
            'scores': self.object.get_list_of_scores(),
        })
        return kwargs

 def save_scores(self, form, instance):
    for field in form.cleaned_data:
        # The score fields will be numbers:
        if "axe_" in field:
            form_field = form.fields[field]
            # Save the field's label as the key and score as value
            instance.scores[form_field.label] = form.cleaned_data[field]
    return instance

    def form_valid(self, form):
        instance = self.get_object()
        instance = self.save_scores(form, instance)
        instance.save()
        return super().form_valid(form)

The Form

class ReviewForm(forms.ModelForm):
    """ Form for new reviews of submissions """
    class Meta:
        model = Review
        fields = []

    def __init__(self, *args, **kwargs):
        review_fields = kwargs.pop("review_fields")
        scores = kwargs.pop("scores")
        super().__init__(*args, **kwargs)
        if review_fields:
            for i in review_fields:
                self.fields["axe_%s" % i] = forms.ChoiceField(
                    choices=NUMBER_CHOICES,
                    label=review_fields[i],
                    widget=forms.RadioSelect)
                if scores:
                    self.fields["axe_%s" % i].initial = scores[int(i)]

        self.helper = FormHelper()
        self.helper.layout = Layout()
        for i in review_fields:
            self.helper.layout.fields.append(
                InlineRadios("axe_%s" % i)
            )
        self.helper.layout.fields.append(
            ButtonHolder(
                Submit('submit', 'Submit', css_class='btn btn-primary')
            )

The Model

class Review(TimeStampedModel):
    """ Review is a model for collecting answers from reviewers """
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
    )
    user = models.ForeignKey(
        get_user_model(),
        null=False,
        on_delete=models.PROTECT)
    submission = models.ForeignKey(
        Submission,
        null=False,
        on_delete=models.CASCADE
    )
    scores = JSONField(null=True, blank=True)
    avg_score = models.DecimalField(max_digits=3, decimal_places=2, default=0)

    class Meta:
        unique_together = ("user", "submission")
        ordering = ['-avg_score', '-created']

    def save(self, *args, **kwargs):
        self.avg_score = self.calc_avg_score()
        import pdb; pdb.set_trace()
        # save() is called twice and when it runs a second time, it errors because no values are set
        super().save(*args, **kwargs)
        self.submission.save()

Answer

@dirkgroten pointed me in the right direction. The code solution is the following-

def test_form_valid_with_object(self):
    user = User.objects.create_superuser('foo', '[email protected]', 'bar')
    self.review.user = user
    self.review.save()
    self.submission.conference.reviewers.add(user)
    self.client.login(username='foo', password='bar')
    response = self.client.post(
        reverse('submissions:review_update', kwargs=self.kwargs),
        data={'axe_0': '4', 'axe_1': '4', 'axe_2': '4'})
    self.assertEqual(302, response.status_code)

Solution

  • You're doing this the wrong way. You should test your form and views in separate tests.

    Test your form by instantiating it with data and no instance for object creation and adding the instance for object updates. Check validity of form for valid and invalid input. E.g:

    form = ReviewForm(data=kwargs, instance=self.submission)
    self.assertFalse(form.is_valid())
    self.assertTrue(form.errors['some_field'])  # check some_field has an error
    

    Then test your views by just making a request to them and testing the response:

    self.client.force_login(user) # if you test for logged in user
    response = self.client.post(url, data={...}) # this runs all your view code
    self.assertEqual(302, response.status_code)  # form was valid
    self.assertTrue(Review.objects.exists())
    # or in case of invalid data
    self.assertEqual(200, response.status_code)
    self.assertTrue(response.context['form'].errors)