18d. Django入門4(お勧め店を投稿できるランチマップアプリの作成)

アプリの概要

お勧めのお店を投稿できる「ランチマップ」アプリの機能について整理します。

ルーティング

ルートビュー表示ページ
(ドメイン名)/lunchmapIndexView.as_view()一覧
(ドメイン名)/lunchmap/(id)DetailView.as_view()詳細
(ドメイン名)/lunchmap/createCreateView.as_view()新規
(ドメイン名)/lunchmap/(id)/updateUpdateView.as_view()更新
(ドメイン名)/lunchmap/(id)/deleteDeleteView.as_view()削除
(ドメイン名)/lunchmap/adminadmin.site.urls管理サイト

プロジェクトの準備

Djangoでは、機能別にアプリケーションを用意して、それを1つのプロジェクトにまとめていきます。

Djangoのプロジェクト
Djangoのプロジェクトは、アプリケーションをまとめる単位で、あるウェブサイト向けに設定とアプリケーションを集めたものです。Djangoでは、機能別にアプリケーションを用意して、それを1つのプロジェクトにまとめていきます。

プロジェクトの作成

$ python --version					# Pythonのバージョン確認
Python 3.6.7

$ python -m django --version		# Djangoのバージョン確認
2.1.3

$ django-admin startproject myapp	# myappプロジェクトの作成

paiza cloud用にsettings.pyを修正

# myapp/myapp/settings.py
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['*']		# どのホストからもアクセス可能に設定

DjangoのWebページにアクセス

$ cd myapp
$ python manage.py runserver	# Webサーバを起動

https://localhost:8000/で、DjangoのWebサーバーにアクセスできます。

DjangoのWebサーバを停止
「ctrl+c」キーでWebサーバを停止できます。

アプリケーションの作成

$ python manage.py startapp lunchmap

アプリケーションの登録

# myapp/myapp/settings.py
INSTALLED_APPS = [
    'lunchmap.apps.LunchmapConfig',		# apps.pyのLunchmapConfigを登録
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

データベースのモデルの作成

ShopCategoryのモデルを作成(カテゴリidを紐付け)して、マイグレーションでデータベースに適用していきます。

Shop
ショップidid
店名name
住所address
カテゴリidcategory
投稿者idauthor
作成日時created_at
更新日時updated_at
Category
カテゴリidid
カテゴリ名name
投稿者idauthor
作成日時created_at
更新日時updated_at
Author
投稿者idid
カテゴリ名user

settings.pyでデータベースを設定
phpMyAdminで、lunchmapdbを作成しておきます。

MySQLのインストールがまだの場合、下記を参考にインストール
【Ubuntu 18.04 LTS】MySQLサーバを動かす

# myapp/myapp/settings.py
# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases

# Python3以降はmysqlclientを使用する
#import pymysql					# 追加
#pymysql.install_as_MySQLdb()	# 追加

# プロジェクトからDBへの接続設定
DATABASES = {
    'default': {
#        'ENGINE': 'django.db.backends.sqlite3',
#        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'lunchmapdb',
        'USER': 'root',
        'PASSWORD': 'XXXX',
        'HOST': '127.0.0.1',
        'PORT': '3306',
    }
}

mysqlclientのインストールは、mysqlclient Python3でMySQLに接続 を参照

models.pyを記述

# myapp/lunchmap/models.py
from django.db import models

class Category(models.Model):		# Categoryモデルの定義
    name = models.CharField(max_length=255)
    author = models.ForeignKey(
        'auth.User',
        on_delete=models.CASCADE,
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name

class Shop(models.Model):		# Shopモデルの定義
    name = models.CharField(max_length=255)
    address = models.CharField(max_length=255)
    author = models.ForeignKey(
        'auth.User',
        on_delete=models.CASCADE,
    )
    # CategoryモデルとShopモデルの関連付け
    category = models.ForeignKey(
        Category,
        on_delete=models.PROTECT,
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name

マイグレーションファイルの作成と実行

$ python manage.py makemigrations lunchmap 	# マイグレーションファイルの作成
Migrations for 'lunchmap':
  lunchmap/migrations/0001_initial.py
    - Create model Category
    - Create model Shop

$ python manage.py migrate						# マイグレーションの実行
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, lunchmap, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  :

Djangoの管理サイトの設定

管理サイトを使用して、データベースやユーザー情報を簡単に設定することができます。

admin.pyにモデルを登録

# myapp/lunchmap/admin.py
from django.contrib import admin
from .models import Category, Shop

admin.site.register(Category)
admin.site.register(Shop)

管理サイトにアクセスする

$ python manage.py runserver		# Webサーバを起動

https://localhost:8000/adminで、管理サイトにアクセスできます。

管理ユーザをコマンドで登録

$ cd myapp
$ python manage.py createsuperuser
Username: admin
Email address: admin@XXXXX.jp
Password: **********
Password (again): *********
Superuser created successfully.

お店一覧ページの作成

テンプレートにデータを供給するのはViewです。手短にコーディングできるクラスベース汎用ビューを使用して、登録しておいたサンプルデータを一覧表示していきます。

プロジェクトのurls.pyを記述

# myapp/myapp/urls.py
from django.contrib import admin
from django.urls import path, include
from django.views.generic import RedirectView

urlpatterns = [
    path('admin/', admin.site.urls),
    # lunchmapのルートにアクセスした場合、lunchmapディレクトリのurls.pyを呼び出す
    path('lunchmap/', include('lunchmap.urls')),
    path('',  RedirectView.as_view(url='/lunchmap/')),
]

lunchmapのurls.pyを作成

# myapp/lunchmap/urls.py
from django.urls import path
from . import views

app_name = 'lunchmap'

urlpatterns = [
    # lunchmapの後に何も追加されてなかったら、views.IndexView.as_view()を呼び出す
    path('', views.IndexView.as_view(), name='index'),
]

views.pyにindexViewクラスを記述

# myapp/lunchmap/views.py
from django.urls import reverse_lazy
from django.views import generic	# クラスベース汎用ビューを使用
from .models import Category, Shop

# IndexViewクラスを作成
class IndexView(generic.ListView):	# モデルで取り出したデータで一覧ページを作成
    model = Shop

テンプレートを作成

<!-- myapp/lunchmap/templates/lunchmap/shop_list.html -->
< !DOCTYPE html>
<html>
    <head>
        <meta charset='utf-8'/>
        <title>Lunchmap</title>
        <style>body {padding: 10px;}</style>
    </head>
    <body>
        <h1>お店一覧</h1>

        {% for shop in object_list %}
            <p> {{ shop.category.name }}, {{ shop.name }}</p>
        {% endfor %}
    </body>
</html>

共通テンプレートにBootstrapの導入

CSSフレームワークのBootstrapを導入して、ナビゲーションバーを追加していきます。

base.html(共通テンプレート)を作成

<!-- myapp/lunchmap/templates/lunchmap/base.html -->
< !DOCTYPE html>
<html>
    <head>
        <meta charset='utf-8'/>
        <!-- bootstrapはインタネット上にライブラリとして公開されているので、直接読み込む -->
        <meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'/>
        <link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css'/>
        <style>body {padding-top: 80px;}</style>
        <title>Lunchmap</title>
    </head>
    <body>
    	<!-- ナビゲーションバーを設定 -->
        <nav class='navbar navbar-expand-sm navbar-dark bg-dark fixed-top'>
            <a class='navbar-brand' href='{% url "lunchmap:index" %}'>Lunchmap</a>
        </nav>
        <div class='container'>
            {% block content %}		<!-- contentというブロックを指定 -->
            {% endblock %}
        </div>
    </body>
</html>

お店一覧テンプレートを修正

<!-- myapp/lunchmap/templates/lunchmap/shop_list.html -->
{% extends './base.html' %}
<!-- 以下のブロックの内容をbase.htmlに当てはめる -->
{% block content %}
    <h1>お店一覧</h1>

    {% for shop in object_list %}
        <p>{{ shop.name }}, {{ shop.category.name }}</p>
    {% endfor %}
{% endblock %}

お店の詳細ページの作成

詳細ページも、クラスベース汎用ビューを使うことで、少ないコードで記述できます。

lunchmapのurls.pyに追記

# myapp/lunchmap/urls.py
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    # lunchmapの後にidを指定すると、DetailViewクラスを呼び出してお店の個別ページを表示します
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
]

views.pyにDetailViewクラスを記述

# myapp/lunchmap/views.py
from django.urls import reverse_lazy
from django.views import generic
from .models import Shop

class IndexView(generic.ListView):
    model = Shop

# 以下を追加
class DetailView(generic.DetailView):
    model = Shop

詳細ページのテンプレートを作成

<!-- myapp/lunchmap/templates/lunchmap/shop_detail.html -->
{% extends './base.html' %}
{% block content %}
<h1>{{ shop.name }}</h1>
<div>
    <p>{{ shop.category.name }}</p>
    <p>{{ shop.address }}</p>
</div>
<div>
    <a href='{% url "lunchmap:index" %}'>一覧</a>
</div>
{% endblock %}

お店一覧ページをテーブルにして、詳細ページにリンクする

<!-- myapp/lunchmap/templates/lunchmap/shop_list.html -->
{% extends './base.html' %}
{% block content %}
	<h1>お店一覧</h1>
	<table class='table table-striped table-hover'>
	    <tr>
	        <th>カテゴリ</th><th>店名</th><th>住所</th>
	    </tr>
	    {% for shop in object_list %}
	    <tr>
	        <td>{{ shop.category.name }}</td>
	        <td><a href='{% url "lunchmap:detail" shop.pk %}'>{{ shop.name }}</a></td>
	        <td>{{ shop.address }}</td>
	    </tr>
	    {% endfor %}
	</table>
{% endblock %}

新規と編集のフォームの作成

Djangoでは、クラスベースの汎用ビューを使うことで、新規と編集のフォームを共通のテンプレートで作ることができます。

shopsのurls.pyに追記

# myapp/lunchmap/urls.py
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('</int:pk><int:pk>/', views.DetailView.as_view(), name='detail'),
    # CreateViewクラスを呼び出して、新規の投稿フォームを表示
    path('create/', views.CreateView.as_view(), name='create'),
    # UpdateViewクラスを呼び出して、既存データを編集するフォームを表示
    path('</int:pk><int:pk>/update/', views.UpdateView.as_view(), name='update'),
]

※ プラグインを使用しているせいか、どうしても<\int:pk>等余分なコードが入り込んでしまいますので、適宜読み替えてください。

views.pyにCreateViewを記述

# myapp/lunchmap/views.py
from django.urls import reverse_lazy
from django.views import generic
from .models import Shop

class IndexView(generic.ListView):
    model = Shop

class DetailView(generic.DetailView):
    model = Shop

# 新規作成ビューの追加
class CreateView(generic.edit.CreateView):
    model = Shop
    fields = '__all__'		# 全てのカラムを入力できるフォームを指定

# 編集ビューの追加
class UpdateView(generic.edit.UpdateView):
    model = Shop
    fields = '__all__'

テンプレートとしてshop_form.htmlを作成

<!-- myapp/lunchmap/templates/lunchmap/shop_form.html -->
{% extends './base.html' %}
{% block content %}
	<!-- 新規と編集で共通のテンプレート -->
	<h1>お店情報の{{ object|yesno:'更新,作成'}}</h1>

	<form action='' method='post'>{% csrf_token %}		<!-- このフォームが正式であることを証明する -->
	    {{ form.as_p }}									<!-- 全ての項目を表示 -->
	    <button type='submit' class='submit'>{{ object|yesno:'更新,作成'}}</button>
	</form>
	<div>
		<!-- JavaScriptで戻る機能を実装 -->
	    <a href='JavaScript:history.back()'>< 戻る</a>
	</a></div>
{% endblock %}

投稿フォームの保存機能の作成

一覧ページと詳細ページから、投稿フォームを呼び出すようにして、データの保存機能を完成させます。

reverse関数
reverse関数は、ビューの名前から、リダイレクト先のURLを調べる関数です。モデルやビューの中でルートを直接記述せずに済みます。

一覧ページから新規ページにリンク

<!-- myapp/lunchmap/templates/lunchmap/shop_list.html -->
<div>
    <a href='{% url "lunchmap:create" %}'>新しいお店</a>
</div>

詳細ページから更新ページにリンク

<!-- myapp/lunchmap/templates/lunchmap/shop_detail.html -->
<div>
    <a href='{% url "lunchmap:index" %}'>一覧</a> |
    <a href='{% url "lunchmap:update" shop.pk %}'>編集</a> |
</div>

更新できるようにmodels.pyを修正

# myapp/lunchmap/models.py
from django.db import models
from django.urls import reverse 		# 追加

class Category(models.Model):
    name = models.CharField(max_length=255)
    author = models.ForeignKey(
        'auth.User',
        on_delete=models.CASCADE,
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name

class Shop(models.Model):
    name = models.CharField(max_length=255)
    address = models.TextField(blank=True)
    author = models.ForeignKey(
        'auth.User',
        on_delete=models.CASCADE,
    )
    category = models.ForeignKey(
        Category, on_delete=models.PROTECT)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        # reverse関数を使用して、viewの名前からリダイレクト先を取得
        return reverse('lunchmap:detail', kwargs={'pk': self.pk})

削除ページの作成

ルートで「delete」にアクセスした時、該当のお店情報を削除するように設定していきます。

shopsのurls.pyに追記

# myapp/lunchmap/urls.py
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('</int:pk><int:pk>/', views.DetailView.as_view(), name='detail'),
    path('create/', views.CreateView.as_view(), name='create'),
    path('</int:pk><int:pk>/update/', views.UpdateView.as_view(), name='update'),
    # (pk)/delete/を指定した場合、DeleteViewクラスを呼び出して削除の確認ページを表示
    path('</int:pk><int:pk>/delete/', views.DeleteView.as_view(), name='delete'),
]

※ プラグインを使用しているせいか、どうしても<\int:pk>等余分なコードが入り込んでしまいますので、適宜読み替えてください。

views.pyにDeleteViewクラスを記述

# myapp/lunchmap/views.py
from django.views import generic
from .models import Category, Shop
from django.urls import reverse_lazy 		# 追加

class IndexView(generic.ListView):
    model = Shop

class DetailView(generic.DetailView):
    model = Shop

class CreateView(generic.edit.CreateView):
    model = Shop
    fields = '__all__'

class UpdateView(generic.edit.UpdateView):
    model = Shop
    fields = '__all__'

class DeleteView(generic.edit.DeleteView):
    model = Shop
    # reverse_lazy関数は、viewの名前からリダイレクト先のURLを取得
    success_url = reverse_lazy('lunchmap:index')

テンプレートとしてshop_confirm_delete.htmlを作成

<!-- myapp/lunchmap/templates/lunchmap/shop_confirm_delete.html -->
{% extends './base.html' %}
{% block content %}

<h1>お店情報の削除</h1>
<p>'{{ object.name }}'を削除しますか?</p>
<form action='' method='post'>{% csrf_token %}
    <button type='submit' class='submit delete'>削除する</button>
</form>

<div>
    <a href='JavaScript:history.back()'>< 戻る</a>
</a></div>

{% endblock %}

詳細ページから削除ページにリンク

<!-- myapp/lunchmap/templates/lunchmap/shop_detail.html -->
<div>
    <a href='{% url "lunchmap:index" %}'>一覧</a> | 
    <a href='{% url "lunchmap:update" shop.pk %}'>編集</a> | 
    <a href='{% url "lunchmap:delete" shop.pk %}'>削除</a>		<!-- 追加 -->
</div>

Googleマップの表示

このアプリケーションにGoogleマップを組み込んでいきます。

APIとは
APIとは、Application Programming Interfaceの略で、プログラムから別のプログラムの機能を呼び出すために用意された命令や関数のことです。

Google Maps API
Google Maps Platform - Geo-location API
Developer Guide | Maps Embed API | Google Developers

APIキーの取得手順

  1. Google Developers Consoleにアクセス
  2. プロジェクトを作成を選択
  3. Google APIが表示されたら、Google Maps APIから「Google Maps Embed API」を選択
  4. 「有効にする」をクリック
  5. 「認証情報を作成」をクリックして、「必要な認証情報」ボタンをクリック
  6. 表示されたAPIキーを記録する

※ 特定のWebサービスだけから利用できるよう、「API利用制限」を設定することをお勧めします
※ この手順や利用範囲はGoogle側で変更される場合があります

shop_detail.htmlにマップを追加

<!-- myapp/lunchmap/templates/lunchmap/shop_detail.html -->
<iframe id='map'
    src='https://www.google.com/maps/embed/v1/place?key=XXXXXXXXXXXXXXXXXXXX&q={{ shop.address }}'
    width='100%'
    height='320'
    frameborder='0'>
</iframe>

Webプログラミング入門

タイトルとURLをコピーしました