2
\$\begingroup\$

This is my first go at Django, so I'm looking first to learn how to make this more idiomatic. Also, is there a better way to set up my tests? To my HTTP status codes make sense?

chores/tests.py

import datetime
import json

from django.test import TestCase, Client
from django.urls import reverse
from rest_framework import status

from .models import Chore


class ChoreTest(TestCase):

    def test_update(self):
        chore = Chore(name='sweep',
                      period=7,
                      due=datetime.date.today())
        chore.update()
        self.assertEqual(chore.due,
                         datetime.date.today() + datetime.timedelta(days=7))

    def test_update_overdue_chore(self):
        chore = Chore(name='sweep',
                      period=7,
                      due=datetime.date.today() - datetime.timedelta(days=1))
        chore.update()
        self.assertEqual(chore.due,
                         datetime.date.today() + datetime.timedelta(days=7))


class TestChoresView(TestCase):
    def setUp(self) -> None:
        Chore.objects.create(name='sweep',
                             period=7,
                             due=datetime.date.today())
        Chore.objects.create(name='wipe',
                             period=3,
                             due=datetime.date.today() + datetime.timedelta(days=1))
        self.client = Client()

    def test_get_all_chores(self):
        response = self.client.get(reverse('all'))
        response_content = json.loads(response.content)
        self.assertEqual(2, len(response_content))
        all_names = [chore['name'] for chore in response_content]
        self.assertIn('sweep', all_names)
        self.assertIn('wipe', all_names)

    def test_chores_correctly_serialized(self):
        response = self.client.get(reverse('all'))
        response_content = json.loads(response.content)
        expected_keys = ('name', 'period', 'due', 'id')
        for key in expected_keys:
            self.assertIn(key, response_content[0].keys())
            self.assertIn(key, response_content[1].keys())
        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_update_chore(self):
        chore = Chore.objects.get(name='sweep')
        response = self.client.generic('POST', reverse('update'),
                                       json.dumps({'id': chore.pk}))
        chore = Chore.objects.get(name='sweep')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(chore.due,
                         datetime.date.today() + datetime.timedelta(days=chore.period))

    def test_update_invalid_chore(self):
        response = self.client.generic('POST', reverse('update'),
                                       json.dumps({'id': 1337}))
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

    def test_get_due_chores(self):
        response = self.client.get(reverse('due'))
        response_content = json.loads(response.content)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(1, len(response_content))
        self.assertEqual(response_content[0]['name'], 'sweep')

    def test_get_no_chores_due(self):
        # Start by updating all chores, so none will be due
        chores = Chore.objects.all()
        for chore in chores:
            chore.update()

        response = self.client.get(reverse('due'))
        response_content = json.loads(response.content)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(1, len(response_content))

chores/views.py

import datetime
import json

from django.shortcuts import render
from django.views.decorators.csrf import ensure_csrf_cookie

from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response

from .models import Chore
from .serializers import ChoreSerializer


@ensure_csrf_cookie
def index(request):
    return render(request, 'chores/index.html')


@ensure_csrf_cookie
@api_view(['GET'])
def chores(request):
    all_chores = Chore.objects.all()
    serializer = ChoreSerializer(all_chores, many=True)
    return Response(serializer.data)


@ensure_csrf_cookie
@api_view(['POST'])
def update_chore(request):
    content = json.loads(request.body)
    chore_id = content['id']
    try:
        chore = Chore.objects.get(pk=chore_id)
    except Chore.DoesNotExist:
        return Response(status=status.HTTP_400_BAD_REQUEST)

    chore.update()
    chore.save()
    return Response(status=status.HTTP_200_OK)


@api_view(['GET'])
def due_chores(request):
    today = datetime.date.today()
    all_chores = Chore.objects.filter(due__lte=today)
    serializer = ChoreSerializer(all_chores, many=True)
    return Response(serializer.data)

chores/models.py

import datetime
from django.db import models


class Chore(models.Model):
    name = models.CharField(max_length=100)

    # Number of days in between instances of scheduled chore
    period = models.IntegerField()

    # Due date of chore
    due = models.DateField()

    def __str__(self):
        return self.name

    def update(self) -> None:
        """
        Mark a chore as complete by updating the due date.
        :return: None
        """
        self.due = datetime.date.today() + datetime.timedelta(days=self.period)

\$\endgroup\$
2
\$\begingroup\$

Some suggestions:

  1. Chore's period field should probably be period_days to avoid the comment.
  2. The comment on update makes it look like it should be called mark_complete or something, so that you can remove the comment. In any case I would expect the view rather than the model to do that update.
  3. It is customary to use a ModelViewSet to encapsulate the sort of functions you have in views.py.
  4. Chore.DoesNotExist should result in a HTTP 404 response code, which is the default. No need for custom code. In general, letting DRF decide the response code is much easier.
  5. The test classes have inconsistent naming.
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.