LoginSignup
1
0

More than 3 years have passed since last update.

Djangoのチュートリアルを読んでいく!【その2 Django初心者】

Posted at

はじめに

私はプログラミング歴1年の初心者です。
実務でWebサイトのコーディングを1年間行ってきました。
そろそろシステム開発もできるようになりたいということで
LaravelやReactをこれから勉強していこうと思っております。

今回の目的

Djangoでアプリを実際に作っていく流れを学ぼうと思います。
公式チュートリアルを読んでいきます。

目次

  1. オーバービュー
  2. 簡単なフォーム
  3. テスト
  4. モデル
  5. アプリ公開

実践

オーバービュー

一覧ビューを実装

polls/views.py
# ビューを追加
def detail(request, question_id):
  return HttpResponse("You7re looking at question %s." % question_id)

def results(request, question_id):
  response = "You're looking at the results of question %s."
  return HttpResponse(response % question_id)

def vote(request, question_id):
  return HttpResponse("You're voting on question %s." % question_id)
polls/urls.py
# Routingを追加
urlpatterns = [
    # ex: /polls/
    path('', views.index, name='index'),
    # ex: /polls/5/
    path('<int:question_id>/', views.detail, name='detail'),
    # ex: /polls/5/results/
    path('<int:question_id>/results/', views.results, name='results'),
    # ex: /polls/5/vote/
    path('<int:question_id>/vote/', views.vote, name='vote'),
]
polls/views.py
# 動的なサイトを作ってみる(index()を変更)
from django.http import HttpResponse
from .models import Question

def index(request):
  latest_question_list = Question.objects.order_by('-pub_date')[:5]
  output = ', '.join([q.question_text for q in latest_question_list])
  return HttpResponse(output)
polls/templates/polls/index.html
# テンプレートフォルダを作成してビューを作成する(不完全HTML)
{% if latest_question_list %}
  <ul>
    {% for question in latest_question_list %}
      <li><a href="/polls/{{ quetsion.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
  </ul>
{% else %}
  <p>No polls are available.</p>
{% endif %}
polls/views.py
# indexビューを変更する
from django.template import loader

def index(request):
  latest_question_list = Question.objects.order_by('-pub_date')[:5]
  template = loader.get_template('polls/index.html')
  context = {
    'latest_question_list': latest_question_list,
  }
  return HttpResponse(template.render(context, request))
polls/view.py
# renderショートカットを使う(リファクタリング)
こうすることでloaderやHttpResponseは必要なくなる
from django.shortcuts import render

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)

詳細ビューを実装

polls/views.py
# 詳細ビューを実装(404エラー画面)
from django.http import Http404

def detail(request, question_id):
  try:
    question = Question.objects.get(pk=question_id)
  except Question.DoesNotExist:
    raise Http404("Question does not exist")
  return render(request, 'polls/detail.html', {'question': question})
polls/templates/polls/detail.html
# とりあえず簡単に実装
{{ question }}
polls/views.py
# 404エラーショートカットを使う(リファクタリング)
from django.shortcuts import get_object_or_404, render

def detail(request, question_id):
  question = get_object_or_404(Question, pk=question_id)
  return render(request, 'polls/detail.html', {'question': question})
polls/templates/polls/detail.html
# 詳細画面を実装する
<h1>{{ question.questionn_text }}</h1>
<ul>
  {% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
  {% endfor %}
</ul>

ハードコードされているindexのURLを変更

polls/templates/polls/index.html
# 今までの書き方だと変更に強くない(テンプレートを直す必要あり)
<li><a href="/polls/{{ quetsion.id }}/">{{ question.question_text }}</a></li>

↓↓↓↓ 変更 ↓↓↓↓

# 変更に強くする(urlsモジュールを変更すればいい)
<li><a href="{% url 'detail' question_id %}">{{ question.question_text }}</a></li>

名前空間を使ってリファクタリング

polls/urls.py
# 名前空間を追加する
app_name = 'polls'
polls/templates/polls/index.html
# 名前空間のついた記述に変更する
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

↓↓↓↓ 以下のように変更 ↓↓↓↓
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

作成保存機能

簡単なフォーム実装

polls/templates/polls/details.html
<h1>{{ question.question_text }}</h1>

{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}

<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
{% for choice in question.choice_set.all %}
    <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
    <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
{% endfor %}
<input type="submit" value="Vote">
</form>
polls/views.py
# urlsにはvoteを作成してあるので、vote()を実装
from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse
from .models import Choice, Question

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/details.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,)))

結果画面を実装する

polls/views.py
# 結果画面ビューを編集する
from django.shortcuts import get_object_or_404, render

def results(request, question_id):
  question = get_object_or_404(Question, pk=question_id)
  return render(request, 'polls/results.html', {'question': question})
polls/templates/polls/results.html
# 結果画面の実装
<h1>{{ question.question_text }}</h1>

<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>

リファクタリング

polls/urls.py
# indexとdetailとresultのRoutingを変更する
urlpatterns = [
    # ex: /polls/
    path('', views.IndexView.as_view(), name='index'),
    # ex: /polls/5/
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    # ex: /polls/5/results/
    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
    # ex: /polls/5/vote/
    path('<int:question_id>/vote/', views.vote, name='vote'),
]
polls/views.py
# index,detail,resultsのビューをDjangoの汎用ビューに変更する
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views import generic

class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-pub_date')[:5]


class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'


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

テスト

プログラムが正しく動くことは先に確認しておくべき。
のちのち動かないことがわかったときに困る。

テストの無いコードは、デザインとして壊れている。

テスト駆動開発
→コードを書く前にテストを書く

テストコードは長くなってしまってもいいので
何をしているのかをわかりやすく

テスト作成

# shellでバグを確認する
$ python manage.py shell

>>> 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()
polls/tests.py
# コメントも書きつつ、テストコードを書く
# テスト用のメソッドはtestから始まる
import datetime

from django.test import TestCase
from django.utils import timezone

from .models import Question

class QuestionModelTests(TestCase):

  def test_was_published_recently_with_future_questions(self):
    """
    was_published_recently() returns False for questions whose pub_date is in the future.
    """
    time = timezone.now() + datetime.timedelta(days=30)
    future_question = Question(pub_date=time)
    self.assertIs(future_question.was_published_recently(), False)
# テスト実行
$ python3 manage.py test polls

バグ修正

polls/models.py
# バグ修正をする
  def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

↓↓↓↓ 変更する ↓↓↓↓

def was_published_recently(self):
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.pub_date <= now

他のテストも追加する

polls/tests.py
# 他の包括的なテストも同じクラスに追加する
import datetime

from django.test import TestCase
from django.utils import timezone

from .models import Question

class QuestionModelTests(TestCase):

  def test_was_published_recently_with_future_questions(self):
    """
    was_published_recently() returns False for questions whose pub_date is in the future.
    """
    time = timezone.now() + datetime.timedelta(days=30)
    future_question = Question(pub_date=time)
    self.assertIs(future_question.was_published_recently(), False)

  def test_was_published_recently_with_old_question(self):
    """
    was_published_recently() returns False for questions whose pub_date
    is older than 1 day.
    """
    time = timezone.now() - datetime.timedelta(days=1, seconds=1)
    old_question = Question(pub_date=time)
    self.assertIs(old_question.was_published_recently(), False)

  def test_was_published_recently_with_recent_question(self):
    """
    was_published_recently() returns True for questions whose pub_date
    is within the last day.
    """
    time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
    recent_question = Question(pub_date=time)
    self.assertIs(recent_question.was_published_recently(), True)

ビューに関するテストも行う

ユーザーがブラウザを通して経験する動作をチェックする

# shellを使って確認する
$ python3 manage.py shell

>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
>>> from django.test import Client
>>> client = Client()
>>> response = client.get('/')
>>> response.status_code
>>> from django.urls import reverse
>>> response = client.get(reverse('polls:index'))
>>> response.status_code
>>> response.content
>>> response.context['latest_question_list']
polls/views.py
# get_querysetを修正する
class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-pub_date')[:5]

↓↓↓↓ 以下のように変更する ↓↓↓↓
from django.utils import timezone

def get_queryset(self):
      """
      Return the last five published questions (not including those set to be
      published in the future).
      """
      return Question.objects.filter(
          pub_date__lte=timezone.now()
      ).order_by('-pub_date')[:5]
polls/tests.py
# 以下のビューのテストコードを追記する
from django.urls import reverse

def create_question(question_text, days):
  """
  Create a question with the given `question_text` and published the
  given number of `days` offset to now (negative for questions published
  in the past, positive for questions that have yet to be published).
  """
  time = timezone.now() + datetime.timedelta(days=days)
  return Question.objects.create(question_text=question_text, pub_date=time)


class QuestionIndexViewTests(TestCase):
  def test_no_questions(self):
      """
      If no questions exist, an appropriate message is displayed.
      """
      response = self.client.get(reverse('polls:index'))
      self.assertEqual(response.status_code, 200)
      self.assertContains(response, "No polls are available.")
      self.assertQuerysetEqual(response.context['latest_question_list'], [])

  def test_past_question(self):
      """
      Questions with a pub_date in the past are displayed on the
      index page.
      """
      question = create_question(question_text="Past question.", days=-30)
      response = self.client.get(reverse('polls:index'))
      self.assertQuerysetEqual(
          response.context['latest_question_list'],
          [question],
      )

  def test_future_question(self):
      """
      Questions with a pub_date in the future aren't displayed on
      the index page.
      """
      create_question(question_text="Future question.", days=30)
      response = self.client.get(reverse('polls:index'))
      self.assertContains(response, "No polls are available.")
      self.assertQuerysetEqual(response.context['latest_question_list'], [])

  def test_future_question_and_past_question(self):
      """
      Even if both past and future questions exist, only past questions
      are displayed.
      """
      question = create_question(question_text="Past question.", days=-30)
      create_question(question_text="Future question.", days=30)
      response = self.client.get(reverse('polls:index'))
      self.assertQuerysetEqual(
          response.context['latest_question_list'],
          [question],
      )

  def test_two_past_questions(self):
      """
      The questions index page may display multiple questions.
      """
      question1 = create_question(question_text="Past question 1.", days=-30)
      question2 = create_question(question_text="Past question 2.", days=-5)
      response = self.client.get(reverse('polls:index'))
      self.assertQuerysetEqual(
          response.context['latest_question_list'],
          [question2, question1],
      )

詳細ビューのテスト

polls/views.py
# 以下のget_queryset()を追加する
class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'

    def get_queryset(self):
      """
      Excludes any questions that aren't published yet.
      """
      return Question.objects.filter(pub_date__lte=timezone.now())
polls/test.py
# 以下のテストを追加する
class QuestionDetailViewTests(TestCase):
    def test_future_question(self):
        """
        The detail view of a question with a pub_date in the future
        returns a 404 not found.
        """
        future_question = create_question(question_text='Future question.', days=5)
        url = reverse('polls:detail', args=(future_question.id,))
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_past_question(self):
        """
        The detail view of a question with a pub_date in the past
        displays the question's text.
        """
        past_question = create_question(question_text='Past Question.', days=-5)
        url = reverse('polls:detail', args=(past_question.id,))
        response = self.client.get(url)
        self.assertContains(response, past_question.question_text)

モデル

アプリ公開

さいごに

views.pyは
ビューコントローラみたいなものか?

チュートリアルを読むと
実際にPythonってこうやって使うのかというのがわかる

postメソッドというのは
サーバー側のデータの更新につながるものに使う

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0