0
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Djangoアプリ開発(逆引き)

Last updated at Posted at 2022-09-02

この記事では、Djangoのプロジェクト、アプリを作成しているという前提のもと、アプリ開発における逆引きを紹介する。

ルーティングの設定

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

プロジェクト.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の中身をコピペする
ここに、実行していくルーティングを書いていく

アプリ.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に移動してから上記のコマンドを入力

モデル

抽象基底クラスでモデル定義の効率化

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クラス:他のクラスで共通的に利用するcreated(作成時刻)とmodified(更新時刻)のフィールドが定義されている
  • Metaクラス以降のabstract = Trueが定義されているため、TimeStampedModelクラスは抽象基底クラスとなる
  • from model_utils.models import TimeStampedModelをインポートすることで、TimeStampedModelをそのまま使用することができる
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サイトに表示

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を作成
プロジェクト/urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('report.urls')),
    path('accounts/', include('accounts.urls')), # 追加
]
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

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
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の実装

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 ログイン済みユーザのみに制限してレスポンスを返す
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を参照
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も同じ)

サインアップ機能の実装

  • カスタムユーザモデルを作成しておく
  • ルーティング設定
  • サインアップ用の汎用ビューがないのでCreateViewで作成
  • サインアップ用のフォームクラス(UserCreationForm)を用いてサインアップフォームを作成
  • サインアップ、サインアップ後のテンプレートを作成

ルーティング設定

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で作成
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のフィールドもフォームに表示される
    • フィールドの表示優先度は、フォームフィールド<モデルフィールド
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を参照

マイページの作成、変更(アクセス制限)

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

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

ルーティング設定

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'), # 登録情報の更新

独自の制限を設定したマイページビューとマイページ更新ビューの作成

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に以下のコードを追加
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を参照

ユーザのパスワード変更機能

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

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

ルーティング設定

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クラスを継承してパスワード変更後の処理を行うビューを定義
    • パスワード変更後にリダイレクトするテンプレートを指定する
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 クラスを継承してフォームクラスを定義する
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を参照

デプロイ(本番環境)

表示するモデルのデータをユーザごとに切り替える

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

ページネーションの作成

表示するデータ数の指定

  • ListView内で、paginate_by変数をオーバーライドする
views.py
class Index(ListView):
    template_name = 'report/index.html'
    paginate_by = 2
  • paginate_by変数には、1ページに表示するデータ数を指定する

ページネーションを作成

pagination.png

  • ListViewのtemplate_nameに指定しているテンプレートの最後に以下のコードを追加する
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>

カレンダーフォームの作成

検索フォームの作成

0
6
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
0
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?