Djangoアプリ開発（逆引き）

この記事では、Djangoのプロジェクト、アプリを作成しているという前提のもと、アプリ開発における逆引きを紹介する。
- プロジェクトの作成は以下の記事を参考にする
    - [Djangoプロジェクト立ち上げ](https://qiita.com/kkk777/items/555296a88a2b871b1403)



# ルーティングの設定

includeを追加する
以後、プロジェクト.urls.pyを触ることわない
```python:プロジェクト.urls.py

from django.contrib import admin
from django.urls import path, include #include追加

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('アプリ名.urls')), # 追加
]
```

プロジェクト.urls.pyの中身をコピペする
ここに、実行していくルーティングを書いていく
```python:アプリ.urls.py
from django.urls import path
from .views import Index

app_name = 'アプリ名'

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

# 開発サーバの停止
## 新たにディレクトリを追加した場合
```:vscode
Quit the server with CONTROL-C.
```
- tmplatesなど、新たにdirを作成した場合など
    - 開発サーバーを再起動する必要がある（再起動：`control` + `c`）

# マイグレーション
## モデルを新規作成、変更した場合
```:vscode
$ python3 manage.py makemigrations
$ python3 manage.py migrate
```
- モデルを作成、変更した場合は、上記のコマンドを入力してマイグレーションを行う必要がある必要がある
- `python3 manage.py makemigrations`：マイグレーションファイルにモデルの作成、変更内容を書き込む
- `python3 manage.py migrate`：マイグレーションファイルを元にマイグレーションを行う。これによって、モデルをDB内のテーブルとして扱い、設定内容を反映する

# Git管理
## 一旦pushしたい場合
```
$ git add .
$ git commit -m "コミットメッセージ"
$ git status
$ git push origin master
```
- manage.pyを含むdirに移動してから上記のコマンドを入力

# モデル
## 抽象基底クラスでモデル定義の効率化
```python:models.py
from django.db import models
from model_utils.models import TimeStampedModel

class Post(TimeStampedModel):
    # created = models.DateTimeField(auto_now_add=True)　新規作成
    # modified = models.DateTimeField(auto_now=True) 更新
    title = models.CharField(max_length=255)
    body = models.TextField()

    def __str__(self):
        return self.title
```
- [TimeStampedModelクラス](https://github.com/jazzband/django-model-utils/blob/157ee9bc5a03e794ef1e300f7fe6e023e3485bfa/model_utils/models.py#L19)：他のクラスで共通的に利用するcreated(作成時刻）とmodified(更新時刻)のフィールドが定義されている
- Metaクラス以降のabstract = Trueが定義されているため、TimeStampedModelクラスは抽象基底クラスとなる
- from model_utils.models import TimeStampedModelをインポートすることで、TimeStampedModelをそのまま使用することができる
```python:model_utils.models.py
class TimeStampedModel(models.Model):
 
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)
    
    class Meta:
        abstract = True
```

## 作成したモデルをadminサイトに表示
```python:app/admin.py
from django.contrib import admin
from .models import Post

admin.site.register(Post) # 作成したPostモデルを登録
```

# ログイン・ログアウト機能の実装
## アプリ作成（ログイン・ログアウト）　※任意

```:vscode
$ python3 manage.py startapp accounts
```

## ルーティング
- accounts/urls.pyを作成
```python:プロジェクト/urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('report.urls')),
    path('accounts/', include('accounts.urls')), # 追加
]
```
```python:accounts/urls.py
from django.urls import path
from .views import Login, Logout

app_name = 'accounts'

urlpatterns = [
    path('login/', Login.as_view(), name='login'),
    path('logout/', Logout.as_view(), name='logout'),
]
```

## settings.py
```python:settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'アプリ名',
    'acounts', # 追加
]

LOGIN_URL = 'accounts:login'
LOGIN_REDIRECT_URL = '/' # TOPページへリダイレクト
LOGOUT_REDIRECT_URL= 'accounts:login'
```

|環境変数|説明|
|-|-|
|LOGIN_URL|ログイン必須のページにログインしていないユーザがアクセスした場合のリダイレクト先URL、ルーティング名を指定|
|LOGIN_REDIRECT_URL|ログイン後のリダイレクト先URL、ルーティング名を指定|
|LOGOUT_REDIRECT_URL|ログアウト後のリダイレクト先URL、ルーティング名を指定|

## ユーザ認証フォームの作成（ログインフォーム）
- accounts/forms.pyを作成
- Django標準の[AuthenticationFormクラス](https://qiita.com/kkk777/items/de28073dcff7eb2a0025)を継承して、ログインフォームを実装
```python:accounts/forms.py
from django.contrib.auth.forms import AuthenticationForm 

class LoginForm(AuthenticationForm):
    """ログオンフォーム"""
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control' # ①
            field.widget.attrs['placeholder'] = field.label # ②   

```
※①：全てのフォームの部品のclass属性に「form-control」を指定（bootstrapのフォームデザインを利用するため）
※②：全てのフォームの部品にpaceholderを定義して、入力フォームにフォーム名が表示されるように指定。

## ログイン・ログアウトViewの実装

```python:accounts/views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import LoginView, LogoutView
from .forms import LoginForm


class Login(LoginView):
    """ログインページ"""
    form_class = LoginForm
    template_name = 'accounts/login.html'

class Logout(LoginRequiredMixin, LogoutView):
    """ログアウトページ"""
    template_name = 'accounts/login.html'
# ログアウト後は、LOGOUT_REDIRECT_URL = 'products:login'にリダイレクトするため、ログアウト用のHTMLは空でもいい
```
|ユーザ認証用ビュークラス|説明|
|-|-|
|[LoginRequiredMixin](https://qiita.com/kkk777/items/6f467a14b0f9616a0a79)|ログイン済みユーザのみに制限してレスポンスを返す|
|LoginView|ログイン機能|
|LogoutView|ログアウト機能|

form_classに先ほどforms.pyで定義したフォームクラス「LoginForm」を指定することでログイン処理時にLoginFormで定義したフォームデザインが利用されるようになります。


複数のクラスを承継する場合は、LoginRequiredMixinを一番最初に指定するようにしましょう。    
一番最初に指定しないとうまく動作しなくなりますので注意しましょう。
- Logoutのtemplate_nameに指定するhtmlファイルは空でも可。template_nameを指定しないと、Django管理サイトのものが使用される。

## テンプレート
- accounts/templates/accounts/を作成
- accounts/templates/accounts/login.htmlを作成
※詳細は、[GitHub](https://github.com/Keito777/daily_report)を参照

```html:accounts/login.html
      <div class="card-body">
        <form action="" method="POST">{% csrf_token %}
          {{ form.non_field_errors }}
          {% for field in form %}
            {{ field }}
            {{ field.errors }}
            <br>
          {% endfor %}
          <button type="submit" class="btn btn-primary" >ログイン</button>
          <input type="hidden" name="next" value="{{ next }}" />
        </form>
      </div>
```
- フィールドの表示：
    - for文によって、フォームオブジェクト（form）からフォームクラスのフィールド（入力値）を1つずつ取り出して`{{field}}`で表示している。
- フィールドのエラー表示：
    - 認証系のエラーは`form.non_field_errors`に情報が格納されるため、for文で`{{ field.errors }}`としてテンプレートに表示できる。
- POSTボタンの作成（ログイン用ボタン）：
    - フォームオブジェクトを表示するだけでは、POST用のボタンは作成されないため、ボタンを作成する。

## ログイン中のユーザ情報を表示
userオブジェクトはリクエスト（request)オブジェクト内に格納されている。
ビューのget_context_data()によって、これらはテンプレートに渡されるため、以下のようにuser変数に対して、**ユーザモデルのフィールドを指定**することで、ユーザに関する情報をテンプレートに表示することができる。
- user.is_authenticated ・・・ユーザが認証済みの場合Trueを返す。
- user.id・・・認証済みユーザのID番号
- user.username・・・認証済みユーザのユーザ名 (user.get_usernameも同じ）

# サインアップ機能の実装
- [カスタムユーザモデルを作成](https://qiita.com/kkk777/items/ae8eb88d504420e9f5cd)しておく
- ルーティング設定
- サインアップ用の汎用ビューがないのでCreateViewで作成
- サインアップ用のフォームクラス（UserCreationForm）を用いてサインアップフォームを作成
- サインアップ、サインアップ後のテンプレートを作成

## ルーティング設定
```python:accounts/urls.py
from .views import Login, Logout, Signup, SignupDone # 一部追加

urlpatterns = [
    # 省略
    path('signup/', Signup.as_view(), name='signup'), # サインアップ
    path('signup_done/', SignupDone.as_view(), name='signup_success'), # サインアップ完了
]
```
## ビューの作成
- サインアップ用の汎用ビューがないのでCreateViewで作成

```python:accounts/views.py
#signup
from django.views.generic import TemplateView
from django.views.generic.edit import CreateView
from .forms import LoginForm, SignupForm
from django.shortcuts import redirect
from django.urls import reverse_lazy

'''サインアップ'''
class Signup(CreateView):
    template_name = 'accounts/signup.html'
    form_class =SignupForm
    success_url = reverse_lazy('accounts:signup_success')

    # templateに表示するデータを追加するためオーバーライド
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["process_name"] = "Sign up"
        return context

    '''定義されているためオーバーライド不要
    def form_valid(self, form):
        user = form.save() # formの情報を保存
        self.object = user
        return super().form_valid(form)
    '''

'''サインアップ完了'''
class SignupDone(TemplateView):
    template_name = 'accounts/signup_success.html'
```

## フォームの作成
- サインアップ用のフォームクラス（UserCreationForm）を用いてサインアップフォームを作成
    - UserCreationFormフォームクラスには、パスワードとパスワード確認用のフィールドが定義されている
    - Metaクラス内のfieldsで指定したCustomUserモデルクラスのフィールドとは別に、UserCreationFormのフィールドもフォームに表示される
    - フィールドの表示優先度は、フォームフィールド＜モデルフィールド

```python:accounts/forms.py
#signup
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm 
from .models import CustomUser

'''サインアップ用フォーム'''
class SignupForm(UserCreationForm):

    '''UserCreationFormのフィールド'''
    # CustomUserモデルのフィールドとは別でフォームに表示される
    # password1 = forms.~
    # password2 = forms.~ （確認用）

    class Meta:
        model = CustomUser
        fields = ('username', 'email')
```


## サインアップ、サインアップ後のテンプレートを作成
- base.htmlのナビゲーションバーにsignupを追加
- accounts/templates/signup.htmlを作成
- accounts/templates/signup_success.htmlを作成
※詳細は、[GitHub](https://github.com/Keito777/daily_report)を参照


# マイページの作成、変更（アクセス制限）
他のユーザはアクセスできず、自分のみアクセスできるサイト（マイページ）を作成する。
また、マイページから自分のユーザ情報を変更する機能も追加する。

- カスタムユーザモデルを作成しておく
- ルーティング設定
- 独自の制限を設定したマイページビューとマイページ更新ビューの作成
- マイページ更新用のフォームを作成
- マイページ、マイページ更新用のテンプレートを作成

## ルーティング設定
```python:accounts/urls.py
from .views import Login, Logout, Signup, SignupDone, MyPage,UserUpdate # 一部追加

urlpatterns = [
    # 省略
    path('my_page/<int:pk>/', MyPage.as_view(), name='my_page'),
    path('user_update/<int:pk>', UserUpdate.as_view(), name='user_update'), # 登録情報の更新
```
## 独自の制限を設定したマイページビューとマイページ更新ビューの作成
- 自分以外のユーザがアクセスできない用に、UserPassesTestMixinクラスを使って独自のアクセス制限を設定する。
    - 詳細は、[Djangoでアクセス制限を行う（AccessMixinクラス）](https://qiita.com/kkk777/items/6f467a14b0f9616a0a79)を参照
```python:accounts/views.py
# my_page
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import UserPassesTestMixin
from django.views.generic import DetailView
from .models import CustomUser

'''自分しかアクセスできないようにするMixin(共通化させるため)'''
# 詳細はQiita：https://qiita.com/kkk777/items/6f467a14b0f9616a0a79
class OnlyYouMixin(UserPassesTestMixin):
    
    def test_func(self):
        # 今ログインしてるユーザーのpkと、そのマイページのpkが同じならアクセスを許可
        user = self.request.user
        return user.pk == self.kwargs['pk']

    # test_func関数がFalseの場合のリダイレクト先を指定
    def handle_no_permission(self):
        return redirect("report:index")

'''マイページ'''
class MyPage(OnlyYouMixin, DetailView):
    model = CustomUser
    template_name = 'accounts/my_page.html'

'''ユーザー登録情報の更新'''
class UserUpdate(OnlyYouMixin, UpdateView):
    model = CustomUser
    form_class = UserUpdateForm
    template_name = 'accounts/signup.html'

    def get_success_url(self):
        return resolve_url('accounts:my_page', pk=self.kwargs['pk'])

    # contextデータ作成
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["process_name"] = "Update"
        return context
```
## マイページ更新用のフォームを作成
- accounts/forms.pyに以下のコードを追加
```python:accounts/forms.py
'''ユーザー情報更新用フォーム'''
class UserUpdateForm(forms.ModelForm):

    class Meta:
        model = CustomUser
        fields = ('username', 'email')

    # bootstrap4対応
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'
            field.widget.attrs['required'] = '' # 全フィールドを入力必須
```

## マイページ、マイページ更新用のテンプレートを作成
base.htmlのナビゲーションバーにmypageを追加
accounts/templates/my_page.htmlを作成
※詳細は、[GitHub](https://github.com/Keito777/daily_report)を参照


# ユーザのパスワード変更機能
マイページから自分（ユーザ）のパスワードを変更できる機能を追加する。

- 上記のマイページ作成の続きから
- ルーティング設定
- ユーザパスワード変更機能と変更後の処理
- パスワード変更用のフォームを作成
- パスワード変更用、変更後のテンプレートを作成

## ルーティング設定
```python:accounts/urls.py
from .views import Login, Logout, Signup, SignupDone, MyPage,UserUpdate, PasswordChange, PasswordChangeDone # 一部追加

urlpatterns = [
    # 省略
# password
    path('password_change/', PasswordChange.as_view(), name='password_change'), # パスワード変更
    path('password_change_done/', PasswordChangeDone.as_view(), name='password_change_done'), # パスワード変更完了name='user_update'), # 登録情報の更新
```
## ユーザパスワード変更機能と変更後の処理
- PasswordChangeViewクラスを継承してパスワード変更を処理を行うビューを定義
    - get_context_data()をオーバーライドすることで、テンプレートに表示する値を追加
- PasswordChangeDoneViewクラスを継承してパスワード変更後の処理を行うビューを定義
    - パスワード変更後にリダイレクトするテンプレートを指定する
```python:accounts/views.py
# password
from .forms import LoginForm, SignupForm, UserUpdateForm, MyPasswordChangeForm
from django.contrib.auth.views import LoginView, LogoutView, PasswordChangeView, PasswordChangeDoneView
from django.urls import reverse_lazy

'''パスワード変更'''
class PasswordChange(PasswordChangeView):
    form_class = MyPasswordChangeForm
    success_url = reverse_lazy('accounts:password_change_done')
    template_name = 'accounts/signup.html'

    # contextデータ作成
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["process_name"] = "Change Password"
        return context


'''パスワード変更完了'''
class PasswordChangeDone(PasswordChangeDoneView):
    template_name = 'accounts/password_change_done.html'
```
## パスワード変更用のフォームを作成
- accounts/forms.pyに以下のコードを追加
- パスワード変更用のフォームを作成するPasswordChangeForm　クラスを継承してフォームクラスを定義する
```python:accounts/forms.py
'''パスワード変更フォーム'''
class MyPasswordChangeForm(PasswordChangeForm):

    # bootstrap4対応で、classを指定
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'
```

## パスワード変更用、変更後のテンプレートを作成

accounts/templates/my_page.htmlにリンクを追加
accounts/templates/password_change_done.htmlを作成
※パスワード更新用のテンプレートは、サインインとユーザー登録情報の更新用のテンプレートと同じものを使用
※詳細は、[GitHub](https://github.com/Keito777/daily_report)を参照

# デプロイ(本番環境)
- [Herokuにデプロイ](https://qiita.com/kkk777/items/730641a0e8b0c80bdd9b)

# 表示するモデルのデータをユーザごとに切り替える
- [表示するモデルのデータをユーザごとに切り替える](https://qiita.com/kkk777/items/1081aaff1bec9e8049f1)
    - ログイン機能を実装しただけではデータベースから表示するデータが全ユーザーで共有されてしまう。
    - 特定のユーザーが登録した情報だけを表示する処理を実装する。
    - ListViewのget_queryset()をオーバーライドする。

# ページネーションの作成
## 表示するデータ数の指定
- ListView内で、paginate_by変数をオーバーライドする
```python:views.py
class Index(ListView):
    template_name = 'report/index.html'
    paginate_by = 2
```
- paginate_by変数には、1ページに表示するデータ数を指定する

## ページネーションを作成
![pagination.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/729011/9f015fbf-b22f-c1c5-9362-040030388d7f.png)

- ListViewのtemplate_nameに指定しているテンプレートの最後に以下のコードを追加する

```HTML:report/index.html
<div class="row mx-auto" style="width: 600px;">
    <ul class="pagination">
        <!-- 前へ の部分 -->
        {% if page_obj.has_previous %}
        <li><a class="page-link text-primary d-inline-block" href="?page={{ page_obj.previous_page_number }}">＜</a></li>
        {% endif %}

        <!-- 数字の部分 -->
        {% for num in page_obj.paginator.page_range %}
        {% if page_obj.number == num %}
        <li class="disabled"><div class="page-link text-secondary d-inline-block disabled">{{ num }}</div></li>
        {% else %}
        <li><a class="page-link text-primary d-inline-block" href="?page={{ num }}">{{ num }}</a></li>
        {% endif %}
        {% endfor %}

        <!-- 次へ の部分 -->
        {% if page_obj.has_next %}
        <li><a class="page-link text-primary d-inline-block" href="?page={{ page_obj.next_page_number }}">＞</a></li>
        {% endif %}
        <li class="p-md-2">
            {{ page_obj.number }} / {{ page_obj.paginator.num_pages }}を表示
        </li>
    </ul>
</div>
```
# カレンダーフォームの作成
- [カレンダーフォームの作成](https://qiita.com/kkk777/items/58a230003f60d1be1ad6)

# 検索フォームの作成
- [検索フォームの作成](https://qiita.com/kkk777/items/1c1a0f74139c39f6799b)