djangoで作る簡易ECサイト Part2
djangoで簡単なTodoアプリ等は作成できたけれども、初心者から中級者への道のりが見えない。オリジナルWebアプリを作る前に、もう少しだけ色々なアプリを作って実践練習をしたい方を対象としています。
Part2では、③ECサイトのモデル作成と管理画面から商品登録と④トップページと商品詳細ページの作成について説明します。Part1の続きとなりますのでそれらを最初にご確認ください。
また、認証周りはallauthを使っていますのでそちらについては、djangoで作る本格的なSNSアプリケーション 番外編 allauthによる認証をご覧下さい。
開発の手順のおさらい
⓪ECサイトのワイヤーフレームと必要なテーブルの検討 ← Part 1
①ECサイトプロジェクトの開始とアプリの作成 ← Part 1
②allauthによる認証機能の実装とECサイト用カスタマイズ ← Part 1
③ECサイトのモデル作成と管理画面から商品登録 ← 今回
④トップページ、商品一覧と商品詳細ページの作成 ← 今回
⑤カート機能実装と関連ページ作成 ← Part 3
⑥チェックアウト画面とPDFによる領収書作成 ← Part 3
Part1でユーザー関連のaccountsアプリが完成しましたので、次はECサイト関連のecsiteアプリを作成していきます
③ECサイトのモデル作成と管理画面から商品登録
ecsiteアプリケーションと必要ファイルの作成
まずは、ターミナルからecsiteアプリケーションを作成します。
% python manage.py startapp ecsite
ecsiteで必要となるファイルとフォルダーをまとめて作成します。ecsiteフォルダー内には、2つのファイルを作成します。urls.pyとservices.pyです。services.pyですが、馴染みないファイル名と思います。このファイルは、models.pyやviews.pyをなるべく見通しよくするためにビジネスロジックや関数などを記載していきます。
templatesフォルダー直下にecsiteフォルダーを作成し、ecsiteで使うhtmlを全て保存します。必要なページはトップページ=shophome.html、商品一覧ページ=list.html、商品詳細ページ=detail.html、ショッピングカートページ=mycart.html、チェックアウトページ=checkout.htmlです。
ECサイトのモデル作成
ER図(下図参照)をもとに、ECサイトで必要となるモデルをまとめて作成していきます。必要なモデルは、商品マスタとなるProductモデル、商品画像マスタとなるProduct_pictures、ショッピングカートとなるCart_itemモデル、注文を登録するOrderモデル、注文詳細を登録するOrder_itemsモデルの5つとなります。
まずは、商品マスタとなるProductモデルを作成します。商品名=name、種類=type、価格=price、在庫数=stock、商品説明=comments、サイズ=size、キャンペーン情報=campaign、原材料= ingredientsを登録できるようにします。
商品は、管理画面から登録することにします。pattiserrieNSで取り扱う商品の種類は、生菓子、焼菓子、備品の3種類とする予定です。管理画面からの登録の際に、プルダウンリストで選択出来るようにすれば入力が便利になりますのでtypeフィールドに設定します。また、verbose_nameをそれぞれのfieldに指定することも忘れないようにしましょう。管理画面では、日本語でそれぞれのフィールド名を表示させた方が分かり易いと思います。
class Product(models.Model):
Cakes = 'cakes'
BakedCakes = 'bakedcakes'
Goods = 'goods'
TYPE = [
(Cakes, '生菓子'),
(BakedCakes, '焼き菓子'),
(Goods, '備品'),
]
name = models.CharField(max_length=50, verbose_name='商品名')
type = models.CharField(max_length=20, verbose_name='種類', choices=TYPE)
price = models.IntegerField(verbose_name='価格')
stock = models.IntegerField(verbose_name='在庫')
comments = models.CharField(max_length=100, verbose_name='商品説明')
size = models.CharField(max_length=50, verbose_name='サイズ')
campaign = models.CharField(max_length=100, verbose_name='キャンペーン説明', blank=True, null=True)
ingredients = models.CharField(max_length=50, verbose_name='原材料')
class Meta:
db_table = 'products'
def __str__(self):
return self.name
商品には写真が必要ですので、商品画像マスタとなるProduct_pictureモデルを作成します。商品画像は、商品に紐づきますので商品ID=product_idを外部キーに持たせますのでForeignKeyとします。画像は複数枚登録しますので、画像に優先順位=priorityを登録できるようにしましょう。
詳細画面で一番大きく表示させたい画像に1番を付けるなどのルールを決めて運用すれば良いでしょう。また、画像の優先順位が重複しないユニーク制約もclass Meta:内にconstraintsで付与します。
constraints = [ models.UniqueConstraint(fields=['product', 'priority'], name='picture_priority') ]
class ProductPicture(models.Model):
picture = models.FileField(upload_to='product_pictures')
product = models.ForeignKey(Product, on_delete=models.CASCADE)
priority = models.IntegerField()
class Meta:
db_table = 'product_pictures'
ordering = ['priority']
# 画像の優先順位が重複しないように制約を追加
constraints = [
models.UniqueConstraint(fields=['product', 'priority'], name='picture_priority')
]
def __str__(self):
return self.product.name + ' ( ' + str(self.priority) + ' )'
ショッピングカートとなるCartItemモデルは、カートの持ち主を指定するためユーザーID=user_idの外部キーを持ちます。また、カートに入っている商品を商品ID=product_idの外部キーで識別しますので、それぞれにForeignKeyを設定します。商品数量=qtyは0以上の整数を隣りますので、でPositiveIntegerFieldを指定します。また、ショッピングカート内に同じ商品を何度も入れられないようにするユニーク制約をproductとuser間に指定します。これにより、同じ商品を複数購入したい場合は、数量=qtyで指定する事になります。
constraints = [ models.UniqueConstraint(fields=['product', 'user'], name='incart_product')]
class CartItem(models.Model):
qty = models.PositiveIntegerField()
product = models.ForeignKey(Product, on_delete=models.CASCADE)
user = models.ForeignKey(
CustomUser, on_delete=models.CASCADE
)
class Meta:
db_table = 'cart_items'
constraints = [
models.UniqueConstraint(fields=['product', 'user'], name='incart_product')
]
Orderモデルは、注文合計金額=total_priceと注文したユーザーを紐づけるユーザーID=user_idの外部キーを持ちます。ユーザーの退会などでユーザー情報が削除された場合も受注履歴は残したいので、on_delete= models.SET_NULLをuserフィールドに指定します。
今後、ユーザーの登録住所を複数設定したい場合などは、ここに住所モデルを外部キーと持たせてカラムを追加してもよいでしょう。
class Order(models.Model):
total_price = models.PositiveIntegerField()
user = models.ForeignKey(
CustomUser, on_delete=models.SET_NULL, blank=True, null=True)
class Meta:
db_table = 'orders'
最後にOderItemモデル=注文明細を登録します。注文明細には、商品ID= product_idおよび注文ID=order_idそして商品数量=qtyが必要です。商品IDと注文IDを外部キーとしてForeignKeyを指定します。限定販売品などの商品がモデルから削除されてしまった場合の事を想定して、productフィールドにはon_delete = models.SET_NULLを指定します。また、orderフィールドは、on_delete=models.CASCADEとします。これは、注文が取り消された場合などに注文明細も同時に削除されるようにするために指定しています。また、商品と注文にはユニーク制約を付けます。同じ注文で同じ商品が登録するようにするためです。
constraints = [models.UniqueConstraint(fields=['product', 'order'], name='order_constrain')]
class OrderItem(models.Model):
qty = models.PositiveIntegerField()
product = models.ForeignKey(
Product, on_delete=models.SET_NULL, blank=True, null=True)
order = models.ForeignKey(
Order, on_delete=models.CASCADE, blank=True, null=True)
class Meta:
db_table = 'order_items'
constraints = [
models.UniqueConstraint(fields=['product', 'order'], name='order_constrain')
]
最終的なecsite/model.pyは、以下のようなコードとなります。
ecsite/model.py
from django.db import models
from accounts.models import CustomUser
class Product(models.Model):
Cakes = 'cakes'
BakedCakes = 'bakedcakes'
Goods = 'goods'
TYPE = [
(Cakes, '生菓子'),
(BakedCakes, '焼き菓子'),
(Goods, '備品'),
]
name = models.CharField(max_length=50, verbose_name='商品名')
type = models.CharField(max_length=20, verbose_name='種類', choices=TYPE)
price = models.IntegerField(verbose_name='価格')
stock = models.IntegerField(verbose_name='在庫')
comments = models.CharField(max_length=100, verbose_name='商品説明')
size = models.CharField(max_length=50, verbose_name='サイズ')
campaign = models.CharField(max_length=100, verbose_name='キャンペーン説明', blank=True, null=True)
ingredients = models.CharField(max_length=50, verbose_name='原材料')
class Meta:
db_table = 'products'
def __str__(self):
return self.name
class ProductPictures(models.Model):
picture = models.FileField(upload_to='product_pictures')
product = models.ForeignKey(Product, on_delete=models.CASCADE)
priority = models.IntegerField()
class Meta:
db_table = 'product_pictures'
ordering = ['priority']
# 画像の優先順位が重複しないように制約を追加
constraints = [
models.UniqueConstraint(fields=['product', 'priority'], name='picture_priority')
]
def __str__(self):
return self.product.name + ':' + str(self.priority)
class CartItem(models.Model):
qty = models.PositiveIntegerField()
product = models.ForeignKey(Product, on_delete=models.CASCADE)
user = models.ForeignKey(
CustomUser, on_delete=models.CASCADE
)
class Meta:
db_table = 'cart_items'
constraints = [
models.UniqueConstraint(fields=['product', 'user'], name='incart_product')
]
class Order(models.Model):
total_price = models.PositiveIntegerField()
user = models.ForeignKey(
CustomUser, on_delete=models.SET_NULL, blank=True, null=True)
class Meta:
db_table = 'orders'
class OrderItem(models.Model):
qty = models.PositiveIntegerField()
product = models.ForeignKey(
Product, on_delete=models.SET_NULL, blank=True, null=True)
order = models.ForeignKey(
Order, on_delete=models.CASCADE, blank=True, null=True)
class Meta:
db_table = 'order_items'
constraints = [
models.UniqueConstraint(fields=['product', 'order'], name='order_constrain')
]
モデル作成が完了しましたので、config/settings.pyのINSTALLED _APPSにecsiteアプリを追加します。
config/settings.py
省略
INSTALLED_APPS = [
・・・省略・・・
'allauth.socialaccount',
'ecsite', # 追加
]
・・・省略・・・
ターミナルからecsiteアプリのマイグレーションを行いDBにテーブルを作成します。
python manage.py makemigrations ecsite
python manage.py migrate ecsite
マイグレーションに成功しましたら、データベースに指定したテーブルが作成されているかを確認します。以下の画像のように5つのテーブルは作成されているでしょうか?それぞれのフィールドが正しく設定されているかも確認してみて下さい。
管理画面から商品と商品画像登録
スーパーユーザーを作成して、管理画面を使えるようにしていきます。ecsite/admin.pyを開き管理画面の設定をしていきます。
ecsite/admin.py
from django.contrib import admin
from .models import Product, ProductPicture
class ProductAdmin(admin.ModelAdmin):
list_display = ('name', 'type', 'price', 'stock')
class ProductPicturesAdmin(admin.ModelAdmin):
list_display = ('product', 'priority', 'picture')
admin.site.register(Product, ProductAdmin)
admin.site.register(ProductPicture, ProductPicturesAdmin)
ターミナルを開き以下のコマンドでスーパーユーザーを作成します。メールアドレスとパスワードを登録して問題が無ければSuperuser created successfullyとターミナルに表示されていると思います。
% python manage.py createsuperuser
ターミナルからサーバーを立ち上げてhttp://127.0.0.1:8000/admin/へアクセスし、先ほど設定したスーパーユーザーでログインして下さい。以下のような管理画面が表示されると思います。
左のECSITEのProductsからいくつか商品を登録してみましょう。種類のフィールドは、プルダウンリストから選べるようにモデル内で設定していました。今回は、ショートケーキなので生菓子を選んでいます。
以下の画像は、いくつか商品を登録した後の画像になります。
左のECSITEのProduct picturessから画像も登録します。Productへの外部キーを持たせていましたので、登録済みの商品からドロップダウンリストで選びます。優先順位を付けるのを忘れないようにしてください。また、優先順位と商品にユニーク制約を付けていましたので、1つの商品には同じ優先順位を登録できなくなっていることも確かめて下さい。
④トップページ、商品一覧と商品詳細ページの作成
実使用上はトップページもjavascriptなども使って動くページにして印象的にしたいですが、今回はサンプルアプリですので簡易的に準備します。
トップページの作成
まずは、ecsite/views.pyを開いてHomeViewクラスをTemplateView を継承して作成します。template_nameは'ecsite/home.html'とします。この時点でのecsite/views.pyは以下となります。
ecsite/views.py
from django.views.generic import TemplateView
class HomeView(TemplateView):
template_name = 'ecsite/home.html'
次にurlを設定していきます。まずは、config/urls.pyにecsiteアプリへのパスを通します。config/urls.pyを開き、urlpatternsにpath('ecsite/', include ('ecsite.urls')),を追加します。
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from . import settings
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('allauth.urls')),
path('accounts/', include('accounts.urls')),
path('ecsite/', include('ecsite.urls')), # 追加
]
if settings.DEBUG:
urlpatterns += static(settings.IMAGE_URL, document_root=settings.IMAGE_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
次は、アプリ側のurls.pyを設定します。ecsite/urls.pyを開き、先ほど作成したHomeViewをインポートします。 app_nameはecsiteとします。
urlpatternsにHomeViewのurlとして'home/'を設定し、名前空間を'home'とします。これで、ecsite/homeへアクセすればHomeViewで指定したhome.htmlを画面に表示出来るようになります。
ecsite/urls.py
from django.urls import path
from .views import HomeView
app_name = 'ecsite'
urlpatterns = [
path('home/', HomeView.as_view(), name='home'),
]
ログイン直後は、このhome.htmlへリダイレクトさせたいのでconfig/settings.pyを開いてそのように設定を変更します。
config/settings.py
・・省略・・・・
SITE_ID = 1
LOGIN_REDIRECT_URL = '/ecsite/home' # 変更
ACCOUNT_LOGOUT_REDIRECT_URL = 'account_login'
・・・省略・・・
パスを通しましたので、対応するtemplateを作成します。ここでは、トップページをhome.htmlとして作成していきます。コードは以下の通りです。
{% extends 'base.html' %}でbase.htmlを読み込みます。django-bootstrap4を使いますので、{% load bootstrap4 %}で読み込ませます。
{% block content %} 〜 {% endblock %}内にトップページのコンテンツを記載していきます。今回は、1つの画像と簡単な説明を表示させるだけの簡単なページとします。<div class="center jumbotron">〜 </div>でbootstrap4のジャンボトロンを使っています。<div class="text-center">〜 </div>内に説明をh3タグで記載して、その下にimgタグでお店の画像を表示させます。画像は、/media/images/フォルダー内に好きなものを保存して下さい。
templates/ecsite/home.html
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block content %}
<div class="center jumbotron">
<div class="text-center">
<h3>素材にこだわった</h3>
<h3>いつものケーキをご自宅で</h3>
<img src="/media/images/cake_ya_building.png">
</div>
</div>
{% endblock %}
トップページ用画像
ターミナルからサーバーを立ち上げてhttp://127.0.0.1:8000/ecsite/home/へアクセスし正しく画面が表示されるか確認します。以下のように表示されていればトップページの完成です。
商品一覧ページの作成
商品一覧ページを作ります。まずは、出来上がりのイメージを以下の画像から掴んでください。一覧ページなので、ListViewを継承してクラスベースビューで作成するのが良いでしょう。
商品一覧ページを作ります。ecsite/views.pyにListViewを継承してProductListViewクラスを作成します。また、Productモデルを使いますのでコードの最初でインポートします。 templateは、templates/ecsiteフォルダー内に作成する事にしますので、商品一覧画面をlist.htmlとすると、template_name = 'ecsite/list.html'となります。
商品情報をproductsテーブルからget_context_dataで取得します。Productモデルからobjectsを全件取得するクエリセットを設定しproductへ入れます。テンプレート側では、商品種類毎で表示させるようにしたいのでコンテクストを種類毎に設定します。context['cakes']へは、生菓子=cakesだけを取り出したいのでフィルターでtype='cakes'を指定します。
context['cakes'] = product.filter(type='cakes')
焼菓子=bakedcakesと備品=goodsも同様にフィルターを用いてcontext ['bakedcakes']とcontext['goods']へ格納してテンプレートで使えるようにreturnでcontextを返します。最終的なコードは以下となります。
ecsite/views.py
from django.views.generic import TemplateView, ListView # 追加
from .models import Product # 追加
・・・省略・・・
class ProductListView(ListView):
model = Product
template_name = 'ecsite/list.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
product = Product.objects.all()
context['cakes'] = product.filter(type='cakes')
context['bakedcakes'] = product.filter(type='bakedcakes')
context['goods'] = product.filter(type='goods')
return context
次にパスを通します。ecsite/urls.pyを開いて下さい。まずは、 ProductListViewをインポートしましょう。その後は、urlpatternsにProductListViewのurlとして'list/'を設定し、名前空間を'product_list'とします。これで、ecsite/listにアクセするとProductListViewで指定したecsite/list.htmlを画面に表示する事が出来ます。
ecsite/urls.py
from django.urls import path
from .views import HomeView,ProductListView #追加
app_name = 'ecsite'
urlpatterns = [
path('home/', HomeView.as_view(), name='home'),
path('list/', ProductListView.as_view(), name='product_list'), #追加
]
対応するecsite/list.html作成します。少し長くなりますので、少しづつ説明していきます。{% extends 'base.html' %}〜{% block content %}までは、お決まりの部分ですので特に説明は不要かと思います。
<div class="col-md-8 offset-md-2">でbootstrap4のグリッド設定をしています。htmlは大きく3つのsectionで構成され、それぞれのセクションをhrタグで区切っています。
<section class="products" id="cakes">
{# ここに生菓子覧を並べる #}
</section>
<hr class="hr-text" data-content="AND">
<section class="products" id="bakedcake">
{# ここに焼菓子覧を並べる #}
</section>
<hr class="hr-text" data-content="AND">
<section class="products" id="goods">
{# ここに備品一覧を並べる #}
</section>
まずは、{# ここに生菓子覧を並べる #}のところですがh3とh5の見出しタグでケーキ一覧の説明を加えます。先程、ProductListViewでcontext['cakes']= product.filter(type='cakes')としてケーキ一覧をcakesとしてテンプレートから取り出せるようにしていました。{% for product in cakes %}〜{% endfor %}内では、for文でデータベースに登録されている生菓子を一つづつcakeというオブジェクトへ取り出しています。
また、ProductpictureモデルはProductモデルへ外部キーを持っており、そのオプションとしてrelated_name='pictures'を指定していました。このrelated_nameを使って、オブジェクトに紐づくデータを取得する事が出来ます。
オブジェクト.related_name.all()
cake.pictures.all()
今回の場合は、for文で取り出したcakeオブジェクト(例えば、ショートケーキ)に紐づくProductmodelのオブジェクト(pictures)を全て(all())取り出します。この取り出したオブジェクトをさらにfor文でpictureへ一つづつ取り出したのが{% for picture in cake.pictures.all %} 〜 {% endfor %}になります。
少し複雑ですので、各オブジェクトに何が入っているのかを整理します。
・cakes : Productテーブル内で種類=生菓子のオブジェクトリストが格納されている。
・cake:cakesオブジェクトリストからfor文で取り出したオブジェクトが格納されている。
・cake.pictures.all:productpictureテーブル内でcakeオブジェクトに紐付くオブジェクトリストが格納されている。
これより、pictureにはproductの画像が1つ格納される事になります。src={{ picture.picture.url }とすることで、商品画像を表示させる事が出来ます。また、1つの商品に複数の画像を登録できるようにしていましたが、商品一覧には1商品1つの画像のみを表示させたいので{% if forloop.first %}でforループの1つ目の画像のみを表示させるようにしています。商品画像の下には、商品名を表示させたいのでcake.nameとする事で商品名を表示させます。
生菓子、焼菓子、備品をhrタグで区切ります。少し色や形状を変更したいのでCSSで指定しています。<hr class="hr-text" data-content="AND">
焼菓子と備品の一覧についても同じですので説明は割愛します。最終的なコードは以下となります。CSSはコピー&ペーストで貼り付けていただければ同じ表示となります。
ecsite/list.html
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block content %}
<div class="col-md-8 offset-md-2">
<section class="products">
<h3 class="text-center">ケーキのご案内</h3>
<h5 class="text-center font-italic">Cakes</h5>
<ul class="clearfix">
{% for product in cakes %}
{% for picture in product.productpictures_set.all %}
{% if forloop.first %}
<li><a href="{% url 'ecsite:product_detail' pk=product.id %}">
<img class="rounded img-fluid" src={{ picture.picture.url }}>
{{ product.name }}</a></li>
{% endif %}
{% endfor %}
{% endfor %}
</ul>
</section>
<hr class="hr-text" data-content="AND">
<section class="products" id="bakedcake">
<h3 class="text-center">焼菓子のご案内</h3>
<h5 class="text-center font-italic">Baked cakes & cookies</h5>
<ul class="clearfix">
{% for product in bakedcakes %}
{% for picture in product.productpictures_set.all %}
{% if forloop.first %}
<li><a href="{% url 'ecsite:product_detail' pk=product.id %}">
<img class="rounded img-fluid d-inline-block " width="200px" height="200px"
src={{ picture.picture.url }}>
{{ product.name }}</a></li>
{% endif %}
{% endfor %}
{% endfor %}
</ul>
</section>
<hr class="hr-text" data-content="AND">
<section class="products" id="goods">
<h3 class="text-center">備品</h3>
<h5 class="text-center font-italic">Goods</h5>
<ul class="clearfix">
{% for product in goods %}
{% for picture in product.productpictures_set.all %}
{% if forloop.first %}
<li><a href="{% url 'ecsite:product_detail' pk=product.id %}">
<img class="rounded img-fluid d-inline-block " width="200px" height="200px"
src={{ picture.picture.url }}>
{{ product.name }}</a></li>
{% endif %}
{% endfor %}
{% endfor %}
</ul>
</section>
</div>
{% endblock %}
static/style.css
.products ul {
margin: 0 auto;
display: block;
flex-wrap: wrap;
-webkit-box-pack: justify;
justify-content: space-between;
}
ul {
list-style: none;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0;
margin-inline-end: 0;
padding-inline-start: 40px;
}
.clearfix:after{
content: ".";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
.products
ul li {
margin: 0;
display: inline;
}
.products
ul li a {
display: block;
width: 180px;
float: left;
margin-right: 20px;
}
.product_pict {
text-align: center;
}
#bakedcake {
padding-top: 30px;
}
#goods {
padding-top: 30px;
}
#gallery img{
width: 60px;
margin-top: 5px;
margin-right: 20px;
}
.hr-text {
line-height: 1em;
position: relative;
outline: 0;
border: 0;
color: hotpink;
text-align: center;
height: 1.5em;
opacity: .5;
}
.hr-text:before {
content: '';
background: -webkit-linear-gradient(left, transparent, #FF69B4, transparent);
background: linear-gradient(to right, transparent, #FF69B4, transparent);
position: absolute;
left: 0;
top: 50%;
width: 100%;
height: 1px;
}
.hr-text:after {
content: attr(data-content);
position: relative;
display: inline-block;
color: hotpink;
padding: 0 .5em;
line-height: 1.5em;
background-color: #fffafe;
}
section {
display: block;
}
body {
background-color: #f8f8f8;
}
ターミナルからサーバーを立ち上げてhttp://127.0.0.1:8000/ecsite/list/へアクセスし、画面が正しく表示されるか確認します。ご自身で登録された画像は表示されたでしょうか?表示が崩れてしまう場合は、CSSで調整してみて下さい。今の段階では、商品画像をクリックしても商品の詳細へはリンクされていません。次に商品詳細ページを作っていきます。
商品詳細ページの作成
ここから、商品詳細ページを作成します。ecsite/views.pyにDetailViewを継承してProductDetailViewクラスを作成していきます。使用するモデルはProduct、templateは、templates/ecsiteフォルダー内に作成する事にしましたので、商品詳細画面をdetail.htmlとすると、template_name = 'ecsite/ detail.html'となります。
商品情報をproductsテーブルからget_context_dataで取得します。ここで、ログインしている人のみ購入するボタンを表示させたいので、if文で条件を指定します。if self.request.user.is_anonymous:は未ログインの場合を示します。この場合は、passとしてcontextデータをテンプレートに渡しません。DetailViewを継承していますので、objectとしてurlで指定するpkに対応する商品情報が渡ります(ListViewの場合は、object_listとしてオブジェクトリストが渡ります)。
else:内には、ユーザーがログインしている場合の処理をコードしていきます。
まずは、user = self.request.userでログインしているユーザー情報を取得します。ショッピングカートにすでに商品が入っている場合には、重複して購入ボタンを押せないようにしたいと思います。mycartitemとして、自分のカートに入っているを商品IDを格納します。まずは、CartItems.objects.filter(user_id=user)で 自分のカートに入っているオブジェクトをCartItemsモデルから検索します。
values_list('フィールド名')メソッドを使用して、先程取得したオブジェクトの商品IDのみをリスト形式でvalues_list('product_id')として取得します。
自分のカートに入っている商品情報は、Productモデルからmycartitemでフィルタリングして取得しcontext['cart_contents']へ格納します。return contextでテンプレートへ渡します。最終的なコードは以下となります。
ecsite/views.py
・・・省略・・・
from django.views.generic import ListView, DetailView, TemplateView
・・・省略・・・
class ProductDetailView(DetailView):
model = Product
template_name = 'ecsite/detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.user.is_anonymous:
pass
else:
user = self.request.user
mycartitem = (CartItems.objects.filter(user_id=user)).values_list('product_id')
context['cart_contents'] = Product.objects.filter(id__in=mycartitem)
return context
次にパスを通すのでecsite/urls.pyを開きます。まずは、 ProductDetailViewをインポートしましょう。その後は、urlpatternsにProductDetailViewのurlとして'detail/<int:pk>'を設定します。DetailViewですので特定の製品についてのページですので<int:pk>が必要になります。名前空間を'product_detail'とします。これで、detail/<int:pk>にアクセすると商品ID=pkの商品詳細画面が表示されます。
ecsite/urls.py
from django.urls import path
from .views import HomeView,ProductListView,ProductDetailView # 追加
app_name = 'ecsite'
urlpatterns = [
path('home/', HomeView.as_view(), name='home'),
path('list/', ProductListView.as_view(), name='product_list'),
path('detail/<int:pk>', ProductDetailView.as_view(), name='product_detail'), # 追加
]
対応するecsite/detail.html作成します。コードは下の方に載せています。{% extends 'base.html' %}〜{% block content %}までは、お決まりの部分ですので特に説明は不要かと思います。画面左側は、bootstrap4のカードを用いて、そこに商品画像を表示させています。右側は、商品説明と購入するボタンを表示させます。出来上がり画像でイメージを掴んでください。一番上の画像は、ログインしていない状態での画面です。真ん中は、ログインしている状態で、まだカートに商品を入れていない状態。最後の画像は、カートに商品を入れた後の画面になります。
未ログイン状態での商品詳細画面
ログイン状態での商品詳細画面
ログイン状態かつすでに商品をカートに入れた状態の商品詳細画面
まずは、画像を表示させる左側のカード部分について説明します。コードは以下の通りです。
<aside class="col-md-5 offset-md-1">
<div class="card">
<div class="card-header">
{{ object.name }}
</div>
<div class="card-body text-center">
{% for picture in object.pictures.all %}
{% if forloop.first %}
<img class="rounded mx-auto d-block"
src={{ picture.picture.url }}>
<hr>
{% else %}
<section class="d-flex flex-row bd-highlight mb-3">
<div id="gallery">
<img class="d-flex justify-content-start"
src={{ picture.picture.url }}>
</div>
{% endif %}
{% endfor %}
</section>
</div>
</div>
</aside>
<div class="card">でbootstrap4のカードを指定します。<div class="card-header">〜</div>内にカードヘッダーに表示させる内容を指定します。ここでは、商品名を表示させたいので{{ object.name }}とします。DetailViewからは、objectとして対象商品の情報が渡されています。
<div class="card-body text-center">〜</div>内に画像を表示させていきます。ここでも、objectに紐付く画像をrelated_nameを指定して取得する事ができます。{% for picture in object.pictures.all %}とすれば、画像をfor文で一つづつ取り出しpictureオブジェクトに入れることが出来ます。画像は、1毎だけ大きく表示させたいので、{% if forloop.first %}〜一番最初の画像の処理〜{% else %}〜それ以外の画像の処理〜{% endif %}によりfor文の一番最初の画像とそれ以外の画像をif文で条件分岐させます。一番最初の画像は、imgタグで表示させますがidなどは指定しません。src=src={{ picture.picture.url }}とする事で画像を表示させる事が出来ます。
2番目以降の画像の処理は{% else %}〜{% endif %}内にコーディングします。画像を小さく表示させたいのでimgタグをdivタグで囲みid='gallery'としてCSSで画像サイズを調整します。cssは、以下の通りです。
#gallery img{
width: 60px;
margin-top: 5px;
margin-right: 20px;
}
次に、商品説明についてを画面右側に表示させます。コードは以下の通りです。
<div class="col-md-5">
<p>商品説明:{{ object.comments }}</p>
<p>大きさ:{{ object.size }}</p>
<p>原材料:{{ object.ingredients }}</p>
<hr>
<p>価格:{{ object.price }}</p>
{% if object.stock %}
{% if object.stock < 30 %}
<p>残りわずか:{{ object.stock }}です</p>
{% endif %}
{% if user.is_authenticated %}
{% if object in cart_contents %}
<button class="btn btn-sm btn-secondary" name="button">
すでにカートに入っています<i class="fas fa-shopping-cart"></i>
</button>
{% else %}
<button type="submit" class="btn btn-sm btn-success" name="button">
購入する<i class="fas fa-shopping-cart"></i>
</button>
{% endif %}
{% endif %}
{% endif %}
</div>
画面右の上から、商品説明、大きさ、原材料、価格などをそれぞれpタグで囲み {{ object.comments }}、{{ object.size }}、{{ object.ingredients }}、{{ object.price }}として表示させます。在庫がある場合にのみ、購入ボタンを押せるようにしたいので、if文で在庫がある場合の処理を {% if object.stock %} 〜 {% endif %}内にコーディングしていきましょう。在庫が少ない場合は、残りわずかというメッセージを入れたいので、以下のようにif文で在庫数による条件を加えます。
{% if object.stock < 30 %}
<p>残りわずか:{{ object.stock }}です</p>
{% endif %}
商品は、ログインユーザーに限定しますので{% if user.is_authenticated %} 〜{% endif %}内にその処理をコーディングしていきます。自分のカートに同じ商品が入っている場合と入っていない場合をif文で表示を変えます。ビューから、cart_contentsとして自分のショッピングカート内のオブジェクトを受け取っています。そのオブジェクトに、現在の商品objectが入っている場合{% if object in cart_contents %}は、すでにカートに入っていますというメッセージを表示させます。
まだ商品がカートに入っていない場合{% else %}は、購入するボタンを表示させます。今の段階では、カートに商品を入れる機能を実装していませんのでボタンを押しても何も起こりません。機能の実装はPart3で説明していきます。ここまでのコードは以下のようになります。
templates/ecsite/detail.html
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block content %}
<div class="content-wrapper">
<div class="container-fluid">
<div class="row">
<!--ページタイトル-->
<aside class="col-md-5 offset-md-1">
<div class="card">
<div class="card-header">
{{ object.name }}
</div>
<div class="card-body text-center">
{% for picture in object.pictures.all %}
{% if forloop.first %}
<img class="rounded mx-auto d-block"
src={{ picture.picture.url }}>
<hr>
{% else %}
<section class="d-flex flex-row bd-highlight mb-3">
<div id="gallery">
<img class="d-flex justify-content-start"
src={{ picture.picture.url }}>
</div>
{% endif %}
{% endfor %}
</section>
</div>
</div>
</aside>
<div class="col-md-5">
<p>商品説明:{{ object.comments }}</p>
<p>大きさ:{{ object.size }}</p>
<p>原材料:{{ object.ingredients }}</p>
<hr>
<p>価格:{{ object.price }}</p>
{% if object.stock %}
{% if object.stock < 30 %}
<p>残りわずか:{{ object.stock }}です</p>
{% endif %}
{% if user.is_authenticated %}
{% if object in cart_contents %}
<button class="btn btn-sm btn-secondary" name="button">
すでにカートに入っています<i class="fas fa-shopping-cart"></i>
</button>
{% else %}
<button type="submit" class="btn btn-sm btn-success" name="button">
購入する<i class="fas fa-shopping-cart"></i>
</button>
{% endif %}
{% endif %}
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
ターミナルからサーバーを立ち上げてhttp://127.0.0.1:8000/ecsite/detail/1へアクセスしてみて下さい。商品詳細が表示されたでしょうか?ログイン有無で表示が変わるようになっているでしょうか?
Part3では、⑤カート機能実装と関連ページ作成と⑥チェックアウト画面とPDFによる領収書作成について説明していきます。
この記事が気に入ったらサポートをしてみませんか?