新しく個人ブログを立ち上げました
その名も元婚活戦士ダイキっち
婚活ネタを書き連ねていきます。
マッチングアプリ作成記事もこちらにお引越しします。
是非ご覧ください。
買ってよかった2023かきかきします~
ずばり、給料3か月分の婚約指輪です
去年の2023年にダイキは結婚したのですが、その時に結婚指輪を購入しました
その時に買った給料3か月分の指輪が、本当にイマ思っても買ってよかったものだなと思っています
ダイヤに関しては、調べると、ダイヤ自体の価値って右肩下がりになるような未来しか想定されていないようですね
自分の時期ではダイヤの値段が高騰していましたが、人工ダイヤモンドの技術革新があって、それとの差別化ができなくなってきているようですね
その部分に関しては、理解していました
しかし、それでもなお買いました
なぜなら、ダイヤが大事だからではないからです
僕が大事なのは妻です
その妻との思い出が一番大事だからです
給料3か月というエピソードこそが最も尊いものだと感じたからです
そして、実際に買ってみてその通りだったと信じています
ちなみに、これはプロポーズの後に知ったことですが、プロポーズにダイヤモンドを女性に贈るのは、ダイヤ売り会社のマーケティング戦略らしいですね
僕はまんまとその戦略に乗せられたようですが、全然それでよかったなと思います
むしろそういった王道ストーリーを用意してくれてよかったなと思います
給料3か月分とかそういうのも非常に王道で僕はこういうの好きです
しかし、最近だと給料3か月って言うのをやっている男性は少ないらしいですね
この辺の流れは、指輪を買いに行くまで知らなくて、周囲の男性はあんまし金使わないのかというのに驚きました
周囲の男性は一斉一大のイベントなんだから頑張ったらいいのにと思いますが、そのムーブメントのおかげで自分のやっていることが余計に妻想いのエピソードになったので、むしろありがたいかなと今では思っています
やはり、自分のしたことはすごく漢気を見せられたと思います
やはり男はかっこつけてなんぼですなと思いました
あと、ダイヤの値段が高騰している時期でしたが、自分は滑り込みセーフでまだ価格が爆上がりしていない頃のダイヤを買うことができました
なので、値段の割にしょぼすぎるしょぼぼぼんというようなダイヤでもありませんでした
かなりいいものです
個人的にめちゃくちゃいいものを買えたと思っています
すごくきれいで、そのダイヤ一生見てられるくらいきれいでした
他のダイヤと違って何かが違うと思ってそれにしましたが、やっぱりなんか違うんですよね
僕たちに買われるためのダイヤだったんだろうなと思っています
これから、プロポーズを控えている男性は、がんばれるなら背伸びしましょう
きっとものすごく喜んでくれるはずです
健闘を祈ります
▲ ここに「買ってよかった2023」を書こう
Three.jsで何か作ろうかなーとずっと思っていましたが、パッと思いついてできそうなのがおみくじアプリだったので、作りました
こんな感じです!
詳細は別ブログで書くのではてブではこの辺にしときます~
いいかんじでしょ!
Three.jsの学習を記録していきます
Three.jsを使いこなしてゆくゆくは3次元的なサービスを提供できるフロントエンジニアを目指します
Viteを利用します
ja.vitejs.dev
自分はユーザー名に半角スペースを含むため、yarnではコマンドが失敗するためnpmでインストールします
ユーザー名の半角スペースでつまずく場合の参考:https://qiita.com/koseidaiki/items/d2be30379a7f05c37f62
npm create vite@latest [プロジェクト名]
純粋なJSで開発する場合はvanilla
vueやreactで開発する場合はそちらを選択します
以下では純粋にJavaScriptで開発する方法で基本的な使い方を記載します
その後基本準備として
その後プロジェクトフォルダに移動して、
npm install three
package.jsonの内容をインストールする
npm install
JSで開発する場合にどうやって使うかですが、JSファイルに以下のモジュールをインポートして使います
import * as THREE from "three";
以下の3つの要素が必要であり、基本形は以下のようになる
1. シーン
2. カメラ
3. レンダー
main.js
import * as THREE from "three"; let scene, camera, renderer; window.addEventListener("load", init); function init() { // シーン scene = new THREE.Scene(); // カメラ(視野角、アスペクト比、開始距離、終了距離) camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, // ブラウザの横幅 / ブラウザの縦幅 0.1, 1000 ); camera.position.set(0, 0, 500) // レンダラ renderer = new THREE.WebGLRenderer({alpha: true}); // 背景を設定する場合は透過度を設定する renderer.setSize(window.innerWidth, window.innerHeight) document.body.appendChild(renderer.domElement); renderer.render(scene, camera); }
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ThreeJs</title> </head> <body> <script src="main.js" type="module"></script> </body> </html>
上記では物体が表示されないので物体を配置したい場合はテクスチャ、ジオメトリ、マテリアル、メッシュを設定する
基本形は以下
main.js
// テクスチャ = 柄 (https://threejs.org/docs/index.html?q=texture#api/en/loaders/TextureLoader) let textures = new THREE.TextureLoader().load("画像のPath") // ジオメトリ = 骨格 (https://threejs.org/docs/index.html?q=geometry#api/en/geometries/SphereGeometry) let ballGeometry = new THREE.SphereGeometry(100, 64, 32) // 例:球形 // マテリアル = 材質 (https://threejs.org/docs/index.html?q=material#api/en/materials/MeshPhysicalMaterial) let ballMaterial = new THREE.MeshPhysicalMaterial({ map: textures // テクスチャを設定 }); // メッシュ = ジオメトリ + マテリアル let ballMesh = new THREE.Mesh(ballGeometry, ballMaterial) scene.add(ballMesh) // 最後にシーンに追加する // 平行光源を設定することで物体を視認することができる let directionalLight = new THREE.DirectionalLight(0xffffff, 2); directionalLight.position.set(1,1,1) scene.add(directionalLight) // 最後にシーンに追加する
また、基本的に動きのある画面を作りたいはずなので、アニメーションを使う
以下のようにアニメーション関数を作って、その中にレンダリングを入れて、変更のたびに再レンダリングをするような実装を行う
main.js
function animate() { // おまじない requestAnimationFrame( animate ); // 動きのロジック - 例えばメッシュ(物体)の位置を変更するアニメーションをつけるなど // ballMesh.position.x += 時間に応じて加算 // レンダラをこちらに入れる renderer.render( scene, camera ); } // アニメーションを実行 animate();
ところで、これらにはバリエーションがありそのユースケースは以下のようになる
シーンについては基本的にバリエーションはなく思考停止で
scene = new THREE.Scene();
を使用すればよい
https://threejs.org/docs/index.html#api/en/scenes/Scene
カメラについては2つほどバリエーションがある
自分のアプリをどのように見せたいかを考えて適切なカメラを選択するとよい
1. PerspectiveCamera
遠近法(遠くのものは小さく)が適用される一番直観的な普通のカメラ
ユースケースは一番多いと思われる
https://threejs.org/docs/index.html#api/en/cameras/PerspectiveCamera
2. OrthographicCamera
遠近法が適用されないカメラ
遠くのものも近くのものも同じ大きさに見える
https://threejs.org/docs/index.html#api/en/cameras/OrthographicCamera
カメラや物体のコントロール制御をできる簡便な機能がある
基本的には一番直観的なOrbitControlsを使っておけばよい
https://threejs.org/docs/index.html?q=Controls#examples/en/controls/OrbitControls
他一人称視点でコントロールしたいときは、FirstPersonControlsがある
https://threejs.org/docs/index.html?q=Controls#examples/en/controls/FirstPersonControls
Three.jsでは光源を必要とするマテリアル(MeshPhysicalMaterial)と必要としないマテリアル(MeshBasicMaterial、MeshNormalMaterial)があり、光源を必要とするマテリアルを使う場合に光源を設定する必要がある
光源にも種類があり、およそ4パターンほどある
1. 全体光源 AmbientLight
全方向から均一の光が当たる光源です
ちなみにThree.jsでは同じ物体の表裏の影はシミュレーションで表現されるが、他の物体に光がさえぎられても影ができることは基本的になかったり、他の物体から反射する光などはシミュレーションされていないので、物体の裏側の影の強さを表現することに使ったりすることができます
https://threejs.org/docs/index.html?q=Light#api/en/lights/AmbientLight
2. 平行光源 DirectionalLight
名が体を表している光源です
https://threejs.org/docs/index.html?q=Light#api/en/lights/DirectionalLight
3. 半球光源 HemisphereLight
2つの平行光源が互いに逆方向からやってくる光源です色を変えるなどができます
ユースケースは空と大地の対比を表現したいときに便利です
https://threejs.org/docs/index.html?q=Light#api/en/lights/HemisphereLight
4. 点光源 PointLight
とある1点から拡散する光源です
豆電球のイメージです
https://threejs.org/docs/index.html?q=Light#api/en/lights/PointLight
5. スポットライト光源 SpotLight
スポットライトや懐中電灯の光源のイメージです
https://threejs.org/docs/index.html?q=Light#api/en/lights/SpotLight
6. RectAreaLight
スタジオにおいてある四角形に広がる光源です
THREE デフォルトにないパッケージであることに注意が必要です
https://threejs.org/docs/index.html?q=Light#api/en/lights/RectAreaLight
こちらもあまりバリエーションは考えなくていい
WebGLRendererを使う
https://threejs.org/docs/index.html#api/en/renderers/WebGLRenderer
テクスチャーは画像を用意して物体に貼るというユースケースが最も多いと思われる
その場合は思考停止でTextureLoaderを使えばよい
https://threejs.org/docs/index.html?q=texture#api/en/loaders/TextureLoader
ジオメトリがそもそも何なのかというと物体の表面の任意の頂点を結んだ格子上の骨格のことです
不規則な形をとる物体の場合、任意の頂点の数が多ければ多いほど再現度が高くなります
フラーレンのイメージです
ジオメトリは種類が多く、こちらから使いたい出来合いのジオメトリを使うことが便利です
https://threejs.org/docs/index.html?q=geometry
よく使うものとしてはBoxGeometry, PlaneGeometry, SphereGeometry, TorusGeometry あたりだと思います
任意の形を自作する場合はBufferGeometryのベーシッククラスを継承して作成するか他の3Dソフトで作成したものをインポートする必要がある
マテリアルは物体の材質に相当するものです
大きく分けて光源を必要とするものと光源を必要としないものに分かれます
光源を必要としないものはMeshBasicMaterialとMeshNormalMaterialがあります
光源を必要とするものにはMeshPhysicalMaterialがあります
他のパラメータとしては金属光沢や粗さ、色などがあります
パラメータが多くデザインの範囲なのでUIデバッグをつけると便利です
https://threejs.org/docs/index.html?q=material
フラーレンに皮をつければサッカーボールになるイメージです
メッシュも基本的には組み立て作業に相当するためバリエーションは特にありません
思考停止でMeshオブジェクトを生成します
これが物体に相当します
注意点としてはこれを作った後にシーンに追加することを忘れないようにします
例
const box = new THREE.Mesh( new THREE.BoxGeometry(1,1,1), new THREE.MeshNormalMaterial() ) scene.add(box) // これを忘れない
UIデバッグというのはわざわざコードに戻らなくても画面上でパラメータの変更ができる機能のことです
この機能を使うと画面右上にパラメータをいじれるUIが出てくる
lil-guiを使用する
インストール方法は下記
lil-gui.georgealways.com
インスタンスを生成して、パラメータを追加するとすぐに利用できる(CDNを利用)
sceneにメッシュを追加するのと同じ要領
import GUI from 'https://cdn.jsdelivr.net/npm/lil-gui@0.16/+esm'; const gui = new GUI() // boxというオブジェクトを作成 gui.add(box.position, "x") // x軸に沿って動かせるようになる gui.add(box.position, "x", -3, 3, 0.01) // ドラッグで動かせるようになる gui.add(box.position, "x").min(-3).max(3).step(0.01) // こういう書き方もできる
.nameを使用すると名前を付けることができる
THREE デフォルトにないパッケージを使う場合、jsmフォルダ内にある可能性がある
フォルダパス自体は以下
three.js-r[VERSION]\three.js-r[VERSION]\examples\jsm
光源のhelpers関数などがこちらに入っている場合がある
今後は本格的なフロントエンド開発を行っていきたいので、Vue.jsにおけるThree.jsの使い方を調査していこうとおもいます
[追記]
残念ながらVue.js3.0でThree.js開発を行うことは現実的ではない様子
Three.jsをそのまま使う場合、Vueの書き方との相違が多くかなり使いずらいことが分かっている
そのためVueGLというライブラリが用意されているが、それもVue3.0系にはまだ対応しておらず、実質VueでのThree.js開発はあきらめた方が良いことが分かった
最近のフロントエンドはほぼReactによってきつつあるようにも見えるので、まだフロントエンドのどのフレームワークにも染まっていない人はReactを選んでおけば間違いがない気がします(Vue3.0簡単とか言ってるやつ、あれは詐欺です。Vueは書き方がありすぎてもはやブログなどで検索して出てきた情報が全くアテにならなかったりするので、ぶっちゃけ初心者には難しいです。Vueのドキュメントで読んで理解が進められる人はReactでも同じではと思ったりします。サポートされているライブラリもReactの方が多い印象ですし。)
PowerShellのセキュリティまわりの設定が原因である場合、以下の記事に対処法が書いてある
https://qiita.com/ponsuke0531/items/4629626a3e84bcd9398f
hoge.psはyarn.psに読み替える
結論としてはこのコマンドを実行する
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
powershellでyarnを実行させたいときは毎回このコマンドをたたくこと
根本的な解決方法ではなく、毎回コマンドを叩かないといけなくてめんどくさいが他の方法が分からなかったため暫定
※npmを実行できない場合も同様のコマンドで対処が可能となる
※以下が実行できない場合も同様
vue create [プロジェクト名]
できること(抜粋)
講座1
講座2
目次
仮想環境の準備は以下参照
https://shinseidaiki.hatenablog.com/entry/2021/11/07/232602
使うパッケージ requirements.txt
asgiref==3.4.1 certifi==2021.5.30 charset-normalizer==2.0.3 Django==3.2.5 django-environ==0.4.5 idna==3.2 Pillow==8.3.2 pytz==2021.1 requests==2.26.0 sqlparse==0.4.2 stripe==2.60.0 urllib3==1.26.6
requirements.txtをインストールする場合は上記ファイルを任意のフォルダに配置して、
仮想環境になっていることを確認して
以下のコマンドを仮想環境で実行する
pip install -r requirements.txt
なお記事投稿者が記事記入時はDjangoが4にアップグレードされていたため、上記の方法で環境を合わせた
初期準備
django-admin startproject udemyec django-admin startapp base
フォルダの作成
templates, secrets, static
setting.pyのBASE_DIRはpathlibの組み込み変数でプロジェクトのルートパスを差す
BASE_DIR / 'templates' の書き方でもOSに応じて臨機応変にパスを作ってくれる
GitHubにおおよそのことは書いていた
https://github.com/TakumaFujimoto/vegeket_project/blob/main/sec03/README.md
環境変数を扱うためにインストールする
django-environ
secretsフォルダ直下に以下を作成
.env.dev .env.prod
環境に応じた環境変数を用意しておくことで開発を円滑に行うことが出来るようにする
.env.devは開発環境用
.env.prodは本番環境用(productの略)
環境変数に格納する値はクオテーションなしで登録
.env.dev(開発環境)
SECRET_KEY=django-xxxxxxxxxxxxxx DEBUG=True ALLOWED_HOSTS=*
.env.prod(本番環境 - デプロイする場合)
SECRET_KEY=django-xxxxxxxxxxxxxx DEBUG=false ALLOWED_HOSTS=[特定のホスト]
setting.pyに環境変数を適用
import environ env = environ.Env() root = environ.Path(BASE_DIR / 'secrets') # 本番環境 # env.read_env(root('.env.prod')) # 開発環境 env.read_env(root('.env.dev')) SECRET_KEY = env.str('SECRET_KEY') DEBUG = env.bool('DEBUG') ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
rootはプロジェクト直下に.envファイルを置く場合は記述する必要はない
本番環境と開発環境で自動で分ける場合はif文を使って条件分岐をする必要がある模様
具体的な例はわからなかったが、以下が参考になるかもしれない
https://ikura-lab.hatenablog.com/entry/2019/06/16/142339
.gitignoreに関してはこちらの記事などを参照にする
どこかでまとめる必要がある
https://leading-tech.jp/wiseloan/gitignore/
このあたりを記述
*.log *.pyc __pycache__/ secrets/ db.sqlite3
省略
この人のフォルダ構成はtemplatesフォルダ内に以下のフォルダとファイルがある
base.html
pagesフォルダ
snippetsフォルダ
modelsフォルダを作成してその直下に__init__.pyファイルを配置すればフォルダがmodelsとして認識される
modelsが増えてきた場合に有効
フォルダ化にはmodelごとに自由なファイル名のモデルを定義することができる
なお、initiは初回に読み込まれるので、以下のような記述を行っておくとモデルが読み込まれる
__init__.py
from .item_models import *
画像を別のストレージで使用する場合については別途調査が必要
Itemの外部キーのcategoryのondeleteについてはon_delete=models.SET_NULLにした方がよい
なぜならカテゴリ削除と同時にItemを削除したくはないから
カテゴリが削除された場合はNullを詰める
ManyToManyでリレーションをとる場合はtagsのような複数形の名前にする方が慣例の模様
ManyToManyの場合は中間テーブルが内部的に作成されて直接つながっていないためondeleteは設定する必要がない
ここではカスタムユーザーモデルは先に作ってない
https://github.com/TakumaFujimoto/vegeket_project/blob/main/sec06/README.md
admin画面はunregisterを使用すると非表示にすることもできる
クラスベース
class IndexListView(ListView): model = Item template_name = 'pages/index.html'
関数ベースで書いた場合
def index(request): object_list = Item.objects.all() context = { 'object_list': object_list, } return render(request, 'index.thml', context )
for文の中で使える便利なものが存在する
{{ forloop }}
for文が何回回ったかなどを出力できる
def postやdef getをクラスベースビューで記述するとPOSTの時やGETメソッドの時に実行される関数を記述することができる
class AddCartView(View): def post(self, request):
OrderedDict()はPython標準ライブラリで搭載されている
辞書の場合、追加するときにListと違って順番が保持されないために順番を保持するために使用できるメソッド
順序月の辞書が作れる
get_querysetについて
データをDBなどから取得するメソッド
ListViewがget_querysetをもともと持っており、デフォルトではモデルのすべてのアイテムを返すようになっている
カスタマイズする場合はget_querysetをオーバーライドする
また、obj.quantityやobj.subtotal のようにmodelsに定義されていないquantityのような値はviewにおいて新たに定義することのできるインスタンス変数となり、DBには保存されないがプログラムで自由に使用することができるようになる
あらかじめモデルで定義していなくても自由にインスタンス変数を追加することができる
self.queryset = []で定義してフォー分で回して、self.queryset.append(obj)したりしてquerysetを作成するようにして作る
get_context_data
テンプレートに値を渡すメソッド
context["total"] = self.total のような記述をすることでテンプレート上でtotalというキーで値にアクセスすることができるようになる
また以下のようにjsonから辞書型に変換して画面に値を渡すことも可能
context["items"] = json.loads(obj.items)
管理画面者の編集
以下のadminの中のファイルローカルにコピーして上書きすることで編集することが可能(元ファイルは当然ながら編集しない)
https://github.com/django/django/tree/main/django/contrib/admin/templates
例えば、base_site.htmlをtemplateルートフォルダ下(setting.pyのDIRSのところで設定している場所、基本はプロジェクト直下にテンプレートを指定することが一般的)にadminフォルダを作成し、その下にgithub上のdjangoテンプレートと同じフォルダ構成でファイルを配置する
base_site.htmlの内容を上書きするとadminサイトに変更が反映される
CSSを書き換えたい場合は 管理画面はstatic: admin/css/base.cssを継承しているためこのファイルを作成して継承すればいい(CSSなのでstaticフォルダ以下であるところに注意)
Github上の場所は以下
https://github.com/django/django/tree/main/django/contrib/admin/static/admin/css
決済機能についての簡単な説明がある
https://github.com/TakumaFujimoto/vegeket_project/blob/main/sec10/README.md
ここではStripeを利用する
Stripeで開発者向けの開発用の決済機能を無料で試すことができる
Stripe: 世界で最もシェアを獲得するクレジットカード決済代行サービス
各種セキュリティ・法規制に対応しているため決済代行サービスを使わないで課金が必要な自作サービスを実装する選択肢はほぼ皆無だと思われる(特に個人)
使用時の手数料3%以外の費用がないため個人でマッチングアプリを実装したい場合(年齢確認用)などで利用できる
Stripeのドキュメント
Stripe のドキュメント
アカウントの作成
上記ストライプの公式サイトからアカウントを作成する(メール認証があるので認証を済ませる)
アカウントを作成するとダッシュボードに移動する
最初から開発環境を提供してくれている模様
本番環境を利用する場合は別途申請が必要となる
ここではすべて開発環境だけを使用する
Stripeパッケージのインストール
pip install stripe
Stripeドキュメントに記載されるテンプレートを使用する
https://stripe.com/docs/payments/checkout
こちらにサンプルプロジェクトが記載されている
https://stripe.com/docs/checkout/quickstart
講座の時とは内容が更新されている模様
フィルタリングがあるので、HTML PYTHONを選択して今回はPythonのテンプレートを表示して模倣する
テンプレートとしては以下がある
テンプレートはあくまで三本であり、部分的に使用できる場合は、必要な箇所の機能だけを模倣すればよい
templates/pages/success.html
{% extends 'base.html' %} {% block main %} <div class="container my-5"> <div class="row"> <div class="col-12"> <h1>Thank you!</h1> <!-- 注文履歴確認ページ --> <p>注文履歴は<a href="/orders/">こちら</a></p> </div> </div> </div> {% endblock %}
templates/pages/cancel.html
{% extends 'base.html' %} {% block main %} <div class="container my-5"> <div class="row"> <div class="col-12"> <h1>Cancel</h1> <p>うまく処理されませんでした。カートは<a href="/cart/">こちら</a></p> </div> </div> </div> {% endblock %}
環境変数にSecrets情報を格納しておく
今回は開発環境のみなので、.env.devのみを編集する
secrets/.env.dev
STRIPE_API_KEY=[STRIPEのAPIキー] MY_URL=http://127.0.0.1:8000
[STRIPEのAPIキー]は公式のダッシュボードからコピーして貼り付ける
https://dashboard.stripe.com/
ホームの真ん中の方に開発者向けという項目があるので、シークレットキーをコピペする
なお、公開可能キーは自分でカスタマイズした決済ページを実装する場合に必要になってくるものになる
Stripeの決済ページにリダイレクトする今回のような方法の場合ではシークレットキーのみでよい
今使用しているシークレットキーは開発環境用のテスト用シークレットキーとなっており、本番環境で使用する場合は、別途申請を行って本番環境用のシークレットキーを入手して、 .env.prod に本番環境用のシークレットキーをコピペして使用する
設定ファイルにシークレットキーの環境変数を読み込ませる
setting.pyに以下を追記
# Stripe API Key STRIPE_API_SECRET_KEY = env.str('STRIPE_API_SECRET_KEY') # スキーマ&ドメイン MY_URL = env.str('MY_URL')
base/views/pay_views.py
from django.shortcuts import redirect from django.views.generic import View, TemplateView from django.conf import settings from stripe.api_resources import tax_rate from base.models import Item import stripe # Stripeのシークレットキーを読み込む stripe.api_key = settings.STRIPE_API_SECRET_KEY class PaySuccessView(TemplateView): template_name = 'pages/success.html' def get(self, request, *args, **kwargs): # 最新のOrderオブジェクトを取得し、注文確定に変更 # カート情報削除 del request.session['cart'] return super().get(request, *args, **kwargs) class PayCancelView(TemplateView): template_name = 'pages/cancel.html' def get(self, request, *args, **kwargs): # 最新のOrderオブジェクトを取得 # 在庫数と販売数を元の状態に戻す # is_confirmedがFalseであれば削除(仮オーダー削除) return super().get(request, *args, **kwargs) tax_rate = stripe.TaxRate.create( display_name='消費税', description='消費税', country='JP', jurisdiction='JP', percentage=settings.TAX_RATE * 100, # 100分率整数で渡す必要がある inclusive=False, # 外税(税別)を指定(内税(税込)の場合はTrue) ) def create_line_item(unit_amount, name, quantity): return { 'price_data': { 'currency': 'JPY', 'unit_amount': unit_amount, 'product_data': {'name': name, } }, 'quantity': quantity, 'tax_rates': [tax_rate.id] } class PayWithStripe(View): def post(self, request, *args, **kwargs): cart = request.session.get('cart', None) if cart is None or len(cart) == 0: return redirect('/') line_items = [] for item_pk, quantity in cart['items'].items(): item = Item.objects.get(pk=item_pk) line_item = create_line_item( item.price, item.name, quantity) line_items.append(line_item) checkout_session = stripe.checkout.Session.create( # customer_email=request.user.email, payment_method_types=['card'], line_items=line_items, mode='payment', success_url=f'{settings.MY_URL}/pay/success/', cancel_url=f'{settings.MY_URL}/pay/cancel/', ) return redirect(checkout_session.url)
PaySuccessViewは決済が成功した場合のリダイレクトのためのView
PayCancelViewは決済が失敗した場合のリダイレクトのためのViewであり、
実際の決済を行う処理はPayWithStripeとなる
PayWithStripeはpostのみでアクセス可能とする
公式ドキュメントのテンプレートのserver.pyを参考に作成していく
line_itemsはStripeの決済ページに表示させるデータを準備している
載せられるデータは詳しくはドキュメント参考だが、今回載せている'price_data'は価格であり、ドキュメントの'price'よりもより詳しく記載できている
'quantity': quantity, は購入数量
'tax_rates': [tax_rate.id] は任意で載せることができるが、これを載せていると自動で税金計算をしてくれるなど便利になる
checkout_sessionにline_itemsといったデータを追加送信すると決済ページにその情報を表示させることが可能となる
Stripeとの通信に必要な情報はcheckout_sessionを使用するので、これを必ず定義する
checkout_sessionの要素はドキュメント(https://stripe.com/docs/checkout/quickstart)を参照
なお、success_url=f'{settings.MY_URL}/pay/success/',のように{settings.MY_URL}を付与しておかないとStripeの決済画面のURLからの相対パスにリダイレクトしてしまうため、URLにドメインを追加しておく
最後は return redirect(checkout_session.url) でStripe公式がデフォルトで用意している決済画面にリダイレクトする
views.pyではなくviewsフォルダを作成してviewsを実装している場合はviews/__init__.pyのpay_viewsのインポートを忘れないように
from .pay_views import *
'pay/checkout/'のURLエンドポイントはStripeの決済画面にリダイレクトするページとなっている
urls.py
# Pay path('pay/checkout/', views.PayWithStripe.as_view()), path('pay/success/', views.PaySuccessView.as_view()), path('pay/cancel/', views.PayCancelView.as_view()),
サーバーを起動させる
商品を選択してカートからcheckoutボタンを押す
成功させたい場合は4242 4242 4242 4242
支払いに確認を必要とする場合は4000 0025 0000 3155
支払いが拒否される場合は4000 0000 0000 9995
を入力する
カード番号以外は適当な値を入力してかまわない
それぞれのパターンの挙動が正しければ動作確認はOK
これで基本的な決済機能を実装できるようになった
以下のようなエラーが発生した場合は、Stripeのアカウント名を変更する必要がある模様(参考https://note.com/daikinishimatsu/n/n029a7ff01f62)
InvalidRequestError at /pay/checkout/
In order to use Checkout, you must set an account or business name at https://dashboard.stripe.com/account.
ダッシュボードにアクセスし(https://dashboard.stripe.com/)、プロフィール>アカウントのところで「名称未設定」のアカウントと表示されていればアカウント名を登録する
登録方法は 設定(歯車アイコン)>アカウントの詳細>アカウント名 で好きなアカウント名を入力する
保存を押すことでアカウント名は保存される
公式ドキュメント
https://docs.djangoproject.com/en/4.0/ref/views/#error-views
事前準備
setting.pyのDEBUGをFalseにする
DEBUGでない場合はホストALLOWED_HOSTの追加必要 '127.0.0.1'
プロジェクトのurls.pyにステータスコードを設定したハンドラーを追加する
handler404 = views.関数名()
なお、デフォルトではNot Foundエラーやサーバーエラー画面の味気ない画面が出力される
間違ったURLを正しいURLにリダイレクトなどカスタムしたい場合にエラーハンドリングをカスタマズする
例:間違ったURLの場合はリダイレクトする例
プロジェクト側 urls.py
handler404 = show_error_page
views.py
def show_error_page(request, exception): # return render(request, ' 404.html') return redirect('store:home')
Djangoは通常models.pyにカスタムユーザーを使用することを明示せずにマイグレーションをすると、初めのマイグレーションでデフォルトユーザーを作成されてしまう
Djangoの公式ドキュメントに記述されている通りデフォルトユーザーを使用してしまうと後からの変更が困難となるので、基本的には最初からカスタムユーザーを作成しておくことが推奨されている
今回は一度マイグレーションを上記でしてしまっているが、この状態からカスタムユーザーを作成することも一応できる
後からカスタムユーザーを作成する大まかな流れとしては、カスタムユーザーをmodels.pyで作成して、DBを削除して、migrationsフォルダのinit以外のファイルを削除して、cacheも削除して、マイグレーションの初期化を行って、その後もう一度マイグレーションを行う(マイグレーションを一からやり直すことでカスタムユーザーを適用させることができる)
カスタムユーザーの作り方としてはAbstractUserを継承する方法と、AbstractBaseUserを継承する方法の2種類がある
ただし、メールアドレスをログインIDにするなどのことができないため、限定的な利用にとどまる
自由度が高くこちらをカスタムユーザーとするのが一般的
以下はAbstractBaseUserのカスタム実装例
models.pyにドキュメントを参考にしたモデルを作成する
https://docs.djangoproject.com/ja/3.2/topics/auth/customizing/#a-full-example
もしくは下の章で紹介しているカスタムモデルの例を参考にする
AbstractBaseUserを使用する場合は、データ挿入取得更新処理をするマネージャークラスを作成する必要がある
以下のコード例ではclass UserManager(BaseUserManager): の部分である
ほぼお決まりの書き方があり、通常ユーザーcreate_userと管理者ユーザーcreate_superuserの2種類のユーザー新規作成メソッドをオーバーライドする
自力で書こうとするとcreate_superuserを忘れがちなため注意が必要
カスタムモデルのログインIDを変更する場合はUSERNAME_FIELDに使用したいフィールドを記載する
ここでは'username'をログインIDにしているが、'email'を指定することもできる
'email'を指定した場合は、ログインIDにEmailを使用することが可能となる
REQUIRED_FIELDSはただの必須項目の設定である
また、ユーザーの生年月日やニックネームなどの情報はUserモデルに直接フィールドを持たせるよりも分離したProfileモデルに持たせるような実装で作成するのが慣習となっている
Userモデルと1to1のリレーションでテーブル分離したProfileモデルを作成する
models.OneToOneFields(User, ....) を使用すると1対1のリレーションを張ることができる
account_models.py
from django.dispatch import receiver from django.db.models.signals import post_save from django.db import models from django.contrib.auth.models import BaseUserManager, AbstractBaseUser class UserManager(BaseUserManager): def create_user(self, username, email, password=None): if not email: raise ValueError('Users must have an email address') user = self.model( username=username, email=self.normalize_email(email), ) user.set_password(password) user.save(using=self._db) return user def create_superuser(self, username, email, password=None): user = self.create_user( username, email, password=password, ) user.is_admin = True user.save(using=self._db) return user class User(AbstractBaseUser): id = models.CharField(default=get_random_string(22), primary_key=True, max_length=22) username = models.CharField( max_length=50, unique=True, blank=True, default='匿名') email = models.EmailField(max_length=255, unique=True) is_active = models.BooleanField(default=True) is_admin = models.BooleanField(default=False) objects = UserManager() USERNAME_FIELD = 'username' EMAIL_FIELD = 'email' REQUIRED_FIELDS = ['email', ] def __str__(self): return self.email def has_perm(self, perm, obj=None): "Does the user have a specific permission?" # Simplest possible answer: Yes, always return True def has_module_perms(self, app_label): "Does the user have permissions to view the app `app_label`?" # Simplest possible answer: Yes, always return True @property def is_staff(self): "Is the user a member of staff?" # Simplest possible answer: All admins are staff return self.is_admin class Profile(models.Model): user = models.OneToOneField( User, primary_key=True, on_delete=models.CASCADE) name = models.CharField(default='', blank=True, max_length=50) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) # 住所や年齢などアプリに必要なユーザー情報を設定する def __str__(self): return self.name # OneToOneFieldを同時に作成 @receiver(post_save, sender=User) def create_onetoone(sender, **kwargs): if kwargs['created']: Profile.objects.create(user=kwargs['instance'])
@receiverの処理はUserが作成されると同時にProfileモデルのフィールドにもデータを作成する処理となっているが、プロフィールはユーザーがアクティブなってから項目を入力されて新規作成されるものだとも考えられるので、この処理はあってもなくてもよい
ただこの処理があるとUserが作成された際にProfileが作成されるためProfileの新規作成処理を別途作成する必要がなくなる
カスタムユーザーモデルを使用する宣言を行うために以下のコードを追加する
setting.py
AUTH_USER_MODEL = 'base.User'
※baseの部分はアプリ名
この設定を忘れるとDjangoのデフォルトユーザーモデルが作成されてしまう
またログインのURL情報や、ログイン成功時のリダイレクトURLもsetting.pyに記載することができる
ログアウトも同様
setting.py
LOGIN_URL = '/login/' LOGIN_REDIRECT_URL = '/' LOGOUT_URL = '/logout/' LOGOUT_REDIRECT_URL = '/login/'
base/forms.pyを作成する
from django import forms from django.contrib.auth import get_user_model class UserCreationForm(forms.ModelForm): password = forms.CharField() class Meta: model = get_user_model() fields = ('username', 'email', 'password', ) def clean_password(self): password = self.cleaned_data.get("password") return password def save(self, commit=True): user = super().save(commit=False) user.set_password(self.cleaned_data["password"]) if commit: user.save() return user
clean_passwordはバリデーション
不正なパスワードはここで弾かれる
admin.py
from base.forms import UserCreationForm from django.contrib.auth.admin import UserAdmin class ProfileInline(admin.StackedInline): model = Profile can_delete = False class CustomUserAdmin(UserAdmin): fieldsets = ( (None, {'fields': ('username', 'email', 'password',)}), (None, {'fields': ('is_active', 'is_admin',)}), ) list_display = ('username', 'email', 'is_active',) list_filter = () ordering = () filter_horizontal = () add_fieldsets = ( (None, {'fields': ('username', 'email', 'is_active',)}), ) add_form = UserCreationForm inlines = (ProfileInline,) admin.site.register(User, CustomUserAdmin)
ちなみに行を分けると管理画面上の表示が改行されて見やすくなる
fieldsの中身は列挙すると順に横に表示されていく
fieldsets = (
(None.......), # 1行目表示
(None.........), # 2行目表示
)
またこの例ではInlineを利用して、ProfileをUserの管理画面に表示させている
管理者画面でのユーザー追加にはforms.pyで作成したUserCreationFormを指定することで管理者画面でユーザーを作成することができる
またこの例ではadd_fieldsetsにpasswordのフィールド項目が抜けているように思われる
アプリ(base)下のmigrationsフォルダ下の__init__.py以外の0001_initial.pyなどのファイルをすべて削除する
同様に、migrationsフォルダ下の__pycache__フォルダ下も同様にinitだけ残して削除する
また、データベースも削除するので、DjangoのデフォルトのSQLiteを使用している場合はdb.sqlite3を削除するだけでよい
別のデータベースを使用している場合はそのデータベースの中身を削除するか、setting.pyのデータベースの接続設定などを変えるなどしてDBを新規作成する
初めてのマイグレーションかもしくはマイグレーション初期化が終わったら、マイグレーションを行って、先ほど作成したカスタムユーザーを適用する
py .\manage.py makemigrations py .\manage.py migrate
管理者を作成できるか確認し、管理者画面にログインできるか確認する
py .\manage.py createsuperuser
ログイン・新規登録兼用画面
login_signup.html
{% extends 'base.html' %} {% block main %} <div class="container my-5"> <div class="row "> <div class="col-12"> <h1> {% if 'login' in request.path %} Login {% elif 'signup' in request.path %} Signup {% endif %} </h1> <form method="POST"> {% csrf_token %} <div class="form-row"> <div class="form-group col-md-4"> <input type="text" class="form-control" name="username" placeholder="Username"> </div> </div> <div class="form-row"> <div class="form-group col-md-4"> <input type="email" class="form-control" name="email" placeholder="Email" required> </div> </div> <div class="form-row"> <div class="form-group col-md-4"> <input type="password" class="form-control" name="password" placeholder="Password" required> </div> </div> <button class="btn btn-info btm-sm" type="submit"> {% if 'login' in request.path %} Login {% elif 'signup' in request.path %} Signup {% endif %} </button> </form> </div> </div> </div> {% endblock %}
ユーザーが自身のアカウント情報を変更することができる画面を作成する
account.html
{% extends 'base.html' %} {% block main %} <div class="container my-5"> <div class="row"> <div class="col-12"> <h1>Account</h1> <form method="POST"> {% csrf_token %} <div class="form-row"> <div class="form-group col-md-4"> <label>Username</label> <input class="form-control" type="text" name="username" placeholder="name" value="{{user.username}}"> </div> </div> <div class="form-row"> <div class="form-group col-md-4"> <label>Email</label> <input class="form-control" type="email" name="email" placeholder="Email" value="{{user.email}}" required> </div> </div> <button type="submit" class="btn btn-primary">Save</button> </form> </div> </div> </div> {% endblock %}
プロフィールを変更することのできる画面を作成する
profile.html
{% extends 'base.html' %} {% block main %} <div class="container my-5"> <div class="row"> <div class="col-12"> <h1>Profile</h1> <form method="POST"> {% csrf_token %} <div class="form-group "> <label>Name</label> <input class="form-control" type="text" name="name" placeholder="name" value="{{user.profile.name}}"> </div> <div class="form-row"> <div class="form-group col-md-2"> <label>フォーム入力値1</label> <input class="form-control" type="text" name="zipcode" placeholder="zipcode" value="{{user.profile.zipcode}}"> </div> <button type="submit" class="btn btn-primary">Save</button> </form> </div> </div> </div> {% endblock %}
form-rowで入力項目を増やす場合はform-groupで囲われている箇所を繰り返し記述する
アカウント情報を画面に表示するためのViewを作成する
account_views.py
from django.views.generic import CreateView, UpdateView from django.contrib.auth.views import LoginView from django.contrib.auth import get_user_model from base.models import Profile from base.forms import UserCreationForm class SignUpView(CreateView): form_class = UserCreationForm success_url = '/login/' template_name = 'pages/login_signup.html' def form_valid(self, form): return super().form_valid(form) class Login(LoginView): template_name = 'pages/login_signup.html' def form_valid(self, form): return super().form_valid(form) def form_invalid(self, form): return super().form_invalid(form) class AccountUpdateView(UpdateView): model = get_user_model() template_name = 'pages/account.html' fields = ('username', 'email',) success_url = '/account/' def get_object(self): # URL変数ではなく、現在のユーザーから直接pkを取得 self.kwargs['pk'] = self.request.user.pk return super().get_object() class ProfileUpdateView(UpdateView): model = Profile template_name = 'pages/profile.html' fields = ('name', '..', ......) success_url = '/profile/' def get_object(self): # URL変数ではなく、現在のユーザーから直接pkを取得 self.kwargs['pk'] = self.request.user.pk return super().get_object()
SignUpViewは新規アカウント作成のためのView
LoginViewはログインのためのView
Viewはメソッドを二つに分ける模様
AccountUpdateViewはアカウント情報の更新用View
またユーザーアカウント情報の更新の際の注意点は、URLからIDを取得するのではなく、現在のユーザーから直接PKを取得する(self.request.user.pk)
またフィールドはリストでもよいが、タプルを使用するのが一般的らしい
LoginRequiredMixinを使用すると、ログイン制約をViewにつけることができる(ログインしていなければアクセスさせないということが実現できる)
LoginRequiredMixinはViewクラスの第一引数に記載すると自動的に適用される
urls.py
from django.contrib.auth.views import LogoutView # Account path('login/', views.Login.as_view()), path('logout/', LogoutView.as_view()), path('signup/', views.SignUpView.as_view()), path('account/', views.AccountUpdateView.as_view()), path('profile/', views.ProfileUpdateView.as_view()),
ログアウト機能は組み込みのViewが存在するため、それを利用する
上記で設定しているこの部分で実装が完了する
urls.py
from django.contrib.auth.views import LogoutView path('logout/', LogoutView.as_view()),
{% if user.is_authenticated %} でログイン判定することが可能
Modelクラスはテーブル定義を記述するだけでなくデータ挿入取得更新処理をするマネージャーに分ける場合がある
以下のユーザークラスはマネージャクラスを用いている
※ UserManager(BaseUserManager)の処理
以下のようなコードを書けばよい
models.py カスタムユーザーを使用する場合
from django.db import models from django.contrib.auth.models import ( BaseUserManager, AbstractBaseUser, PermissionsMixin ) class UserManager(BaseUserManager): def create_user(self, username, email, password=None): if not email: raise ValueError('Enter Email!') user = self.model( username=username, email=email ) user.set_password(password) user.save(using=self._db) return user def create_superuser(self, username, email, password=None): user = self.model( username=username, email=email, ) user.set_password(password) user.is_staff = True user.is_active = True user.is_superuser = True user.save(using=self._db) return user class Users(AbstractBaseUser, PermissionsMixin): username = models.CharField(max_length=255) age = models.PositiveIntegerField() email = models.EmailField(max_length=255, unique=True) is_active = models.BooleanField(default=False) is_staff = models.BooleanField(default=False) picture = models.FileField(null=True, upload_to='picture/') USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username'] class Meta: db_table = 'users'
views.py
def regist(request): regist_form = forms.RegistForm(request.POST or None) if regist_form.is_valid(): try: regist_form.save() return redirect('accounts:home') except ValidationError as e: regist_form.add_error('password', e) return render( request, 'accounts/regist.html', context={ 'regist_form': regist_form, } )
form.py
from django.contrib.auth.password_validation import validate_password class RegistForm(forms.ModelForm): username = forms.CharField(label='名前') age = forms.IntegerField(label='年齢', min_value=0) email = forms.EmailField(label='メールアドレス') password = forms.CharField(label='パスワード', widget=forms.PasswordInput()) confirm_password = forms.CharField(label='パスワード再入力', widget=forms.PasswordInput()) class Meta(): model = Users fields = ('username', 'age', 'email', 'password') def clean(self): cleaned_data = super().clean() password = cleaned_data['password'] confirm_password = cleaned_data['confirm_password'] if password != confirm_password: raise forms.ValidationError('パスワードが異なります') def save(self, commit=False): user = super().save(commit=False) validate_password(self.cleaned_data['password'], user) user.set_password(self.cleaned_data['password']) user.save() return user
方針
Djangoのユーザーの場合はユーザークラスにもともと登録されているis_activateを使用して制御する
Djangoのシグナル機能を利用してユーザー作成時作成後などの特定のイベントに起因するイベント処理を簡単に実行することができる
この機能をDjangoではシグナルと言う
@receiverアノテーションを関数の先頭に付与して使うことができる
@receiver(post_save, sender=User)
def_save_profile(sender, instance, **kwargs):
instance.profile.save()
のような使い方となる
この場合はユーザーモデルが保存されたタイミングで処理が実行される関数となる
他にもpre_save, pre_delete, post_deleteなどがある
このシグナルを利用して、Djangoでユーザーが作成されたタイミングでトークンを発行するような処理を作る
コンソールにトークンを表示する処理を記述する(本番サービスではメール認証にするべき)
models.py
from django.db.models.signals import post_save from django.dispatch import receiver class UserActivateTokens(models.Model): token = models.UUIDField(db_index=True) expired_at = models.DateTimeField() user = models.ForeignKey( 'Users', on_delete=models.CASCADE ) objects = UserActivateTokensManager() class Meta: db_table = 'user_activate_tokens' @receiver(post_save, sender=Users) def publish_token(sender, instance, **kwargs): user_activate_token = UserActivateTokens.objects.create( user=instance, token=str(uuid4()), expired_at=datetime.now() + timedelta(days=1) ) # 本番サービスではメールでURLを送る実装とする print(f'http://127.0.0.1:8000/accounts/activate_user/{user_activate_token.token}')
accounts/activate_user/{user_activate_token.token}
のエンドポイントでアカウントを有効化する処理をviews.pyの中に追記する
models.pyに有効化処理をまず記述してそのあとviews.pyにこの処理を呼び出す処理を追加
models.py
class UserActivateTokensManager(models.Manager): def activate_user_by_token(self, token): user_activate_token = self.filter( token=token, expired_at__gte=datetime.now() ).first() user = user_activate_token.user user.is_active = True user.save()
エンドポイントから呼び出される処理 (urls.pyのルーティング処理などは基本なので省略)
views.py
def activate_user(request, token): user_activate_token = UserActivateTokens.objects.activate_user_by_token(token) return render( request, 'accounts/activate_user.html' )
models.py
特になし
views.py
def user_login(request): login_form = forms.LoginForm(request.POST or None) if login_form.is_valid(): email = login_form.cleaned_data.get('email') password = login_form.cleaned_data.get('password') user = authenticate(email=email, password=password) if user: if user.is_active: login(request, user) messages.success(request, 'ログイン完了しました。') return redirect('accounts:home') else: messages.warning(request, 'ユーザがアクティブでありません') else: messages.warning(request, 'ユーザかパスワードが間違っています') return render( request, 'accounts/user_login.html', context={ 'login_form': login_form, } )
forms.py
class LoginForm(forms.Form): email = forms.CharField(label="メールアドレス") password = forms.CharField(label="パスワード", widget=forms.PasswordInput())
django.contrib.messagesのパッケージに各種エラーやデバッグ・ログイン時ログアウト時などのタイミングでメッセージを簡単に表示させることのできるmessagesというパッケージがある
以下のような書き方となる
messages.debug(request, 'メッセージ') messages.info(request, 'メッセージ') messages.success(request, 'ログイン完了しました') messages.warning(request, '注意') messages.error(request, 'エラー発生')
例:ログアウトの処理などで以下のように書くとよい
views.py
from django.contrib.auth.decorators import login_required @login_required def user_logout(request): logout(request) messages.success(request, 'ログアウトしました') return redirect('accounts:home')
テンプレートでは使用するテンプレートごとに (user_login.htmlなど) にmessagesを書いておく
user_login.html
{% if messages %} {% for message in messages %} <div>{{ message.message }}</div> {% endfor %} {% endif %}
パスワードは変更後にsession情報を更新する必要がある
またパスワード確認フィールドもあることもポイント
フォームの情報でDBの (上書き) 更新は以下のような形になる
第二引数のinstanceに上書きしたい情報が格納される
forms.PasswordChangeForm(request.POST or None, instance=request.user)
この情報のままで保存するのは危険であるので、viewsのtryの処理のようなvalidationチェックを行う
update_session_auth_hash(request, request.user)でセッション情報を更新する
models.py
特になし
views.py
@login_required def change_password(request): password_change_form = forms.PasswordChangeForm(request.POST or None, instance=request.user) if password_change_form.is_valid(): try: password_change_form.save() messages.success(request, 'パスワード更新完了しました。') update_session_auth_hash(request, request.user) except ValidationError as e: password_change_form.add_error('password', e) return render( request, 'accounts/change_password.html', context={ 'password_change_form': password_change_form, } )
forms.py
class PasswordChangeForm(forms.ModelForm): password = forms.CharField(label='パスワード', widget=forms.PasswordInput()) confirm_password = forms.CharField( label='パスワード再入力', widget=forms.PasswordInput()) class Meta(): model = Users fields = ('password', ) def clean(self): cleaned_data = super().clean() password = cleaned_data['password'] confirm_password = cleaned_data['confirm_password'] if password != confirm_password: raise forms.ValidationError('パスワードが異なります') def save(self, commit=False): user = super().save(commit=False) validate_password(self.cleaned_data['password'], user) user.set_password(self.cleaned_data['password']) user.save() return user
models.py
特になし
views.py
@login_required def user_edit(request): user_edit_form = forms.UserEditForm(request.POST or None, request.FILES or None, instance=request.user) if user_edit_form.is_valid(): messages.success(request, '更新完了しました。') user_edit_form.save() return render(request, 'accounts/user_edit.html', context={ 'user_edit_form': user_edit_form, })
forms.py
class UserEditForm(forms.ModelForm): username = forms.CharField(label='名前') age = forms.IntegerField(label='年齢', min_value=0) email = forms.EmailField(label='メールアドレス') picture = forms.FileField(label='写真', required=False) class Meta: model = Users fields = ('username', 'age', 'email', 'picture')
画面遷移せずにクライアント側からjQueryを用いてサーバサイドにリクエストを投げてデータをやり取りする技術
クライアント側
$.ajax({ url : “create_post/”, // 実行するURLを指定 type : “POST”, // HTTPメソッド data : { the_post : $(‘#post-text’).val() }, // 送信するデータ success : function(json) { // 実行が成功した場合の処理 }, error : function(xhr,errmsg,err) { // 実行が失敗した場合の処理 } });
サーバーサイド側はJSONを返す処理を記述する
from django.http import JsonResponse from django.core import serializers if request.is_ajax: # 処理 json_instance = serializers.serialize(‘json’, [ instance, ]) # jsonに変換する return JsonResponse({“instance”: json_instance}, status=200) # レスポンスを返す または return HttpResponse( json.dumps(response_data), content_type="application/json" )
1. Memcached: メモリー上キャッシュ、処理が高速、最も一般的に使われ複数サーバで共有も可能
2. Database: データベースに保存するキャッシュ、取得速度遅いが大容量
3. File system: ファイル上に分割して保存するキャッシュ、速度は遅いが管理が楽
4. Local memory: ローカルPCメモリー上に保存するキャッシュ、デフォルトキャッシュ
5. Dummy: 開発環境用ダミーキャッシュ
Djangoのドキュメント Cache
Django’s cache framework | Django documentation | Django
キャッシュの設定例
setting.py
CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 'LOCATION': [ '172.19.26.240:11211', '172.19.26.242:11212‘, ] } }
キャッシュ操作
from django.core.cache.cache import * cache.set(‘KEY’, ‘VALUE’) cache.get(‘KEY’, ‘DEFAULT’) # 存在しない場合はデフォルト値が返ってくる cache.clear() cache.delete('KEY')
JQuery CDNのインポート
jQuery CDN
uncompressedをクリック
スクリプト部分をコピーしてbase.pyのようなHTMLファイルのheadタグ部分にペーストする
Ajaxの処理を記述
base.pyのbodyタグ内にブロックを記述し、使いたいテンプレートで処理を記述する
<body> {% block javascript %}{% endblock %} </body>
例: フォームの一時保存機能を追加する場合
テンプレートのフォームコンポーネントにidを付与してscriptブロックにscriptタグで処理を記述する
<form method="POST"> {% csrf_token %} {{ post_comment_form.as_p }} <input type="button" value="一時保存" id="save_comment"> <input type="submit" value="コメント送信"> </form> {% block javascript %} <script> $("#save_comment").click(function(){ var comment = $("#id_comment").val(); $.ajax({ url: "{% url 'boards:save_comment' %}", type: "GET", data: {comment: comment, theme_id: "{{ theme.id }}"}, dataType: "json", success: function(json){ if(json.message){ alert(json.message); } } }); }); </script> {% endblock %}
urlでセーブ用のViewの処理のエンドポイントを呼ぶ処理を実装しているので、viewの処理も実装する
urls.pyの実装例は省略
キャッシュを設定する処理
views.py
from django.core.cache import cache from django.http import JsonResponse def save_comment(request): if request.is_ajax: comment = request.GET.get('comment') theme_id = request.GET.get('theme_id') if comment and theme_id: cache.set(f'saved_comment-theme_id={theme_id}-user_id={request.user.id}', comment) return JsonResponse({'message': '一時保存しました!'})
saved_comment-theme_id={theme_id}-user_id={request.user.id} は長いがただのKEYである
例: キャッシュを取出す処理
views.py
def post_comments(request, theme_id): # フォームにレンダリングするためのデータを渡す処理 saved_comment = cache.get(f'saved_comment-theme_id={theme_id}-user_id={request.user.id}', '') post_comment_form = forms.PostCommentForm(request.POST or None, initial={'comment': saved_comment})
画面読み込み時にレンダリングするためのデータを渡す処理の先頭にキャッシュされたデータを取得する処理を記述しておく
フォームの第二引数 initial にキャッシュの初期値を設定する
また、フォーム画面でデータをサーバー側に保存した場合はcache.deleteを使用してキャッシュデータを削除しておくことも忘れないようにしておく
Djangoはクラスベースビューを用いる方が一般的で実践的
ただし別のフレームワークでフロントエンド開発をする場合はDjangoはREST APIとして使用するため関数ベースビューで記述する方が一般的かもしれない
Djangoで画面まで作成する場合にはクラスベースビューで作成する方が便利だと思われる
以下のテンプレートがある
View | 全てのViewの元になるView |
TemplateView | テンプレートを表示する。ホーム画面など |
CreateVIew | データベースにデータを挿入するView。データ作成画面 |
UpdateView | データを更新するView |
DeleteView | データを削除するView |
ListView | 特定のテーブルのデータ一覧を表示するView |
DetailView | テーブルのレコードの詳細を表示するView |
FormView | Formを表示してデータを送信するView |
RedirectView | リダイレクトを行うView |
上記のテンプレートでうまく実装できなさそうな処理を記述するときに継承するViewクラス
リクエストに応じた処理を記述する
class MyView(View): def get(self, request, **kwargs): # GETの場合の処理 def post(self, request, **kwargs): # POSTの場合の処理
urls.pyに直接記述
from django.views.generic.base import RedirectView urlpatterns = [ path('/search/<term>/', RedirectView.as_view(url='https://google.co.jp/?q=%(term)s')), ]
データ更新削除時等のメッセージ表示に便利なクラス
Pythonは複数クラスの継承ができるため作成するViewの2つ目のクラスとしてSuccessMessageMixinを継承する
例 views.py
class BookUpdateView(UpdateView, SuccessMessageMixin): model = Books success_message # 成功時メッセージ(静的) def get_success_message(self, cleaned_data): # 成功時メッセージ(動的
そのほかのテンプレートビューは使用する際に配布資料参照のこと
クラスベースのログインビューが存在するためそれを利用する
from django.views.generic import View class LoginView(View): def post(self, request, *args, **kwargs): username = request.POST[‘username’] # usernameを取得 password = request.POST['password’] # passwordを取得 user = authenticate(username=username, password=password) # userが存在するか確認 if user is not None: if user.is_active: login(request, user) # ログイン処理 return render(request, "index.html") class LogoutView(View): def get(self, request, *args, **kwargs): logout(request) # ログアウト処理 return HttpResponseRedirect(settings.LOGIN_URL)
LoginRequiredMixin
ログインユーザーのみViewにアクセスできるように制限するクラス
使用するviewに継承する
class MyView(LoginRequiredMixin, View): # ログインが必要なViewに継承させる login_url = '/login/' redirect_field_name = 'redirect_to’
@method_decorator(login_required)
アノテーションでもログイン制約を付与することができる
Viewの中の一部のメソッドのみログイン制約をつけたい場合に使用できる
class ProtectedView(TemplateView): @method_decorator(login_required) def dispatch(self, request, *args, **kwargs):
セッション時間はデフォルトで2週間となっている
変更する場合はsettingにセッション情報を追記する
プログラム中で個別にセッション時間を指定したい場合には request.session.set_expiry(value) を使用する
settings.py
SESSION_COOKIE_AGE: セッションの保存時間(秒)
https://docs.djangoproject.com/ja/3.1/ref/settings/#std:setting-SESSION_COOKIE_AGE
request.session.set_expiry(value): セッションの保存時間を引数の時間(秒)に変更する
※なお、引数が0の場合、ブラウザを閉じるとセッションが破棄される
value に datetime または timedelta オブジェクトを指定すると指定された日時に破棄される
https://docs.djangoproject.com/ja/3.1/topics/http/sessions/#django.contrib.sessions.backends.base.SessionBase.set_expiry
class RegistUserView(CreateView): template_name = 'regist.html' form_class = RegistForm
class UserLoginView(LoginView): template_name = 'user_login.html' authentication_form = UserLoginForm def form_valid(self, form): remember = form.cleaned_data['remember'] if remember: self.request.session.set_expiry(1200000) return super().form_valid(form)
def form_validはセッション情報保持ボタンを押すとログイン情報を保持することができるような処理にしている
※講座の中でデフォルトのセッション時間を5秒にしているため
実際の実装の場面ではform_validの部分は省いて実装するケースがほとんどだと思う
FormViewで定義する例
class UserLoginView(FormView): template_name = 'user_login.html' form_class = UserLoginForm def post(self, request, *args, **kwargs): email = request.POST['email'] password = request.POST['password'] user = authenticate(email=email, password=password) next_url = request.POST['next'] if user is not None and user.is_active: login(request, user) if next_url: return redirect(next_url) return redirect('accounts:home')
class UserLogoutView(LogoutView): pass
Viewで定義する例
class UserLogoutView(View): def get(self, request, *args, **kwargs): logout(request) return redirect('accounts:user_login')
# @method_decorator(login_required, name='dispatch') class UserView(LoginRequiredMixin, TemplateView): template_name = 'user.html' # @method_decorator(login_required) def dispatch(self, *args, **kwargs): return super().dispatch(*args, **kwargs)
以下の理由にて実務で必須
loggingモジュール(デフォルトでログレベルはwarning)
import logging # ログの出力 logging.critical(‘エラー内容') .. rtc
logging.basicConfig(filename=‘app.log’, filemode=‘w’)
ogging.error(‘%s raised error’, name) # 変数をログに出力 logging.error(f‘{name} raised error’) # 変数をログに出力(3.6以降) logging.error(f‘{name=} raised error’) # 変数をログに出力(3.8以降) logging.error(“”, exc_info=True) #スタックトレースの出力 logging.basicConfig(format=‘%(asctime)s-%(process)s-%(levelname)s-%(message)s’) # 出力するログのフォーマットの設定%(asctime): 出力時間、%(process): プロセスID、%(levelname): ログレベル、%(message):メッセージ datefmt: 時刻のフォーマットを指定
スタックトレースは特によく使う模様
原因特定するために何行目のログ情報かの情報がとても有益となる
ログの一般的な使い方 tryとかで使う
try: method() except Exception as e logggin.error(e, exc_info=True) logging.error(“”, exc_info=True) #スタックトレースの出力
logger = logging.getLogger(__name__)
__name__はファイル名を差し、ロガーの識別子となる
使用を推奨されている
handlerを設定する
logger.setLevel(logging.DEBUG) # ログレベルのデフォルトはwarning
confフォルダを作成して、設定ファイルを logger.conf 作る
[loggers] # loggerの一覧を設定
[handlers] # Handlerの一覧を設定
[formatters] # Formatterの一覧を設定
[logger_${logger名}] # 各loggerの設定
[handler_${handler名}] # handlerの設定
[formatter_${formatter名}] # 各formatterの設定
logger.conf 例
[loggers] keys=root [handlers] keys=consolehandler, filehandler [formatters] keys=sampleformatter [logger_root] level=DEBUG handlers=consolehandler, filehandler [handler_consolehandler] class_StreamHandler level=INFO formatter=samleformatter args=(sys.stdout,) [handler_filehandler] class=FileHandler level=Error formatter=samleformatter args=['logs/app.log', 'a', 'utf-8'] [formatter_sampleformatter] format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
ログのインスタンスを使用してログを出力させる
logging.py
import logging import logging.config logging.config.fileConfig(fname='conf/logger.conf') logger = logging.getLogger(__name__) logger.debug('') logger.info('') logger.warning('') logger.error('') logger.critical('')
logsフォルダを作ってログをファイルとして出力させる
今回はinfo以上のログが出力される
複数のloggerを設定して別々のログを出力させることも可能
confファイル
[loggers] keys=root, samplelogger [handlers] keys=consolehandler, filehandler, sampleconsolehandler [formatters] keys=sampleformatter [logger_root] level=DEBUG handlers=consolehandler, filehandler [logger_samplelogger] level=DEBUG handlers=filehandler, sampleconsolehandler qualname=samplelogger propagate=0 [handler_consolehandler] class=StreamHandler level=INFO formatter=sampleformatter args=(sys.stdout,) [handler_filehandler] class=FileHandler level=ERROR formatter=sampleformatter args=['logs/app.log', 'a', 'utf-8'] [handler_sampleconsolehandler] class=StreamHandler level=DEBUG formatter=sampleformatter args=(sys.stdout,) [formatter_sampleformatter] format=%(asctime)s-%(name)s-%(levelname)s-%(message)s
logging.pyファイル
import logging import logging.config logging.config.fileConfig(fname='conf/logger.conf') logger = logging.getLogger(__name__) logger.debug('デバッグログ') logger.info('インフォログ') logger.warning('ワーニングログ') logger.error('エラーログ') logger.critical('クリティカルログ') logger = logging.getLogger('samplelogger') logger.debug('デバッグログ') logger.info('インフォログ') logger.warning('ワーニングログ') logger.error('エラーログ') logger.critical('クリティカルログ')
一定の条件を設定して、別ファイルにログを新たに切り出す設定
管理上見やすくしたり1ファイルのファイルサイズの肥大化を防ぐことができる
例: logger.confファイルに以下を追記
[handler_fileHandler] class=handlers.RotatingFileHandler level=INFO formatter=sampleFormatter args=('./logs/log.out', when='S', interval=10, backupCount=5, encoding=‘utf-8’, maxBytes=1000)
時間によるローテーションを設定したい場合は以下
[handler_fileHandler] class=handlers.TimedRotatingFileHandler level=INFO formatter=simpleFormatter args=(‘logger.log’,when=’D’,interval=1,backupCount=3,encoding=‘utf-8’)
上記はpythonの基本のログ出力方法
以下ではDjangoに設定する方法
Djangoのログ出力は setting.py に記述する
ロギング | Django ドキュメント | Django
setting.py
LOGGING = { ‘version’: 1, # ロガーバージョン通常は1 ‘disable_existing_loggers’: False, # デフォルトログの無効化通常はFalse 'formatters': { : # フォーマッターの一覧を記載 }, 'handlers': { : # ハンドラーの一覧を記載 }, 'loggers': { : # ロガーの一覧を記載 } }
setting.py 例
LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'simple': { 'format': '%(asctime)s %(levelname)s [%(pathname)s:%(lineno)s] %(message)s', } }, 'handlers': { 'console_handler': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', 'formatter': 'simple', }, 'timed_file_handler': { 'level': 'INFO', 'class': 'logging.handlers.TimedRotatingFileHandler', 'filename': os.path.join('logs', 'application.log'), 'when': 'S', 'interval': 10, 'backupCount': 10, 'formatter': 'simple', 'encoding': 'utf-8', 'delay': True, }, 'timed_error_handler': { 'level': 'ERROR', 'class': 'logging.handlers.TimedRotatingFileHandler', 'filename': os.path.join('logs', 'application_error.log'), 'when': 'S', 'interval': 10, 'backupCount': 10, 'formatter': 'simple', 'encoding': 'utf-8', 'delay': True, }, 'timed_performance_handler': { 'level': 'INFO', 'class': 'logging.handlers.TimedRotatingFileHandler', 'filename': os.path.join('logs', 'application_performance.log'), 'when': 'S', 'interval': 10, 'backupCount': 10, 'formatter': 'simple', 'encoding': 'utf-8', 'delay': True, } }, 'loggers': { 'application-logger': { 'handlers': ['console_handler', 'timed_file_handler'], 'level': 'DEBUG', 'propagate': False, }, 'error-logger': { 'handlers': ['timed_error_handler'], 'level': 'ERROR', 'propagate': False, }, 'performance-logger': { 'handlers': ['timed_performance_handler'], 'level': 'INFO', 'propagate': False, } } }
ログの出力の処理
views.pyやmodels.pyなど
import logging # 前処理 application_logger = logging.getLogger('application-logger') error_logger = logging.getLogger('error-logger') # メソッド内で記述する application_logger.debug('エラーメッセージ') error_logger.error('エラーメッセージ')
リクエスト・レスポンス処理にフックを加えて入力と出力の値を置き換えるフレームワークのことをミドルウェアという
ミドルウェア (Middleware) | Django ドキュメント | Django
django.utils.deprecation.MiddlewareMixinを継承する
Djangoのビューを呼び出す前に実行される処理を記述する場合は
def process_view(self, request, view_func, view_args, view_kwargs):のメソッドを記述する
def process_viewのreturnは通常Noneを返す
ビューで例外が発生した場合に実行される処理を記述する場合は
def process_exception(self, request, exception) のメソッドを追記する
returnはNoneとHttpResponseオブジェクトを返す
Noneをreturnする場合は他のエラーハンドリング処理を記述する
ミドルウェアの定義の逆順に実行される(setting.pyのMIDDLEWAREの下から順番に実行される)
ビューの実行後に呼び出される場合
def process_template_response(self, request, response):
middleware.pyを記述してsetting.pyに記述する
middleware.py 例(プロジェクトフォルダに作成)
# middleware.py import logging from django.utils.deprecation import MiddlewareMixin import time application_logger = logging.getLogger('application-logger') error_logger = logging.getLogger('error-logger') performance_logger = logging.getLogger('performance-logger') class MyMiddleware(MiddlewareMixin): def process_view(self, request, view_func, view_args, view_kwargs): # viewを呼び出す前に実行 application_logger.info(request.get_full_path()) def process_exception(self, request, exception): error_logger.error(exception, exc_info=True) class PerformanceMiddlware(MiddlewareMixin): def process_view(self, request, view_func, view_args, view_kwargs): start_time = time.time() request.start_time = start_time def process_template_response(self, request, response): response_time = time.time() - request.start_time performance_logger.info(f'{request.get_full_path()}: {response_time}s') return response
setting.py 例
MIDDLEWARE = [ 'class_based_view.middleware.PerformanceMiddlware', # リクエストでは最初・レスポンスでは最後に実行される # ... 'class_based_view.middleware.MyMiddleware' # リクエストでは最後・レスポンスでは最初に実行される ]
offlineアクセスのオフラインとはネットワークにつながっていないということではなくユーザーがいない状態のことを差す
settings.pyのINSTALLED_APPに以下のものを追加する
settings.py
'django.contrib.sites', 'allauth', 'allauth.account', 'allauth.socialaccount', ‘allauth.socialaccount.providers.google’, # google認証
ログイン処理時に認証で行うクラスにallauthを追加する
settings.py
AUTHENTICATION_BACKENDS = ( “django.contrib.auth.backends.ModelBackend”, # デフォルトの認証 "allauth.account.auth_backends.AuthenticationBackend", # allauthの認証 )
settings.pyの下部に記載
SITE_ID = 1 # django_site テーブル上の認証に用いるサイトの指定 通常1(django_siteは最初のマイグレーションで作成されるテーブル) ACCOUNT_EMAIL_REQUIRED = True # 認証にメールアドレスが必要か(デフォルトはFalse) ACCOUNT_USERNAME_REQUIRED = False # 認証にユーザ名が必要か(デフォルトはTrue) ACCOUNT_AUTHENTICATION_METHOD = ‘email’ # 認証に利用する要素(email or username or username_email) SOCIALACCOUNT_PROVIDERS = { # プロバイダーごとの設定 'google': { ‘SCOPE‘: [ # Google APIで何を取得するか 'profile', 'email', ], 'AUTH_PARAMS': { ‘access_type’: ‘onine’, #バッチ処理の場合はオフラインで利用する }}}
アクセスタイプのofflineはネットにつながっていないことでなくユーザーがいない状態例えばバッチetc. のこと
SOCIALACCOUNT_PROVIDERSの中身は以下のドキュメントのProviderの項目を見て記述する
https://django-allauth.readthedocs.io/en/latest/installation.html
1
google cloud platformで検索してgcpのトップページにアクセス
2
利用規約に同意
3
コンソール画面でプロジェクトの選択>新しいプロジェクト
プロジェクト名と場所(組織なし)を入力して作成
4
プロジェクトを選択をおし
APIサービス>OAuth同意画面
UserTypeを外部に選択して作成
5
アプリ名には先ほどのプロジェクト名を
ユーザーサポート名には適当なメールアドレスを入力する
デベロッパーの連絡先情報は適当なメールアドレスを入力する
保存して次へ
6
スコープはそのままで保存して次へ
テストユーザーもそのままで保存して次へ
概要もそのままでダッシュボードに戻る
7
APIサービス>認証情報作成>認証情報を作成>OAuthクライアントIDを選択
アプリケーションの種類:ウェブアプリケーション
名前:ウェブクライアント1
承認済のJavaScript生成元にローカルのURLを記載
http://127.0.0.1:8000
承認済のリダイレクトURIにも以下のようなURIを記載
http://127.0.0.1:8000/oauth_accounts/google/login/callback/
作成
8
作成されたクライアントIDとシークレットキーをメモなどどこかに保存する
エディタに戻ってDjangoのソースコードを編集していく
マイグレーションを実行する
py manage.py migrations
これでマイグレーションの履歴を見るとsocialaccount_○○というOAuthで使用されるDBテーブルが4つほど作成されていることがわかる
スーパーユーザーを作成する
py manage.py createsuperuser
作成した管理者ユーザーで管理者画面にログインする
SOCIAL ACCOUNTSの中に
social accounts
social application tokens
social applications
テーブルがあることがある
この中にOAuthの設定を記載していく
まず、Siteのテーブルの中にexampleのドメイン名があるがこれを127.0.0.1に変更する
次にSocial Applicationsをクリックして以下を追加する
Provider: google
name: OAuth App
クライアントIDは先ほどコピーした値
シークレットキーも先ほどコピーした値
Keyは空のまま
Sitesの127.0.0.1を左から右に移動させる
これで保存する
こうすることでGoogle認証でログインすることが可能となる
この後、実際にOathでログインしたユーザーはsocial accountsテーブルにメースアドレスや名前などが追加される
urls.pyにOauthの画面への遷移を追加する
urls.py
path('oauth_accounts/', include('allauth.urls’))
ユーザーログインのテンプレート画面にgoogleログインのためのリンクを貼り付ける
user_login.html
{% load socialaccount %} <a href=“{% provider_login_url ‘google’ %}”>googleログイン</a>
例:user_login.html
{% extends 'base.html' %} {% load socialaccount %} {% block content %} <form method="POST"> {% csrf_token %} {{ form.as_p }} <input type="hidden" name="next" value="{{ request.GET.next }}"> <input type="submit" value="ログイン"> </form> <a href="{% provider_login_url 'google' %}">Googleログイン</a> {% endblock %}
Djangoサーバーを起動してアプリケーションにアクセス
ユーザーログイン画面からgooglerログインを選択して、よく見るグーグル認証の画面に遷移して、その後認証が通って元の画面にリダイレクトされればよい
DBの中身を確認するとSOCIAL ACCOUNTSテーブルの中にログインされたUserのEmailアドレスなどが格納されていることがわかる
Account Userの中身を見るとユーザーのEmailが見れることが確認できれば良い
http://127.0.0.1/oauth_accounts/signup/
などと指定すると、oauthのライブラリが持っている変な画面が表示されてしまう
これを表示させたくないない場合はurls.pyに記述していた箇所を以下のように書き換える
from allauth.socialaccount.providers.google.urls import urlpatterns as google_url path('oauth_accounts/', include(google_url)),
こうするとピンポイントにgoogleのurlだけを入れることができる
END
ドキュメント
https://docs.djangoproject.com/ja/3.1/howto/custom-management-commands/
アプリケーションフォルダの直下にmanagementフォルダを作成して、commandsフォルダを作成してその中にコマンド(sample.py)を配置
app\management\commands\sample.py
from django.core.management.base import BaseCommand class Command(BaseCommand): def handle(self, *args, **options): print("バッチ実行")
このバッチファイルを実行する場合は以下のコマンドで実行できる
py manage.py sample
storesアプリケーションフォルダにも同じようなフォルダ階層でバッチファイルを作成することもできる
stores\management\commands\sample.py
from django.core.management.base import BaseCommand class Command(BaseCommand): def handle(self, *args, **options): print('storeのバッチを実行')
なお別のアプリケーションに同じ名前のバッチファイルを作成した場合は、setting.pyに記載したアプリの上から順番に実行される(sample.pyが二つあった場合は、py manage.py sampleでは先に登録されているアプリ(app)の方のsample.pyが実行される)
add_argments: コマンド内で利用する引数を追加する
app/management/sample.py
class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument(‘first’) # 第1引数をキーfirstに格納する parser.add_argument(‘second’) # 第2引数をキーsecondに格納する parser.add_argument(‘--option1’) # option1で指定した引数をキーoption1に格納する def handler(self, *args, **options): print(f’{options[“first”]}’) # add_argumentsで格納した値を取り出す print(f’{options[“option1”]}’) # add_argumentsで格納した値を取り出す
呼び出す場合は必ず引数を必要となり、以下のようなコマンドで呼び出すことができる
python manage.py sample A(第1引数) B(第2引数) --option1 C(option1に対する値)
type: 数値型(int)、文字列型(str)で指定する
help: 説明文の追加 「python manage.py help コマンド名」 で使用できる
python manage.py help コマンド名で表示することができる
default: デフォルトの値(格納する引数が存在する場合に指定した値が格納される)
nargs: 格納する値の数を指定してリスト型で格納。呼び出す際はコマンドの引数に配列の数だけ引数を列挙して渡して実行する
action: store_true(引数が存在する場合はTrue)、store_false (引数が存在する場合はFalse)が格納される
choices: 格納できる値の一覧を記述する
例: sapmle.py
from django.core.management.base import BaseCommand class Command(BaseCommand): help='ユーザ情報を表示するバッチです' def add_arguments(self, parser): parser.add_argument('name', type=str, help='名前') # 1引数 parser.add_argument('age', type=int) # 2引数 parser.add_argument('--birthday', default='2020-01-01') parser.add_argument('three_words', nargs=3) parser.add_argument('--active', action='store_true') # 引数--activeを指定した場合にTrueを格納する parser.add_argument('--color', choices=['Blue', 'Red', 'Yellow']) # 例えばBlueを指定すると以下のハンドルの対応する青が表示される def handle(self, *args, **options): name = options['name'] age = options['age'] birthday = options['birthday'] three_words = options['three_words'] active = options['active'] print( f'name = {name}, age = {age}, birthday = {birthday}, three_words = {three_words}' ) print(active) color = options['color'] if color == 'Blue': print('青') elif color == 'Red': print('赤') elif color == 'Yellow': print('黄')
Ordersモデルで定義されたDBの中のOrdersテーブルのデータをファイルに出力するバッチファイルの作成
例: stores/management/commands/export_orders.py
from django.core.management.base import BaseCommand from stores.models import Orders from ecsite_project.settings import BASE_DIR from datetime import datetime import os import csv class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument('--user_id', default='all') def handle(self, *args, **options): orders = Orders.objects user_id = options['user_id'] if user_id == 'all': orders = orders.all() else: orders = orders.filter(user_id=user_id) file_path = os.path.join(BASE_DIR, 'output', 'orders', f'orders_{datetime.now().strftime("%Y%m%d%H%M%S")}_{user_id}') with open(file_path, mode='w', newline='\n', encoding='utf-8') as csvfile: fieldnames = ['id', 'user', 'address', 'total_price'] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() for order in orders: writer.writerow({ 'id': order.id, 'user': order.user, 'address': order.address, 'total_price': order.total_price, }) || こうすることで日繰りでファイルにデータをレポートとして出力させるバッチにしたりすることができる ** メッセージフレームワーク *** messages.html を作成して、base.htmlに配置してすべての画面でメッセージを出力できるようにしておく まずはsnippetsに以下のように {% for msg in message %} を使用したメッセージ出力用の画面を作成しておく messages.html >|html| {% for msg in messages %} <nav class="py-2 mb-0 {{ msg.tags }}"> <div class="container"> {{ msg }} </div> </nav> {% endfor %}
この中で
次にbase.htmlのどこかに{% include 'snippets/messages.html' %}を取り込んでおく
例 base.html
<header> {% block header %} {% include 'snippets/messages.html' %} {% include 'snippets/header.html' %} {% endblock %} </header>
こうすることですべての画面にメッセージが適用されるようになる
settings.pyに以下のような記述をしておく
from django.contrib import messages # messages MESSAGE_TAGS = { messages.ERROR: 'rounded-0 alert alert-danger', messages.WARNING: 'rounded-0 alert alert-warning', messages.SUCCESS: 'rounded-0 alert alert-success', messages.INFO: 'rounded-0 alert alert-info', messages.DEBUG: 'rounded-0 alert alert-secondary', }
ビューの処理毎にmessagesを付け加えることでメッセージを表示させる
成功した場合の例
from django.contrib import messages class View(CreateView): def form_valid(self, form): messages.success(self.request, '成功しました') return super().form_valid(form)
失敗した場合の例
from django.contrib import messages class Login(LoginView): def form_invalid(self, form): messages.error(self.request, '失敗しました') return super().form_invalid(form)
これで処理毎に想定したメッセージが表示されていればよい
コンテキストプロセッサーとは動的なデータを表示させようと思うと画面毎に個別にViewsで画面にコンテキストを渡す必要があるが、あらゆる画面で共通して表示させたい情報があった場合はview毎に明示して記載すると面倒でかつメンテナンス性も落ちてしまうため、コンテキストプロセッサーとは記載しておけば、全ての画面にコンテキストとしてデータを渡す機能である
プロジェクト(config)フォルダの中にcustom_context_processors.pyとして作成することが一般的な模様
例 config/custom_context_processors.py
from django.conf import settings from base.models import Item def base(request): return { 'TITLE': settings.TITLE, 'POPULAR_ITEMS': items.order_by('-sold_count') }
上はタイトルや人気のアイテムをすべての画面にコンテキストとして渡す処理となっており、こうすることでどの画面でも人気商品をいつでも表示できるようになる
ちなみにコンテキストプロセッサーの変数は大文字で記載するのが慣習なのかもしれない
主にTEMPLATESにcontext_processorsの項目に先ほど作成したファイルのパスを記述する
configはプロジェクト名になるため、別の名前になっている場合はその名前に合わせる
settings.py
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'config.custom_context_processors.base', # 追記 ], }, }, ] # custom_context_processors TITLE = 'VegeKet'
通常のテンプレートに変数を表示させるのと同じようにブラケット記法で {{ TITLE }} 表示させることができる
例: header.html
<nav> <a class="py-2 text-white" href="/">{{ TITLE }}<!--VegeKet--></a> </nav>
Webコンポーネントで言及されるバッジとは、Webアプリの通知(ベルのマーク)などで何件通知があるかなどを表示してくれるコンポーネントのことである
ヘッダーなどに設定する
コンテキストプロセッサーに登録した変数を使用すると便利
header.html
<a class="py-2 d-none d-md-inline-block text-white" href="/cart/">Cart {% if request.session.cart and request.session.cart.items|length != 0 %} <span class="badge badge-danger">{{request.session.cart.items|length}}</span> {% endif %} </a>
表示の方法としてはclass="badge badge-danger"クラスでbadgeを指定するだけでよい(Bootstrapの場合)
今回はコンテキストプロセッサーではなくセッションのcartデータから値を取得している
特にテンプレート内で比較構文を入れるとこれが起きやすい
例 {% if compare < 10 %} の < がHTMLのタグと認識されることが起きたりする
これは文法的にも問題はなく対処法も特にないので、見過ごすしかない模様
templatesフォルダ直下に 404.html を作成して、settings.pyの
DEBUG=False
ALLOWED_HOSTS = ['127.0.0.1']
を設定すると、404.htmlの内容が表示されるようになる
DEBUGがTrueだったり、ALLOWED_HOSTSが設定されていなかったりすると、エラーが返ってくる
500.htmlなど他のステータスコードも同様に実装することができる
404.html
<!--省略--> <div>404 Not Found アクセスしようとしたページは見つかりません</div> <!--省略-->
この記事はUdemyの学習メモです
テストに関して解説してくれる教材はなかなか見つからないのでかなり重宝させてもらいました
www.udemy.com
Next.jsで実装するプロジェクトのテストの導入と静的テストの基本形の内容だけ記載しておきます
目次
1. プロジェクト作成手順
https://github.com/GomaGoma676/nextjs-testing/blob/main/README.md
この通りにやるとテストの導入ができます
1-2. 必要 module のインストール
インストールすると以下のようなメッセージが出ます
- axios@0.24.0
- swr@1.0.1
- msw@0.35.0
added 92 packages from 86 contributors and audited 534 packages in 28.619s
96 packages are looking for funding
run `npm fund` for detailsfound 0 vulnerabilities
上記のパッケージ警告は0件だがWindows(OSに関係ないのかもしれないが)でReactでパッケージインストールしていると結構な数の警告が出ます
警告が出るのは気持ち悪い派の人のために対処法(https://shinseidaiki.hatenablog.com/entry/2021/10/09/204921)はまとめてみましたが、あんまり意味がないようなので警告が出た場合は放置でいいと思います
2-1.
jest-css-modulesはテスト時に不具合を出す可能性のあるCSSのモック化のためのライブラリ
2-2.
windowsユーザーはtouch使えないので右クリックでファイルを作る
2-3.
testPathIgnorePatternsはテストとは関係ないフォルダを無視する記述
"moduleNameMapper"はCSSファイルがプロジェクトに存在した場合はmock化する記述
2-4.
package.jsonのscripts部分はnpm run ○○にコマンドを設定できる部分なので、testを記述してnpm run testでテスト実行できるようにしている
デフォルトでは全テストケースの成功失敗しか出力されない
3-1.
windowsユーザーは touch は右クリック作成
3-3.
npm run devを実行するとtsconfig.jsonの内容が自動的に生成されます
3-4.
apiフォルダは削除
4-2.
初期化コマンドを実行すると設定ファイルが2つ自動生成される
4-3.
これはpagesやcomponentsフォルダ下の実際に使用されているtsxファイルのユーティリティファイルに対してデプロイするときに自動生成されるCSSファイルについて、記載したフォルダのものだけをCSSに自動生成する記述
これを設定しておかないと存在するtailwindのクラスユーティリティすべてがCSSに出力されてファイルサイズが膨大になる
5.2のところ
Next.jsのテストはテストフォルダ__test__に○○.test.tsxという形のファイルとして保存する
ファイルの中身としては以下のように書く
○○.test.tsx
import { render, screen } from '@testing-library/react' import '@testing-library/jest-dom/extend-expect' import コンポーネント from '../pages/index' it('テストケース説明', () => { render(<コンポーネント />) expect(screen.getByText('期待する文字列')).toBeInTheDocument() })
renderはコンポーネントに変数などがあれば変数に値を代入してHTMLを作り出します
画面としては実際に表示しませんがitの中では仮想的に画面をいじっているのと同じ環境を作り出しています
screenはrenderでレンダリングした画面を差していて、getByTextで画面の中に'期待する文字列'、教材の場合は'Hello Nextjs'があるかどうか探します
toBeInTheDocumentでは探した文字が画面上に表示されていれば正解であると判定します
これが基本形です
npm run test でテストが失敗した場合は原因などの情報を含めて出力される
この失敗のケースでは"期待している文字列"が見つからなかったので、コンポーネントの方に"期待している文字列"をちゃんと実装しなさいと言っています
しかしながら"期待している文字列"もスペルミスなどで間違う可能性があるので真に受けすぎないのも大事です
FAIL __tests__/Home.test.tsx
× Should render hello text (40 ms)● Should render hello text
TestingLibraryElementError: Unable to find an element with the text: Hello Nextjs2. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
......
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 3.296 s
Ran all test suites.
npm ERR! Test failed. See above for more details.
また上記のテスト結果だけでは原因を特定できない時にテストのデバッグがscreen.debug()でできます
ただし明示的にscreen.debug()を入れなくてもテスト失敗の時はデフォルトでデバッグ出力は出力されます
it('テストケースの説明', () => { render(<コンポーネント />) screen.debug() expect(screen.getByText('期待している文字列と違う文字列')).toBeInTheDocument() })
デバックを入れているときのテストが成功する場合のコンソールへの出力例
(テスト失敗の時はデフォルトでデバッグ出力されている模様)
レンダリングされているHTMLの要素を出力させている
console.log <body> <div> 本当の文字列 </div> </body>
静的なコンテンツをテストするパターン
まずはテストするための画面を2画面作成します
Next.jeではすべての画面で共通して使われるベースの画面components/Layout.tsxを作成して各画面はそれを継承するのが通常の実装になる
Layout.tsx 基本形
interface TITLE { title: string } const Layout: React.FC<TITLE> = ({ children, title = 'Next.js' }) => { return ( <div></div> ) } export default Layout
Layout.tsx のreturnの中身は Headとheaderとmainやfooterなどで通常構成される
Link href="/"タグはルーティングでSPAとして差分だけを読み込みに行ってくれる
対してa href="/"タグを使うと全量ページを読み込みに行くのでLayout.tsxのヘッダーに使われる画面遷移ではLinkを使うのが一般的
ホーム画面とブログ画面の2画面に遷移できるようにしておく
Layout.tsxのreturn の中身だけ記載
return ( <div> <Head> <title>{title}</title> </Head> <header> <nav> <Link href="/"> <a data-testid="home-nav" >Home</a> </Link> <Link href="/blog"> <a data-testid="blog-nav" >Blog</a> </Link> </nav> </header> <main> {children} </main> </div> )
使用しているHeadやLinkのインポートはこちら
import Head from 'next/head'; import Link from 'next/link';
次に画面を作ります
ホーム画面となるindex.tsxを編集します
Layout.tsxをベースに作成するので、return 直下をLayoutで囲みます
index.tsx
import Layout from "../components/Layout" const Home: React.FC = () => { return ( <Layout title="Home"> <p>ホーム画面です</p> </Layout> ) } export default Home
次に2つ目の画面となるblog-page,tsxをpagesフォルダ下に作成します
next.jsはpagesフォルダ直下に格納したファイル名から自動的にルーティングするので、blog-pageという名前を使うと/blogでblog-page,tsxが自動的に参照されるような仕掛けとなっています
blog-page,tsxもindex.tsxと同じようなコードになります
blog-page,tsx
import Layout from '../components/Layout' const BlogPage:React.FC = () => { return ( <Layout title="Blog"> <p>ブログページです</p> </Layout> ) } export default BlogPage
まずはindex.tsxの静的な画面をテストします
これはテスト基本形のところで学んだ通り__test__フォルダを作成してHome.test.tsxを作成し、以下のような手順でコードを作成していっててテストを実装していく
まずは基本的なテストモジュールのインポートをする
import {render, screen} from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect';
次にテストしたい画面モジュールをインポートする
import Home from '../pages/index'
次にテストケースのひな型を書く
describe('テストケースの説明_○○を確認する', () => { it('テストケース1_正常系_○○', async () => { }) it('テストケース2_異常系_○○', async () => { }) })
テストケースの中身を書く
it('テストケース_○○', async () => { render(<Home />) # テストしたい画面のレンダリング expect(screen.getByText('期待する文字列')).toBeInTheDocument() # 検証する内容 })
このようにしていくと完成形は以下のようになる
Home.test.tsx
import { render, screen } from '@testing-library/react' import '@testing-library/jest-dom/extend-expect' import Home from '../pages/index' it('テストケース説明', () => { render(<Home />) expect(screen.getByText('ホーム画面です')).toBeInTheDocument() })
これが静的テストの基本形にあたり、Home画面の表示項目が増えたらexpectを増やしていくことで確認したい項目を増やしてテストを作っていく
次にBlog画面のテストだが、リンクをクリックして画面遷移をする挙動に関してもテストが行えるため画面遷移のテストもここで行うことにする
そのためには画面遷移つまりLinkコンポーネントのテストには別途必要なコンポーネントがあるためインストールする
npm install next-page-tester
では2つ目の画面をテストするテストファイル__tests__/NavBar.test.tsxを作成する
NavBar.test.tsxでは画面上のリンクのクリックを模擬するのでテスターがユーザーイベントをするためのモジュールをインポートします
またそれに伴い各種モジュールもインポートします
NavBar.test.tsx
import useEvent from '@testing-library/user-event'; // テスターが画面上の要素をクリックできるモジュール import { getPage } from 'next-page-tester'; import { initTestHelpers } from 'next-page-tester'; // 初期化を行う initTestHelpers()
次にルーティングからページを取得してきてレンダーする処理を施します
今回は画面が遷移するのでコンポーネントを直接レンダリングするのではなくルーティング経由にします
NavBar.test.tsx
const { page } = await getPage({ route: '/index', }) render(page)
今回はクリックイベントが発生するのでクリックイベントのコードは以下になります
useEvent.click(screen.getByTestId('実装時のタグに付与したtestId属性の値'))
なおクリックイベントで画面遷移をするにはLinkタグをクリックする必要があり、テスト上ではuseEvent.clickを使用することでクリックできるが、何をクリックするかのキーとなる部分に関してはgetByTestIdを使用しており、実装時の段階でLayout.tsxのLinkタグの中にdata-testidの属性を付与する必要があることは注意が必要
Layout.tsx
<Link href="/blog"> <a data-testid="blog-nav" ... >Blog</a> </Link>
※クリックイベントにはタグ属性にtestIdを付与する必要がある
これらを実装した画面遷移を伴うテストであるNavBar.test.tsxテストの完成形はこちら
import { render, screen } from '@testing-library/react' import '@testing-library/jest-dom/extend-expect' import userEvent from '@testing-library/user-event' import { getPage } from 'next-page-tester' import { initTestHelpers } from 'next-page-tester' initTestHelpers() describe('画面遷移を伴うテスト', () => { it('テストケース1', async () => { const { page } = await getPage({ route: '/index', }) render(page) userEvent.click(screen.getByTestId('blog-nav')) expect(await screen.findByText('ブログページです')).toBeInTheDocument() }) })
Next.js は基本的にデフォルトでビルド時にHTMLを事前生成する
ビルド時にDBからPre-FetchをしてHTMLを事前生成する方式とアプリ運用時のサーバーアクセス時にDBからHTMLを生成する方法が大きく4種類あるので整理する
SSGはStatic Site Generationの略でユーザーに渡す情報は基本的には静的データを渡そうというコンセプトのもとに作られたアプリがユーザーにデータを渡す仕組みである
従来のデータ配信方式ではDBに格納された情報を毎回動的にHTMLにレンダリングして渡していたが、SSGで実装されたアプリでは一度HTMLにレンダリングされた情報は静的コンテンツにしておいて次回のアクセス時には静的コンテンツとして配信される
このHTMLにレンダリングするタイミングの違いによってSSGは以下の4種類のパターンに分類される
SSGについて例えを出すならば今までは飲食店で注文を受けたらその都度料理していたが、開店前に作り置きを作っておいて注文を受けたらそれを出すようなものです
また飲食店は新規メニューのオーダーも都度受け付けていて新規メニューの注文があればその場で料理を行って提供し、その後は作り置きを提供するような仕組みになっています
ISRはIncremental Static Regenerationの略で最新情報をHTMLにレンダリングしておいて静的コンテンツにしておく技術である
ちなみにややこしい話ですがSSRという単語もあって、こちらはServer Side Renderingの略でHTMLは従来クライアント側で生成していたものをサーバ側で作成して渡すようにしたことにあえて名前がついたものになっています
SSRについて例えを出すならば今までコープで食材だけもらって料理は自宅でするのが当たり前だったが生活様式が変化してきてUserEatsを使う人が増えたので"出前"という言葉がはやり始めたみたいな状況です
DocumentやHelpなどのように「完全に静的なページ」
SEOは特に意識しない
代表的なものとしては Index ページがこの実装にあたる場合が多いと思われる
あとはDocumentやHelpページ
ビルド時にデータベースから取得してきた情報を事前にHTMLにレンダリングして静的データとしておくパターンです
Googleのクローラーに読み込まれるため、SEO対策に有効です
使われるページの例としてはブログページや商品一覧などが考えられています
ただしそのままだとビルド時の情報のみ使用されるため最新情報を反映させ合たい場合は別途対策が必要です
最新情報を取得する場合にはISRが必要になり、基本的にはISRをセットで利用する場合が多いと思われます
ISRを加えた2.1のパターンがNext.jsで一番よく実装されるであろうと考えられます
SEOを意識する必要はないが、最新情報を取得したいページに利用されるパターンです
通常の「create-react-app」(従来のウェブアプリの実装)と似たようなレンダリングの仕組みになります
Client side fetching(通常のAxiosなどのAPI経由でデータを取得する方法)を使って、その都度バックエンドから取得してきたデータをレンダリングしてページを表示させます。
最新データはHTMLにレンダリングされないために新規作成もしくは更新された情報はDBから情報を取得して毎回レンダリングされる仕様となります
DBから情報を取得しないとレンダリングされないのでグーグルのクローラーは有益な情報は何もないと判断してしまい最新の情報に関してはSEOされることはありません
代表的なものとしてはDashboardやTodoリストなどのようなページです
データをリアルタイムで作成・更新する必要があって、かつ、最新情報にもSEO対策を施す必要があるようなページに利用されるパターンです
2のパターンとほぼ同様ですが、Client side fetchingを使用することで、データの更新をリアルタイムに反映することができます
ただしこの状態では画面表示においてビルド時の過去データと最新情報に乖離がある場合に過去データが表示されたのちに最新データに更新されるという挙動が起きてしまうので、以下の最新データから静的コンテンツを作成しておくISRと組み合わせて使う実装が実用的である
Next.jsはビルド時の情報が使われてその後情報が更新されません。
後々情報を更新したい場合はISR(DBの最新データで静的サイトを作り直す技術)を使って更新します
ビルド時にデータを事前にレンダリングするだけでなく、ビルド以降に作成・更新された最新情報に関してもHTML事前レンダリングが実行されて(初回アクセスがトリガー)グーグルのクローラーに分析してもらえるようになり、最新の情報にもSEO対策が施されます
初回アクセス以降は事前レンダリングされた最新情報のHTMLが配信されるためISRを使用しないと過去データが表示されたのちに最新データが表示される現象も解決する(具体的にはISRのrevaridateを設定する必要がある)
Next.js開発においてAPIエンドポイントのURLを環境変数化した場合にアプリ自体は動くがテストが通らなくなる事案が発生
調べによるとJESTは環境変数を.env.test.localに記載してもうまく読み込んでくれない場合がある模様
OS: Windows 10
node -v: v14.18.0
yarn -v: 1.22.17
package.json
{ "name": "hoge-project", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", "test": "jest --env=jsdom --verbose", "gen-types": "graphql-codegen --config codegen.yml" }, "dependencies": { "@apollo/client": "^3.4.17", "@apollo/react-hooks": "^4.0.0", "@heroicons/react": "^1.0.5", "autoprefixer": "^10.4.0", "cross-fetch": "^3.1.4", "graphql": "^16.0.1", "next": "11.1.2", "postcss": "^8.3.11", "react": "17.0.2", "react-dom": "17.0.2", "setimmediate": "^1.0.5", "tailwindcss": "^2.2.19" }, "devDependencies": { "@babel/core": "^7.16.0", "@graphql-codegen/cli": "2.3.0", "@graphql-codegen/typescript": "^2.4.1", "@graphql-codegen/typescript-operations": "2.2.1", "@graphql-codegen/typescript-react-apollo": "3.2.2", "@testing-library/dom": "^8.11.1", "@testing-library/jest-dom": "^5.15.0", "@testing-library/react": "^12.1.2", "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.0.3", "@types/node": "^16.11.8", "@types/react": "^17.0.35", "babel-jest": "^27.3.1", "eslint": "7", "eslint-config-next": "12.0.4", "jest": "^27.3.1", "jest-css-modules": "^2.1.0", "msw": "^0.35.0", "next-page-tester": "^0.30.0", "typescript": "^4.5.2" }, "jest": { "testPathIgnorePatterns": [ "<rootDir>/.next/", "<rootDir>/node_modules/" ], "moduleNameMapper": { "\\.(css)$": "<rootDir>/node_modules/jest-css-modules" } } }
process.env.NEXT_PUBLIC_HASURA_URL = 'https://xxx.hasura.app/v1/graphql'
こちらはテストをGitHubにアップロードするのをためらってしまうため実用的ではない
以下の記事に解決策は書いてあったものその通りの方法を実施した場合ではエラーが出たため自環境では各テストケースで環境変数を読み込む実装とした
Jestを用いたNext.jsのテスト内で環境変数を利用する - Breath Note
プロジェクト直下に
test/setupEnv.ts を作成し、以下を実装
import { loadEnvConfig } from '@next/env' export const setupEnv = async (): Promise<void> => { loadEnvConfig(process.env.PWD || process.cwd()) }
各テストのテストケースで上記のsetupEnvを実行する
XXXXX.test.tsx
// .......................... import { setupEnv } from "../test/setupEnv" //....................... describe('.................................', () => { it ('.................................', async () => { await setupEnv() // ......................................
こちらは各テストケースに記載する手間はあるが、外部ファイルから環境変数を読み込む形でテストを利用できる
対処2ではhandlersがsetupEnvより前に記述されている場合に環境変数を読み込んでくれないという事案が発生する
解決方法としては await setupEnv() の処理をテスト準備処理をしているコード上部に持ってきてawaitを取り除いてsetupEnv() を行う
こうすることでsetupEnv()を各テストケースに記載する手間も省けるのでこの形が最も良い
XXXXX.test.tsx
// .......................... import { setupEnv } from "../test/setupEnv" setupEnv() //.......................テスト準備の処理 // 本チャンテストにsetupEnvを記述する必要なし describe('.................................', () => { it ('.................................', async () => { // ......................................
Windows環境でnodeを使うといろいろなところで支障が出るとネットの記事に書いていましたが、例にもれずそのあおりを受けました
npx craate-react-app [アプリ名]
のコマンドを打つと、実行はされるが、以下のような警告が発生
気持ち悪いので、どうにかしたい
153 packages are looking for funding run `npm fund` for details found 24 vulnerabilities (8 moderate, 15 high, 1 critical) run `npm audit fix` to fix them, or `npm audit` for details
この場合、
npm audit fix
のコマンドでうまくいくケースがおおいらしいが、自分の環境ではうまくいかず、手動でどうにかしないといけない模様
[参考]
npm audit fixでは解決できなかった脆弱性の修正
npm audit
を実行。24個の修正対象パッケージが表示される。以下抜粋
=== npm audit security report === Manual Review Some vulnerabilities require your attention to resolve Visit https://go.npm.me/audit-guide for additional guidance Moderate Inefficient Regular Expression Complexity in chalk/ansi-regex Package ansi-regex Patched in >=5.0.1 Dependency of react-scripts Path react-scripts > webpack-dev-server > yargs > cliui > string-width > strip-ansi > ansi-regex More info https://github.com/advisories/GHSA-93q8-gq69-wqmw ...他多数 Critical Prototype Pollution in immer Package immer Patched in >=9.0.6 Dependency of react-scripts Path react-scripts > react-dev-utils > immer More info https://github.com/advisories/GHSA-33f9-j839-rf8h High Uncontrolled Resource Consumption in ansi-html Package ansi-html Patched in No patch available Dependency of react-scripts Path react-scripts > @pmmmwh/react-refresh-webpack-plugin > ansi-html More info https://github.com/advisories/GHSA-whgm-jr23-g3j9 ....他多数 found 24 vulnerabilities (8 moderate, 15 high, 1 critical) in 1956 scanned packages 24 vulnerabilities require manual review. See the full report for details.
ここでcriticalになってるimmerを調べる
npm ls immer
すると、依存関係がわかる
project_name@0.1.0 C:\Users\[MyUser]\Documents\Development\ReactTutorial\tutorial `-- react-scripts@4.0.3 `-- react-dev-utils@11.0.4 `-- immer@8.0.1
これはpackage.jsonには記述されていない箇所なので、この依存関係を修正していく
package-lock.jsonの方を修正する(lockの方ね)
immerで検索してヒットした箇所のバージョンを変更する
[修正前]
"immer": "8.0.1",
[修正後]
"immer": "9.0.6",
node-modulesを削除して、
npm install
を実行
これをエラーがなくなるまで警告が出ているパッケージに繰り返す
パッチがないものもあり、最後まで警告解消できないものもある
気持ち悪いが仕方ない....
npm install 実行後、npm audit実行
=== npm audit security report === Manual Review Some vulnerabilities require your attention to resolve Visit https://go.npm.me/audit-guide for additional guidance High Uncontrolled Resource Consumption in ansi-html Package ansi-html Patched in No patch available Dependency of react-scripts Path react-scripts > @pmmmwh/react-refresh-webpack-plugin > ansi-html More info https://github.com/advisories/GHSA-whgm-jr23-g3j9 High Uncontrolled Resource Consumption in ansi-html Package ansi-html Patched in No patch available Dependency of react-scripts Path react-scripts > webpack-dev-server > ansi-html More info https://github.com/advisories/GHSA-whgm-jr23-g3j9 found 2 high severity vulnerabilities in 1941 scanned packages 2 vulnerabilities require manual review. See the full report for details.
しかしながら、この状態でまた何らかのパッケージをインストール(npm install redux)すると、package-lockがもとに戻ってまたエラーが出てしまった
本番環境に挙げるときにこの作業するか、Linux環境作ってそこで実行する方がいいのかもと思った......
修正したpackage-lock.jsonは一応どこかに保管して差し替えて使おうと思う(自分の場合はuser直下のprojectsフォルダ直下に格納)
次にCreate-React-Appした際にこのlockファイルにさしえることで今回の手間を削減できるはず