django tutorialメモ

pythonのウェブフレームワークであるDjangoの公式チュートリアル
「はじめての Django アプリ作成」その1～5までのメモです。


ほんとにただのメモです。

本家サイトはこちら
https://docs.djangoproject.com/ja/3.1/intro/tutorial01/

# 1. インストール

python環境を作ってpipで入ります

```terminal:terminal
pip install django
```

入ってるか確認してみます

```terminal:terminal
$ python -m django --version
3.1.2
```

django 3.1.2が入ってるようです

# 2. Djangoプロジェクトを作る

django-admin startproject hogeを実行すると、プロジェクトに最低限必要なフォルダとファイルを自動作成してくれます。

```terminal:terminal
django-admin startproject mysite
```
作成されるファイルは以下のような構成です

```terminal:ディレクトリ構造
mysite/
    manage.py
    mysite/
        __init__.py
        settings.py
        urls.py
        asgi.py
        wsgi.py
```
DBはpython標準のsqlite3になっています。

とりあえずこの状態でサンプルページを表示できるのでやってみましょう。manage.py runserverと実行すると開発用の簡易サーバーが起動します。

```terminal:terminal
python manage.py runserver
```

起動したサーバーはlocal hostの8000番ポートに待機するので以下のURLをブラウザで開くと見られます。
http://127.0.0.1:8000
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/0446b63f-8649-4e73-6f67-81d440f2deef.png)

なお、この簡易サーバーは開発中の利用だけを考えて作られているので「絶対に運用環境では使わないでください」と公式チュートリアルに書かれています。本番環境ではApacheなどを使って公開しましょう。

# 3. プロジェクト内にアプリケーションを作る

Djangoは複数のアプリケーションを連携して使えるようにしてくれています。以下のように実行すればアプリに最低限必要なファイルを自動生成してくれます。

```python:terminal
python manage.py startapp polls
```

```python:ディレクトリ構造
polls/
    __init__.py
    admin.py
    apps.py
    migrations/
        __init__.py
    models.py
    tests.py
    views.py
```

アプリはいくつでも作れるので、機能別に上手く分割すると生産性が上がりそうです。

# 4. pollsアプリのviewを作る

viewはviews.pyに書きます。

以下は/polls/index.htmlを読みに来たらhttpで「Hello, world. You're at the polls index.」と返すようなviewになります。

```python:polls/views.py
from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, world. You're at the polls index.")
```

上記views.pyへの関連付けをurls.pyに書いていきます。

django.urls.path()に書くと関連付けてくれます。以下のように書くとmysite/urls.py → polls/urls.py → polls/views.pyの順で参照してpolls/views.py内のindex()を見つけてくれるようになります。

```python:mysite/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('polls/', include('polls.urls')),
    path('admin/', admin.site.urls),
]
```

```python:polls/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
]
```

これで関連付けできたので、簡易サーバーを起動して確認しましょう。

```terminal:terminal
python manage.py runserver
```

urlにpolls/views.pyに書いた内容が待機してくれます。
http://localhost:8000/polls/

# 5. DBの変更

デフォルトではsqlite3を使うようになっていますので特にDBを設定しなくても出来るようになっています。本番ではpostgreSQLやmySQLに変更すると思いますのでその場合は以下を参照します。
https://docs.djangoproject.com/ja/3.1/intro/tutorial02/

# 6. TIME_ZONEの設定

標準だとUTCで設定されています

```python:mysite/setting.py変更前
TIME_ZONE = 'UTC'
```
日本標準時にする場合は以下のように変更します

```python:mysite/setting.py変更後
TIME_ZONE = 'Asia/Tokyo'
USE_TZ = True
```

# 7. migrateする

setting.pyの内容に基づいてDBを構築します

```terminal:terminal
python manage.py migrate
```

djangoはDBのデータをオブジェクトとして扱ってくれるのでデータのハンドリングが楽になるみたいです。(書いてる僕は現時点でよく分かってません。)

# 8. modelを作る

```python:polls/models.py
import datetime

from django.db import models
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):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)


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
```

```python:setting.py
INSTALLED_APPS = [
    'polls.apps.PollsConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
```
modelに変更があったことをdjangoに認識させます

```terminal:terminal
python manage.py makemigrations polls
```


# 9. django shellを使う

djangoは整理されたディレクトリ構造を強制することで生産性を高めるように作られていますが、ファイルの繋がりが複雑になりdjangoから関数を実行したときにどうなるか確認しにくいという問題があります。これを確認しやすいように用意されているのがdjango shellで、python manage.py shellで実行することが出来ます。

```python:django_shell
>>> from polls.models import Choice, Question
>>> Question.objects.all()
<QuerySet []>
```


```python:django_shell
>>> from django.utils import timezone
>>> q = Question(question_text="What's new?", pub_date=timezone.now())
>>> q.save()
```


```python:django_shell
>>> q.id
1
```

```python:django_shell
>>> q.question_text = "What's up?"
>>> q.save()
```


```python:django_shell
>>> Question.objects.all()
<QuerySet [<Question: Question object (1)>]>
```


```python:django_shell
>>> q.question_text
>>> q.pub_date
datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=<UTC>)
```

```python:django_shell
python manage.py shell
In [1]:
```

```python:django_shell
>>> from polls.models import Choice, Question
>>> Question.objects.all()
<QuerySet [<Question: What's up?>]>
```

```python:django_shell
>>> Question.objects.filter(id=1)
<QuerySet [<Question: What's up?>]>
```


```python:django_shell
>>> Question.objects.filter(question_text__startswith='What')
<QuerySet [<Question: What's up?>]>
```


```python:django_shell
>>> from django.utils import timezone
>>> current_year = timezone.now().year
>>> Question.objects.get(pub_date__year=current_year)
<Question: What's up?>
```


```python:django_shell
>>> Question.objects.get(id=2)
Traceback (most recent call last):
    ...
DoesNotExist: Question matching query does not exist.
```


```python:django_shell
>>> Question.objects.get(pk=1)
<Question: What's up?>
```


```python:django_shell
>>> q = Question.objects.get(pk=1)
>>> q.was_published_recently()
True
```


```python:django_shell
>>> q = Question.objects.get(pk=1)
>>> q.choice_set.all()
<QuerySet []>
```


```python:django_shell
>>> q.choice_set.create(choice_text='Not much', votes=0)
>>> q.choice_set.create(choice_text='The sky', votes=0)
>>> c = q.choice_set.create(choice_text='Just hacking again', votes=0)
>>> c.question
<Question: What's up?>
```


```python:django_shell
>>> q.choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
```

```python:django_shell
>>> q.choice_set.count()
3
```

```python:django_shell
>>> Choice.objects.filter(question__pub_date__year=current_year)
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
```

```python:django_shell
>>> c = q.choice_set.filter(choice_text__startswith='Just hacking')
>>> c.delete()
```

# 10. 監理ユーザーを作る

djangoプロジェクト監理用のsuperuserを作ります。このsuperuserはdjango用なのでLinux OSのuserとは別になります。

```terminal:django_shell
python manage.py createsuperuser
```

IDとパスワードを入力すれば登録完了です。これによりadminページにログインできるようになるので簡易サーバーを起動してadminページにログインしましょう。

```terminal:django_shell
python manage.py runserver
```

adminページのURL
http://127.0.0.1:8000/admin/ 
![admin01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/da45d539-fb52-9411-dcff-b48a86a6a5f0.png)
ログインすると以下のように表示されます
![admin02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/091dd370-0c52-4f8f-5e01-6c361c44d0b6.png)

# 11. adminページからアプリを編集する


```python:polls/admin.py
from django.contrib import admin
from .models import Question

admin.site.register(Question)
```
以下のようにpollsアプリの欄が追加されました
![admin03t.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/d8bfaf7b-6c5c-9872-863a-e80e47622318.png)

Questionsをクリックするとオブジェクト内の値を操作できます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/63aa894a-c531-d9c5-e3de-8a62e209c38d.png)

shellから操作するよりこっちの方が分かりやすいので上手く使いたいですね。


# 12. Viewを足していく

はじめての Django アプリ作成、その3
https://docs.djangoproject.com/ja/3.1/intro/tutorial03/

## URLによって開くviewを変える

```python:polls/views.py
from django.http import HttpResponse


def index(request):
    return HttpResponse("Hello, world. You're at the polls index.")


def detail(request, question_id):
    return HttpResponse("You're 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)
```

```python:polls/urls.py
from django.urls import path
from . import views

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'),
]
```
これでpython manage.py runserverして以下URLをブラウザで開くと、polls/views.pyのdetail(), results(), vote() がそれぞれ実行されます。
http://127.0.0.1:8000/polls/34/
http://127.0.0.1:8000/polls/34/results/
http://127.0.0.1:8000/polls/34/vote/

## インデックスページを作る

polls/views.pyのindex()を以下のように書き換えてQuestion.

```python:polls/vies.py
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)


def detail(request, question_id):
    return HttpResponse("You're 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)
```

# 13. templateを作る

上記ではviews.pyに直接画面レイアウトを書き込んでしまっていますが、修正しやすいようにtemplateに記述を分けるようにしましょう。views.pyにloader.get_template('polls/index.html')を書いてtemplate/polls/index.htmlを読むようにします。

```python:polls/views.py
from django.http import HttpResponse
from django.template import loader

from .models import Question


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))
```

```html:polls/templates/polls/index.html
{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}
```

## render

templateを読み込んでrenderするのをrender()で置き換えると短く書けますし、importするパッケージも少なくて済みます。

```python:polls/views.py
from django.shortcuts import render
from .models import Question


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)
```

# 14. 404エラー表示

try/exceptで例外になったときに404を出すようにします。

```python:polls/views.py
from django.http import Http404
from django.shortcuts import render
from .models import Question


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):
    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})
```

## get_object_or_404

いちいちtry/exceptするのは面倒なので、getして失敗したら404を返す関数であるget_object_or_404()に置き換えてみます

```python:
from django.shortcuts import get_object_or_404, render

from .models import Question
# ...
def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})
```

# 15. アプリの名前空間

polls/urls.pyのapp_nameを指定するとアプリの名前空間から参照できるようになります。これをやらないと別アプリに同じ名前のviewがあったときに動作が怪しくなりそうなので、忘れない内にview作るときに指定しておいた方が良いと思います。

```python:polls/urls.py
from django.urls import path
from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('<int:question_id>/results/', views.results, name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]
```
これにより、viewの参照がdetailからpolls:detailに変更になります。他のviewも同様にpolls:hogeのように指定し直しましょう。

```python:polls/templates/polls/index.html
{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}
```

# 16. フォームを書く

はじめての Django アプリ作成、その4
https://docs.djangoproject.com/ja/3.1/intro/tutorial04/

投票ボタンを追加します

```html:polls/detail.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>
```
こんな感じでボタンができます
http://127.0.0.1:8000/polls/1/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/ab8f2388-c485-7cbd-a017-ee40a5523c4a.png)
formのactionを「polls:voteのviewにpostする」にしてあるので、これに対応するviewを作ります。

polls:voteのviewはpolls/urls.pyに以下のように書いてますのでpoll/views.pyに書いてやれば良いという事になります。

```python:polls/urls.py
path('<int:question_id>/vote/', views.vote, name='vote'),
```

choiceされている対象を1つ足して/polls/results/にリダイレクトするようにしてやります

```python:polls/views.py
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
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):
        # Redisplay the question voting form.
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
```

リダイレクトで呼ばれる/polls/results/のviewも書いてやります

```python:
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})
```

# 17. 汎用ビューを使う

汎用ビューを使うことで無駄な記述が減らせるようです。

```python:polls/urls.py
from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]
```



```python:polls/views.py
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic

from .models import Choice, Question


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'
```

まだまだ続くんですが
https://docs.djangoproject.com/ja/3.1/intro/tutorial05/