今回はECサイトを作成で学んだことを4つに分けて記事にしていきます。
以下の箇条書きの記事をそれぞれ作成します。
記事が出来次第、リンクへ変更になりますのでお待ちください。
- エンジニアリングについて
- Djangoについて
- Pythonの文法
- 汎用ビューについて
Djangoについて
- Validation Error出力までの流れ
- Formクラスでのフィールドの再定義
- messageフレームワークの使用
- 各カラムの制約
- clsmethodとpropatyの違い
Validation Error出力までの流れ
Django では、フォームのバリデーションエラーを処理する際に add_error() を活用することで、エラーメッセージを form.errors に蓄積し、ユーザーに適切にフィードバックできます。
本来なら以下の3つを実行してバリデーションエラーをチェックしていきます
-
Field.clean()
-
CharField
,IntegerField
などのフィールドごとのバリデーション
-
-
clean_<field>()
- 各フィールのカスタムバリデーション
-
clean()
- デフォルトでは実行されない
- フォーム全体のバリデーションをしたい場合にオーバーライドして実行
しかし、今回のECサイトではclean()
を定義しませんでした。
以下2点が理由です。ご了承ください。
-
clean_<field>()
で対応可能だったから - 複数フィールドのバリデーションが必要ではなかったから
- password, confirm_passwordなど
add_error()
を使うメリット
Django のフォームバリデーションでは、add_error() を使うことで、複数のエラーを一度にチェックし、フォーム全体のバリデーションを適切に処理できます。
raise ValidationError を使うと…
- エラーが発生した時点で処理が 中断 される
- 1つのエラーが検出された時点で、後続のバリデーションが実行されない
add_error() を使うと…
- 複数のエラーを一度に処理できる
- フォーム全体のバリデーションをまとめて実行できる
- エラーメッセージを form.errors に蓄積できる
今回は購入処理の部分の請求先住所と支払い情報の部分にFormが必要になりましたので、そこを実例として書いていきます。
Validation Error出力までの流れ
- POSTリクエストの送信
class OrderView(View):
def post(self, request):
billing_address_form = BillingAddressForm(request.POST)
payment_form = PaymentForm(request.POST)
is_valid()
の実行(バリデーションチェック)
Django では、フォームの form.errors にアクセスすると is_valid()が自動的に実行される。
for field, errors in billing_address_form.errors.items():
for error in errors:
error_messages.append(f"{billing_address_form[field].label}: {error}")
-
form.errors
にアクセスすると、Django は内部的にis_valid()
を実行する -
is_valid()
はclean_<field>()
→clean()
の順でバリデーションを実行
# clean_<field>() フィールド単体のバリデーション
def clean_last_name(self):
last_name = self.cleaned_data.get("last_name", "")
if not re.fullmatch(r'[a-zA-Zぁ-んァ-ン一-鿿々-〇]+', last_name):
self.add_error("last_name", "姓はアルファベット、ひらがな、カタカナ、漢字で入力してください")
return last_name
バリデーションエラーの収集
エラーがform.errors
に格納された後、メッセージをmessages.error()
に追加
error_messages = []
for field, errors in billing_address_form.errors.items():
for error in errors:
error_messages.append(f"{billing_address_form[field].label}: {error}")
for message in error_messages:
messages.error(request, message)
エラーをセッションに保存
エラーがある場合、ユーザーの入力内容を保持しながらリダイレクト
request.session["billing_address_form_data"] = request.POST.dict()
request.session["payment_form_data"] = request.POST.dict()
return redirect(reverse("cart:cart_page"))
.html
でエラーメッセージを出力
エラーメッセージはDjangoのmessage
を使ってテンプレートに出力
(messageフレームワークの詳しい説明はスクロールしてご確認ください)
{% if messages %}
<div class="alert alert-danger">
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
Formクラスでのフィールドの再定義
Formクラスでフィールを再定義する理由
DjangoのModelForm
を使用する場合、デフォルトではMeta.fields
に定義されたフィールドがModel
の定義に基づいて自動で作成されます。しかし、フォームの仕様に応じて以下のような理由でフィールドを再定義することがあります。
- モデルのフィールドと異なるバリデーションを適用したい
- ラベルやヘルプテキストを変更したい
- デフォルトとな異なるウィジェット(フォームの入力欄)を使用したい
- ユーザー入力の前処理を行いたい(ハイフンを削除するなど)
ポートフォリオということで今回だけ特別にクレカ情報をDBに保存しています。本来は絶対にやってはいけないことです。その点を理解した上でご覧ください。
実際のコードを見ていきましょう
class Payment(models.Model):
card_holder = models.CharField("カード名義")
card_number_hash = models.CharField("カード番号(暗号化)", max_length=255)
card_last4 = models.CharField("カード下4桁", max_length=19)
expiration_date = models.CharField("有効期限", max_length=5)
以下の PaymentForm
では card_number
と expiration_date
を フォームレベルで再定義 しています。
class PaymentForm(forms.ModelForm):
card_number = forms.CharField(
label="カード番号",
max_length=19,
required=True,
help_text="クレジットカード番号(ハイフンなしまたは 0000-0000-0000-0000)"
)
expiration_date = forms.CharField(
label="有効期限",
max_length=5,
required=True,
help_text="MM/YY の形式で入力してください"
)
class Meta:
model = Payment
fields = ("card_holder",)
-
card_number
は モデルには存在しないが、フォームで追加されている -
expiration_date
は モデルフィールドと同じだが、ヘルプテキストやラベルを変更している
フィールドを再定義するメリット
- バリデーションをフォーム側で制御可能
再定義した card_number
は、フォームレベルでバリデーションが追加できます。
-
clean_card_number()
を実装することで、フォームのバリデーションを柔軟に変更可能 - ユーザーが「
-
」を入力しても削除するといった前処理ができる
def clean_card_number(self):
card_number = self.cleaned_data.get("card_number", "").replace("-", "").strip()
if not card_number.isdigit():
self.add_error("card_number", "カード番号は数字を入力してください")
if not (14 <= len(card_number) <= 16):
self.add_error("card_number", "カード番号は14~16桁で入力してください")
return card_number
- モデルに保存しない一時的なフィールドを追加できる
モデルに存在しない card_number をフォームに追加し、保存時に 暗号化して card_number_hash に保存 する処理を実装できます。
今回のモデルには card_number を保存せず、ハッシュ化して保存することにしました。
def save(self, commit=True):
instance = super().save(commit=False)
card_number = self.cleaned_data["card_number"]
instance.card_number_hash = Payment.hashed_card_number(card_number) # ハッシュ化
instance.card_last4 = Payment.card_number_slice(card_number) # 下4桁を保存
if commit:
instance.save()
return instance
messagesフレームワークの使用
messages
フレームワークとはユーザーへの通知メッセージを管理するための仕組みです。
今回の実装では以下の2点で使用しました。
フォーム送信後の成功メッセージ出力
messages.success(request, "ご購入ありがとうございます")
送信失敗時のエラーメッセージ出力
messages.error(request, "カート内に商品がありません")
共通でhtml
を出力します。
-
messages
が存在する場合、すべてのメッセージを表示 -
message.tags
を使うことで、メッセージの種類 (success
,error
など) に応じた CSS を適用できる
{% if messages %}
<div class="alert-messages">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
他にも種類がありますので詳しくは以下のリンクよりご覧ください。
各カラムの制約
DB設計をしてmodelを書き始める際に
unique
, not null
などはどのように書けばいいのかということで調べました。
max_length (最大文字数)
max_length
を設定することで、DBレベルでの最大文字数を制限できます。
name = models.CharField("商品名", max_length=20)
-
max_length=20
により、20文字以内の文字列のみ保存可能 - フォーム(forms.CharField)にも自動的に適用される
unique制約
unique=True
を設定すると、そのカラムの値が重複しないように制約されます。
username = models.CharField("ユーザー名", unique=True)
- 同じ値を持つレコードを作成しようとするとエラーが発生
- DBレベルで
UNIQUE
制約が設定 - CharField, IntegerFieldなど、他のフィールドにも適用可能
verbose_name (管理画面の表示名)
verbose_name
を設定すると、Djangoの管理画面(admin)やフォームでフィールド名の表示をカスタマイズできます。
cart = models.ForeignKey("cart.Cart",
verbose_name=("カートID"),
on_delete=models.CASCADE,
related_name="cart_products")
default(デフォルト値の設定)
default
を設定すると、新しいレコードを作成する際に、値を入力しなくてもデフォルト値が自動で設定されます。
quantity = models.IntegerField(verbose_name='個数', default=1)
- フィールドがからの場合でも
default
の値が自動で入る - データベースレベルのデフォルト値として設定される
blankとnullの違い
blank=True
:フォームで必須入力にしない
→バリデーションの制御
null=True
:データベースでNULLを許可
→DBレベルの制御
今回は実装しませんでしたが例を紹介
class UserProfile(models.Model):
nickname = models.CharField(max_length=50, blank=True, null=True)
validators(値のバリデーション)
validators
を使用すると、保存する値の最小値・最大値を制限可能
from django.core.validators import MinValueValidator
...
price = models.PositiveIntegerField("商品価格", validators=[MinValueValidator(1)])
quantity = models.PositiveIntegerField("個数", validators=[MinValueValidator(1)])
subtotal = models.PositiveIntegerField("小計", validators=[MinValueValidator(1)])
クラスメソッドとプロパティの違い
クラスメソッド(@classmethod)とは?
@classmethod
とは、クラスに対して直接呼び出せるメソッドのことです。クラス全体に関わる処理を定義する際に使用します。
特徴
- 第一引数が
cls
(クラス自身) - インスタンス化せず呼び出せる
- DBの検索やオブジェクトの作成に役立つ
実装例 (Cart.get_or_create_cart())
次に実装例を見ていきましょう
Cart.get_or_create_cart() は、セッション ID を利用してカートオブジェクトを取得または作成するクラスメソッドです。
class Cart(models.Model):
session_id = models.CharField(max_length=32, unique=True, default="temp_session_id")
@classmethod
def get_or_create_cart(cls, request):
if not request.session.session_key:
request.session.create()
session_id = request.session.session_key
cart, _ = cls.objects.get_or_create(session_id=session_id)
return cart
-
request.session.session_key
を確認して、セッションがなければ作成 -
session_id
を使ってCart
モデルのオブジェクトを検索 - 該当するカートがなければ、新しく作成して返す
- インスタンス化せずに `Cart.get_or_create_cart(request)として呼び出せる
def get_context_data(self, **kwargs):
# カートオブジェクトを取得
cart = Cart.get_or_create_cart(self.request)
プロパティ(@property)とは
@property
とは、インスタンスメソッドを「属性」のように使えるデコレーターのことです。計算結果を変えるプロパティを作成するのに適しています。
特徴
- インスタンスごとに異なる値を計算できる
- メソッドのように呼び出す必要がなく、プロパティのようにアクセスできる
- DBの集計処理に適している
実装例 (Cart.total_price & Cart.total_quantity)
class Cart(models.Model):
session_id = models.CharField(max_length=32, unique=True, default="temp_session_id")
......
@property
def total_price(self):
return sum(cart_product.sub_total_price() for cart_product in self.cart_products.all())
@property
def total_quantity(self):
return sum(cart_product.quantity for cart_product in self.cart_products.all())
-
total_price
は、カート内のすべての商品価格を合計して返す -
total_quantity
は、カート内のすべての商品数を合計して返す - メソッドのように
cart.total_price()とは書かず、
cart.total_price`としてアクセス可能 - DBのフィールドを直接持たず、計算処理を実装できる
最後に
今回はECサイト作成で学んだこと②ということで
Djangoについて記載しました。
次回はPythonの文法についてを書きます!