diadia

興味があることをやってみる。自分のメモを残しておきます。

サーバーに画像を送信する

djangoのimagefieldはメディアのファイルパスを記録するだけとも言えるので、ファイルパスをフィールドに記録したらあとはメディアのディレクトリに画像を配置すればちゃんと表示される。
で今の段階はサーバーに画像を送信する段階だ。調べてみるとzipファイルで送るのがよさそうだと分かった。
当初は画像を一件ずつ送って時間もデータ容量も大きなコストになると思われていたが、zipにすると時間が節約できるらしい。またzipにしたからと言って画像の質が下がるわけでもないらしい。そういうことでzipで送ろうと思う。

今疑問はzip化するとどれだけデータ容量が削減されるのか。サーバーに送るプランとしては、画像をzip化してscpで送る。windowsってscpコマンドを使うためにはgit for windowsを利用するとよい。

DRF ImageFieldのファイルパスを登録したい

DRFでImageFieldにデータを登録したい

なんとなくだけどModelSerializerを使ってもimageFieldに登録できないような気がしてきた。その辺を調べてみる。

資料:https://www.django-rest-framework.org/api-guide/fields/#file-upload-fields
なんかファイルやイメージに関しては特別な扱いを要する、ということが分かった。DRFで準備したものは使わずdjangoで準備したFILE_UPLOAD_HANDLERSを使ってファイル類はアップロードしろ、と。それ以外よくわからないが。。。

ドキュメントをゴリゴリ読んでいくのが良いのか、先陣を切った方のサンプルコードを参考にするべきか、どうしよう。。。

https://www.techiediaries.com/django-rest-image-file-upload-tutorial/
これのAPIViewのサブクラスにparser_class = (FileUploadParser,)とアトリビュートを定めていた。ドキュメントで"The FileField and ImageField classes are only suitable for use with MultiPartParser or FileUploadParser. Most parsers, such as e.g. JSON don't support file uploads. Django's regular FILE_UPLOAD_HANDLERS are used for handling uploaded files."と書いてあるのでファイル系を使うときはMultiPartParser or FileUploadParserをアトリビュートに定めることが必要なのかもしれない。まだ仮説段階。

資料:https://murabo.hatenablog.com/?_ga=2.75217554.1072598020.1559131999-1895290581.1558032931&page=1521708738

DRF JSONデータの成形

成形する必要性

django rest framework の場合request.POSTには注意事項がある。
それはキーに対する値が空(ブランク?null?)の項目はDRFはエラーを吐き出す。
辞書型データに例えて説明すると、ex_dict["key1"]=""の要素を含むデータをrequests.POSTするとエラーが出るという意味だ。
したがってJSONデータをPOSTする前に空データを削除する工程を加える必要がある。これはJSONデータに変換してから該当要素を削除するよりも、辞書型データの段階で空の値を検出して当該データから削除し、JSONデータに変換する方法をとるのがおすすめだ。

以下のコードは実装例になる。

実装例

前提はcsvファイルからJSONを生成する。

fieldnames = ("id","title","length","datetime")

with open(INPUT_FILE,"r",encoding="utf-8") as f:
	reader = csv.DictReader(f, fieldnames)
	for row in reader:           # rowはdictデータ
		for key, value in list(row.items()):
			if value == "":                 # valueが空の場合にdictデータからその要素を削除する
				del(row[key])

		headers = {'content-type': 'application/json'}
		URL     = "http://127.0.0.1:8000/api/"

        data    = json.dumps(row)          # dict型データをJSONに変換する
		res     = requests.post(URL, data=data, headers=headers)
		print(res)

エラー:AssertionError: You cannot call `.save()` on a serializer with invalid data.

AssertionError: You cannot call `.save()` on a serializer with invalid data.

上記のエラーが表示された。
エラーが出た状況:
インスタンスを生成することを目的にdjango rest apiでrequests.post()をする。するとdjango アプリケーションで上記のエラーが表示された。
詳しくは、csvファイルをcsv.DictReaderを使ってdict型に変換する。変換したものをjson.dumps()関数を使った戻り値をdataとし、requests.post(URL, data=data)を投げる。しかしながらエラー発生。

具体的なコード

import csv, sqlite3, json, requests
INPUT_FILE   = "/Users/*****/*****/myjson_test.csv"


fieldnames = ("id","title","length","datetime")
with open(INPUT_FILE,"r",encoding="utf-8") as f:
	reader = csv.DictReader(f, fieldnames)
	for row in reader:
		headers = {'content-type': 'application/json'}
		URL = "http://127.0.0.1:8000/api/"
		res = requests.post(URL, data=json.dumps(row), headers=headers)

エラーの原因を探る

REST apiの設計にエラー原因があるか?

適切にRest APIの設計はできているか。不適切だからエラーが発生しているのではないか?
この問に対して、django rest frameworkの設計によるエラーではないことが判明した。

test = {}
test["id"] = "1234567890123"
test["title"] = "test_title"
headers = {'content-type': 'application/json'}
URL = "http://127.0.0.1:8000/api/"

res = requests.post(URL, data=json.dumps(test), headers=headers)
print(res)

上記のコードが通り、django adminにおいてインスタンスが作成されたのを確認できた。したがって他が原因であることが判明した。

csvファイルをcsv.DictReader()で辞書型にするのに原因があるのか?

上記のデータtestは実際にdict型のデータをjsonのデータに変換している。しかしながらエラーが出たコードはcsvをdict化しているのであってdict型データではない。dict型データでないモノをjson化した際にエラーが発生の原因となってしまう可能性が考えられる。そこで直接的な原因検証ではないがtype()関数を使いデータ型を確認してみた。

test = {}
test["id"] = "1234567890123"
test["title"] = "test_title"
headers = {'content-type': 'application/json'}
URL = "http://127.0.0.1:8000/api/"
res = requests.post(URL, data=json.dumps(test), headers=headers)
print(type(test))
print(type(json.dumps(test))
#結果
<class 'dict'>
<class 'str'>

つづいてエラー原因のコードを検討する。

import csv, sqlite3, json, requests
INPUT_FILE   = "/Users/*****/*****/myjson_test.csv"

fieldnames = ("id","title","length","datetime")
with open(INPUT_FILE,"r",encoding="utf-8") as f:
	reader = csv.DictReader(f, fieldnames)
	for row in reader:
		headers = {'content-type': 'application/json'}
		URL = "http://127.0.0.1:8000/api/"
		res = requests.post(URL, data=json.dumps(row), headers=headers)
		print(type(row))
		print(type(json.dumps(row)))
# 結果
<class 'collections.OrderedDict'>
<class 'str'>

やはりdict()関数とcsv.DictReader()関数でデータ型は厳密には異なるようだ。しかしながらjson.dumps()の返り値はstr型なので適切にjson型のデータに変換したと暫定的にみなす。

jsonデータに空の値がある場合どのような挙動になるか?

REST apiにおいて受け渡されるJSONデータのキーに対する値が空である場合、適切にインスタンスが生成されるか検証していなかった。空の値の場合を検証する。

test = {}
test["id"] = "1234567890123"
test["title"] = "test_title"
test["length"] = ""    # 空の値を設置
headers = {'content-type': 'application/json'}
URL = "http://127.0.0.1:8000/api/"

res = requests.post(URL, data=json.dumps(test), headers=headers)
print(res)

上記のようにしたところエラーが発生した。

AssertionError: You cannot call `.save()` on a serializer with invalid data.

当初と同じエラーが出てきたので、これが原因だと推定できる。したがって値が空の場合についてどう対処するか考えていく。

jsonデータから空の値のデータは削除してrequests.postしたところインスタンスを生成することができた。対処法はここを参照。しかしながらできないものも出てきた。
結論から言うとmodelsのCharField max_lengthが120に対しそれ以上のものを登録しようとしていた時に同じエラーが起きた。これに対してはmax_lengthをさらに大きくしたところエラーをはかずすべてインスタンスを生成することができた。

 

まとめ:エラーAssertionError: You cannot call `.save()` on a serializer with invalid data.の原因

まずJSONのキーに対して値が空の場合にこのエラーが出てしまう。
さらに加えてCharFieldのmax_lengthを超えた文字数を登録しようとした場合に同じエラーが出てしまうことが分かった。

追記:その他の原因としてImgaeFieldにファイルパスだけ登録しようと、文字列を渡しても同じようにAssertionError: You cannot call `.save()` on a serializer with invalid data.が出る。

エラー:json.decoder.JSONDecodeError: Extra data

エラー内容とエラーが出た経緯

Traceback (most recent call last):
  File "execute.py", line 38, in <module>
    json.load(f)
  File "/anaconda3/lib/python3.6/json/__init__.py", line 299, in load
    parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, **kw)
  File "/anaconda3/lib/python3.6/json/__init__.py", line 354, in loads
    return _default_decoder.decode(s)
  File "/anaconda3/lib/python3.6/json/decoder.py", line 342, in decode
    raise JSONDecodeError("Extra data", s, end)
json.decoder.JSONDecodeError: Extra data: line 20 column 2 (char 3707)
</module>

経緯:JSONモジュールをいじった際に生じた。
csvファイルからcsv.DictReader()によりdict型のデータを生成した。そのdict型データをjson.dump()関数を用いてJSONファイルに変換した。このファイルは複数のデータがJSON形式で格納されている。そしてこのJSONファイルをjson.load()関数で読み込もうとした。その際に上記のエラーが生じてしまった。

同じエラーに対処しているもの:
https://qiita.com/takugenn/items/b78f3c3bb34c5de5b5f8#json

自己検証の結果、自分の場合json.load()に渡すjsonデータが多すぎるとExtra dataとなるようだ。json.load()に渡す引数をJSONデータ一つずつイテレートするとエラーを避けることができた。

結論

json.load関数を使う場合一つずつ読み込まなければならない。

csvファイル 辞書型データ jsonモジュールメモ

https://qiita.com/motoki1990/items/0274d8bcf1a97fe4a869 https://docs.python.org/ja/3/library/csv.html#csv.DictReader https://qiita.com/pscreator/items/6040bddbbf9e9c05f77a http://hktech.hatenablog.com/entry/2018/09/10/235725

djangoRest APIを使う場合にはインプット情報はJSON形式でなければならない。そして現在sqlite3を使っている。調べてみたらsqlite3からjson形式で直接ファイルの出力は用意されていない?ようでcsvに出力したデータをjson形式に変換しようと試みている。

そういうわけで上記のURLがcsvからjsonに変換する資料を控えておいた。

 

jsonモジュールを使えるようにしてみる

jsonモジュールをある程度使えるようにならないといけないことが発覚したので、ここに使い方で気になった点をメモする。

つまりjsonモジュールとは?

jsonモジュールはpythonの組み込み型のデータとJSONデータの相互変換する機能を提供する。つまり、JSONデータをpythonで扱いたい時とpythonで組み立てたデータをJSONデータとして出力したい時に使われるモジュールだ。

JSONpythonのデータ型に変換する
json.loads(JSON文字列)
#または、
json.load(ファイル)

引数によって使う関数が異なることに注意すること。
apiでPOSTするために使いたいのであまりこの関数は使わないかもしれない。apiのgetを使う際もrequestsオブジェクトにjson()メソッド使うし本当に使うか不明...。

 

pythonのデータ型をJSONに変換する
json.dumps(pythonオブジェクト)
# または
json.dump(pythonオブジェクト)

dumpsの場合はJSON文字列に変換するだけである。dumpの場合はJSON文字列に変換してファイルに書き出す。この違いがある。

django migration

makemigrations でpostgresql上にテーブルが作られるのか、またはmigrateでテーブルが作られるのか?

 

該当するドキュメント

疑問に思うテーマについてdjangoドキュメントがある。こちらを見てみる。
https://docs.djangoproject.com/ja/2.2/topics/migrations/#workflow

python manage.py migrate

migrateコマンドをたたいた時に初めてデータベースのテーブルのカラム構成に変更が加えられるようだ。
この変更は各アプリのmigrationsディレクトリに存在するマイグレーションファイルをもとに行われる。言い換えればマイグレーションファイルはテーブルの設計書の位置づけだ。

python manage.py makemigrations

このコマンドによってマイグレーションファイルが作成される。
マイグレーションファイルが作成されるときは、models.pyのモデルと最新のマイグレーションファイルが不一致の時でモデルに即したマイグレーションファイルが作成される。

メモ:https://stackoverflow.com/questions/28035119/should-i-be-adding-the-django-migration-files-in-the-gitignore-file

migrationsファイルについて理解を深める

特に調べる必要もなかったのでスルーしてきたが、調べる機会があったのでメモしておく。
ドキュメント:https://docs.djangoproject.com/ja/2.2/ref/migration-operations/#module-django.db.migrations.operations

マイグレーション操作は大別して3つの種類ある。

  • スキーマ操作
  • 特別な操作
  • 自分で操作を書く

マイグレーション操作はどう使う?

結論はoperationsリスト内の一つの要素としてマイグレーション操作を記述する

スキーマ操作の一つCreateModelを見れば理解が深まる。

https://docs.djangoproject.com/ja/2.2/ref/migration-operations/#createmodel

新たにContactモデルを作成後 python manage.py makemigrationsコマンドを実行する。するとcontacts/migrations以下に0001_initial.pyが作成される。中身は以下になる。

from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Contact',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('client_name', models.CharField(max_length=40)),
                ('subtitle', models.CharField(max_length=100)),
                ('message', models.TextField()),
                ('telephone_number', models.CharField(max_length=30)),
                ('email_address', models.EmailField(max_length=254)),
                ('datetime', models.DateTimeField(auto_now=True)),
            ],
        ),
    ]

このファイルに基づいて実際のテーブルが作成される。0001_initial.pyにはoperations内にCreateModelが書かれていることから、要するに実際にデータベースやテーブルに加える操作はoperationsに書くことだと推定することは容易だろう。 つまりデータベースのテーブルを変更をしたいなら、operationsのリストに、変更内容(migrations内の各クラスオブジェクト)を記述する。

IPにかかわるものについて

IPアドレスは普通REMOTE_ADDRという変数名で取得できる。

しかしロードバランサやプロキシを介した通信になると、REMOTE_ADDRがロードバランサ等のアドレスになる。多くのロードバランサ等では、HTTPのリクエストヘッダにX-Forwarded-Forフィールドを追加する。

 

REMOTE_ADDR...IPレイヤの情報

X-Forwarded-For...HTTPレイヤの情報

データ取得先が変わるので値(IP)の取得方法も変わる。

django用に.gitignoreを設ける

.gitignoreを設けないとどうなるのか?

まずソースコードgithub,bitbucketからクローンして手に入れる。そしてそのままmigrateするとコマンドが通らない。おそらくこれはmigrations以下の.pyファイルやキャッシュファイルが存在しているかと思われる。実際削除すると動くからそのへんが関係あるのでしょう。そういうわけで.gitignoreを設けることが必要になる。

大前提

gitignoreファイルの作成はまずローカルの環境で行わう。ここがはっきりと意識できていなかったため無駄な作業を多くしてしまった。

じゃあ.gitignoreファイルはプロジェクトのどこに配置するべきか?

gitignoreはどこに配置するべきか?このどこに配置するべきかは重要なのか?これについて情報を集めておきたい。

参考文献

https://qiita.com/anqooqie/items/110957797b3d5280c44f

gitignoreに記述するファイルはdjangoの場合何を書くべきか?

まずキャッシュファイル

django-allauthを使ってメールを送信を絡めてユーザ登録したい

前提

sendgridを使ったユーザー登録を実行

emailを送信するためにはsettings.pyをいじると送れることは知っておくこと。
ここの設定によってemailの送信を例えばgmailから送ったり、sendgridのようなメール配信サービスのAPIを利用して送ったり、またはメールをターミナルに送信して確認することができる。

情報収集

まず本家のドキュメントを見ると,

ACCOUNT_EMAIL_REQUIRED (=False) The user is required to hand over an e-mail address when signing up. ACCOUNT_EMAIL_VERIFICATION (=”optional”) Determines the e-mail verification method during signup – choose one of "mandatory", "optional", or "none". When set to “mandatory” the user is blocked from logging in until the email address is verified. Choose “optional” or “none” to allow logins with an unverified e-mail address. In case of “optional”, the e-mail verification mail is still sent, whereas in case of “none” no e-mail verification mails are sent.

ACCOUNT_EMAIL_REQUIREDをTrueにする事が必要そうだ。
次にACCOUNT_EMAIL_VERIFICATIONには、値としてoptional,mandatory,noneがある。メール受信できたかどうか知りたいのでmandatoryにすれば良いと思われる。

気になること

sendgridを使ってメールを送信するのでユーザー登録時(sign up時)のviews.pyにsend_mail関数を付け加える必要があるのか無いのか?django-allauthのviewsの仕組みを理解する必要がある。

結論

django-allauthにメール送信機能も備え付けてあるため、自分でsend_mail関数を使って送信する必要がない。単純にsettings.pyにmail受信の確認設定さえすれば良い。

sendgridに関わるメモ

メモ

さくらVPSからsendgridを使うとお得。
https://vps-news.sakura.ad.jp/sendgrid1/

https://simpleit.rocks/python/django/adding-email-to-django-the-easiest-way/

上記リンクについて補足 
djangoのメール送信はsend_mailで送信できるが、sendgridを使う場合当該ライブラリをインストールしてsettings.pyを指定の通りに変更するだけでsendgridからメールを送信する状態に切り替えることができる。

どうやってメモ本文をhtml形式にして表示させることができるのか

 

ホワイトリストとは

https://salt.iajapan.org/wpmu/anti_spam/universal/measure/whitelist-blacklist/
メール受信の選別基準のようだ。受信するか否かの判断をブラックリスト,ホワイトリストで定めるようだ。ホワイトリストは指定した人からのみメールを受信する構造でブラックリストは指定された人からのメールは受信しないようにする構造らしい。

 

メールの見出し字大きさ指定とかしたい場合

まずdjangoにはメール送信に関わるスクリプトが準備してある。メールを送信するならsend_mail()関数を利用すれば良い。djangoのsend_mail()関数の引数の本文は文字列なので、いろいろ細工したいときには不都合だと感じた。
そこでいかなる方法を取れば単なる文字列から細工した状態に、もっと欲を言えば、htmlを反映したメール本文を表示できるか調べてみる。

考えられる方法として引数にhtmlファイルを渡すという方法が考えられる。方法としてsendgridにテンプレートを作り、それを送信する。引数にjsonのデータを使うことでテンプレート内容も編集可能のようだ。下記参照。

https://qiita.com/kikutaro/items/6513acd7420ee88839e9

その他の方法としてrender_to_string関数を用いるとテンプレートにカスタマイズできるようだ。下記参照
https://narito.ninja/blog/detail/64/
https://docs.djangoproject.com/ja/2.2/topics/templates/#django.template.loader.render_to_string

from django.template.loader import render_to_string
from django.core.mail import send_mail

subject = "件名"
context = dict()
context["client_name"] = client.name
context["orderId"] = orderId
template = "orders/mails/order_cofirm.txt"
message  = render_to_string(template,context)
recipient_list = []
recipient_list.append(client.email)
send_mail(subject, message, from_email, recipient_list)

変数templateはdjangoでテンプレートを指定するやり方で指定する。これはrender_to_strung関数の動きがテンプレート探索の方法をとっているからだ。つまり上記の変数templateを定めるならばorder_confirm.txtのパスは以下になる。project/orders/templates/orders/mails/order_confirm.txt

 

sendgridで改行コード"¥n"を使いたい

https://stackoverflow.com/questions/38158192/unable-to-get-a-single-linebreak-while-sending-email-through-sendgrid/38159026

上記によるとsendgridの仕様による影響で改行コードが反映されていないようだ。これについては以下を見るともっと分かりやすい。

https://qiita.com/Sekky0905/items/955a73e51ceb1671d4af

sendgridの仕様では、string型の文字列からhtmlに変換したものをメールとして配信するようだ。
”””Turn on if you don't want to convert your plain text email to HTML”””settings/mail settings/ plain content参照。 自分の設定もoffになっていたのでonにして試してみる。試した結果無事に改行が反映された文面でメールを送ることができました。

windowsでpython のgoogletransをインストールする方法

googletransはエラーが出る

よく考えられるpip installでgoogletransをインストールするとエラーが出て使えない状況に陥る。この状況にはパッチされた新たなgoogletransをインストールすることで解決できる。

環境

  • windows10
  • anaconda

やり方

https://stackoverflow.com/questions/52455774/googletrans-stopped-working-with-error-nonetype-object-has-no-attribute-group
上記のanswer通りgit cloneすればよい。しかしながらwindowsではmacとは違いgitはインストールされていない。したがってgitコマンドが存在しない。

そのためgit for windowsを使う方法も考えられるが、以下のようにして対処した。以下の方法はgitが全く分からなくても何しているか想像できる。

パッチされた新たなgoogletransのソースコードgithubから取得する。https://github.com/BoseCorp/py-googletrans

上記urlからclone or downloadボタンからソースコードをダウンロードする。

次にコマンドプロンプトを開き、googletransがあるディレクトリに移動する。

# 例えばデスクトップにグーグルトランスをダウンロードしたなら
C:\Users\USER\Desktop>cd py-googletrans

そして以下のコマンドを入力する。

pip install setup.py

これでグーグルトランスを使えるようになりました。

セキュリティ

不正アクセスの種類

ほとんどの不正アクセスSSH,FTP,HTTP/HTTPSであるらしい。セキュリティを上げるためにはこれらの設定を整えれば良いらしい。

httpに関しては海外IPアドレスからのアクセスを遮断する方法が有効であるらしい。

参考:https://www.cyberbrain.co.jp/news/security/01/
参考:https://www.seohacks.net/basic/terms/htaccess/
上の参考資料によると、http,httpsのアクセスは.htaccessで制限できるようだ。ただしこれはapacheでの使用を前提にしている記事を読んだので、nginxで.htaccessを使用できるかは不明である。

apache.htaccessをnginx用に変換するサービスが有るようだ。下記参照
https://teratail.com/questions/7098

nginxでは一般的にどのようにしてIP制限をするのだろうか

こちらを参考すると/etc/nginx/conf.d/project.confの設定ファイルを編集するだけでIP の制限ができるようだ。
https://qiita.com/ShinyaOkazawa/items/50b6ba4dcc8cb116ca91

allowにアクセスを許可するIPアドレスを記入する。deny allで全てを拒否する設定になる。

注意点

ipにはグローバルIPとプライベートIPが存在している。nginxの設定ファイルで必要としているIPはグローバルIPだ。プライベートIPはローカルのマシンから調べることができるが、この値を使ってはならない。グローバルIPは以下にアクセスすると自分のIPを把握することができる。
http://www.axisnetworks.biz/tools/gip/