torinaブログ

DjangoとBootstrap4で作成したブログ
Python, Django, Kivy, Bootstrap, Apache等のメモです
ソースコード

Djangoで、汎用ビュー(ListView, DetailView)

Python Django Bootstrap4 Django
2016年9月14日9:08
まず、このようなトップ画面。


続きを読む、を押すとこの画面に。シンプルです。


これを、汎用ビューを使って作成していきます。
汎用ビューを使うのですが、他にも変更点があります。

Django1.10
Python3.5
プロジェクト名は「testproject」
アプリケーション名は「testapp」
です。

まず、プロジェクトの構成が大きくかわりました。
testproject
さっぱりしました。


testproject/testproject
ここは前と同じ。


testproject/testapp
アプリケーションの中に、staticとtemplatesができています。


testproject/testapp/static/
testapp/staticの中に、またtestapp...ちょっと冗長ですが、やむをえない!


testproject/testapp/static/testapp/
cssやjsのフォルダです。



testproject/testapp/templates/
templatesも、staticと構成は同じです。



testproject/testapp/templates/testapp/
ここにhtml!


これはDjangoのチュートリアルどおりの構成ですが、これはアプリケーションの再利用が非常に簡単に行えます。
具体的には、今回だとtestappをコピーして別プロジェクトに張り付けるだけです。便利ですね。


testproject/testproject/settings.py
startprojectしたときの、ほぼデフォルト設定です。
TEMPLATESのDIR設定や、STATICFILES_DIRSも消しました。
"""
Django settings for tes project.

Generated by 'django-admin startproject' using Django 1.10.

For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'eoaaa*bhbmw#hwqwqa%--y#qwaq23ao9oaap('

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ["*"]


# Application definition

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'testapp',
)

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'testproject.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'testproject.wsgi.application'


# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/

LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/

STATIC_URL = '/static/'


staticとtemplatesが遠い場所にあるように感じますが、これは問題ありません。
templatesは、settings.pyの APP_DIRS のオプションが Trueの場合、
INSTALLED_APPS のそれぞれのサブディレクトリの “templates” を検索します。
他に読み込んでほしいディレクトリがあれば、以下のようにして追加できます。
        'DIRS': [
            os.path.join(BASE_DIR, 'other_templates'),
        ],


staticも同様で、アプリケーションのサブディレクトリの「static」を探します。
他に追加したいときは、以下のようにして追加をしていきます。
ただ、本番環境では別のことをちょっと別のことをしなければいけません。
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, "other_static"),
)



testproject/testproject/urls.py
ここはおんなじ。
from django.conf.urls import include, url
from django.contrib import admin


urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    url(r'^', include('testapp.urls', namespace='testapp')),
]


testproject/testapp/models.py
ブログっぽいモデルです。記事となるPostと、Categoryですね。
from django.contrib.auth.models import User
from django.db import models


class Category(models.Model):
    """カテゴリー"""

    name = models.CharField(max_length=255)

    def __str__(self):
        return self.name


class Post(models.Model):
    """ブログのポスト"""

    title = models.CharField(max_length=255)
    text = models.TextField()
    category = models.ManyToManyField(Category)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title



testproject/testapp/urls.py
ここは少し変わりました。
from django.conf.urls import url
from . import views


urlpatterns = [
    url(r'^$', views.IndexView.as_view(), name='index'),
    url(r'^detail/(?P<pk>[0-9]+)/$', views.DetailView.as_view(), name='detail'),
]


as_view()は、汎用ビューを呼び出す際に使う書き方です。
url(r'^$', views.IndexView.as_view(), name='index'),



DetailViewという汎用ビューを使う際のお約束として、「pk」という名前を使います。
変更することもできます。
url(r'^detail/(?P<pk>[0-9]+)/$', views.DetailView.as_view(), name='detail'),


testproject/testapp/views.py
一番変わったのはここです。コードが少ないのはいいことだ。
from django.views import generic
from .models import Post, Category


class IndexView(generic.ListView):
    model = Post


class DetailView(generic.DetailView):
    model = Post


ListViewは、一覧ページ等に使います。今回の例だと、記事の一覧表示画面ですね。
model = Postとすることで、今回の例だとPost.objects.all()を裏側で行ってくれます。
class IndexView(generic.ListView):
    model = Post



もしmodelを指定しないと、以下のようなエラーがでます。
IndexView is missing a QuerySet. Define IndexView.model, IndexView.queryset, or override IndexView.get_queryset().


つまり、以下のようにするか...
class IndexViepw(generic.ListView):
    queryset = Post.objects.all()


又は...
class IndexView(generic.ListView):
    def get_queryset(self):
        return Post.objects.all()


最初に書いた、3つの方法のどれかで実装していないといけません。
model = Post


このListViewですが、他にも色々と設定ができます。
例えば、以下のようにすると、どのテンプレートを使うか指定できます。
class IndexView(generic.ListView):
    model = Post
    template_name = 'testapp/index.html'


指定しなくてもよいということは、デフォルト値があります。
デフォルト値は
<app name>/<model name>_list.html

となっています。今回の例だとtestapp/post_list.htmlになります。
今回はこの名前のhtmlを作ったので、template_nameの指定はやめています。

以下のような指定もあります。
これは、データベースから取り出した一覧データを、templateに渡す際の名前です。
class IndexView(generic.ListView):
    model = Post
    context_object_name = 'all_post'


context_objetc_nameが空だったならば、以下のような名前になります。
今回だとpost_listです。
object_list という名前でもアクセス可能なので、面倒な場合はこちらにしてもよいでしょう。
<model name>_list



続いてDetailViewですが、これは詳細ページに使うことができます。
model、queryset、又はget_querysetのオーバーライドが必要です。ListViewと同じですね。
class DetailView(generic.DetailView):
    model = Post


querysetを使う際は、単純に以下のようにしても大丈夫です。
内部では、このquerysetに対して、pkを使って.filter(pk=pk)のようなことを行っています。
class DetailView(generic.DetailView):
    queryset = Post.objects.all()



このDetailViewにも、template_nameを指定できます。
デフォルトは以下のようになっており、今回ならばtestapp/post_detail.htmlです。
<app name>/<model name>_detail.html


context_object_nameを指定できますが、デフォルトは、モデル名を小文字にした名前です。
「Post」モデルならば、「post」でtemplateへわたります。又は、「object」でもアクセス可能です。

以下のようにすると、pkという文字列を変えれます。urls.pyにも反映しときましょう。
class DetailView(generic.DetailView):
    model = Post
    pk_url_kwarg = 'id'


pkを使わず、他のモデルの項目を使いたい場合...特にurlに数字ではなく、記事のタイトルを入れたい場合などもありますね。
そのような場合は以下のようにします。urls.pyにも反映しておきましょう。
class DetailView(generic.DetailView):
    model = Post
    slug_field = "title"  # モデルのフィールドの名前
    slug_url_kwarg = "title"  # urls.pyでのキーワードの名前


このように、デフォルト設定であれば驚くほどコード量が減ります。はるかにいいですね。

testproject/testapp/static/testapp/css/main.css
testproject/testapp/static/testapp/js/main.js
は、今回空っぽです。

testproject/testapp/templates/testapp/base.html
実は、今回からBootstrap4を使用しています。
{% load static %}
<!DOCTYPE html>
<html lang="ja">
  <head>
    <!-- Required meta tags always come first -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta http-equiv="x-ua-compatible" content="ie=edge">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.4/css/bootstrap.min.css" integrity="2hfp1SzUoho7/TsGGGDaFdsuuDL0LX2hnUp6VkX3CUQ2K4K+xjboZdsXyp4oUHZj" crossorigin="anonymous">

    <!-- Custom CSS -->
    <link rel="stylesheet" href="{% static 'testapp/css/main.css' %}">

  </head>
  <body>

    {% block content %} {% endblock %}

    <!-- jQuery first, then Tether, then Bootstrap JS. -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.0.0/jquery.min.js" integrity="sha384-THPy051/pYDQGanwU6poAc/hOdQxjnOEXzbT+OuUAFqNqFjL+4IGLBgCJC3ZOShY" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.2.0/js/tether.min.js" integrity="sha384-Plbmg8JY28KFelvJVai01l8WyZzrYWG825m+cZ0eDDS1f7d/js6ikvy1+X+guPIB" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.4/js/bootstrap.min.js" integrity="VjEeINv9OSwtWFLAtmc4JCtEJXXBub00gtSnszmspDLCtC0I4z4nqz7rEFbIZLLU" crossorigin="anonymous"></script>

    <!-- Custom JS -->
    <script src="{% static 'testapp/js/main.js' %}"></script>

  </body>
</html>



testproject/testapp/templates/testapp/post_list.html
{% extends "testapp/base.html" %}
{% block content %}

<div class="container">
	<h1>トップ画面</h1>
	{% for post in post_list %}
		<p>
			タイトル:{{ post.title }} 
			日付:{{ post.created_at }}
			<a href="{% url 'testapp:detail' post.pk %}">続きを読む</a>
		</p>
	{% endfor %}
</div>
{% endblock %}


testproject/testapp/templates/testapp/post_list.html
{% extends "testapp/base.html" %}
{% block content %}
<div class="container">
	<div class="alert alert-success" role="alert">
	  <h4 class="alert-heading">{{ post.title }}</h4>
	  <p>{{ post.text | linebreaksbr}}</p>
	  <p class="text-muted">作成日:{{ post.created_at 	}}</p>
	  <p class="text-muted">更新日:{{ post.updated_at }}</p>
	  <p>{% for category in post.category.all %}{{ category }},{% endfor %}</p>
	  <a class="btn btn-primary" href="javascript:void(0);" onclick="window.history.back();">戻る</a>
	</div>
</div>
{% endblock %}



ListViewソース
https://github.com/django/django/blob/master/django/views/generic/list.py

DetailViewソース
https://github.com/django/django/blob/master/django/views/generic/detail.py

Djangoチュートリアル(今回扱ったあたりは4番目)
https://docs.djangoproject.com/ja/1.10/intro/tutorial04/