Go Back

Django Crash Course-2

Toku

December 19, 2022

参考:UdemyDjango 3.0 MasterClass - Learn How To Create Django Apps
Django Girls のチュートリアル:https://tutorial.djangogirls.org/ja/django_installation/

user@mbp Django-tutorial-blog % source venv/bin/activate
(venv) user@mbp Django-tutorial-blog % python manage.py runserver
(venv) user@mbp Django-tutorial-blog % pip freeze
asgiref==3.5.2
Django==4.1.4
Pillow==9.3.0
sqlparse==0.4.3

id:user pass: 123

ログイン要求機能(Login Required FUnction)

ログインしていない人がCreateページに遷移できないようにする=>login_requiredデコレータを使う

# posts/views.py
from django.contrib.auth.decorators import login_required # added

@login_required(login_url='/')  # added
def create_view(request):
  form = PostForm(request.POST or None, request.FILES or None)
  
  if form.is_valid():
    post = form.save()
    return HttpResponseRedirect(post.get_absolute_url())
    
  context = {
    'form': form
  }
  return render (request, 'posts/create.html', context)

表示するブログをIDの降順にして最新のものを先頭にする
.order_by('-id')をつける(マイナスをつけることで降順になる)

# posts/views.py
def index(request):
  context = {
    'posts': Post.objects.all().order_by('-id') # Changed
  }
  return render(request, 'posts/index.html', context)

Navbarの’Home'をクリックするとHomeページに行くようにする

# templates/base.html
<li class="nav-item">
  <a class="nav-link active" aria-current="page" href="{% url 'index' %}">Home</a>
</li>

Disableのところにログインした人の名前を表示させる

# templates/base.html
<li class="nav-item">
  {% if user.is_authenticated %}
    <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">{{user.username}}</a>
  {% endif %}
</li>

Postを削除する

# posts.views.py
from django.shortcuts import render, get_object_or_404, HttpResponseRedirect, redirect # redirect added
from django.contrib.auth.decorators import login_required

@login_required(login_url='/')
def delete_view(request, id):
  post = get_object_or_404(Post, id=id)
  post.delete()
  return redirect('/')

blog/urls.pyにDelete用のpathを追加する

# blog/urls.py
from django.urls import path
# from posts.views import index
from posts.views import *
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', index, name="index"),
    path('detail/<int:id>', detail_view, name="detail"),
    path('delete/<int:id>', delete_view, name="delete"),  # added
    path('create/', create_view, name="create")
    
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

詳細ページのdetail.htmlに削除ボタンを追加する

# templates/posts/detail.html
{% extends 'base.html' %}

{% block body %}
  <div class="container" style="padding-top:45px; padding-bottom:45px">
    <h3>詳細ページ</h3>
        <div class="card" style="width: 48rem; margin-bottom:30px">
          <img src="{{ post.image.url }}" class="card-img-top" alt="Post image">
          <div class="card-body">
            <h5 class="card-title">{{post.title}}</h5>
              <small>{{ post.date }}</small>
            <p class="card-text">{{ post.content}}</p>
            <a href="{% url 'index' %}" class="btn btn-primary">戻る</a>
            <a href="{% url 'delete' post.id %}" class="btn btn-danger">削除</a> # added

            # <a href="http://www.google.co.jp/" onclick="return confirm('外部のページへ移動します。よろしいですか?')">リンクをクリックして下さい。</a> 

          </div>
        </div>
  </div>
{% endblock %}

上の削除ボタンに削除確認メッセージを追加する

 <!-- templates/posts/detail.html -->
  <a href="{% url 'delete' post.id %}" class="btn btn-danger" 
  onclick="return confirm('削除してもいいですか?')">削除</a>
// static/js/jquery-3.6.3.min.jsの末尾に以下を追加する
$(document).on('click', 'btn btn-danger', function(){
  return confirm('削除してもいいですか?');
})

Postをアップデートする

# posts/views.py
@login_required(login_url='/')
def update_view(request, id):
  post = get_object_or_404(Post, id=id)
  form = PostForm(request.POST or None, request.FILES or None, instance=post)
  if form.is_valid():
    post = form.save()
    return HttpResponseRedirect(post.get_absolute_url())
  context = {
    'form': form
  }
  return render (request, 'posts/create.html', context)

blog/urls.pyにUpdate用のpathを追加する

# blog/urls.py
from django.contrib import admin
from django.urls import path
from posts.views import *
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', index, name="index"),
    path('detail/<int:id>', detail_view, name="detail"),
    path('delete/<int:id>', delete_view, name="delete"),
    path('update/<int:id>', update_view, name="update"), # added
    path('create/', create_view, name="create")
    
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

詳細ページに更新ボタンを追加する

{% extends 'base.html' %}

{% block body %}
  <div class="container" style="padding-top:45px; padding-bottom:45px;">
    <a href="{% url 'index' %}" style="text-decoration:none; color:green; font-size: 18px;" >戻る</a>
    <h3>詳細ページ</h3>
        <div class="card" style="width: 48rem; margin-bottom:30px">
          <img src="{{ post.image.url }}" class="card-img-top" alt="Post image">
          <div class="card-body">
            <h5 class="card-title">{{post.title}}</h5>
              <small>{{ post.date }}</small>
            <p class="card-text">{{ post.content}}</p>
            <a href="{% url 'delete' post.id %}" class="btn btn-danger" onclick="return confirm('削除してもいいですか?')">削除</a>
            <a href="{% url 'update' post.id %}" class="btn btn-primary">更新</a> # updated
          </div>
        </div>
  </div>
{% endblock %}

ページネーションを作る

# posts/views.py
from django.core.paginator import Paginator # added

def index(request):
  post_list = Post.objects.all().order_by('-id')
  paginator = Paginator(post_list, 3)
  page = request.GET.get('page')
  post_list = paginator.get_page(page)
  
  context = {
    'posts': post_list
  }
  return render(request, 'posts/index.html', context)

参考:https://docs.djangoproject.com/en/4.1/topics/pagination/

# サンプル
<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?page=1">&laquo; first</a>
            <a href="?page={{ page_obj.previous_page_number }}">previous</a>
        {% endif %}

        <span class="current">
            Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
        </span>

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}">next</a>
            <a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
        {% endif %}
    </span>
</div>

index.htmlの最後に以下のページネーションを追加する

# templates/posts/index.html
<div class="container">
  <div class="pagination">
    <span class="step-links">
        {% if posts.has_previous %}
            <a href="?page=1">&laquo; first</a>
            <a href="?page={{ posts.previous_page_number }}">previous</a>
        {% endif %}

        <span class="current">
            Page {{ posts.number }} of {{ posts.paginator.num_pages }}.
        </span>

        {% if posts.has_next %}
            <a href="?page={{ posts.next_page_number }}">next</a>
            <a href="?page={{ posts.paginator.num_pages }}">last &raquo;</a>
        {% endif %}
    </span>
  </div>
</div>

サーチ機能

サーチのフォームにmethod="get"とname = "q" を追加する
ボックスに'test'と入力してクリックするとURLがhttp://localhost:8000/?q=test となる

# templates/base.html
from django.db.models import Q  # added
  <form class="d-flex" method="get">
    <input class="form-control me-2" type="search" name = "q" placeholder="Search" aria-label="Search">
    <button class="btn btn-outline-success" type="submit">Search</button>
  </form>

icontainsと先頭にiをつけるとinsensitiveになりケースを気にしなくなる
Qオブジェクトは、モデルのデータの中からor検索をする時に使われる

# posts/views.py
def index(request):
  post_list = Post.objects.all().order_by('-id')
  query = request.GET.get('q')
  if query:
    post_list = post_list.filter(Q(title__icontains=query) | Q(content__icontains=query))
  
  paginator = Paginator(post_list, 2)
  page = request.GET.get('page')
  post_list = paginator.get_page(page)
  
  context = {
    'posts': post_list
  }
  return render(request, 'posts/index.html', context)

サーチ結果の出力も正しくページネーションさせる方法は
{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}を追加する
URLは http://localhost:8000/?page=2&q=text のような形になる

# templates/posts/index.html
<div class="container">
  <div class="pagination">
    <span class="step-links">
        {% if posts.has_previous %}
            <a href="?page=1{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">&laquo; first</a>
            <a href="?page={{ posts.previous_page_number }}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">previous</a>
        {% endif %}

        <span class="current">
            Page {{ posts.number }} of {{ posts.paginator.num_pages }}.
        </span>

        {% if posts.has_next %}
            <a href="?page={{ posts.next_page_number }}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">next</a>
            <a href="?page={{ posts.paginator.num_pages }}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">last &raquo;</a>
        {% endif %}
    </span>
  </div>
</div>

Users Appを作成する

(venv) user@mbp Django-tutorial-blog % python manage.py startapp u
sers
# settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'posts.apps.PostsConfig',
    'users.apps.UsersConfig'  # added
]

ログインフォームを作成する

formで使用するcleaned_dataってなに?
1forms.pyでフォームを作成
2フォームにデータを入力
3入力データをバリデート(validate)する→login_viewでform.is_valid()と書く
 ここでデータがフォームに適切か判断
4適切だった場合、cleaned_dataに入る

# users/forms.py
from django import forms
from django.contrib.auth import authenticate

class LoginForm(forms.Form):
  username = forms.CharField(max_length=100, label='ユーザ名')
  password = forms.CharField(max_length=100, label='パスワード', widget=forms.PasswordInput)
  
  def clean(self):
    username = self.cleaned_data.get('username')
    password = self.cleaned_data.get('password')
  
    if username and password:
      user = authenticate(username=username,password=password)
      
      if not user:
        raise forms.ValidationError("ユーザネームまたはパスワードが不正です")
      return super(LoginForm, self).clean()

ログインのview

# users/views.py
from django.shortcuts import render, redirect
from .forms import LoginForm
from django.contrib.auth import authenticate, login

def login_view(request):
  form = LoginForm(request.POST or None)
  if form.is_valid():
    username = form.cleaned_data.get('username')
    password = form.cleaned_data.get('password')
    
    user = authenticate(username=username,password=password)
    login(request, user)
    return redirect('index')
  
  context = {
    'form': form
  }
  
  return render(request, 'users/login.html', context)

ログインのHtmlテンプレートを作成する

# templates/users/login.html
{% extends 'base.html' %}

{% block body %}

<div class="container">
  <h4>ログインフォーム</h4>
  <form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <input type='submit' value='ログイン' >
  </form>
</div>

{% endblock %}

ユーザ登録用のビューとHtmlを作成する

register_viewを作成

# users/views.py
from django.shortcuts import render, redirect
from .forms import LoginForm
from django.contrib.auth import authenticate, login
from django.contrib.auth.forms import UserCreationForm # added

def login_view(request):
  ・・・・・・・・

def register_view(request):
  form = UserCreationForm(request.POST or None)
  if form.is_valid():
    username = form.cleaned_data.get('username')
    password = form.cleaned_data.get('password')
    user = form.save()
    login(request, user)
    return redirect('index')
  else:
    form = UserCreationForm()
  
  return render (request, 'users/register.html', {'form': form})

request.POST or Noneとすることで、GetのときはNoneとなり引数なしで呼び出したのと同じフォームが作れる

# templates/users/register.html
{% extends 'base.html' %}
{% block body %}

<div class="container">
  <h4>登録フォーム</h4>
  <form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <input type='submit' value='登録' >
  </form>
</div>

{% endblock %}

blog/urls.pyにregisterのpathを追加

# blog/urls.py
from django.contrib import admin
from django.urls import path
# from posts.views import index
from posts.views import *
from users.views import *
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', index, name="index"),
    path('detail/<int:id>', detail_view, name="detail"),
    path('delete/<int:id>', delete_view, name="delete"),
    path('update/<int:id>', update_view, name="update"),
    path('create/', create_view, name="create"),
    path('login/', login_view, name="login"),
    path('register/', register_view, name="register") # added
    
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

blogフォルダにあるsettings.pyのLANGUAGE_CODE = 'ja'にすることで登録フォームが下記のような日本語表記になる

登録フォーム
ユーザー名: 
 この項目は必須です。半角アルファベット、半角数字、@/./+/-/_ で150文字以下にしてください。
パスワード: 
あなたの他の個人情報と似ているパスワードにはできません。
パスワードは最低 8 文字以上必要です。
よく使われるパスワードにはできません。
数字だけのパスワードにはできません。
パスワード(確認用): 
 確認のため、再度パスワードを入力してください。

id: david pass: david12345

ログアウト用のビューを作成する

# users/views.py
from django.shortcuts import render, redirect
from .forms import LoginForm
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.forms import UserCreationForm

def login_view(request):
  form = LoginForm(request.POST or None)
  if form.is_valid():
    username = form.cleaned_data.get('username')
    password = form.cleaned_data.get('password')
    
    user = authenticate(username=username,password=password)
    login(request, user)
    return redirect('index')
  
  context = {
    'form': form
  }
  
  return render(request, 'users/login.html', context)

def register_view(request):
  form = UserCreationForm(request.POST or None)
  if form.is_valid():
    username = form.cleaned_data.get('username')
    password = form.cleaned_data.get('password')
    user = form.save()
    login(request, user)
    return redirect('index')
  else:
    form = UserCreationForm()
  
  return render(request, 'users/register.html', {'form': form})

def logout_view(request): # added
  logout(request)
  return redirect('index')

localhost:8000/logoutと入力するとログアウトされる

# templates/base.html
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ブログページ</title>
  <link rel="stylesheet" href="{% static "css/bootstrap.min.css" %}">
</head>
<body>
  <nav class="navbar navbar-expand-lg navbar-dark bg-primary" style="margin-bottom: 15px">
    <div class="container-fluid">
      <a class="navbar-brand" href="#">Navbar</a>
      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
          <li class="nav-item">
            <a class="nav-link active" aria-current="page" href="{% url 'index' %}">Home</a>
          </li>
          {% if not user.is_authenticated %}
          <li class="nav-item">
            <a class="nav-link" href="{% url 'register' %}">登録</a>
          </li>
          {% endif %}
          {% if user.is_authenticated %}
            <li class="nav-item dropdown">
              <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                {{user.username}}
              </a>
              <div class="dropdown-menu" aria-labelledby="navbarDropdown">
                <a class="dropdown-item" href="{% url 'create' %}">新規投稿</a>
                <a class="dropdown-item" href="{% url 'logout' %}">ログアウト</a>
              </div>
            </li>
          {% else %}
            <li class="nav-item">
              <a class="nav-link" href="{% url 'login' %}">ログイン</a>
            </li>
          {% endif %}
          {% comment %} <li class="nav-item">
            {% if user.is_authenticated %}
              <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">{{user.username}}</a>
            {% endif %}
          </li> {% endcomment %}
        </ul>
        <form class="d-flex" method="get">
          <input class="form-control me-2" type="search" name = "q" placeholder="Search" aria-label="Search">
          <button class="btn btn-outline-success" type="submit">Search</button>
        </form>
      </div>
    </div>
  </nav>

  {% block body %}

  {% endblock %}

  <script src="{% static 'js/jquery-3.6.3.min.js' %}"></script>
  <script src="{% static 'js/bootstrap.min.js' %}"></script>
</body>
</html>

メッセージの出力方法

https://docs.djangoproject.com/en/4.1/ref/contrib/messages/

# templates/messages.html
{% if messages %}
<ul class="messages">
    {% for message in messages %}
    <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
    {% endfor %}
</ul>
{% endif %}

base.htmlに上のmessages.htmlをincludeで含める

# templates/base.html
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ブログページ</title>
  <link rel="stylesheet" href="{% static "css/bootstrap.min.css" %}">
</head>
<body>
  <nav class="navbar navbar-expand-lg navbar-dark bg-primary" style="margin-bottom: 15px">
    <div class="container-fluid">
      <a class="navbar-brand" href="#">Navbar</a>
      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
          <li class="nav-item">
            <a class="nav-link active" aria-current="page" href="{% url 'index' %}">Home</a>
          </li>
          {% if not user.is_authenticated %}
          <li class="nav-item">
            <a class="nav-link" href="{% url 'register' %}">登録</a>
          </li>
          {% endif %}
          {% if user.is_authenticated %}
            <li class="nav-item dropdown">
              <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                {{user.username}}
              </a>
              <div class="dropdown-menu" aria-labelledby="navbarDropdown">
                <a class="dropdown-item" href="{% url 'create' %}">新規投稿</a>
                <a class="dropdown-item" href="{% url 'logout' %}">ログアウト</a>
              </div>
            </li>
          {% else %}
            <li class="nav-item">
              <a class="nav-link" href="{% url 'login' %}">ログイン</a>
            </li>
          {% endif %}
        </ul>
        <form class="d-flex" method="get">
          <input class="form-control me-2" type="search" name = "q" placeholder="Search" aria-label="Search">
          <button class="btn btn-outline-success" type="submit">Search</button>
        </form>
      </div>
    </div>
  </nav>
  {% include 'messages.html' %} # added

  {% block body %}

  {% endblock %}

  <script src="{% static 'js/jquery-3.6.3.min.js' %}"></script>
  <script src="{% static 'js/bootstrap.min.js' %}"></script>
</body>
</html>

ログアウト時にメッセージを出力する

# users/views.py
from django.shortcuts import render, redirect
from .forms import LoginForm
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.forms import UserCreationForm
from django.contrib import messages # added

def login_view(request):
  ・・・

def register_view(request):
  ・・・

def logout_view(request):
  logout(request)
  messages.success(request, 'ログアウトしました')
  return redirect('index')
# posts.views.py
from django.contrib import messages 

@login_required(login_url='/')
def create_view(request):
  form = PostForm(request.POST or None, request.FILES or None)
  
  if form.is_valid():
    post = form.save()
    messages.success(request, '投稿を保存しました')
    return HttpResponseRedirect(post.get_absolute_url())
    
  context = {
    'form': form
  }
  return render (request, 'posts/create.html', context)

@login_required(login_url='/')
def delete_view(request, id):
  post = get_object_or_404(Post, id=id)
  post.delete()
  messages.success(request, '投稿を削除しました')
  return redirect('/')