2020/04/27追記
・開発途中にモジュールを追加したくなったときのpip install
について追加
・docker-compose run
コマンドについて大いに間違った理解をしていたのでそれを修正して、代替としてdocker exec
コマンドを利用した書き方に変更。
はじめに
Pythonの基本をさらったところで次はDjangoに慣れていくために環境を構築して、企画主様の記事にちょうどDjangoの基礎のチュートリアルがあるので
それを行ったときのアウトプットです。
今回の記事を作成するにあたっての企画主様の記事
Django(Python)でシステム開発できるようになる記事_入門編
作る環境とディレクトリ構成
docker-composeでdjango環境を作ってみる(MariaDB + Nginx + uWSGI)
Docker-ComposeでPython3.6+NGINX+MariaDB10+uWSGIのDjango環境欲張りセット
上記2つの記事と環境を参考に以下の環境で構築しました。
環境
・ Windows10 home
・ Docker version 19.03.1(DockerToolbox仕様)
・ VirtualBox 6.0.16
・ python3.8
・ Django 3
・ mariadb 10.1
・ nginx 1.13
ディレクトリ
.
├── db
│ ├── data
│ └── sql
│ └── init.sql
├── docker-compose.yml
├── nginx
│ ├── conf
│ │ └── app_nginx.conf
│ ├── log
│ └── uwsgi_params
├── python
│ ├── Dockerfile
│ └── requirements.txt
├── src
│ └── project
│ ├── app
│ │ └── __init__.py
│ │ └── settings.py
│ │ └── urls.py
│ │ └── uwsgi.log
│ │ └── views.py
│ │ └── wsgi.py
│ │
│──── ──├── migrations
│ │
│ ├── project
│ │ └── __init__.py
│ │ └── settings.py
│ │ └── urls.py
│ │ └── uwsgi.log
│ │ └── views.py
│ │ └── wsgi.py
│ │
│ └── manage.py
│ └── uwsgi.ini
└── static
Dockerfile
FROM python:3.8-alpine
ENV PYTHONUNBUFFERED 1
ENV PYTHONIOENCODEING utf-8
ENV APP_PATH code
# /$APP_PARHでも可能、今回はバグ回避と練習なので記載。
WORKDIR /code
# /codeは$APP_PATHでも可能
COPY ./requirements.txt /$APP_PATH
RUN apk add --no-cache --virtual .build-deps bash gcc musl-dev libffi-dev \
g++ libgcc libstdc++ libxml2-dev libxslt-dev openssl-dev curl \
&& apk add --no-cache --virtual --update-cache\
jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev \
mariadb-dev mariadb-connector-c-dev \
&& pip install cython && pip install -U pip && pip install -r requirements.txt
RUN rm -rf /var/cache/apk/* && \
rm -rf /tmp/*
改めてここでおさらい。
FROMやENVについては呪文のようなものだとして初出のところをさらっていく。
まず、alpineという表記。
これはAlpine LinuxというLinuxの超軽量版のようなものみたいです。
そしてDockerはLinuxが大本みたいなものなのでこれをベースにDockerにおけるイメージも軽量化しようというのが-alpine表記……というような感覚でひとまずおいておきます。。
なお-slim表記というのもあり、-alpineだとNumpyなど一部のパッケージをDockerでビルドするときにひと悶着あったりするようです。
これは、そういったパッケージにC系の処理が組み込まれていたりするのですが、alpine表記のイメージを
とりあえず、ここではDockerにおけるーalpine表記はイメージを軽量化するものという理解に留めておきます。
次に、WORKDIRとCOPYですね、WORKDIRは仮想環境での作業ディレクトリの指定です。
後ほど、docker-compose.ymlで設定しますが、今回はホスト側のsrcフォルダと仮想環境のcodeフォルダを共有設定にするので、つまりアプリのプロジェクトフォルダはこの中に展開されていくことになります。
COPYはパスに存在するファイルを仮想環境の$APP_PATHで指定したフォルダにコピーするという命令です。
ホスト側のパスの指定はDockerfileがあるディレクトリが基準なのでつまり、Dockerfileから見た親フォルダから1個下がった場所を指定するので上記のとおりになります。
あとはRUNで各種パッケージをインストールします、主にalpineを使うことで起きるC系の処理でのエラーを回避するためのもの(gcc~libxslt-devくらいまで)とMariaDBへ接続するコネクタなどなど。
最後にpipでcython(C言語のノウハウを使ってPythonを高速化したもの)を入れて、pipのアップグレードを行い、requirements.txtに記述したパッケージをインストールするということになります。
requirements.txt
Django==3.0
django-bootstrap4
flake8==3.7.9
ipython==7.10.1
mysqlclient==1.4.6
Pillow==6.2.1
uwSGI==2.0.18
dj-static
whitenoise
お試しとして入れてあります、私のような勉強し始めのレベルで使うにはDjango、django-bootstrap4、mysqlclientくらいあれば十分ですね。
uwSGIについては参考元で実装されているのでせっかくなので挑戦してみます。
django-bootstrap4について
Bootstrapの登録・修正係のフォームをPythonで簡略に書くためのモジュール。
詳しい使い方を説明するにはDjangoにおけるViewの仕組みなどをもう少し理解する必要があるので、Djangoでbootstrap4を使うなら入れておくという理解で一先ず先に進みましょう。
mysqlclient
PythonでMySQLに接続するために必要なモジュール
uWSGIについて
簡単に言うとAPサーバー、つまりアプリケーションサーバーのことである。
ここまで、自分もWebサーバーとアプリケーションサーバーについてとりあえず用意しなきゃいけないくらいのふわっとした理解で学習を進めてきましたが、
今回uWSGIという技術に振れる機会を得たので、以下の参考記事を元に少し掘り下げてみました。
Rails開発におけるwebサーバーとアプリケーションサーバーの違い(翻訳)
uWSGI入門
「uWSGI」とアプリケーションサーバーについて
Webサーバとアプリケーションサーバの違いをわかりやすく解説します
APサーバの役割とは?Webサーバとの違いをしっかり理解しよう
まとめていくと
・ WSGIとは
→「Web Server Gateway Interface」の略称でPythonのWebアプリケーションとWebサーバーとのやり取りの規則……つまりプロトコルのこと。
プロトコルがなければ当然通信はできないわけですが、どうやらフレームワークのWebサーバーに対しての対応・非対応がまちまちになっているというのが問題になっている中、
Pythonにおいてはこうして共通のプロトコルを作成することでWSGIに対応しているWebサーバーであればどのフレームワークでも運用ができるようにするというのがWSGI導入の背景みたいです。
・ じゃあuWSGIとは?
→WSGIに対応したアプリケーションサーバーの一種ということになります。
HTTP通信とUNIXドメインソケットという通信方式を使うことができますが、このあたりは今深堀りしても仕方ないので置いておきます。
・ そもそもWebサーバーとアプリケーションサーバーってなんですか?
→ Webサーバーはユーザーからのリクエストを受け取ったあとなんらかの処理を行うためのもの。
つまり、ローカル環境でもない限り、送信されたリクエストは基本的にはまずWebサーバーへいきつきそこで処遇をどうするか決まるというイメージ。
主に動的なリクエスト(CSS・Javascript・画像・動画など頻繁に変化することが考えうるものへのリクエスト)以外はここで処理をすることができる。
逆にいうとそれらのリクエストが来た場合、アプリケーションサーバーにそのリクエストをパスするという役目を持っているとも言える。
もちろんCSSや画像においてはそれらが静的だった場合はこちらで処理をすればいい。
では、アプリケーションサーバーとはなにかというとこれは端的に言ってアプリケーションを動かしているものということになる。
わかりやすいところではリクエストやレスポンスの受け渡しになる。
ログイン処理を例にすると
- ログインしたいというリクエストがWebサーバーに飛ぶ。
- ログインというリクエストは動的なリクエスト(リクエストしたユーザーによって情報のIDやパスワードが違う)なのでアプリケーションサーバーにパスされる。
- Webサーバーからパス(リクエスト)が来たのでアプリケーションサーバーはアプリケーションに処理を依頼する
- アプリケーションが処理を実行、必要に応じてDBにリクエストしたりして動的なコンテンツを生成する、この場合はログイン情報を取得してログインを実行する。
- 4をレスポンスとしてWebサーバーに返す
- Webサーバーに実行結果が渡されて処理、ログインが完了する
ということになる。
ログイン1つとっても実は割と複雑な処理なんだということを感じた次第です。
docker-compose.yml
version: '3.7'
volumes:
django.db.volume:
name: django.db.volume
services:
nginx:
image: nginx:1.13
container_name: app_nginx
ports:
- "8000:8000"
volumes:
- ./nginx/conf:/etc/nginx/conf.d
- ./nginx/uwsgi_params:/etc/nginx/uwsgi_params
- ./static:/static
- ./nginx/log:/var/log/nginx
depends_on:
- python
db:
tty: true
image: mariadb:10.1
container_name: app_mariadb
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: root
TZ: 'Asia/Tokyo'
volumes:
- django.db.volume:/var/lib/mariadb
- ./db/sql:/docker-entrypoint-initdb.d
python:
build: ./python
image: python_blog
container_name: app_python
command: uwsgi --ini /code/project/uwsgi.ini
environment:
- DATABASE_HOST=db
volumes:
- ./src:/code
- ./static:/static
- ./python:/code/python
expose:
- "8888"
depends_on:
- db
おさらいとしてはvolumesにおけるパス指定はホスト側:仮想環境側
であるということとtty:true
がそのコンテナをupし続けるようにする記述であることくらいです。
今回、沼にハマったところとしてはWindowsでDockerを扱っているからなのか、元々Dockerのコンテナというシステムの都合なのかDBへの接続エラーを解消するのにかなり難儀したところがありました。
調べた結果割りと、どちらとも理由として上がっていたのですが後者に関しては私が変更反映・及びDockerの終了をdownで行っていたからというのが理由な気がします。
ともあれ、本来ならDBのvolumesのところはホスト側にDBの情報をおくディレクトリを用意してそれを共有させることでDockerにおけるDBのデータの保持をするところを今回は、
まず冒頭でvolumeマウントを行うために、mariadbコンテナの永続化のためだけに作るということで解決した。
ちなみにWindows側のディレクトリにdjango.db.volumeなんて存在しないけど?となったのですが、Docker Toolboxの場合はここが非常にややこしくてつまり
Windows⇒Virtual Box⇒Docker == 大本のホスト⇒Dockerから見たホスト(Windowsから見るとゲスト)⇒ゲスト
という関係になっていて、今回作ったVolumeはVirtual BoxにおいてあるということになるのでWindows上では表示されないということになるのでしょう、ややこしい。
ともあれ、ここで大事なのはDockerはコンテナを生成・削除を気軽にやれることが利点の1つであるが、DBなどのデータは永続化していないと接続エラーを引き起こしてしまうので、必ず永続化の手段を取りましょうというですね。
ちなみにdokcer-compose.ymlを使っている場合はおそらくDockerfileではなく、こちらをおもに弄って各種変更や更新を行うはずですがその場合は、buildは必要なくupで反映できるということをこれで覚えました。
Dockerfileの場合はbuildが必要なのでDB永続化していないとおそらくエラーが出ます(体験済み)
nginxについてはあとはconfファイルに設定を書き込んでuWSGI用にパラメータファイルを用意しておく。
upstream django {
ip_hash;
server python:8888;
}
# configuration of the server(サーバーの構成)
server {
# the port your site will be served on
listen 8000;
# the domain name it will serve for
server_name 127.0.0.1; # substitute your machine`s IP address or FQDN
charset utf-8;
# max upload size
client_max_body_size 75M;
location /static {
alias /static;
}
# Finally, send all non-media requests to the Django server.
location / {
uwsgi_pass django;
include /etc/nginx/uwsgi_params; # the uwsgi_params file you installed
}
}
アクセスはまず8000ポートで受け止める。
次に静的ファイルへのリクエストだった場合はnginxのstaticへパスをする。
それ以外のアプリケーションサーバー(uWSGI)へリクエストをパスする必要がある場合は8888ポートにリクエストを飛ばす。
最後にuWSGIのパラメータファイルがあるディレクトリを指定しておくことを忘れない。
uwsgi_param QUERY_STRING $query_string;
uwsgi_param REQUEST_METHOD $request_method;
uwsgi_param CONTENT_TYPE $content_type;
uwsgi_param CONTENT_LENGTH $content_length;
uwsgi_param REQUEST_URI $request_uri;
uwsgi_param PATH_INFO $document_uri;
uwsgi_param DOCUMENT_ROOT $document_root;
uwsgi_param SERVER_PROTOCOL $server_protocol;
uwsgi_param REQUEST_SCHEME $scheme;
uwsgi_param HTTPS $https if_not_empty;
uwsgi_param REMOTE_ADDR $remote_addr;
uwsgi_param REMOTE_PORT $remote_port;
uwsgi_param SERVER_PORT $server_port;
uwsgi_param SERVER_NAME $server_name;
いわゆる呪文です。
####2020/04/27追記
開発中に追加のパッケージをインストールしたいとなった場合、直接コンテナの中に入ってpython -m pip install
をしてもいいですが、せっかくrequirements.txt
を作ったのだからこれを活用したいですよね。
ただし、このままでは例えばpython -m pip install requirements.txt
としてもエラーが出てしまいます。
DockerFileでCOPY
とADD
したのでは? と疑問を持ったのでとりあえず確認してみることにしました。
# docker exec -it app_python bashからのfind ./コマンドの実行結果
./
./project
./project/app
./project/app/admin.py
./project/app/apps.py
./project/app/migrations
./project/app/migrations/0001_initial.py
./project/app/migrations/0002_sampledb.py
./project/app/migrations/__init__.py
./project/app/migrations/__pycache__
./project/app/migrations/__pycache__/0001_initial.cpython-38.pyc
./project/app/migrations/__pycache__/0002_sampledb.cpython-38.pyc
./project/app/migrations/__pycache__/__init__.cpython-38.pyc
./project/app/models.py
./project/app/tests.py
./project/app/urls.py
./project/app/views.py
./project/app/__init__.py
./project/app/__pycache__
./project/app/__pycache__/admin.cpython-38.pyc
./project/app/__pycache__/apps.cpython-38.pyc
./project/app/__pycache__/models.cpython-38.pyc
./project/app/__pycache__/urls.cpython-38.pyc
./project/app/__pycache__/views.cpython-38.pyc
./project/app/__pycache__/__init__.cpython-38.pyc
./project/manage.py
./project/media
./project/polls
./project/polls/admin.py
./project/polls/apps.py
./project/polls/migrations
./project/polls/migrations/0001_initial.py
./project/polls/migrations/0002_auto_20200408_1848.py
./project/polls/migrations/__init__.py
./project/polls/migrations/__pycache__
./project/polls/migrations/__pycache__/0001_initial.cpython-38.pyc
./project/polls/migrations/__pycache__/0002_auto_20200408_1848.cpython-38.pyc
./project/polls/migrations/__pycache__/__init__.cpython-38.pyc
./project/polls/models.py
./project/polls/tests.py
./project/polls/urls.py
./project/polls/views.py
./project/polls/__init__.py
./project/polls/__pycache__
./project/polls/__pycache__/admin.cpython-38.pyc
./project/polls/__pycache__/apps.cpython-38.pyc
./project/polls/__pycache__/models.cpython-38.pyc
./project/polls/__pycache__/tests.cpython-38.pyc
./project/polls/__pycache__/urls.cpython-38.pyc
./project/polls/__pycache__/views.cpython-38.pyc
./project/polls/__pycache__/__init__.cpython-38.pyc
./project/project
./project/project/settings.py
./project/project/urls.py
./project/project/uwsgi.log
./project/project/views.py
./project/project/wsgi.py
./project/project/__init__.py
./project/project/__pycache__
./project/project/__pycache__/settings.cpython-38.pyc
./project/project/__pycache__/urls.cpython-38.pyc
./project/project/__pycache__/views.cpython-38.pyc
./project/project/__pycache__/wsgi.cpython-38.pyc
./project/project/__pycache__/__init__.cpython-38.pyc
./project/templates
./project/templates/admin
./project/templates/admin/base_site.html
./project/templates/admin/index.html
./project/templates/app_folder
./project/templates/app_folder/page01.html
./project/templates/app_folder/page02.html
./project/templates/app_folder/top_page.html
./project/templates/base.html
./project/templates/polls
./project/templates/polls/detail.html
./project/templates/polls/index.html
./project/templates/polls/results.html
./project/uwsgi.ini
find ./
コマンドはLinuxコマンドでこの場合の意味はルートフォルダ以下のディレクトリを検索して表示する
ということになります。
この記事以降のDjangoチュートリアルの分のファイルもいくつか混ざっていますが、よく確認してもrequirements.txt
がないのがわかります。
DockerFileではCOPY ./requirements.txt /code
していてそのイメージをもとに作られたコンテナなのに一体なぜなのか調べてもわからないです。
ただ、1つ心当たりがあるのは私がdocker-compose run
コマンドの理解が甘いまま多様しすぎていたせいでコンテナが無限にできてしまっていた……というところなのですが、
どなたかご存知の方がいたら教えて下さい。
さて、とりあえずの解決策としてはコンテナにrequirements.txt
をマウントするということです。
なので、./python:/code/python
の記述をdocker-compose.yml
に追加しています。
これであとはup
をしてdocker exec
コマンドで入って中でpip install -r ./python/requirements.txt
とできるようになるので追加のパッケージインストールにも対応できます。
コンテナの立ち上げ
ここまで準備が終わったらdocker-compose up -d --build
でコンテナを生成してみます。
おさらいとしては-dオプションはバックグラウンドでコンテナを起動し続けるものですね。
……とやってもいいのですがこのまま立ち上げると実はうまく立ち上がらないです。
理由はuWSGIの準備がまだできていないためです。
docker-comopose.yml
をもう一度見てみると、nginxとpythonコンテナにdepend_onの指定があるのがわかります。
これは、コンテナの依存関係を示すものであるということを以前に学習しました。
つまり、dbコンテナが立ち上がらないとnginx及びpythonコンテナを立ち上げることはできないのです。
ですが、肝心のdbサーバーはアプリケーションサーバーがないと動かないのでuWSGIの用意ができていない今はコンテナが立ち上がらない……こういうことになります。
なので、まず先にDjangoのプロジェクトを作りuwsgiの設定ファイルを作ります。
docker-compose run --rm python django-admin.py startproject project
runコマンドは新しくコンテナを生成して指定したコマンドを実行します。
なので無闇に多用するとコンテナが無限にできることになります。
なので、必ず特別な意図がなければ--rm
コマンドを使いましょう。
ちなみに、起動中のコンテナでコマンド実行をするdocker-compose exec ${service_name} ${command}
というコマンドもあります。
こちらは例えばdocker-compose exec python bash
とするとpythonサービスのコンテナに入り、以後そこでbashコマンドを実行できるようにするといったような使い方ができます。
さて、runのあとはdocker-compose.yml
設定したサービス名とDjangoプロジェクトを生成する呪文を指定します。
そうするとstartproject
のあとに指定した名前のフォルダがWORKDIRで指定した領域に作成されます。
そして最後にコンテナは--rm
コマンドで削除されます。
ですが、django-admin.py startproject project
の結果はホスト側に残るのでこのあとのコンテナの立ち上げでそれを反映させられるので問題はありません。
[uwsgi]
socket = :8888
module = project.wsgi
wsgi-file = /code/project/project/wsgi.py
logto = /code/project/project/uwsgi.log
chdir=/code/project
py-autoreload = 1
あとは以上のように設定ファイルを作り、それを作ったプロジェクトファイルのmanage.py
と同じディレクトリに配置すればOKです。
書いてあることはuWSGIで使うポートやlogファイル、wsgiファイルの場所の指定などです。
余談だがDjangoのプロジェクトは常にmanage.py
を基準にディレクトリを指定したりコマンドを実行することを覚えておきましょう。
ここまで終わったら
$ docker-compose up -d
$ docker ps -a
と呪文を唱えます。
最後の呪文でコンテナの一覧を表示できるので3つコンテナが立ち上がっていて、全てupになっていることを確認します。
最初の方でも触れたが、dockerでMySQLを使うときはここでupにならずすぐにexitになってしまうということがままあります。
今回は一応ここまででやれる対策はしておいたので問題ないと思うのですが、個人的にはここで呪文を唱える前にまず以下のテーブルの準備をしておいたほうがいいと思いました。
データベース・テーブルの準備
CREATE DATABASE IF NOT EXISTS 任意のデータベース名 CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER IF NOT EXISTS '任意のユーザー名'@'%' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON 上で指定したデータベース名.* TO '上で指定したデータベース名'@'%';
FLUSH PRIVILEGES; # 忘れない
おなじみSQL文体の呪文です。
MySQLは必ずデータベースとユーザーを作り、作ったユーザーにそのデータベースでの全権を与える(テーブル単位で与えることもできる)ということを先に行っておかないと機嫌が悪くなるものなので先にこれを設定しておきます。
ちなみに2行目のユーザー名及びパスワードはdocker-compose.yml
で設定することもできます。
今回はMYSQL_ROOT_PASSWORD: root
という部分でMySQLのルートパスワードを設定しています。
今回はデータベース全てに全権を与えるので、ルートユーザーを設定します。
ちなみにユーザー名もパスワードもrootで設定していますが、もちろんこれはローカルだから許されることで、本番環境の場合はこのままだとセキュリティ的に不味すぎるので、ちゃんとしたものを設定しましょう。
ここまで書けばわかると思いますが、要はこれはSQLの初期設定ファイルということになりますね。
ここまで用意できたら次はDjangoの設定ファイルを覗いてみます。
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'django_test',
'USER': 'root',
'PASSWORD': 'root',
'HOST': 'db',
'PORT': '3306',
'OPTIONS': {
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
'charset': 'utf8mb4',
},
}
}
# 中略
STATIC_ROOT = '/static'
STATIC_URL = '/static/'
書いてあることはだいたい分かると思います。
DjangoのバージョンによってはENGINEの部分が違うことがあるようですがその場合は適宜適切なものに書き換えてください。
あとはデータベースの名前やrootユーザーに関する項目を設定していきます。
HOSTの部分はdocker-compose.yml
で指定したデータベースが入るコンテナのサービス名を、PORTには同じくそこで指定したポートを設定します。
OPTIONについては呪文です。
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"
ここについてはこれを指定することが推奨されているので設定しておきましょう。
MySQLの多くのデータ整合性の問題を解決するというモードみたいです。
ちなみに設定しないとエラーを吐かれました。
最後の部分は静的ファイルのディレクトリ設定になります、指定しないと静的ファイルが読み込まれません。
ここまで終わったら
docker exec -it app_python bash
でコンテナの中に入り
cd ./project/
python manage.py makemigrations
python manage.py migrate
python manage.py runserver
と呪文を唱えましょう。
ここでエラーを吐くことなく、無事にマイグレーションが完了したらデータベースアクセスは無事に成功したということになるので先程のup -d でコンテナを生成しましょう。
settings.pyの設定
ここまで終わったらあとはDjangoの設定をしていきます。
その前にまずプロジェクトの中にアプリケーションフォルダを作ります
docker exec -it app_python bash
でコンテナの中に入り
cd ./project/
python manage.py startapp フォルダ名
実行したあとは以下のようにsettings.py
を設定していきます。
"""
Django settings for project project.
Generated by 'django-admin startproject' using Django 2.1.5.
For more information on this file, see
https://docs.djangoproject.com/en/2.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.1/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/2.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '&$l0ho%f&-w%&t_2l=@98u(i_58t15d-jn+ln5fuyvy(^l88^t'
# 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',
'app.apps.AppConfig', # 追加
'bootstrap4' # 追加
]
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',
'whitenoise.middleware.WhiteNoiseMiddleware', # 追加
]
ROOT_URLCONF = 'project.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')], # htmlテンプレートが入ったフォルダのディレクトリを指定
'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',
],
'builtins':[
'bootstrap4.templatetags.bootstrap4',# ここに追加!
],
},
},
]
WSGI_APPLICATION = 'project.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'django_test',
'USER': 'root',
'PASSWORD': 'root',
'HOST': 'db',
'PORT': '3306',
'OPTIONS': {
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
'charset': 'utf8mb4',
},
}
}
# Password validation
# https://docs.djangoproject.com/en/2.1/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/2.1/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/2.1/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = '/static'
# Custom User
AUTH_USER_MODEL = 'app.User'
# メディアファイルに関する設定
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR,'media')
抜粋していくと
'app.apps.AppConfig', # 追加
'bootstrap4' # 追加
from django.apps import AppConfig
class AppConfig(AppConfig):
name = 'app'
この部分は上がapps.py
の場所の指定になります。
見るとわかりますがapps.py
でdjango.apps
モジュールのAppConfig
オブジェクトをインポートするということになる。
クラスを下に作ったところでname = 'アプリケーションフォルダ名'
の設定を忘れないようにしましょう。
ちなみに'app.apps.AppConfig'
の指定は必ずアプリケーションフォルダを作ってから追加しないとエラーを吐きます(経験談)。
下の部分はDjangoでbootstrapを使うための設定ですのでこれも追加します。
'builtins':[
'bootstrap4.templatetags.bootstrap4',# ここに追加!
],
これもbootstrapを使うための指定なので忘れずに追加。
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
タイムゾーンとDjnago設定言語の指定です。
# Custom User
AUTH_USER_MODEL = 'app.User'
このあと作るカスタムユーザーのための設定です。
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR,'media')
メディアファイルのアクセス設定になります。
アプリにアップロード機能やアップロードされたファイルをコンテンツとして画面に表示する機能などを実装する際に使います。
これで一応Djangoを利用する基本的な準備は完了しました。
テンプレートの作成
ここからはテンプレートの作成をします。
DjangoはフレームワークなのでLaravelのようにMVCモデルで構成されています。
これをDjangoではMVTモデルと呼ぶそうです。
つまり、Model・Tenmplaete・Viewモデル
ですね。
役割としては
Model : データベースの制御
Template : 画面を制御。MVCモデルにおけるViewの役割
View : 全体のコントロールの役割。MVCモデルにおけるcontrollerの役割
となります。MVCモデルにおけるViewとMTVモデルにおけるViewは役割が違うことに注意してください。
よってDjangoでもテンプレートが使えるのでそれを設定していきます。
<!DOCTYPE html>
{% load bootstrap4 %}
{% bootstrap_css %}
{% bootstrap_javascript jquery='slim' %}
{% load static %}
<html lang="ja">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type='text/css' href="{% static 'css/style.css' %}">
<link rel="stylesheet" href="https://cccabinet.jpn.org/bootstrap4/css/style.css">
<title>Django開発サンプル</title>
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- navbar -->
<div class="container">
<div class="row">
<div class="col-12">
<nav class="navbar navbar-expand-lg fixed-top navbar-dark bs-navbar" style="background-color:cadetblue;" id='bs-navbar'>
<a class="navbar-brand mr-md-2" href="/app/top_page">
サンプル開発画面
</a>
<!-- layout_dammy -->
<ul class="navbar-nav mr-auto">
<li class="nav-item"><a class='nav-link' href="#"></a></li>
<li class="nav-item"><a class='nav-link' href="#"></a></li>
</ul>
<!-- layout -->
<ul class="navbar-nav">
<li class="nav-item"><a class="nav-link" href="/#">サンプル1</a></li>
<li class="nav-item"><a class="nav-link" href="/#">サンプル2</a></li>
<li class="nav-item"><a class="nav-link" href="/#">サンプル3</a></li>
<li class="nav-item"><a class="nav-link" href="/accounts/login">ログイン</a></li>
<li class="nav-item"><a class="nav-link" href="/accounts/logout">ログアウト</a></li>
</ul>
</nav>
</div><!-- /.col-12 -->
</div><!-- /.row -->
</div>
<!-- sidebar&main-contents -->
<div class="container-fluid" id="content">
<div class="row flex-xl-nowrap">
<!-- sidebar -->
<div id="sidemenu" class="col-2 d-none d-md-block bg-light sidebar align-self-start">
<div class="sidebar-sticky">
<ul class='nav flex-column'>
<li class="nav-item">
<nav class="navbar navbar-light bg-light">
<a class="navbar-brand" href="/#">
<span data-feather=""></span> TOPページ
</a>
</nav>
</li>
<li class="nav-item">
<nav class="navbar navbar-light bg-light">
<a class="navbar-brand" href="/#">
<span data-feather=""></span> サイドバー1
</a>
</nav>
</li>
<li class="nav-item">
<nav class="navbar navbar-light bg-light">
<a class="navbar-brand" href="/#">
<span data-feather=""></span> サイドバー2
</a>
</nav>
</li>
<li class="nav-item">
<nav class="navbar navbar-light bg-light">
<a class="navbar-brand" href="/#">
<span data-feather=""></span> サイドバー3
</a>
</nav>
</li>
<li class="nav-item">
<nav class="navbar navbar-light bg-light">
<a class="navbar-brand" href="/#">
<span data-feather=""></span> サイドバー4
</a>
</nav>
</li>
<li class="nav-item">
<nav class="navbar navbar-light bg-light">
<a class="navbar-brand" href="/#">
<span data-feather=""></span> サイドバー5
</a>
</nav>
</li>
</ul>
</div><!-- /.sidebar-sticky -->
</div><!-- /#sidemenu -->
<div class="col-10">
{% block contents %}
{% endblock %}
</div><!-- /.col-10 -->
</div><!-- /.row -->
</div><!-- /#content -->
</body>
</html>
<!-- sidebar-icon -->
<!-- use-icon: https://voyager-jp.com/blog/javascript/feather/ -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/feather-icons/4.9.0/feather.min.js"></script>
<script>feather.replace()</script>
まずは各ページ共通の部分のテンプレートを作成します。
ヘッダーとかナビゲーションバーとかですね。
{% %}
みたいな表記はテンプレートで使う呪文です。
いくつか抜粋すると
{% load bootstrap4 %}
{% bootstrap_css %}
{% bootstrap_javascript jquery='slim' %}
{% load static %}
bootstrapモジュールの読み込み及びjQueryの読み込みと静的ファイルの読み込みをします。
指定をしないとbootstrapが適用されないので忘れないように。
このようにコマンドみたいな呪文もあれば
<link rel="stylesheet" type='text/css' href="{% static 'css/style.css'%}">
といったようにパスの指定を簡略化したりするような使い方もできます。
ちなみに
{% block %}
{% endblock %}
というのは領域の指定です。
例えば
{% block extra_css %}{% endblock %}
というのはblock
からendblock
までをextra_css
という名前のblock
にしますよという意味になります。
このように指定したblock
はデフォルト値となり、以下子テンプレートに継承された際に
{% block %}~{% endblock %}
などのようにblock
の名前が指定されていない場合や{% block extra_css %}{% endblock %}
と指定された場合にその領域が挿入されるということになる。
よって
{% block contents %}
{% endblock %}
上記のような呪文を唱えると、子テンプレートで同じ呪文を唱えた部分が挿入されるということがわかる。
ちなみに今回はCDNでBootstrapを読み込んでいるがCDNには色々問題がつきものなのでできればパッケージをダウンロードしてローカルにおいたものを読み込みたい。
あとは子テンプレートを作っていく
{% extends 'base.html' %}
{% block contents %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center mt-5 border-bottom">
<h1 class="h2">トップページ</h1>
</div>
<div>
<h4>ここにコンテンツを記述する</h4>
</div>
{% endblock %}
{% extends 'base.html' %}
{% block contents %}
<h4 class="mt-5 mb-4 border-bottom">検索条件を入力</h4>
<form action="{% url 'app:top_page' %}" method="post">
<div class="container-fluid">
<div class="row">
<div class="col-2">
<input class="form-control" type="next" name="input_data" maxlength="255">
</div><!-- /.col-2 -->
<div class="col-2">
<div class="text-left align-bottom">
<button type="submit" name="button" class="btn btn-info">
検索
</button>
</div><!-- /.text-left -->
</div><!-- /.col-2 -->
</div><!-- /.row -->
</div><!-- /.container-fluid -->
{% csrf_token %}
</form>
{% endblock %}
{% extends 'base.html' %}
{% block contents %}
<h4 class="mt-5 mb-4 border-bottom">DB検索結果を表示</h4>
<div class="container-fluid">
<div class="row">
<div class="col-4">
<p>sample1:{{result_sample1}}</p>
<p>sample2:{{result_sample2}}</p>
</div>
</div>
</div>
{% endblock %}
{% extends 'base.html' %}
の呪文で先程の親テンプレートが継承されます。
page01とpage02の内容としては
・ 画面の入力フォームから検索条件を受け取る
・ 検索条件をもとに、DBからデータを取得
・ 取得したデータをテンプレート(画面)に渡す
という処理を管理画面で行うためのUIを作ったということになりますね。
views.pyとmodels.pyの設定
templateが終わったので今度はViewとついでにModelも設定します。
参考記事に挙げている企画者さんの記事から引用すると
app_config/views.py:全体を大まかにコントロールする
app_folder/views.py:個々の機能をコントロールする
と同じviews.py
でも役割が違うことに注意しましょう、どっちがどっちかややこしいですね。
ちなみに今回は
app_config/views.py
はproject/project/views.py
(settings.pyと同じディレクトリにあります)。
app_folder/views.py
はproject/app/views.py
と置き換えてください。
from django.shortcuts import render, redirect, reverse
from django.views import View
class index(View):
def get(self, request, *args, **kwargs):
return redirect(reverse('app:top_page'))
index = index.as_view()
システムにアクセスがあった場合はトップページに飛ばすという設定になります。
from django.shortcuts import render
from django.views import View
from .models import SampleDB
class SampleView(View):
def get(self, request, *args, **kwargs):
return render(request, 'app_folder/page01.html')
def post(self, request, *args, **kwargs):
input_data = request.POST['input_data']
result = SampleDB.objects.filter(sample1=input_data)
result_sample1 = result[0].sample1
result_sample2 = result[0].sample2
context={'result_sample1':result_sample1, 'result_sample2':result_sample2}
return render(request, 'app_folder/page02.html', context=context,)
top_page = SampleView.as_view()
タイプミスに気をつけましょう。
冒頭のfromやimportは呪文なので間違えないように。
最後の部分はModelからテーブルのオブジェクトをインポートするというものです。
def get(self, request, *args, **kwargs):
return render(request, 'app_folder/page01.html')
renderメソットはHttpResponseオブジェクトを返すメソッドになります。
引数にリクエストを設定して、第2引数に結果を返したい画面のパスを指定することでsession周りの情報を含めた結果をそこに返すことになります。
def post(self, request, *args, **kwargs):
input_data = request.POST['input_data']
result = SampleDB.objects.filter(sample1=input_data)
result_sample1 = result[0].sample1
result_sample2 = result[0].sample2
context={'result_sample1':result_sample1, 'result_sample2':result_sample2}
return render(request, 'app_folder/page02.html', context=context,)
こちらは入力フォームからPOSTされたリクエストを受け取るためのメソッドです。
コンテキスト(context)変数ではテンプレートに渡される「変数名」から「変数の値」へのマッピングし、それをrenderメソッドで変数に渡しているという処理になります。
それをapp_folder/page02.html
に返すということになりますね。
ちょうどいいのでここでModelも設定しましょう。
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
pass
class SampleDB(models.Model):
class Meta:
db_table = 'sample_table' # tablename
verbose_name_plural = 'sample_table' # Admintablename
sample1 = models.IntegerField('sample1', null=True, blank=True) # 数値を格納する変数及び設定
sample2 = models.CharField('sample2', max_length=255, null=True, blank=True) # 文字列を格納する変数及び設定
各クラスが各テーブルということになります。
つまり、こう書くとUserテーブルとSampleDBが作成されるということになりますね。
今回作ったテンプレートで使うテーブルはSampleDBなのでクラスを作り、引数にmodels.Model
を設定し、class Meta以下にテーブルの設定を書いていきます。
db_table = 'sample_table' # tablename
verbose_name_plural = 'sample_table' # Admintablename
この部分はテーブルの名前と管理画面で表示されるテーブルの名前の設定です。
sample1 = models.IntegerField('sample1', null=True, blank=True) # 数値を格納する変数及び設定
sample2 = models.CharField('sample2', max_length=255, null=True, blank=True) # 文字列を格納する変数及び設定
このように書くことでテーブルのデータの設定をすることになります。
つまりこのテーブルにはIDと文字列のデータが格納されることになります。
URLディスパッチャの設定
Web画面からシステムにアクセスする際に必要なURLの設定をします。
Laravelにおけるルーティングの設定ということになるでしょうか。
views.pyと同じように
app_config/urls.py:システム全体のURL設計
app_folder/urls.py:個々のアプリ内のURL設計
と同じファイル名で違う役割なので注意しましょう。
ちなみに今回は
app_config/views.py
はproject/project/urls.py
(settings.pyと同じディレクトリにあります)。
app_folder/views.py
はproject/app/urls.py
と置き換えてください。
from django.urls import path
from . import views
# URL_setting
app_name = 'app'
urlpatterns = [
path('top_page/', views.top_page, name='top_page')
]
urlpatterns
の部分に各ページのパスを書いていきます。
今回はTOPページのみ(page01及びpage02は管理画面のテンプレートなので除外)の作成なので一つのみということになります。
"""project URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from . import views
urlpatterns = [
# 管理サイトにアクセスするURL
path('admin/', admin.site.urls),
# 今回作成するアプリ「app」にアクセスするURL
path('app/', include('app.urls')),
# 何もURLを指定しない場合(app_config/views.pyで、自動的に「app_folder」にアクセスするよう設定済み)
path('', views.index, name='index'),
]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
管理ユーザー及び管理画面の作成
最後に管理画面を作ります。
そのためには管理ユーザーを作らないといけないので
docker exec -it app_python bash
でコンテナの中に入り
cd ./project/
python manage.py createsuperuser
と呪文を唱えます。
つつがなくいくとユーザー名やらメールアドレスやらパスワードを何に設定しますか?と聞かれるので任意に設定していってください。
次に管理サイトの設定です。
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User
from.models import SampleDB
admin.site.register(User, UserAdmin)
admin.site.register(SampleDB)
管理サイトは
本来であればDBを操作するにはMySQLなど覚える必要がある
Python(Django)でも精一杯なのにMySQLなんて無理。。
そんなあなた、管理サイトから画面上でDBを操作しましょう!
というものなので使うテーブルに応じて上記のように設定しておきます。
システム起動
では、システムを起動してみます。
その前にテーブルのマイグレーションと管理サイトにおけるcssの適用を行っておきます。
マイグレーションについては随分前に一回マしましたが、新たにSampleDBテーブルを定義したので再度行います。
呪文はこれです。
docker exec -it app_python bash
でコンテナの中に入り
cd ./project/
python manage.py migrate
次にcssの適用です。
これはuWSGIを使った場合に行うことになります。
呪文は以下の通り
python manage.py collectstatic
無事終わったら最後に
python manage.py runserver
でシステム起動です。
エラーを吐かれなければ無事起動が完了です。お疲れ様でした。
うまく行かない場合はdocker-compose up -d
し直したり適宜エラーメッセージを見て対処しましょう。
最後に
Laravelをやった経験が生きてこのくらいだと仕組みはわかりますね。
ただ、やはりテンプレートの組み込みのメソッドとかそもそも例えばModel部分の呪文の書き方のようなところはこれから使っていって、その都度調べてどんどんインプットとアウトプットを繰り返していかないと覚えられないなというのはLaravelでポートフォリオを作ったときと同じように感じました。
Laravelの練度上げを棚に上げた状態で今回あえて企画に参加することになるわけですから、企画で少しでも貢献及び盗めるところは盗めるようにDjangoの理解やそれに付随するWebの知識の練度を少しでも上げられていけばなと思います。
まずは、Djangoの公式チュートリアルを覗いてみます。
参考サイト
Django(Python)でシステム開発できるようになる記事_入門編
docker-composeでdjango環境を作ってみる(MariaDB + Nginx + uWSGI)
Docker-ComposeでPython3.6+NGINX+MariaDB10+uWSGIのDjango環境欲張りセット
Rails開発におけるwebサーバーとアプリケーションサーバーの違い(翻訳)
uWSGI入門
「uWSGI」とアプリケーションサーバーについて
Webサーバとアプリケーションサーバの違いをわかりやすく解説します
APサーバの役割とは?Webサーバとの違いをしっかり理解しよう