0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ECサイト作成で学んだこと②

Posted at

今回は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リクエストの送信
views.py
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()が自動的に実行される。

views.py
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に保存しています。本来は絶対にやってはいけないことです。その点を理解した上でご覧ください。

実際のコードを見ていきましょう

order/models.py
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_numberexpiration_date を フォームレベルで再定義 しています。

order/forms.py
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, "ご購入ありがとうございます")

スクリーンショット 2025-03-06 1.20.48.png

送信失敗時のエラーメッセージ出力

messages.error(request, "カート内に商品がありません")

スクリーンショット 2025-03-06 1.19.23.png

共通で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 を利用してカートオブジェクトを取得または作成するクラスメソッドです。

cart/models.py
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
  1. request.session.session_keyを確認して、セッションがなければ作成
  2. session_idを使ってCartモデルのオブジェクトを検索
  3. 該当するカートがなければ、新しく作成して返す
  4. インスタンス化せずに `Cart.get_or_create_cart(request)として呼び出せる
cart/views.py
def get_context_data(self, **kwargs):
        # カートオブジェクトを取得
        cart = Cart.get_or_create_cart(self.request)

プロパティ(@property)とは

@propertyとは、インスタンスメソッドを「属性」のように使えるデコレーターのことです。計算結果を変えるプロパティを作成するのに適しています。

特徴

  • インスタンスごとに異なる値を計算できる
  • メソッドのように呼び出す必要がなく、プロパティのようにアクセスできる
  • DBの集計処理に適している

実装例 (Cart.total_price & Cart.total_quantity)

cart/models.py
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())
  1. total_priceは、カート内のすべての商品価格を合計して返す
  2. total_quantityは、カート内のすべての商品数を合計して返す
  3. メソッドのようにcart.total_price()とは書かず、cart.total_price`としてアクセス可能
  4. DBのフィールドを直接持たず、計算処理を実装できる

最後に

今回はECサイト作成で学んだこと②ということで
Djangoについて記載しました。

次回はPythonの文法についてを書きます!

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?