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を切り替えるようにします。
djangoで作るWebアプリ
データベースはsqlite3じゃだめ
herokuではsqlite3でも動くようですが、どうやらbackupの関係で24時間で元に戻ってしまうようです。なのでheroku-postgresqlの機能を追加して利用します。もちろんHobby Devで無料を目指します
作成するとその名前のデータベースが作成されるので、クリックして確認します
settingsを開いて、view creditionalsを見ます
このパラメータをsettings.pyに設定して、データベースに接続します
herokuにposgreSQLを作る
さきほどの確認ページの一番下に heroku CLIが書かれています。これをheroku loginしたコマンドプロンプトで入力するとpostgresqlのSQL入力画面に接続できます。ただし、pg:psqlはローカルのpostgresqlのエンジンで動きますので、ローカルにpostgresqlをインストールしておいてください。現在は14.1.1でした。
このようにデータベースに接続して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のバージョンですが、使用できるバージョンに限りがあります。以下のいずれかですが、指定しなくても動きます
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' %}">« first</a>
</li>
<li class="page-item">
<a class="page-link" href="{% url 'index' %}{{data.previous_page_number}}">« prev</a>
</li>
{% else %}
<li class="page-item">
<a class="page-link">« first</a>
</li>
<li class="page-item">
<a class="page-link">« 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 »</a>
</li>
<li class="page-item">
<a class="page-link" href="{% url 'index' %}{{data.paginator.num_pages}}">last » </a>
</li>
{% else %}
<li class="page-item">
<a class="page-link">next »</a>
</li>
<li class="page-item">
<a class="page-link">last »</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>