nyanpyou Note

主な目的は調べたり作ったりしたプログラミング備忘録(予定)

衝動的にLightsailとDjangoでWebサイトを作る その4

前回からの続き。

nyanpyou.hatenablog.com

構成(再掲)

Apacheとmod_wsgiDjangoを連携させる

サーバーの/etc/apache2/sites-availableに新しく.confファイルを作成し、公開のための設定を書き込む。
サーバーへのファイルのアップロードはWinSCPを使って行っていたが、/etc/apache2/sites-availableはアクセス権の問題でファイルの書き込みができなかったので、/home/ubuntuに一度アップロードしてからsudo mvで移動させることにした。(不用意に外部からのアクセスを許可したくなかったため)
.confファイル作成に必要なパスは、いちいち自分で確認せずとも、mod_wsgi-express module-configを実行すると出力してくれる。

www.51weblab.jp

docs.djangoproject.com

Apacheとmod_wsgiDjangoを連携したつもりが動かない

上述のページ等で入手した情報を参考に設定を行い、Djangoプロジェクトをサーバーにデプロイしたが、実際にブラウザにIPを入力して接続を試した段階でInternalErrorが発生。
/var/log/apache2内にログファイルが残されているので、確認すると以下のエラーが発生していた。

#IPなど一部情報は削除しています
[mpm_event:notice] AH00489: Apache/2.4.41 (Ubuntu) configured -- resuming normal operations
[core:notice] AH00094: Command line: '/usr/sbin/apache2'
[mpm_event:notice] AH00493: SIGUSR1 received.  Doing graceful restart
[mpm_event:notice] AH00489: Apache/2.4.41 (Ubuntu) mod_wsgi/4.7.1 Python/3.8 configured -- resuming normal operations
[core:notice] AH00094: Command line: '/usr/sbin/apache2'
[wsgi:error] mod_wsgi (pid=): Failed to exec Python script file '/home/ubuntu/uma/musu/me/wsgi.py'.
[wsgi:error] mod_wsgi (pid=): Exception occurred processing WSGI script '/home/ubuntu/uma/musu/me/wsgi.py'.
[wsgi:error] Traceback (most recent call last):
[wsgi:error] File "/home/ubuntu/uma/musu/me/wsgi.py", line 12, in <module>
[wsgi:error] from django.core.wsgi import get_wsgi_application
[wsgi:error] ModuleNotFoundError: No module named 'django'

エラーを見ると、wsgi.pyでDjangoをインポートしようとしたときに、Djangoが見つけられずエラーが発生しているみたいなので、これを何とかしようと考えた。

各パッケージがサーバー上のどこにあるのか確認してみると、mod_wsgiだけ/usr/local/lib/python3.8/dist-packagesにインストールされていることが分かった。djangoや他のパッケージは/home/ubuntu/.local/lib/python3.8/site-packagesに入っている。
なんだこれという感じだが、どうもDebian パッケージからインストールされたサードパーティPython ソフトウェアは、site-packages ではなく dist-packages に入るらしい。

stackoverflow.com

正直よくわからない。
全てpipでインストールしたはずなのに、何故別のフォルダに分かれてしまったのか理由は不明。ただ、作成した.confファイルでWSGIPythonHomeとして/usr/lib/python3.8を指定していること、djangoが/usr/配下とは全く別の/home/配下のディレクトリに存在することが影響しているのではないかと予想した。

そこで、必要なパッケージ全てをsite-pakagesにまとめられればDjangoが見つからない事態は防げるかも…と考えたのだが、さらに調べてみるとそもそもUbuntuにプリインストールされているPythonはOS自体の機能で利用されるものらしく、安定性を損なう可能性もあることからパッケージをインストールなどしない方がいいらしい、ということを知った(今更)。

www.python.jp

実は今まで仮想環境を構築せず、Ubuntuに元から入っていたPythonをそのまま使って動かそうとしていた。
上述の参考資料も、仮想環境でPythonが用意されている前提の内容だったため、書かれている通り真似をしていないのだからエラーが出ても当たり前といえば当たり前ではある。しかし、パスの設定等を正しくできれば、仮想環境じゃなくても動くんじゃないかと思い試行錯誤していた。
書いてあることをそのまま真似するだけでなく、色々弄って試してみることも時には大事だと思う。
(最終的には、そもそもプリインストールされたPythonは弄らないほうがいいと知ってしまったため、おとなしく仮想環境を構築して動かすことになったが…。 ) とはいえ仮想環境ならDjangoもmod_wsgiも同じディレクトリに入ってくれてきっとすんなり動くはず。

Pythonの仮想環境を構築する

仮想環境の構築にはvenvを用いる。venvはPython3の標準ライブラリで、pipでインストールしたパッケージのバージョン分けが可能。仮想環境AにはDjango3.2が入っていて、 仮想環境BにはDjango3.0が入っているみたいなことができる。(仮想環境を使わない場合はどれか1つのバージョンしかインストールできない。)
環境の切り分けができるので、プロジェクトAのために入れたパッケージがプロジェクトBのために使いたいパッケージと干渉してしまう、といったことも防ぐことができる。
仮想環境を有効にすると、「python」コマンドを実行したとき、仮想環境のpythonが実行されるようになる。仮想環境に導入したライブラリは全て仮想環境のディレクトリ内で管理される。

www.python.jp

Apacheとmod_wsgiDjangoを連携できた

仮想環境を作成し、Django、mod_wsgi、mysqlclientを導入。
mod_wsgi-express module-configを実行した結果は以下の通り。

#パスの一部をマスクしています
LoadModule wsgi_module "/home/ubuntu/.../venv/lib/python3.8/site-packages/mod_wsgi/server/mod_wsgi-py38.cpython-38-x86_64-linux-gnu.so"
WSGIPythonHome "/home/ubuntu/.../venv"

.confファイルを上の通り変更して実行すると、module XXX(デプロイしたいプロジェクト)が無いと言われたので、改めてチュートリアル通りにWSGIPythonPathを設定し直した。
WSGIPythonPathは、指定したパスによってimport mysiteの状態になるように設定する。例えば/home/ubuntu/に配置したgundamというプロジェクトをデプロイする場合は、WSGIPythonPathに/home/ubuntu/gundamを指定する。
これで無事ページが表示されるようになった。

続く

本日はここまで。あと1~2回で多分終わる予定。

衝動的にLightsailとDjangoでWebサイトを作る その3

前回からの続き。

nyanpyou.hatenablog.com

構成(再掲)

MySQLに外部からアクセスする

デフォルトの設定ではローカルホストからの接続しか許可されていないため、忘れずにmysqls.cofの記述を変更する必要がある。

qiita.com

/etc/mysql/my.cnfを見ると、/etc/mysql/conf.d/と/etc/mysql/mysql.conf.d/配下の.cnfファイルを、追加の設定ファイルとして読み込みますよと書かれているので、どうやら設定ファイルはここにまとめておいておけばいいらしい。

MySQLのパスワードポリシーを変更する

初期設定ではパスワードのレベルがMEDIUMに設定されている。MEDIUMの場合、パスワードは以下の条件で設定する必要がある。

テストで触りたい場合には少々不便なため、パスワードのレベルを下げてLOWにする手もあり。
前述の設定ファイルのどれかに、

[mysqld]
validate_password.policy=LOW

の記述を追加すればOK。 新しく専用の.cnfファイルに分けておくとわかりやすい。

Django事始め

Djangoの使い方は公式に丁寧なチュートリアルが用意されていたので、それを参考にしながら進める形にした。

docs.djangoproject.com

基本的な流れを掴むには丁度良かったのでお勧め。

migrateでエラーが出た時

python manage.py migrate

を実行した時に、

django.db.utils.OperationalError: (1050, "Table 'django_content_type' already exists")

と表示されたら、対象のデータベースに既にdjango_content_typeという名前のテーブルが作成されてしまっているので、サーバーからMySQLに接続し、drop tableコマンドで削除してからもう一回実行すると良い。

既存のデータベースをDjangoで利用する

Djangoにはmodels.pyの記載に従ってデータベースのテーブルを作成してくれる機能があるが、既に作っておいたテーブルを使いたい場合もある。集めておいたデータを使ってどうこうしたいとか。
その場合は、ありがたいことにmodels.pyを手打ちする必要がない。
settings.pyで接続するデータベースの設定(設定 | Django ドキュメント | Django)を済ませた後、

python manage.py inspectdb

を実行すると、自動的にmodels.pyに書くべき内容をDjangoが生成して表示してくれる。
これをコピペしておけばOK。

docs.djangoproject.com qiita.com

後からDjango側からデータを追加したりしたい場合は、

class Meta:
    managed = True

に変更しておく。

Djangoにテーブルを作らせた場合は、models.pyで指定せずとも勝手にidというint型の項目を作るらしい。
既に作成済みのテーブルに、idのような通し番号を示す項目が存在しているのなら、models.pyで該当項目の引数にprimary_key=Trueを追加すると、idの代わりに使ってくれる。
そしてこのprimary_keyの設定をしていないと、DjangoからMySQLのデータを取得しようとして例えば、

from testapp.models import Test
Test.objects.all()

を実行した際に

django.db.utils.OperationalError: (1054, "Unknown column 'test.id' in 'field list'")

のエラーが出る。
加えてnull=Trueになっているとそれもまたエラーになるので、Falseに変えておく必要あり。

qiita.com

Djangoでhtmlのformタグを使う時の覚書

formタグのaction属性とmethod属性に

action="{% url 'test_get'%}" method="get"

と指定すると、urls.pyに記載したURLのうち、name="test_get"のものに対してGETを行うフォームが作れる。

例えば、http://example.com/testに、

<form action="{% url 'test_get'%}" method="get">
        <label>性別</label>
        <select class="form-select" name="gender">
            <option value="M">male</option>
            <option value="F">female</option>
        </select>
        <label>所属</label>
        <select class="form-select" name="belong">
            <option value="EFF">EFF</option>
            <option value="ZEON">ZEON</option>
        </select>
    <button type="submit">GET</button>
</form>

上記のようなaction属性とGETのmethod属性を持つformを設置し、
urls.pyで"test_get/"をname="test_get"と設定していたとする。
このようなformを送信すると、
http://example.com/test_get/?gender=male&belong=ZEON にアクセスすることになる。
従ってtest_get/に対するviewをviews.pyで用意しておけば、formから送信された内容を反映したhtmlを返すことが出来る。

test_get/の後ろにGETの内容がくっついているため、urls.pyで"test_get/"に対するルーティングを設定するだけではエラーが出そうに感じたが、?以下はGETの内容だと勝手に判断されるようで、問題はなかった。

プロジェクトが複数のアプリを含む場合は、URLのnameが被ることがありうるため、名前空間の設定をしておく必要あり。

docs.djangoproject.com

templateに特に渡したい値がない時

GETやPOSTで取得した値によってHTMLの内容を変化させたい場合は、まずviews.pyに、

def test_get(request):
    template = loader.get_template("testapp/test_get.html")
    context = {
        "gender" : M,
        "belong" : ZEON,
    }
    return HttpResponse(template.render(context, request))

のような関数を作ってurls.pyでURLと結びつける。
その後テンプレート側で{{ gender }}のように記載すると、cotextの中身にアクセスできるようになり、晴れてGETの値を反映した簡易的な動的ページが完成する。
逆にいつも同じHTMLを返す静的ページを作りたい場合は、contextとしてNoneを渡しておけばOK。

テンプレート | Django ドキュメント | Django

ちょっと複雑なクエリを実行したい時

Djangoからデータベースのデータにアクセスしたい時は、例えば

Test.objects.filter(name="クワトロ")

とすると、QuerySetオブジェクトが取得できる。このQuerySetオブジェクトからはfor文で個別のデータに分離することができる。
引数を増やしてfilterを実行することも可能だが、残念ながらfilterの引数は、ANDによる結合しかできない。

#filter | QuerySet API reference | Django documentation | Django

OR検索や、より複雑な条件での検索がしたい場合は、Qオブジェクトを使うと実現できる。

#Q() objects | QuerySet API reference | Django documentation | Django

#Complex lookups with Q objects | Making queries | Django documentation | Django

Qオブジェクトを使うと検索条件を使いまわしたり、特定の場合に検索条件を減らしたり、複数の検索条件をANDとORで組み合わせたりとクエリ発行の自由度が上がる。

#例:GETでcountryとgenderの値を受け取る。
#country="all"の時はcountryを検索条件から外す。
#views.py
...
get_country = response.GET["country"]
get_gender = response.GET["gender"]
q_counry = Q(country=get_country)
q_gender = Q(country=get_gender)
if get_country=="all":
    q_counry = Q()

result_queryset = Test.objects.filter(q_country & q_gender)
...

続く

本日はここまで。