Djangoを使ったCRUD機能実装＆Herokuデプロイ

## 概要
Django4がリリースされたこともあり、検証も兼ねて超初歩的なブログアプリを作成し、CRUD機能の実装からHerokuへのデプロイまでを一通り行ってみた。
説明内容に誤りがありましたら、ご教授いただけますと幸いです。

## 開発環境
- Python=3.9.7
- Django=4.0
- PostgreSQL=13.5
- psycopg2=2.8.6

＊Heroku用に追加

- django-heroku=0.3.1
- gunicorn=20.1.0

## アプリケーションの作成からindex.htmlを表示させるまで

### プロジェクトの作成
```
$ django-admin startproject django_project
$ cd django_project
$ python manage.py startapp blog_app
```
作成したアプリの有効化

```py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog_app', # 作成したアプリ名を追加
]
```
アプリケーションで個別に設定したルーティングを大元のルーティングに適用させる（ここでは、`blog_app`のルーティングを適用させている）

[django_project/urls.py]

```py
from django.contrib import admin
from django.urls import path, include

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

### `index.html`の作成
`index.html`の作成
`blog_app`配下に`templates/blog_app`フォルダを作成し、その中に`index.html`を作成する
[blog_app/templates/blog_app/index.html]

```html
<!DOCTYPE html>
<html lang="ja">

    <head>
        <meta charset="utf-8">
        <title>blog_app</title>
    </head>

    <body>
        <h1>Index Page</h1>
    </body>

</html>
```
コントローラの設定
[blog_app/views.py]

```py
from django.shortcuts import render


def index(request):
    return render(request, 'blog_app/index.html')
```
ルーティングの設定
[blog_app/urls.py]

```py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
]
```
サーバを起動し、`http://localhost:8000/blog_app/`にアクセスして`index.html`が表示されれば成功。

## PostgreSQLのインストール
＊ローカルにPostgreSQLがインストールされている前提です。
DjangoでPostgreSQLを使用するために、psycopg2と言うパッケージが必要です。
psycopg2のインストールします。（Macの場合、openssl必要/環境によってpsycopg2のインストール方法が異なるようなので、適宜変更してください。）

```
$ brew install openssl
$ echo 'export PATH="/opt/homebrew/opt/openssl@3/bin:$PATH"' >> ~/.zshrc
$ source ~/.zshrc
$ pip install psycopg2==2.8.6
$ export LDFLAGS="-L/opt/homebrew/opt/curl/lib -L/opt/homebrew/opt/openssl/lib"
$ export CPPFLAGS="-I/opt/homebrew/opt/curl/include -I/user/local/opt/openssl/include"
```
ロールとデータベースの作成

```
$ psql postgres
postgres=# CREATE DATABASE django_project;
postgres=# CREATE USER USERNAME WITH PASSWORD 'password';
```
DjangoでPostgreSQLを使用する設定
[django_project/settings.py]

```py
# 省略
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'django_project',
        'USER': 'ロール名',
        'PASSWORD': 'password',
        'HOST': 'localhost',
        'PORT': '5432',
    }
}
#省略
```
サーバを起動し、ブラウザ上でアプリケーションを表示できれば成功
## モデルの作成
テーブル定義

[blog_app/models.py]

```py
from django.db import models

class Blog(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField(max_length=300)
```
マイグレーションファイルの作成

```
$ python manage.py makemigrations blog_app
```
マイグレーションの実行

```
$ python manage.py migrate
```
コントローラの設定
[blog_app/views.py]

```py
from django.shortcuts import render
from django.http import HttpResponse
from .models import Blog

def index(request):
    data = Blog.objects.all()
    params = {
        'data': data,
    }
    return render(request, 'blog_app/index.html', params)
```
`index.html`を作成
[blog_app/templates/blog_app/index.html]

```html
<!DOCTYPE html>
<html lang="ja">

    <head>
        <meta charset="utf-8">
        <title>blog_app</title>
    </head>

    <body>
        <h1>index</h1>
        <table>
            <tr>
                <th>ID</th>
                <th>TITLE</th>
                <th>CONTENT</th>
            </tr>
            {% for item in data %}
            <tr>
                <td>{{item.id}}</td>
                <td>{{item.title}}</td>
                <td>{{item.content}}</td>
            </tr>
            {% endfor %}
        </table>
    </body>

</html>
```
サーバを起動し、ブラウザ上でアプリケーションを表示できれば成功

## Create機能の追加
`forms.py`を作成し、フォームの内容を記述
[blog_app/forms.py]

```py
from django import forms

class BlogForm(forms.Form):
    title = forms.CharField(label='TITLE', max_length=100)
    content = forms.CharField(label='CONTENT', max_length=300)
```
`create.html`を作成
[blog_app/templates/blog_app/create.html]

```html
<!DOCTYPE html>
<html lang="ja">

    <head>
        <meta charset="utf-8">
        <title>blog_app</title>
    </head>

    <body>
        <h1>Create</h1>
        <form action="{% url 'create' %}" method="post">
            {% csrf_token %}
            {{ form.as_p }}
            <input type="submit" value="click">
        </form>
    </body>

</html>
```
コントローラに`create`関数を作成
[blog_app/views.py]

```py
from django.shortcuts import render
from django.http import HttpResponse
from django.shortcuts import redirect
from .models import Blog
from .forms import BlogForm

def index(request):
    data = Blog.objects.all()
    params = {
        'data': data,
    }
    return render(request, 'blog_app/index.html', params)

def create(request):
    params = {
        'form': BlogForm(),
    }
    if (request.method == 'POST'):
        title = request.POST['title']
        content = request.POST['content']
        blog = Blog(title=title, content=content)
        blog.save()
        return redirect(to='/blog_app')
    return render(request, 'blog_app/create.html', params)
```
ルーティングを修正
[blog_app/urls.py]

```py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('create', views.create, name='create'),
]
```
サーバを起動し、ブラウザ上でアプリケーションを表示できれば成功

### ModelFormの利用

[blog_app/forms.py]

```py
from django import forms
from .models import Blog

class BlogForm(forms.ModelForm):
    class Meta:
        model = Blog
        fields = ['title', 'content']
```
[blog_app/views.py]

```py
# 省略
def create(request):
    if (request.method == 'POST'):
        obj = Blog()
        blog = BlogForm(request.POST, instance=obj)
        blog.save()
        return redirect(to='/blog_app')
    params = {
        'form': BlogForm(),
    }
    return render(request, 'blog_app/create.html', params)
```
サーバを起動し、ブラウザ上でアプリケーションを表示できれば成功

## Update機能の追加

ルーティングの追加
[blog_app/urls.py]

```py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('create', views.create, name='create'),
    path('edit/<int:num>', views.edit, name='edit'),
]
```
indexページに編集リンクを追加
[blog_app/templates/blog_app/index.html]

```html
<!DOCTYPE html>
<html lang="ja">

    <head>
        <meta charset="utf-8">
        <title>blog_app</title>
    </head>

    <body>
        <h1>Index</h1>
        <table>
            <tr>
                <th>ID</th>
                <th>TITLE</th>
                <th>CONTENT</th>
            </tr>
            {% for item in data %}
            <tr>
                <td>{{item.id}}</td>
                <td>{{item.title}}</td>
                <td>{{item.content}}</td>
                <td><a href="{% url 'edit' item.id %}">Edit</a></td>
            </tr>
            {% endfor %}
        </table>
    </body>

</html>
```
`blog_app/templates/blog_app`に新たに`edit.html`を作成し、以下のコードを記述
[blog_app/templates/blog_app/edit.html]

```html
<!DOCTYPE html>
<html lang="ja">

    <head>
        <meta charset="utf-8">
        <title>blog_app</title>
    </head>

    <body>
        <h1>Edit</h1>
        <form action="{% url 'edit' id %}" method="post">
            {% csrf_token %}
            {{ form.as_p }}
            <input type="submit" value="click">
        </form>
    </body>

</html>
```
edit関数の作成
[blog_app/views.py]

```py
# 省略

def edit(request, num):
    obj = Blog.objects.get(id=num)
    if (request.method == 'POST'):
        blog = BlogForm(request.POST, instance=obj)
        blog.save()
        return redirect(to='/blog_app')
    params = {
        'id': num,
        'form': BlogForm(instance=obj),
    }
    return render(request, 'blog_app/edit.html', params)
```
サーバを起動し、ブラウザ上でアプリケーションを表示できれば成功

## Delete機能の追加

ルーティングの追加
[blog_app/urls.py]

```py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('create', views.create, name='create'),
    path('edit/<int:num>', views.edit, name='edit'),
    path('delete/<int:num>', views.delete, name='delete'),
]
```
indexページに削除リンクを追加
[blog_app/templates/blog_app/index.html]

```html
<!DOCTYPE html>
<html lang="ja">

    <head>
        <meta charset="utf-8">
        <title>blog_app</title>
    </head>

    <body>
        <h1>Index</h1>
        <table>
            <tr>
                <th>ID</th>
                <th>TITLE</th>
                <th>CONTENT</th>
            </tr>
            {% for item in data %}
            <tr>
                <td>{{item.id}}</td>
                <td>{{item.title}}</td>
                <td>{{item.content}}</td>
                <td><a href="{% url 'edit' item.id %}">Edit</a></td>
                <td><a href="{% url 'delete' item.id %}">Delete</a></td>
            </tr>
            {% endfor %}
        </table>
    </body>

</html>
```
`blog_app/templates/blog_app`に新たに`delete.html`を作成し、以下のコードを記述
[blog_app/templates/blog_app/delete.html]

```html
<!DOCTYPE html>
<html lang="ja">

    <head>
        <meta charset="utf-8">
        <title>blog_app</title>
    </head>

    <body>
        <h1>Delete</h1>
        <table>
            <tr>
                <th>ID</th>
                <td>{{obj.id}}</td>
            </tr>
            <tr>
                <th>TITLE</th>
                <td>{{obj.title}}</td>
            </tr>
            <tr>
                <th>CONTENT</th>
                <td>{{obj.content}}</td>
            </tr>
            <form action="{% url 'delete' id %}" method="post">
                {% csrf_token %}
                <tr>
                    <th>
                    <td><input type="submit" value="click"></td>
                    </th>
                </tr>
            </form>
        </table>
    </body>

</html>
```
delete関数の作成
[blog_app/views.py]

```py
# 省略

def delete(request, num):
    blog = Blog.objects.get(id=num)
    if (request.method == 'POST'):
        blog.delete()
        return redirect(to='/blog_app')
    params = {
        'id': num,
        'obj': blog,
    }
    return render(request, 'blog_app/delete.html', params)
```

## Read機能の追加

ルーティングの追加
[blog_app/urls.py]

```py
from django.urls import path
from . import views


urlpatterns = [
    path('', views.index, name='index'),
    path('create', views.create, name='create'),
    path('edit/<int:num>', views.edit, name='edit'),
    path('delete/<int:num>', views.delete, name='delete'),
    path('detail/<int:num>', views.detail, name='detail'),
]
```
indexページに詳細リンクを追加
[blog_app/templates/blog_app/index.html]

```html
<!DOCTYPE html>
<html lang="ja">

    <head>
        <meta charset="utf-8">
        <title>blog_app</title>
    </head>

    <body>
        <h1>Index</h1>
        <table>
            <tr>
                <th>ID</th>
                <th>TITLE</th>
                <th>CONTENT</th>
            </tr>
            {% for item in data %}
            <tr>
                <td>{{item.id}}</td>
                <td>{{item.title}}</td>
                <td>{{item.content}}</td>
                <td><a href="{% url 'detail' item.id %}">Detail</a></td>
                <td><a href="{% url 'edit' item.id %}">Edit</a></td>
                <td><a href="{% url 'delete' item.id %}">Delete</a></td>
            </tr>
            {% endfor %}
        </table>
    </body>

</html>
```
`blog_app/templates/blog_app`に新たに`detail.html`を作成し、以下のコードを記述
[blog_app/templates/blog_app/detail.html]

```html
<!DOCTYPE html>
<html lang="ja">

    <head>
        <meta charset="utf-8">
        <title>blog_app</title>
    </head>

    <body>
        <h1>Detail</h1>
        <table>
            <tr>
                <th>ID</th>
                <td>{{obj.id}}</td>
            </tr>
            <tr>
                <th>TITLE</th>
                <td>{{obj.title}}</td>
            </tr>
            <tr>
                <th>CONTENT</th>
                <td>{{obj.content}}</td>
            </tr>
        </table>
    </body>

</html>
```
detail関数の作成
[blog_app/views.py]

```py
# 省略

def detail(request, num):
    blog = Blog.objects.get(id=num)
    params = {
        'id': num,
        'obj': blog,
    }
    return render(request, 'blog_app/detail.html', params)
```

## Herokuへのデプロイ
公式：https://devcenter.heroku.com/ja/articles/django-app-configuration
### 設定
**django-herokuとgunicornをインストール**

```
$ pip install django_heroku gunicorn
```
`Procfile`と`runtime.txt`と`requirements.txt`の3つのファイルを作成する

[![Image from Gyazo](https://t.gyazo.com/teams/diveintocode/822ca3fa6c88211333769bfadbe03b5e.png)](https://diveintocode.gyazo.com/822ca3fa6c88211333769bfadbe03b5e)

**1. `Procfile`の作成**

```
web: gunicorn django_project.wsgi
```

**2. `runtime.txt`の作成**

```
python-3.9.7
```
**3. プロジェクトディレクトリで以下のコマンドを実行し、`requirements.txt`を作成**

```
$ pip freeze > requirements.txt
```
[django_project/settings.py]

```py
from pathlib import Path
import django_heroku   # 追記

# 省略

# Activate Django-Heroku.
django_heroku.settings(locals())   # 最終行に追記
```
### コミット＆デプロイ
```
$ git add .
$ git commit "add heroku setting"
$ heroku create
$ heroku config:set DISABLE_COLLECTSTATIC=1
$ git push heroku master
$ heroku run python manage.py migrate
$ heroku open
```
`heroku open`コマンドで起動する画面でのページは存在しないため、「Not Found The requested resource was not found on this server.」が表示される。
`https://xxxxx-xxxxx-xxxxx.herokuapp.com`に`/blog_app`パスを追加してアクセスすれば、アプリが表示されるはずです。

### アセット関連については別途設定が必要そう（今回アセットについては未検証）
- https://devcenter.heroku.com/ja/articles/django-assets
- https://note.com/takuya814/n/n209dd3c95496

## 次回
####[2. 1対多のアソシエーション実装＆確認ダイアログ](https://qiita.com/K_Kei/items/f7929929c2799711fdfa)