【Django】独自 ID の自動インクリメントが行われていなかった

問題

Django でモデルを扱う際、デフォルトで作成される id ではないプライマリキーで管理をしたかったので、なんとなくネットでささっと見た情報で 「models.AutoField(primary_key=True)」を設定していました。

ただ、データベース上での設定が正しくできていなかった様で自動インクリメントが機能していませんでした。

そのため Django の admin 画面からデータを登録する際、すでに存在する値が id として登録されていました。

ちなみに、models.py では「models.AutoField(primary_key=True)」となっていたもののテーブル上ではカラムがプライマリキーになっていなかったため、エラーにもなっていませんでした。

修正作業

この時点でのテーブルは下記の状態。「id」カラムを修正していきます。

mysql> describe ig_mst_product;
+-------------------+--------------------------+------+-----+---------+-------+
| Field             | Type                     | Null | Key | Default | Extra |
+-------------------+--------------------------+------+-----+---------+-------+
| id                | int(6) unsigned zerofill | NO   |     | 000000  |       |
| product_nm        | varchar(60)              | YES  |     | NULL    |       |
| product_category  | varchar(20)              | YES  |     | NULL    |       |
| brand_cd          | int(6) unsigned zerofill | YES  |     | NULL    |       |
| product_url       | text                     | YES  |     | NULL    |       |
| product_image_url | text                     | YES  |     | NULL    |       |
+-------------------+--------------------------+------+-----+---------+-------+

プライマリキーとして設定

既存の値が再度使われてしまうのはおかしいので MySQL 上でプライマリキーを設定。

mysql> alter table ig_mst_brand add primary key (id);

値の重複の解決

admin 画面からレコードを追加すると「1062, "Duplicate entry '000000' for key 'ig_mst_product.PRIMARY'"」という感じで値の重複が発生。

「000000」を id として使用しようとしているので、おそらく値の自動インクレメントがされていない状態

デフォルト値の設定削除

デフォルト値として「000000」を設定していたのでとりあえずなくしてみます。

mysql> alter table ig_mst_product alter id drop default;

auto_increment の設定

その上で MySQL でカラムに auto_increment を設定しようとしたところ、また値の重複で拒否されました。

mysql> alter table テーブル名 modify カラム名 int(6) unsigned zerofill not null auto_increment;
ERROR 1062 (23000): ALTER TABLE causes auto_increment resequencing, resulting in duplicate entry '000001' for key 'テーブル名.PRIMARY'

この時点で id カラムには「000000」〜「000069」まで存在していたにもかかわらず、なぜか「000001」を使おうとして重複エラーが返ってきている状況。

調べてみたところ、既存データの対象のカラムの中に 0 もしくは -1 以下の値が入っているとこのエラーになる様です。既存データで「000000」が存在したのでこれを別の値に変更して再度実行したら成功しました。

mysql> alter table ig_mst_product modify id int(6) unsigned zerofill not null auto_increment;
Query OK, 69 rows affected, 2 warnings (0.04 sec)
Records: 69  Duplicates: 0  Warnings: 2

正しい設定をまとめると…

Id として使用するカラムがデータベース上で下記の条件を満たす必要がある様です。

  • デフォルト値なし
  • プライマリキーとして設定
  • AUTO_INCREMENT を設定
    • 既存データで 0 もしくはそれ以下の値が入っていないことを確認

一応 Before / After も載せておきます。Key, Default, Extra が変わっています。

Before

mysql> describe ig_mst_product;
+-------------------+--------------------------+------+-----+---------+-------+
| Field             | Type                     | Null | Key | Default | Extra |
+-------------------+--------------------------+------+-----+---------+-------+
| id                | int(6) unsigned zerofill | NO   |     | 000000  |       |
| product_nm        | varchar(60)              | YES  |     | NULL    |       |
| product_category  | varchar(20)              | YES  |     | NULL    |       |
| brand_cd          | int(6) unsigned zerofill | YES  |     | NULL    |       |
| product_url       | text                     | YES  |     | NULL    |       |
| product_image_url | text                     | YES  |     | NULL    |       |
+-------------------+--------------------------+------+-----+---------+-------+

After

mysql> describe ig_mst_product;
+-------------------+--------------------------+------+-----+---------+----------------+
| Field             | Type                     | Null | Key | Default | Extra          |
+-------------------+--------------------------+------+-----+---------+----------------+
| id                | int(6) unsigned zerofill | NO   | PRI | NULL    | auto_increment |
| product_nm        | varchar(60)              | YES  |     | NULL    |                |
| product_category  | varchar(20)              | YES  |     | NULL    |                |
| brand_cd          | int(6) unsigned zerofill | YES  |     | NULL    |                |
| product_url       | text                     | YES  |     | NULL    |                |
| product_image_url | text                     | YES  |     | NULL    |                |
+-------------------+--------------------------+------+-----+---------+----------------+

【Django】Nginx + Gunicorn でサブドメインを設定する方法

「example.com」というドメイン名ですでに運用しているところに追加で「sub.example.com」も使いたいという想定で書きます。

また、Django の処理には Gunicorn を使っています。

ネームサーバーでサブドメインの設定をする

ネームサーバーでサブドメインを登録します。

さくらインターネットを使っている場合は会員メニュー > ドメイン > ゾーン表示の画面で登録できます。

フィールド
エントリ名sub
種別別名 (CNAME)
@
DNSチェックする
TTLの設定なし

サブドメイン用のディレクトリを作る

任意の場所にサイトをホストするためのディレクトリを作成します。

例えば「example.com」というディレクトリの配下にさらにサブドメイン用に「sub」ディレクトリ、その配下に「html」と作る場合は下記の様な形。

$ cd var/www/example.com
$ mkdir sub
$ cd sub
$ mkdir html

ディレクトリの管理者を変更します。

$ sudo chown -R $USER:$USER /var/www/example.com/sub/html/

注意

一度上記の管理者の変更を飛ばした際、後で Django の「python manage.py startapp アプリ名」を実行した時に「ImportError: Couldn't import Django. Are you sure it's installed and available on your PYTHONPATH environment variable? Did you forget to activate a virtual environment?」というエラーが出ました。

「pip install django」を実行した際に django モジュールが仮想環境内に上手くインストールされなかったことが原因です。

仮想環境と Django プロジェクトを作成する

Python の仮想環境を作成、起動します。

$ cd var/www/example.com/sub/html/
$ python3.9 -m venv dj_proj_venv
$ cd dj_proj_venv
$ source bin/activate

Django を pip install し、プロジェクトとアプリケーションを作成します。

(dj_proj_venv) $ pip install django
(dj_proj_venv) $ django-admin startproject dj_proj
(dj_proj_venv) $ cd dj_proj
(dj_proj_venv) $ python manage.py startapp dj_app

settings.py の INSTALLED_APPS にアプリケーションを追記します。

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

    'dj_app.apps.DjAppConfig', # 追記分
]

settings.py の ALLOWED_HOSTS にサブドメインを含むのを追加します。

ALLOWED_HOSTS = ['sub.example.com']

Gunicorn を設定する

Gunicorn をインストールします。

$ pip install gunicorn

.socket ファイルの作成

# example_sub.socket
[Unit]
Description=gunicorn socket
[Socket]
ListenStream=/run/example_sub.sock
[Install]
WantedBy=sockets.target
項目メモ
Descriptionログ出力の際などに使われる
ListenStreamポートの指定。Nginx 設定ファイルの pass_proxy で指定
WantedBysockets.target

.service ファイルの作成

# example_sub.service
[Unit]
Description=gunicorn daemon
Requires=example_sub.socket
After=network.target
[Service]
User=root
Group=root
WorkingDirectory=/var/www/example.com/sub/html/dj_proj_venv/dj_proj
ExecStart=/var/www/example.com/sub/html/dj_proj_venv/bin/gunicorn --workers 3 --bind unix:/run/sample_django.sock dj_proj.wsgi:application
[Install]
WantedBy=multi-user.target
項目メモ
Descriptionログ出力の際などに使われる
Requires対応する .socket ファイル
After
User
Group
WorkingDirectorymanage.py があるディレクトリのフルパス?
ExecStartsystemctl start した時に実行するコマンド。
.sock と wsgi を bind。
venv 内の gunicorn のフルパス(?)を指定
WantedBy大抵 multi-user.target で大丈夫

socket の待ち受けを開始します。

$ systemctl start example_sub.socket
$ systemctl start example_sub.service

Nginx の設定ファイルにサブドメインの処理を追記する

「sub.example.com」と「example.com」の処理が記述されています。

server {
        listen 80;
        listen [::]:80;

        server_name sub.example.com;

        root /var/www/example.com/sub/html;
        index index.html;

        location / {
                proxy_set_header Host $http_host;
                proxy_set_header X-Forwarder-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
                proxy_pass http://unix:/run/example_sub.sock;
        }

        location /static {
                alias /var/www/example.com/run/html/static;
        }

}

server {
        listen 80;
        listen [::]:80;

        server_name example.com;

        root /var/www/example.com/html;
        index index.html;

        location / {
                proxy_set_header Host $http_host;
                proxy_set_header X-Forwarder-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
                proxy_pass http://unix:/run/example.sock;
        }

        location /static {
                alias /var/www/example.com/html/static;
        }

}

ローカルで作成した Django アプリケーションを Ubuntu の本番環境へ反映する

Mac のローカル環境で作成した Django プロジェクトをさくらの VPS で用意した Ubuntu 環境へ反映する際の流れをメモしています。

下記が完了していることを想定しています。

  • Web サーバーの設定が完了済み(筆者は Nginx を使用)
  • 仮想環境を作るディレクトリが決めてある
  1. データベースを本番環境にコピーする
    • ローカル環境での作業
    • 本番環境での作業
  2. ローカル環境のパッケージを本番環境で再現
    • requirements.txt の作成
    • requirements.txt を本番環境へコピー
    • 本番環境で Python 仮想環境を作成
    • requirements.txt によるパッケージインストール
  3. settings.py の編集
    • ファイルを分割する必要性
    • 開発環境の runserver 実行時に settings_dev.py を指定
    • settings.py(本番環境用)の編集
  4. Django ソースコードを本番環境に配置
    • 本番環境への git のインストール
    • リモートリポジトリから本番環境へファイルをコピー
  5. gunicorn のインストール
  6. データベースのログイン情報をサーバーの環境変数に記述
  7. 静的ファイルの配置

1. データベースを本番環境にコピーする

検証環境で準備したデータベース、テーブル、そしてデータを本番環境でも反映します。

ここはうまくやれば一行スクリプト書いて終了する方法もある様ですが、色々とエラーが出て逆にめんどくさそうだったのでステップバイステップでやっていきます。

ローカル環境での作業

まずローカル環境でコマンド「mysqldump データベース名 > dump.txt」を実行し、dump ファイルを作成します。

% mysqldump graffuhs > dump.txt                                                              
%

それを FTP か何かで本番環境へアップロードします。

本番環境での作業

本番環境に接続し、本番環境の MySQL でデータベースを作成します。

mysql> create database データベース名;

そしてコマンド「sudo mysql データベース名 < dump.txt」を実行します。

$ sudo mysql データベース名 < dump.txt
$

これで本番環境の MySQL にもローカル環境と同じデータベース、テーブル、そしてデータがコピーされました。

2. ローカル環境のパッケージを本番環境で再現

ローカル環境の Python 関連パッケージを本番環境でも再現するため、ローカル環境で requirements.txt ファイルを作成し、それを元に本番環境でパッケージインストールをさせます。

requirements.txt の作成

ローカル環境の Python 仮想環境へ移り、コマンド「pip freeze > requirements.txt」を実行します。

% pip freeze > requirements.txt

requirements.txt を本番環境へコピー

コマンド「scp requirements.txt 本番環境のユーザー名@本番環境のホスト名(もしくは IP アドレス):保存先のディレクトリ」を実行します。

だいぶ長いですが下記の例だとすると。。。

  • 本番環境のユーザー名: vpsadmin
  • 本番環境のホスト名: xx1-234-56789.vs.sakura.ne.jp
  • 保存先のディレクトリ: /var/www/example.com/html

下記の様になります。

scp requirements.txt vpsadmin@xx1-234-56789.vs.sakura.ne.jp:/var/www/example.com/html

無事本番環境に requirements.txt がコピーされました。

$ ls
requirements.txt

本番環境で Python 仮想環境を作成

本番環境のディレクトリへ移動し、Python の仮想環境を作成します。

$ python3.9 -m venv pythonvenv
$ ls
pythonvenv  requirements.txt

requirements.txt によるパッケージインストール

本番環境の Python 仮想環境に入った状態で、コマンド「pip install -r requirements.txt」を実行します。

(pythonvenv) $ pip install -r requirements.txt

すると requirements.txt の内容を元に Django を含めローカル環境で使っていたものと同じパッケージがインストールされます。

3. settings.py の編集

ファイルを分割する必要性

settings.py は本番環境と開発環境で内容が異なる部分が出てくるので、下記の様にファイルを分ける必要があります。(ファイル名は任意)

  • settings_common.py:開発環境と本番環境で共通の部分を記述
  • settings.py:本番環境でのみ適用する部分を記述
  • settings_dev.py:開発環境でのみ適用する部分を記述

settings.py と settings_dev.py それぞれに「from .settings_common import *」と記述し、共通部分を settings_common.py から読み込む様設定します。

開発環境の runserver 実行時に settings_dev.py を指定

runserver 実行時、デフォルトでは settings.py が設定ファイルとして読み込まれますが、今後開発環境(ローカル環境)では settings_dev.py を使う必要があります。

そのため、今後開発環境で runserver を実行する際は settings_dev.py を「runserver --settings=プロジェクトディレクトリ名.settings_dev」の形式で渡して実行します。

(inmybag) % python manage.py runserver --settings=some_project.settings_dev

settings.py(本番環境用)の編集

DEBUG の変更

False に変更

DEBUG = False

ALLOWED_HOSTS の変更

ドメイン名を設定。

ALLOWED_HOSTS = ['example.com']

Web サーバーで www.example.com から example.com のリダイレクトを行っていれば example.com のみを設定するだけで大丈夫。

静的ファイルの本番環境での配置場所

STATIC_ROOT = '/var/www/examlpe.com/html/static'

4. Django ソースコードを本番環境に配置

本番環境への git のインストール

本番環境でコマンド「sudo apt update」を実行しパッケージリストを更新、その後「sudo apt install git」で git をインストールします。

$ sudo apt update
$ sudo apt install git
Reading package lists... Done
Building dependency tree       
Reading state information... Done
git is already the newest version (1:2.25.1-1ubuntu3.1).
git set to manually installed.
0 upgraded, 0 newly installed, 0 to remove and 18 not upgraded.

リモートリポジトリから本番環境へファイルをコピー

プロジェクトフォルダをリポジトリに設定しているので、プロジェクトフォルダを配置したい場所へ移動した上で git clone を実行します。

$ git clone https://github.com/ユーザー名/リポジトリ名.git

これで本番環境にもプロジェクトフォルダがコピーされました。

ローカル環境からリモートリポジトリへファイルを反映させる方法はこちら↓

https://notemite.com/programming/github-local-to-remote

5. gunicorn のインストール

本番環境でのみ必要となるものとして、アプリケーションサーバーがあります。今回は gunicorn を使います。

Python 仮想環境内でアプリケーションサーバーの gunicorn をインストールします。

(pythonvenv) $ pip install gunicorn

socket の待ち受けを開始します。

(pythonvenv) $ systemctl start example_sub.socket
(pythonvenv) $ systemctl start example_sub.service

今後作業する際は何かにつけて「pkill gunicorn」で gunicorn を終わらせた方が良いです。エラーが起きた時にいくらコードを修正しても治らないまま数時間経って、「pkill gunicorn」一発で治ったこともありました。

6. データベースのログイン情報をサーバーの環境変数に記述

MySQL のデータベースを使用していて、settings.py ではユーザー名とパスワードを環境変数から取る様に設定しています。

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'データベース名',
        'USER': os.environ.get('DB_USER'),
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': '',
        'PORT': '',
    }
}

そのため、サーバーの環境変数として記述します。ホームディレクトリの .bashrc ファイルの最下部に下記を追加しました。

export DB_USER='ユーザー名'
export DB_PASSWORD='パスワード'

7. 静的ファイルの配置

コマンド「python manage.py collectstatic」を実行し、settings.py で STATIC_ROOT に設定したパスに静的ファイルを集約します。

$ python manage.py collectstatic

これでとりあえず本番環境でも動くと思います。

Django のフォームで投稿した内容を Postfix でメール送信する方法

Django で作ったサイトのフォームで受け付けた情報を自分の Gmail アカウントへ送信する様に設定します。受信については特に考慮していません。

サーバーの OS は Ubuntu 20.04 となっています。

Postfix の設定

コマンド「sudo apt install postfix」を実行し、Postfix をインストールします。

すると下記の様な画面が表示されます。

「General type of mail configuration」の文言の後にいくつか選択肢が表示されますが、No configuration を選択します。

/etc/postfix/main.cfの編集

インストールが完了したらコマンド「sudo cp /usr/share/postfix/main.cf.debian /etc/postfix/main.cf」を実行し、postconf コマンドで main.cf ファイルを編集していきます。hostname、mydomain、myorigin の3つを確認、設定します。

hostname の設定

DNS の MX レコードで指定されているホスト名を指定します。

$ sudo postconf -e "myhostname = mail.example.com"

コマンド「vi /etc/postfix/main.cf」で main.cf を確認すると「myhostname = mail.example.com」が追記されているはずです。

mydomain の設定

mydomain は hostname の値によって自動的に決まります。コマンド「postconf mydomain」を実行して確認できます。hostname からサブドメインが除外された値になっているはずです。

$ postconf mydomain
mydomain = example.com

もし上記の様な値が返ってこない場合はコマンド「sudo postconf -e "mydomain = example.com"」で明示的に main.cf に設定します。

myorigin の設定

myorigin は デフォルトでは $myhostname を参照しています。

$ postconf myorigin
myorigin = $myhostname

代わりに $mydomain を参照する様変更する必要があります。

コマンド「sudo postconf -e "myorigin = $mydomain"」だとなぜかうまく反映されなかったので「sudo vi /etc/postfix/main.cf」でファイルを開いて直接「myorigin = $mydomain」を記述しました。

$ postconf myorigin
myorigin = $mydomain

Postfix の再起動

コマンド「sudo systemctl restart postfix.service」で Postfix を再起動します。

$ sudo systemctl restart postfix.service
$ 

main.cf には下記の2行が追記されていれば Postfix の設定は完了です。

myhostname = mail.example.com
myorigin = $mydomain

Django の settings.py の編集

Django プロジェクトの settings.py に下記を設定します。

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'localhost'
EMAIL_PORT = 25
EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = ''
EMAIL_USE_TLS = False
DEFAULT_FROM_EMAIL = 'Sender <admin@example.com>'

動作確認

Django サイトのフォームで試しに投稿したところ、一応指定した G メールアカウントには届きました。

この時点で一応最低限の目的は達成されましたが、メールの暗号化とメールの認証について気になったので下記にまとめます。

メールの暗号化

Postfix で Gmail アカウント宛に送信したメールの詳細を見てみると「このメールは sakura.ne.jp で暗号化されませんでした」と表示があります。

これを解決するには、送信先がTLS(Transport Layer Security)に対応する場合はTLSで暗号化して送信する設定を Postfix 側で適用する必要があります。

下記のコマンドで main.cf に「smtp_tls_security_level = may」と追記し、 Postfix を再起動します。

$ sudo postconf -e "smtp_tls_security_level = may"
$ sudo systemctl restart postfix.service
$ 

再度メールを送って同じ部分を確認してみると「標準的な暗号化(TLS)」という表示に変わりました。

これでメールの暗号化は完了です。

メールの認証

Gメールで受信したメールの送信者のアイコンにカーソルを合わせると「Gメールでは、このメールが(スパム発信者からではなく)本当に example.com から送信されたメールであることを確認できませんでした。」という文言も表示されます。

この点に関しては手間がかかりそうな気がするので追々対応した際に追記します。

Django の クラスベースビューで URL のパラメータを使う

まず URL ディレクトリの文字列を使う場合の復習

Django の ListView などでテンプレートに返すデータをフィルタリングする際、urls.py で設定した URL ディレクトリの文字列を取得してそれを元にすることがあると思います。

下記の様な感じで get_queryset 関数で self.kwargs から値を取り出す感じですね。

# urls.py
urlpatterns = [
    path('<str:some_code>/', views.SomeData.as_view(), name="some_data"),
]
# views.py
class SomeData(generic.ListView):
    ### 一部省略 ###
    def get_queryset(self):
        code = self.kwargs['some_code']
        return SomeDataModel.objects.filter(some_code_column=code)

ただ、今回はそうではなく URL のパラメータを「ドメイン名.com/?キー=値&キー=値」の形式で取得してそれをデータ取得に使いたいと思います。

URL のパラメータを使いたい

self.request.GET.get(パラメータキー) で取得

get_queryset 関数をオーバーライドする際に self.request.GET.get(パラメータのキー) で取得できます。

例えば「http://127.0.0.1:8000/devicelist/?brand_nm=apple&category=laptop,phone」の様な URL の場合。

「?brand_nm=apple&category=laptop,phone」の部分をビューの処理に使いたいとします。

# views.py 内 get_queryset 関数
def get_queryset(self):
    brand = self.request.GET.get('brand_nm')
    return SomeDataModel.objects.filter(brand_nm=brand)

値が一つだけの場合

値が一つだけの場合(brand_nm=apple)は「self.request.GET.get(キー)」でそのまま値が一つ取得できます。

print(self.request.GET.get('brand_nm'))
# apple

値が複数の場合

複数の値を「,」で繋げて URL に渡した場合(category=laptop,phone)は、「,」が含まれただけの一つの文字列になっています。

print(self.request.GET.get('category'))
# laptop, phone

なので一旦 .split(',') を通してリストにしてあげる必要があります。

print(self.request.GET.get('category').split(','))
# ['laptop', 'phone']
print(self.request.GET.get('category').split(',')[0])
# laptop
print(self.request.GET.get('category').split(',')[1])
# phone

値を指定していない場合

URL で渡していないキーを指定すると None が返ってきます。

print(self.request.GET.get('product_type'))
# None

Django の ModelForm で select option をテーブルから参照する

やりたいこと:Django の ModelForm を作るときに、ドロップダウンメニューの選択肢(select option)をデータベースのテーブルから持ってきたい。

結論から言いますと Foreign Key を使って別のテーブルのユニークキーをフォームの選択肢として利用します。

models.py でドロップダウンにしたいカラムに models.ForeignKey を適用

まず、models.py でドロップダウン表示にしたい項目に models.ForeignKey を適用します。

別テーブルでユニークになっている値(つまりドロップダウンの候補になる値)が入るという設定をします。

# models.py

class IgMstProduct(models.Model):
    id = models.AutoField(primary_key=True)
    product_nm = models.CharField(max_length=60, blank=True, null=True)
    brand_cd = models.ForeignKey('IgMstBrand',on_delete=models.SET_NULL, null=True, db_column='brand_cd')
    product_url = models.TextField(blank=True, null=True)

    class Meta:
        managed = False
        db_table = 'ig_mst_product'

上記の設定時、「db_column」で DB 側で実際に情報を受け取るカラム名を指定しないと、フォーム Submit 時に「1054, "Unknown column '〜_id' in 'field list'"」というエラーが返ってくると思います。

詳しくは調べていませんがどうやら Django が自動的に「〜_id」のカラム名で処理をしようとするらしいです。

forms.py で ModelChoiceField を設定

下記の様に対象の項目に対して forms.ModelChoiceField(queryset=モデル名.objects.all()) を設定してみると、なんとなくそれっぽくドロップダウンが表示されます。

# forms.py

class IgProductForm(forms.ModelForm):
    brand_cd = forms.ModelChoiceField(queryset=IgMstBrand.objects.all())

    class Meta:
        model = IgMstProduct
        fields = ('product_nm', 'product_category', 'brand_cd', 'product_url',)

が、このままだとドロップダウンの表示名が下記の様に「モデル名 object (1)」とかになるので変更します。

<!-- Chrome で「検証」した例 -->

<select name="brand_cd" required id="id_brand_cd">
  <option value="" selected>---------</option>
  <option value="1">モデル名 object (1)</option>
  <option value="2">モデル名 object (2)</option>
  <option value="3">モデル名 object (3)</option>
  <option value="4">モデル名 object (4)</option>
  <option value="5">モデル名 object (5)</option>
  <option value="6">モデル名 object (6)</option>
</select>

ModelChoiceField を継承する別クラスを作成し、そこで label_from_instance 関数をオーバーライドし、return obj.表示したいカラム名 とすれば表示したいカラム名が表示名が変わります。

# forms.py

class CustomModelChoiceField(forms.ModelChoiceField):
    def label_from_instance(self, obj): # label_from_instance 関数をオーバーライド
         return obj.brand_nm # 表示したいカラム名を return


class IgProductForm(forms.ModelForm):
    brand_cd = CustomModelChoiceField(queryset=IgMstBrand.objects.all()) # 上記のクラスを参照する様変更

    class Meta:
        model = IgMstProduct
        fields = ('product_nm', 'product_category', 'brand_cd', 'product_url',)
<select name="brand_cd" required id="id_brand_cd">
  <option value="" selected>---------</option>
  <option value="1">Nikon</option>
  <option value="2">Canon</option>
  <option value="3">Sony</option>
  <option value="4">Fujifilm</option>
  <option value="5">Leica</option>
  <option value="6">Olympus</option>
</select>

Django の REST framework で JSON を返す API を作る方法

Python 仮想環境内に作ったプロジェクトに API 用の独自のアプリケーションを作り、その中に View を作ります。

ここで紹介するのは単純にテーブル内のデータを全て GET で抽出してそのまま JSON 形式で返すだけのものです。POST や DELETE などは触れていません。

  1. 下準備
    • API 用 Django アプリケーションの作成
    • djangorestframework のインストール
    • urls.py の設定
    • models.py にモデルを記述
  2. シリアライザの作成
  3. ビューの作成
    • class based view の場合
    • function based view の場合
  4. 動作確認

下準備

API 用 Django アプリケーションの作成

API 用にアプリケーション「api」を作成します。

% python manage.py startapp api

settings.py の INSTALLED_APPS に追加します。

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

    'api.apps.ApiConfig', # 追記
]

djangorestframework のインストール

コマンド「pip install djangorestframework」を実行します。

% pip install djangorestframework
Collecting djangorestframework
  Downloading djangorestframework-3.12.4-py3-none-any.whl (957 kB)
     |████████████████████████████████| 957 kB 4.1 MB/s 
Requirement already satisfied: django>=2.2 in /Users/ユーザー名/PythonProjects/仮想環境名/lib/python3.8/site-packages (from djangorestframework) (3.2)
Requirement already satisfied: pytz in /Users/ユーザー名/PythonProjects/仮想環境名/lib/python3.8/site-packages (from django>=2.2->djangorestframework) (2021.1)
Requirement already satisfied: sqlparse>=0.2.2 in /Users/ユーザー名/PythonProjects/仮想環境名/lib/python3.8/site-packages (from django>=2.2->djangorestframework) (0.4.1)
Requirement already satisfied: asgiref<4,>=3.3.2 in /Users/ユーザー名/PythonProjects/仮想環境名/lib/python3.8/site-packages (from django>=2.2->djangorestframework) (3.3.4)
Installing collected packages: djangorestframework
Successfully installed djangorestframework-3.12.4
%

settings.py の INSTALLED_APPS に追加します。

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

    'api.apps.ApiConfig',
    'rest_framework', # 追記
]

urls.py の設定

アプリケーション「api」ディレクトリに url.py を作成した上で、プロジェクトレベルの url.py から参照します。

from django.contrib import admin
from django.urls import path, include # 追記

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('api.urls')), # 追記
]

models.py にモデルを記述

API で使用したいモデルを models.py に記述します。

今回すでにデータベース上にテーブルを用意してあるのでコマンド「python manage.py inspectdb テーブル名」でモデル情報を抽出します。

% python manage.py inspectdb yt_analysis_07
# This is an auto-generated Django model module.
# You'll have to do the following manually to clean this up:
#   * Rearrange models' order
#   * Make sure each model has one field with primary_key=True
#   * Make sure each ForeignKey and OneToOneField has `on_delete` set to the desired behavior
#   * Remove `managed = False` lines if you wish to allow Django to create, modify, and delete the table
# Feel free to rename the models, but don't rename db_table values or field names.
from django.db import models


class YtAnalysis07(models.Model):
    channel_id = models.CharField(max_length=40, blank=True, null=True)
    channel_name = models.TextField(blank=True, null=True)
    view_count = models.BigIntegerField(blank=True, null=True)
    like_count = models.IntegerField(blank=True, null=True)
    dislike_count = models.IntegerField(blank=True, null=True)
    favorite_count = models.IntegerField(blank=True, null=True)
    comment_count = models.IntegerField(blank=True, null=True)
    video_count = models.IntegerField(blank=True, null=True)

    class Meta:
        managed = False
        db_table = 'yt_analysis_07'
% 

クラス文を models.py にコピーしますが、その際に 1 つ「primary_key=True」のフィールドを持つ必要があるそうです。

シリアライザの作成

Python のリスト・辞書形式のデータを JSON に変換するためのシリアライザを作成します。

アプリケーションフォルダ(今回は api/ )に「serializers.py」というファイルを作成します。

from rest_framework import serializers
from .models import YtAnalysis07 # モデルをインポート

class APISerializer(serializers.ModelSerializer):
    class Meta:
        model = YtAnalysis07 # 使用するモデル
        fields = '__all__' # 処理対象にするフィールド(今回は全項目)

ビューの作成

API のビューを作成します。ざっくり言うとモデルからデータを抽出し、シリアライザに渡して JSON に変換し、その JSON データをレスポンスとして返します。

class based view の場合

from rest_framework import viewsets
from .serializers import APISerializer
from .models import YtAnalysis07

class DataListView(viewsets.ModelViewSet):
    queryset = YtAnalysis07.objects.all() # モデルからデータを抽出するクエリセット
    serializer_class = APISerializer # 使用するシリアライザ

urls.py の設定は下記の様にします。

from django.urls import path
from . import views

app_name = 'api'
urlpatterns = [
    path('data-list/', views.DataListView.as_view({'get': 'list'}), name="data-list"),
]

function based view の場合

from rest_framework.decorators import api_view
from rest_framework.response import Response
from .serializers import APISerializer
from .models import YtAnalysis07

@api_view(['GET']) # GET のみに対応
def dataList(request):
    api_data = YtAnalysis07.objects.all() # モデルからデータを抽出する
    serializer = APISerializer(api_data, many=True) # シリアライザにデータを渡す
    return Response(serializer.data) シリアル可されたデータを return で返す

urls.py の設定は下記の様にします。

from django.urls import path
from . import views

app_name = 'api'
urlpatterns = [
    path('data-list/', views.dataList, name="data-list"),
]

動作確認

設定した URL をブラウザに打ち込むと下記の様な画面が表示されます。JSON データも見えます。

ブラウザで URL を叩くと上記の様な画面で表示されますが、例えばターミナルで URL を requests.get すると下記の様に JSON 形式のデータだけが返ってきます。

>>> import requests
>>> r = requests.get('http://127.0.0.1:8000/api/data-list/')
>>> r.text
'[{"channel_id":"UCLkAepWjdylmXSltofFvsYQ","channel_name":"BANGTANTV","view_count":22423409,"like_count":7325674,"dislike_count":20936,"favorite_count":0,"comment_count":381315,"video_count":9},{"channel_id":"UCE_M8A5yxnLfW0KghEeajjw","channel_name":"Apple","view_count":19044060,"like_count":600159,"dislike_count":21023,"favorite_count":0,"comment_count":0,"video_count":10},

これを Django のテンプレートから JavaScript で呼び出すなどして活用します。

Matplotlib で描画したグラフを SVG 化して Django で表示する方法

Django アプリケーション内で、データベースから取得したデータをグラフにして表示したいと思ったので方法を調べました。

ざっくりですが、取得した情報を元にグラフの SVG 画像を作成、そしてページに表出する方法を記載します。

  1. テンプレートに img タグを追加
  2. urls.py に URL ディスパッチャを追加
  3. views.py に SVG 生成の処理を追加

1. テンプレートに img タグを追加

テンプレートファイルのグラフを表示したい場所に img タグを追加し、URL 逆引きで URL ディスパッチャを呼び出します。

<img src="{% url 'sample_django_app:yt_trend_svg' %}">

今回は sample_django_app の yt_trend_svg というディスパッチャを呼び出しています。

2. urls.py に URL ディスパッチャを追加

urls.py の urlpatterns リストの中に下記を追加し、yt_trend_svg が呼び出されたら views.py の get_trend_svg 関数を呼び出す様設定しています。

path('chart/', views.get_trend_svg, name="yt_trend_svg"),

3. views.py に SVG 生成の処理を追加

ビューで get_trend_svg 関数を定義し、データの取得、グラフの作成、SVG への変換をして最後に HttpResponse を返します。

処理の流れは下記の通りです。

  1. URL ディスパッチャからget_trend_svg が呼び出される
  2. YtTrendChannel モデルからデータを取り出す
  3. x と y のリストを作成
  4. make_chart 関数に x と y を渡しグラフを作成
  5. convert_to_svg 関数でグラフを SVG に変換
  6. HttpResponse として SVG を返す
import io
from django.http import HttpResponse
import matplotlib
matplotlib.use('Agg') # Matplotlib の backend を指定
import matplotlib.pyplot as plt

# 1. ルーターから get_trend_svg が呼び出される
def get_trend_svg(request):
    
    def make_chart(x, y):
        # setting up plots
        plt.bar(x, y, color='#00d5ff')
        plt.title("My Chart", color='#3407ba')
        plt.xlabel("channel")
        plt.ylabel("views")

    def convert_to_svg():
        # convert the plots to SVG
        buffer = io.BytesIO() # バッファを作成
        plt.savefig(buffer, format='svg', bbox_inches='tight') # バッファに一時保存
        s = buffer.getvalue() # バッファの内容を s に渡す
        buffer.close() # バッファはクローズ
        return s # s を返す
    
    # 2. YtTrendChannel モデルからデータを取り出す
    channels = YtTrendChannel.objects.filter(data_date='20210104').order_by('channel_name')[:10]

    # 3. x と y のリストを作成
    x = [channel.channel_name for channel in channels]
    y = [int(channel.view_count) for channel in channels]

    # 4. make_chart 関数に x と y を渡しグラフを作成
    make_chart(x, y)

    svg = convert_to_svg() # 5. グラフを SVG に変換
    plt.cla()
    
    # 6. HttpResponse として SVG を返す
    return HttpResponse(svg, content_type='image/svg+xml')

convert_to_svg 関数の中でコメントを色々書いていますが、メモリ上に一時ファイルを保存できるため、実ファイルが作られなくて済むんだそうです。

すると下記の様にチャートが表示されます。表示がだいぶ崩れてますがこれは後ほど調整するということで。。。

【mac】PostgreSQL にテーブルを作って csv のデータを挿入した時のメモ

  1. テーブルの作成
  2. csv ファイルをテーブルに copy (insert)
  3. (ついでに)inspectdb でモデル情報を確認

1. テーブルの作成

ターミナルにコマンド「psql データベース名」を入力し対象のデータベースを開いて、別で書いておいた create 文のコピペで処理可能。最後に「;」をつけるのを忘れない様に気をつけてください。

下記の例では「yt_trend_channel」というテーブルを作りました。

sample_db=# create table yt_trend_channel (
channel_id varchar(30),
view_count varchar(20),
join_date varchar(20),
country varchar(20),
channel_name varchar(100),
thumbnail_url varchar(50),
subscriber_count varchar(20),
data_date varchar(8)
);

2. csv ファイルをテーブルに copy (insert)

csv ファイルを テーブルに copy します。

psql を起動する前に対象のファイルのディレクトリへ cd しておくと、単純にファイル名を渡すだけで済むのでわかりやすいです。

下記の例では先ほど create した yt_trend_channel テーブルに、別で用意した yt_trend_channel.csv という csv ファイルのデータを copy (insert) しています。

sample_db=# \copy yt_trend_channel from 'yt_trend_channel.csv' with csv
COPY 4259
sample_db=#

全部で 4259 件のレコードが copy されました。

select クエリで確認します。

sample_db=# select * from yt_trend_channel;
        channel_id        | view_count  | join_date  |    country     |                            channel_name                             |                   thumbnail_url                    | subscriber_count | data_date 
--------------------------+-------------+------------+----------------+---------------------------------------------------------------------+----------------------------------------------------+------------------+-----------
 UC9OvLVXb-okaVtoK9V8Biwg | 103499474   | 2018/05/23 | 日本           | ABEMA バラエティ【公式】                                            | https://yt3.ggpht.com/ytc/AAUvwnjBKC3y2KycvPfvHdn1 |  19.9万人        | 20210104
 UCZZ0UGjWsRdM8_5bsqtxYaQ | 190130764   | 2015/10/02 | 日本           | RIZIN FIGHTING FEDERATION                                           | https://yt3.ggpht.com/ytc/AAUvwnjDj0e-hS0um-ZKG1Tc |  35万人          | 20210104
 UCaminwG9MTO4sLYeC3s6udA | 3201321432  | 2016/03/18 | 日本           | ヒカル(Hikaru)                                                    | https://yt3.ggpht.com/ytc/AAUvwnhBYwfz6DycI4xBURsZ |  428万人         | 20210104
 UCPn_e-7LD7SbzIBxdU_XBnQ | 21113326    | 2020/07/30 | 日本           | あーたろチャンネル                                                  | https://yt3.ggpht.com/ytc/AAUvwngZDvNAYRF_qgPfOgy9 |  18.7万人        | 20210104
 UC4-TMrb7Mm4KnYx1VsUgcJA | 135489838   | 2012/08/04 | 日本           | あいり                                                              | https://yt3.ggpht.com/ytc/AAUvwngy1VncfSpEQkWmZXYK |  26.8万人        | 20210104

ちなみに MySQL の場合は下記の様な感じ

load data local infile  'yt_trend_channel.csv'
into table yt_trend_channel
fields terminated by ',' 
enclosed by '"'
;

3. (ついでに Django)inspectdb でモデル情報を確認

実はそもそも Django で使うテーブルなので「python manage.py inspectdb テーブル名」でモデル情報を確認してみます。

% python manage.py inspectdb yt_trend_channel
# This is an auto-generated Django model module.
# You'll have to do the following manually to clean this up:
#   * Rearrange models' order
#   * Make sure each model has one field with primary_key=True
#   * Make sure each ForeignKey has `on_delete` set to the desired behavior.
#   * Remove `managed = False` lines if you wish to allow Django to create, modify, and delete the table
# Feel free to rename the models, but don't rename db_table values or field names.
from django.db import models


class YtTrendChannel(models.Model):
    channel_id = models.CharField(max_length=30, blank=True, null=True)
    view_count = models.CharField(max_length=20, blank=True, null=True)
    join_date = models.CharField(max_length=20, blank=True, null=True)
    country = models.CharField(max_length=20, blank=True, null=True)
    channel_name = models.CharField(max_length=100, blank=True, null=True)
    thumbnail_url = models.CharField(max_length=50, blank=True, null=True)
    subscriber_count = models.CharField(max_length=20, blank=True, null=True)
    data_date = models.CharField(max_length=8, blank=True, null=True)

    class Meta:
        managed = False
        db_table = 'yt_trend_channel'
% 

class 文を models.py にコピーすると Django でモデルとして使える様になります。その際に 1 つ「primary_key=True」のフィールドを持つ必要がありますので確認してください。

【Django】static ファイルの配置と設定

プロジェクト単位かアプリケーション単位かで若干ファイルの配置場所と設定方法が違いますが、まずは settings.py の INSTALLED_APPS に「'django.contrib.staticfiles'」の記述があることを確認します。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles', # この記述があることを確認

    'sample_django_app.apps.SampleDjangoAppConfig'
]

プロジェクト単位で使う static の場合

プロジェクトフォルダ(manage.py がある階層)直下に「static」フォルダを作成し、ファイルを配置。

settings.py の「STATICFILES_DIRS」にパスを追加します。

STATICFILES_DIRS = [
    os.path.join(BASE_DIR,'static'),
]

アプリケーション単位で使う static の場合

アプリケーションフォルダ直下に「static」フォルダを作成し、ファイルを配置。

settings.py に「STATIC_URL = '/static/'」の記述があることを確認。

STATIC_URL = '/static/'