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>