pythonでWebアプリ(django編)

djangoを利用してWebアプリ

djangoで作るWebアプリ

pythonを使ってアプリを作成するとなると、画面の作成はなかなか難しく、FlaskなどでWebアプリを作る方が相性がいいように感じます。しかしデータベースを使ってシステムを設計するときなかなか大変です。そこでdjangoの登場です。

djangoとは

djangoはMVCアーキテクチャ使ってデータベースを使ったWebアプリを作成するツールです。ある程度データベースの知識が必要ですが、それは論理的なつながりに関する知識で、正規化などのことになります。

MVCアーキテクチャとは

システムを作るときに、基礎になる考え方で、システムを「Model」「View」「Controller」の3つに分類して設計していくやり方です

Modelモデルがデータベース関係になり、Webとデータベースやり取りを記述する部分になります。

Viewビューが画面表示関係で、それぞれのページで何を表示するかを記述します。

Controllerコントローラーが全体の制御を行う部分で、あまり正面にはでてきません

djangoの初期化処理

djangoでは、作成に当たって最初に初期化処理を動かして、色々なファイルやフォルダーを自動生成します。コマンドプロンプトで作成する場合のコマンドを紹介します。

親フォルダーに決めたフォルダーで(ユーザーフォルダーでもデスクトップでもよい)
django-admin startproject djwebsample
cd djwebsample

うまくできたか実験で
python manage.py runserver
ブラウザで localhost:8000 を開き、djangoのランチャー画面が開けばOK
うまくできたらコマンドプロンプトで コントロールを押しながらC(Ctrl-C)で強制終了させる

同じ場所で続けて
python manage.py startapp bodytemp

続いて、データベースの準備をします。まずはローカルの準備で settings.pyの DATABASES が sqlite3か確認します。

models.py をソースコードまとめのように変更して、マイグレーションをしてデータベースを作成します。

python manage.py makemigrations bodytemp
python manage.py migrate

作成したデータベースの管理者を作成します。

python manage.py createsuperuser
ユーザー名、メールアドレス、パスワードを入力

admin.pyをソースコードまとめのように変更し、

http://localhost:8000/admin
で、ログイン。Btempテーブルが空で表示されるので、+Add でデータの追加の実験をします。今回はユーザーログインがないのでこれだけで結構です。

ディレクトリ・ファイル構造

djwebsample
 db.sqlite3
 manage.py
 Procfile
 README.md
 requirements.txt
 runtime.txt
+djwebsample
| settings.py          #サーバーのprotgreSQL用
| local_settings.py    #ローカルのsqlite3用
| urls.py              #子どものurls.pyへの接続
| __init__.py          #空
| asgi.py              #自動生成
| wsgi.py              #自動生成
+bodytemp
  admin.py             #自動生成
  forms.py             #入力フォームの宣言
  models.py            #データベース宣言
  urls.py              #どの画面とどの関数がつながるかの宣言
  views.py             #どの画面でどの内容を表示、取得するのかの関数
  +migration
  | __init__.py        #空
  | 0001_initial.py    #models.pyから生成されたデータベース宣言
  +static
  | +icon
  | | favicon.ico      #favicon必須ではない
  +templates
    +bodytemo
    | index.html       #マイン画面
    | delete.html      #サブ画面

簡単なサンプルを作る

毎日の体温を保存する

djangoの機能を使って簡単なサンプルを作ってみましょう。

ログインはなしで、日付と体温を入力してSAVEすると、データベースに保存します。同じ日付で入力するとその日の体温を変更します。また、一覧の右にDeleteリンクを作ってあるので、それを押すと削除されます。

データベース設計

データベースはsqllite3で作られるものを利用しますので、プライマリーキーにIDが自動採番で作成されるようにして、後は日付と体温を入れます

項目名フィールド型
id 数値 AutoField
edate 計測した日付 DateField
btemp body temp体温 FloatField

画面設計

測定した体温を日付を指定して入力します。バリデーションなどは作ってありません。yyyy-mm-ddで入力してください。体温は小数点1位まで有効です。

一覧画面

Deleteリンクを押すと削除確認画面を出します。消すかキャンセルするか選んでください

削除画面

settings.py

当初はlocalhostでだけ動くものを考えていましたが、herokuでも動くsようにするために、localとherokuを切り替えるようにします。

herokuに公開する

djangoで作るWebアプリ

データベースはsqlite3じゃだめ

herokuではsqlite3でも動くようですが、どうやらbackupの関係で24時間で元に戻ってしまうようです。なのでheroku-postgresqlの機能を追加して利用します。もちろんHobby Devで無料を目指します

heroku データベース機能追加

作成するとその名前のデータベースが作成されるので、クリックして確認します

heroku 追加された機能のパラメータを見る

settingsを開いて、view creditionalsを見ます

heroku データベースのパラメータを見る

このパラメータをsettings.pyに設定して、データベースに接続します

heroku データベースのパラメータ

herokuにposgreSQLを作る

さきほどの確認ページの一番下に heroku CLIが書かれています。これをheroku loginしたコマンドプロンプトで入力するとpostgresqlのSQL入力画面に接続できます。ただし、pg:psqlはローカルのpostgresqlのエンジンで動きますので、ローカルにpostgresqlをインストールしておいてください。現在は14.1.1でした。

postgresqlに接続する

このようにデータベースに接続してSQLを入力できるようになります。

テーブル名はアプリケーション名_テーブル名で作ります。作成はローカルの時と同じでmigrateします。ローカルの実行時は最初にmakemigrationsしますが、サーバ用はローカルで作成してあるinitialファイルをgitで送り込んでありますから変えない限り必要ありません。

heroku run python manage.py migrate
heroku run python manage.py createsuperuser

もしauth_userがないと言われたら
heroku run python manage.py auth
もしdjango_sessionがないと言われたら
heroku run python manage.py sessions

createsuperuserするとdjango指定のu?????のユーザ名がつくので、メールアドレスとパスワードを入れれば、url+/adminで管理画面にlogin出来るようになります。

ローカルとherokuで両立させる

あとでソースはまとめて表示しますが、sessinsにheroku側でのことを書き、local_sessionにローカルのsqlite3のことを書きます。

このサイトを起動する

これらの設定が終わっても、実は起動が掛かっていません。Webとして起動しないといけません。

現在のステータスを見る
heroku ps

webとして起動する
heroku ps:scall web=1

確認する
heroku ps

web.1: として動いていれば動き始めました。月に550時間までは無料です

ソースコードまとめ

ソースコード一挙公開(heroku用)

色々と紹介してきましたが、ここでまとめて公開します。ただし、そのままでは動きませんので、それぞれ調整してください。

heroku用

Procfile

私はこのファイル名を間違えて1日無駄にしましたので、最初に

web: gunicorn djwebsample.wsgi --log-file -

requirements.txt

使用するライブラリーの名前とバージョンです。自分に合わせてください。psycopg2は2.9以上ではエラーが発生するので2.8にしてください。whitenoiseはstatic対応です。

dj-database-url==0.5.0
Django==3.0.4
django-heroku==0.3.1
dlib==19.22.1
gunicorn==20.1.0
psycopg2==2.8.6
whitenoise==5.3.0

runtime.txt

使用するpythonのバージョンですが、使用できるバージョンに限りがあります。以下のいずれかですが、指定しなくても動きます

  • 3.10.0
  • 3.9.7
  • 3.8.12
  • 3.7.12
  • 3.6.15
python-3.8.12

.gitignore

gitに検知してほしくないファイルです。

__pycache__
db.sqlite3
.DS_Store\local_settings.py

djwebsampleフォルダー用(プロジェクトフォルダー)

settimgs.py

localとherokuの両用です。postgrSQLがありますが、内容はherokuで作成した内容で各自書き換えてください

"""
Django settings for djwebsample project.

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

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

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

import os
import dj_database_url

# 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/3.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!


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

ALLOWED_HOSTS = ['127.0.0.1','.herokuapp.com','localhost']


# Application definition

INSTALLED_APPS = [
	'django.contrib.admin',
	'django.contrib.auth',
	'django.contrib.contenttypes',
	'django.contrib.sessions',
	'django.contrib.messages',
	'django.contrib.staticfiles',
	'bodytemp'
]

MIDDLEWARE = [
	'django.middleware.security.SecurityMiddleware',
	'whitenoise.middleware.WhiteNoiseMiddleware',
	'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 = 'djwebsample.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 = 'djwebsample.wsgi.application'


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

DATABASES = {
	'default': {
		'ENGINE': 'django.db.backends.postgresql_psycopg2',
		'NAME': '<各自書き換えてください>',
		'USER': '<各自書き換えてください>',
		'PASSWORD': '<各自書き換えてください>',
		'HOST': '<各自書き換えてください>',
		'PORT': '',
	}
}




# Password validation
# https://docs.djangoproject.com/en/3.0/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/3.0/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/3.0/howto/static-files/

PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))

STATIC_URL = '/static/'
STATICFILES_DIRS = [(os.path.join(BASE_DIR, "static"))]
STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static')

try:
	from .local_settings import *
except ImportError:
	pass

if not DEBUG:
	SECRET_KEY = os.environ['SECRET_KEY']
	import django_heroku
	django_heroku.settings(locals())

db_from_env = dj_database_url.config(conn_max_age=600, ssl_require=True)
DATABASES['default'].update(db_from_env)

local_settimgs.py

local用の設定です。localでlocalhost:8000でsqlite3で実行することができるようにしています

import os

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

SECRET_KEY = '<各個のシークレットキーを貼り付けてください>'

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

DEBUG = True

urls.py

アクセスのurlをアプリケーションフォルダーにつなぐためのファイル

	from django.contrib import admin
	from django.urls import path, include
	
	urlpatterns = [
		path('admin/', admin.site.urls),
		path('bodytemp/', include('bodytemp.urls')),
		]

bodytempフォルダー用(アプリケーションフォルダー)

admin.py

データベース管理用

from django.contrib import admin
from .models import BTemp

admin.site.register(BTemp)

forms.py

入力画面の部品を宣言します

from django import forms
from django.db.models.fields import FloatField
from django.forms import widgets
from .models import BTemp

class BTForm(forms.ModelForm):
	class Meta:
		model = BTemp
		fields = ['edate', 'btemp']
		widgets = {
			'edate': forms.DateInput(attrs={'class':'form-control','placeholder':'yyyy-mm-dd'}),
			'btemp': forms.NumberInput(attrs={'class':'form-control','placeholder':'36.7(小数点1位まで)'}),
		}

models.py

データベースの型を宣言します

from django.db import models

class BTemp(models.Model):
	edate = models.DateField()
	btemp = models.FloatField(max_length=4)

	def __str__(self):
		return '<BTemp:edate=' + str(self.edate) + '(' + \
			'{:.1f}°C'.format(self.btemp) + ')>'

urls.py

urlの指定を宣言します

from django.urls import path
from . import views

urlpatterns = [
	path('',views.index, name='index'),
	path('<imnt:num>', views.index, name='index'),
	path('delete/<dt>', views.delete, name='delete'),
]

views.py

各画面での処理を宣言します

from django.shortcuts import redirect, render
from django.core.paginator import Paginator
from .models import BTemp
from .forms import BTForm

def index(request, num=1):
	data = BTemp.objects.all().order_by('edate').reverse()
	page = Paginator(data, 5)
	params = {
		'title': 'その日の体温',
		'data': page.get_page(num),
		'form': BTForm(),
		'message': '',
	}
	if (request.method=='POST'):
		#無限に入力されては困るので50件に制限する
		cnt = BTemp.objects.all().count()
		if cnt > 50:
			params = {
				'title': 'その日の体温',
				'data': page.get_page(num),
				'form': BTForm(),
				'message': '50件を超えたため保存しませんでした'
			}
			return render(request, 'bodytemp/index.html', params)
		edate = request.POST['edate']
		btemp = request.POST['btemp']
		#if same date find, overwrite data
		fdate = BTemp.objects.filter(edate=edate)
		if fdate.count() > 0:
			#edit
			obj = BTemp.objects.get(edate=edate)
			btmp = BTForm(request.POST, instance=obj)
		else:
			btmp = BTemp(edate=edate,btemp=btemp)
		btmp.save()
		return redirect(to='/bodytemp')
	return render(request, 'bodytemp/index.html', params)

def delete(request, dt):
	btemp = BTemp.objects.get(edate=dt)
	if (request.method=='POST'):
		btemp.delete()
		return redirect(to='/bodytemp')
	params = {
		'title': 'その日の削除',
		'data': btemp,
	}
	return render(request, 'bodytemp/delete.html', params)
					

bodytemp/migrationフォルダー用

自分でやってください

bodytemp/static/icon/favicon.ico

アイコンがある人は入れてください。自分で作って

bodytemp/templates/bodytemp テンプレートhtmlファイル

index.html

一覧表示用の画面

{% load static %}
<!doctype html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<title>{{title}}</title>
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
	crossorigin="anonymous">
	<link rel="icon" href="{% static '/icon/favicon.ico' %}">
</head>
<body class="container">
	<h1 class="display-5 text-primary">{{title}}</h1>
	<form action="{% url 'index' %}" method="post">
		{% csrf_token /%}
		<div class="form-group">日付{{form.edate}}</div>
		<div class="form-group">体温{{form.btemp}}</div>
		<input type="submit" value="save" class="btn btn-primary">
	</form>
	<table class="table mt-4">
		<tr>
			<th>日付</th>
			<th>体温</th>
		</tr>
	{% for item in data %}
		<tr>
			<td>{{item.edate}}</td>
			<td>{{item.btemp}}°C</td>
			<td><a href="{% url 'delete' item.edate %}">Delete</a></td>
		</tr>
	{% endfor %}
	</table>
	<ul class="pagination justify-content-center">
		{% if data.has_previous %}
		<li class="page-item">
			<a class="page-link" href="{% url 'index' %}">&laquo; first</a>
		</li>
		<li class="page-item">
			<a class="page-link" href="{% url 'index' %}{{data.previous_page_number}}">&laquo; prev</a>
		</li>
		{% else %}
		<li class="page-item">
			<a class="page-link">&laquo; first</a>
		</li>
		<li class="page-item">
			<a class="page-link">&laquo; prev</a>
		</li>
		{% endif %}
		<li class="page-item">
			<a class="page-link">{{data.number}}/{{data.paginator.num_pages}}</a>
		</li>
		{% if data.has_next%}
		<li class="page-item">
			<a class="page-link" href="{% url 'index' %}{{data.next_page_number}}">next &raquo;</a>
		</li>
		<li class="page-item">
			<a class="page-link" href="{% url 'index' %}{{data.paginator.num_pages}}">last &raquo; </a>
		</li>
		{% else %}
		<li class="page-item">
			<a class="page-link">next &raquo;</a>
		</li>
		<li class="page-item">
			<a class="page-link">last &raquo;</a>
		</li>
		{% endif %}		
	</ul>
</body>
</html>

delete.html

削除確認用の画面

{% load static %}
<!doctype html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<title>{{title}}</title>
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
	crossorigin="anonymous">
	<link rel="icon" href="{% static '/icon/favicon.ico' %}">
</head>
<body class="container">
	<h1 class="display-5 text-primary">{{title}}</h1>
	<p>※以下のレコードを削除します。</p>
	<table class="table">
		<tr>
			<th>日付</th>
			<th>体温</th>
		</tr>
		<tr>
			<td>{{data.edate}}</td>
			<td>{{data.btemp}}</td>
		</tr>
	</table>
	<form action="{% url 'delete' data.edate %}" method="post">
		{% csrf_token /%}
		<input type="submit" value="Delete" class="btn btn-danger">
		<a href="/bodytemp" class="btn btn-secondary">Cancel</a>
	</form>
</body>
</html>