Django Tutorial 4

Django TutorialIn this part of my Django tutorial series we’ll focus on Generic Views and Automated Testing. Through testing we can quickly check how are site is operating after each change. You can also simulate how the site will interact with a user depending upon an unlimited number of randomized inputs. With a little work up front you will save yourself from numerous future headaches.

All of the code follows the video below.

If you value videos like this consider donating a $1 on Patreon.

[googleplusone]

Code & Transcript

# ---------- /polls/urls.py ----------

# 1 Generic views are normally used from the beginning
# and they help you avoid having to write a lot
# of custom code

from django.conf.urls import url

from . import views

app_name = 'polls'

# 1 We'll change the urlpatterns

urlpatterns = [
    url(r'^$', views.IndexView.as_view(), name='index'),
    url(r'^(?P<pk>[0-9]+)/$', views.DetailView.as_view(), name='detail'),
    url(r'^(?P<pk>[0-9]+)/results/$', views.ResultsView.as_view(), name='results'),
    url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]

# 1 Now remove our index, detail and results views in polls/views.py

'''
urlpatterns = [
    url(r'^$', views.index, name='index'),
    url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
    url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
    url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]
'''

# ---------- /polls/views.py ----------

from django.http import HttpResponse

from .models import Question, Choice

from django.shortcuts import render

from django.shortcuts import get_object_or_404

from django.http import HttpResponseRedirect

from django.urls import reverse

# 2 Add the generic module
from django.views import generic

# 8 Import so we can get time information
from django.utils import timezone

# 2 The ListView displays your list of questions being
# latest_question_list
class IndexView(generic.ListView):
    template_name = 'polls/index.html'

    # 2 This defines the question list we want to use
    context_object_name = 'latest_question_list'

    # 8 Replace get_queryset and don't return questions
    #  published in the future
    '''
    def get_queryset(self):

        # 2 Return the last 5 questions entered
        return Question.objects.order_by('-pub_date')[:5]
    '''

    def get_queryset(self):

        # 8 Return only questions with a pub_date less than
        # or equal to now
        return Question.objects.filter(
            pub_date__lte=timezone.now()
        ).order_by('-pub_date')[:5]

        # 8 Add Choices to the Admin in admin.py

# 2 The DetailView displays the details on your object
# being the Question model

# 2 The generic view expects the pk (Primary Key) value
# from the URL to be called pk as we set in polls/urls.py
class DetailView(generic.DetailView):
    model = Question

    # 2 Define the template to use with this data
    template_name = 'polls/detail.html'

    # 12 Exclude questions that are not published yet
    def get_queryset(self):
        return Question.objects.filter(pub_date__lte=timezone.now())

    # 12 Add tests in polls/tests.py

# 2
class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'


# 2 Remove these functions

'''
def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]

    context = {
        'latest_question_list': latest_question_list,
    }

    return render(request, 'polls/index.html', context)

def detail(request, question_id):

    question = get_object_or_404(Question, pk=question_id)

    return render(request, 'polls/detail.html', {'question': question})

def results(request, question_id):

    question = get_object_or_404(Question, pk=question_id)

    return render(request, 'polls/results.html', {'question': question})
'''

# 2 Vote stays the same
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)

    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])

    except(KeyError, Choice.DoesNotExist):
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice"
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()

        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

# 3 Now we will explore automated testing. You can either
# check your code by entering values randomly (and miss
# a bunch of errors), or you can automate the process.

# 3 We'll now explore a bug in the was_published_recently() function
# in polls/models.py in the shell : python3 manage.py shell

# 3 Create a Question with a pub_date in the future
'''
import datetime
from django.utils import timezone
from polls.models import Question
future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
future_question.was_published_recently()
Returns true even though that doesn't make sense
'''

# 3 Open the file called tests.py


# ---------- /polls/tests.py ----------

from django.test import TestCase

# 4 Import your needed modules
import datetime
from django.utils import timezone
from django.test import TestCase

from .models import Question

# 4 TestCase runs tests without effecting your data
# by creating a temporary database for testing
class QuestionMethodTests(TestCase):

    # 4 Put the code used in the shell here
    # Start the method name with test
    def test_was_published_recently_with_future_question(self):

        # 4 Create a time 30 days in the future
        time = timezone.now() + datetime.timedelta(days=30)

        # 4 Create a question using the future time
        future_question = Question(pub_date=time)

        # 4 Check to see if the output is False like we expect
        self.assertIs(future_question.was_published_recently(), False)

        # 4 Run the test in the terminal
        # python3 manage.py test polls
        # You'll see that the test failed

        # 4 Fix the bug in models.py

    # 6 Return false if pub_date is older then 1 day

    def test_was_published_recently_with_old_question(self):
        # Should return false if pub_date is older then 1 day
        time = timezone.now() - datetime.timedelta(days=30)
        old_question = Question(pub_date=time)
        self.assertIs(old_question.was_published_recently(), False)

    # 6 Return True if published within the last day

    def test_was_published_recently_with_recent_question(self):
        time = timezone.now() - datetime.timedelta(hours=1)
        recent_question = Question(pub_date=time)
        self.assertIs(recent_question.was_published_recently(), True)

    # 6 Test with : python3 manage.py test polls

    # 7 We can simulate user interaction at the view level in the shell
    # python3 manage.py shell
    '''
    # This allows us access variable values in our templates
    # We will be using our real database here
    from django.test.utils import setup_test_environment
    setup_test_environment()

    # Create the client that we'll use to run our app
    from django.test import Client
    client = Client()

    # Get the status code from localhost:8000/
    response = client.get('/')
    response.status_code

    # Get the status code for localhost:8000/polls/
    from django.urls import reverse
    response = client.get(reverse('polls:index'))
    response.status_code

    # Get the HTML content
    response.content

    # Get the value of latest_question_list
    response.context['latest_question_list']
    '''

    # 7 Let's update polls/views.py so it doesn't show
    # questions published in the future

    # 10 Create a function that creates questions at
    # a specified date

    def create_question(question_text, days):
        time = timezone.now() + datetime.timedelta(days=days)
        return Question.objects.create(question_text=question_text,
                                       pub_date=time)

# 10 Add more question tests

class QuestionViewTests(TestCase):

    # 10 Test to see what happens if there are no questions
    def test_index_view_with_no_questions(self):

        # Get the client
        response = self.client.get(reverse('polls:index'))

        # Check the status code
        self.assertEqual(response.status_code, 200)

        # Verify that response contains this string
        self.assertContains(response, "No polls are available.")

        # Check if latest_question_list is empty
        self.assertQuerysetEqual(response.context['latest_question_list'], [])

    # 10 Make sure questions with a pub_date in past are shown
    def test_index_view_with_a_past_question(self):

        # Create sample question
        create_question(question_text="Past question.", days=-30)

        # Get client
        response = self.client.get(reverse('polls:index'))

        # Verify that the question shows
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            ['<Question: Past question.>']
        )

    # 10 Make sure questions with future pub_date don't show
    def test_index_view_with_a_future_question(self):

        # Create question
        create_question(question_text="Future question.", days=30)

        # Get client
        response = self.client.get(reverse('polls:index'))

        # Verify response contains text
        self.assertContains(response, "No polls are available.")

        # Verify that latest_question_list is empty
        self.assertQuerysetEqual(response.context['latest_question_list'], [])

    # 10 Verify that if past and future questions exist that only
    # past show
    def test_index_view_with_future_question_and_past_question(self):

        # Create questions
        create_question(question_text="Past question.", days=-30)
        create_question(question_text="Future question.", days=30)

        # Get client
        response = self.client.get(reverse('polls:index'))

        # Verify that question list only contains past questions
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            ['<Question: Past question.>']
        )

    # 10 Make sure question list shows multiple questions
    def test_index_view_with_two_past_questions(self):

        # Create questions
        create_question(question_text="Past question 1.", days=-30)
        create_question(question_text="Past question 2.", days=-5)

        # Create client
        response = self.client.get(reverse('polls:index'))

        # Verify that both questions show
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            ['<Question: Past question 2.>', '<Question: Past question 1.>']
        )

# 11 Make sure future questions can't be accessed if the user
# guess the URL in polls/views.py

# 13 Add tests to make sure future posts aren't shown in detail

class QuestionIndexDetailTests(TestCase):

    # 13 Make sure future question detail pages show 404
    def test_detail_view_with_a_future_question(self):

        # Create question
        future_question = create_question(question_text='Future question.', days=5)

        # Open url using the future question in the url
        url = reverse('polls:detail', args=(future_question.id,))

        # Get client response
        response = self.client.get(url)

        # Verify that it returns 404
        self.assertEqual(response.status_code, 404)

    # 13 Verify that past questions show in detail
    def test_detail_view_with_a_past_question(self):

        # Create question
        past_question = create_question(question_text='Past Question.', days=-5)

        # Open url with past question
        url = reverse('polls:detail', args=(past_question.id,))

        # Get response
        response = self.client.get(url)

        # Verify the question shows
        self.assertContains(response, past_question.question_text)


# ---------- /polls/models.py ----------

from django.db import models

import datetime
from django.utils import timezone

class Question(models.Model):
    question_text = models.CharField(max_length=200)

    pub_date = models.DateTimeField('date published')

    def __str__(self):
        return self.question_text

    def was_published_recently(self):
        # 5 return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
        # 5 Fix the code
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.pub_date <= now

        # 5 Now the test will pass

        # 5 Add further tests to polls/tests.py

class Choice(models.Model):

    question = models.ForeignKey(Question, on_delete=models.CASCADE)

    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

    def __str__(self):
        return self.choice_text


# ---------- /polls/admin.py ----------

from django.contrib import admin

# 9 Add choice to imports

from .models import Question, Choice

admin.site.register(Question)

# 9 Add Choice to the Admin page
admin.site.register(Choice)

# 9 Create some future questions

# 9 Add some tests in polls/tests.py

Leave a Reply

Your email address will not be published. Required fields are marked *