Django開発~ショッピングカート構築編1~商品一覧作成①の続きです。
今回は、前回作成した商品一覧を完成させましょう。
商品一覧取得
DBから商品一覧を取得し、product_list.htmlに表示させたいと思います。
まずは、その為のテストコードを書きましょう。
「localhost:8000」にアクセスしたとき、プロダクト一覧を取得するテストです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
app/shop/tests/test_views.py from django.test import TestCase from django.urls import reverse # new from shop.models import Category, Product #new # product listのURLパスを生成 PRODUCT_LIST_URL = reverse('shop:all_product') # Categoryを作成 def sample_category(name, slug, description): return Category.objects.create( name=name, slug=slug, description=description) # Productを作成 def sample_product(name, slug, description, category, price, stock, available): return Product.objects.create( name=name, slug=slug, description=description, category=category, price=price, stock=stock, available=available) class ViewTest(TestCase): def test_first_page(self): response = self.client.get('/') assert response.status_code == 200 def test_productlist_used_template(self): response = self.client.get('/') self.assertTemplateUsed(response, 'shop/product_list.html') # new def test_retrieve_products(self): c = sample_category( name='Black Urban Cushion', slug='black-urban-cushion', description='This is a category for black urban cushion') sample_product( name='dog', slug='dog', description='Dogs are intelligent animals!', category=c, price=30.2, stock=30, available=True) sample_product( name='rabbit', slug='rabbit', description='rabits are so cute animals!', category=c, price=60.1, stock=2, available=True) self.product3 = Product.objects.create( name='cat', slug='cat', description='cats are loved by everyone!', category=c, price=60.1, stock=2, available=False) # プロダクトリストのViewを呼び出す response = self.client.get(PRODUCT_LIST_URL) # レスポンスの中に作成したプロダクトが含まれているかチェック assert 'dog' in [product.name for product in response.context['products']] assert 'rabbit' in [product.name for product in response.context['products']] assert 'cat' not in [product.name for product in response.context['products']] |
現状は、「localhost:8000」にアクセスしても何もデータを返さないので、エラーになるはずです。
「assert ‘dog’ in [product.name for product in response.context[‘products’]]
」部分は少し補足しておきます。
まず、クライアントがリクエストを送って、Djangoがレスポンスを返す時に「context」プロパティに、Productデータのクエリリストを格納するように実装します。
ここで示すデータとは、「dig, rabbit, cat」の事です。
返却されたデータ内に意図したデータが存在するかチェックしたわけです。
また、「[product.name for product in response.context[‘products’]]」は、Pythonの「リスト内方表記」という書き方です。
for文のプログラムをより短縮して記述することができます。
1 2 3 4 |
# for文で書いた場合 for product in products: product.name |
それでは、テストを実行しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
make test ======================================= FAILURES ================================ _____________________________________________________________________ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ def __getitem__(self, key): "Get a variable's value, starting at the current context and going upward" for d in reversed(self.dicts): if key in d: return d[key] > raise KeyError(key) E KeyError: 'products' d = {'False': False, 'None': None, 'True': True} key = 'products' self = [{'True': True, 'False': False, 'None': None}, {'csrf_token': <SimpleLazyObject: <function csrf.<locals>._get_val at 0...t 0x7f55d8921510>, 'DEFAULT_MESSAGE_LEVELS': {'DEBUG': 10, 'INFO': 20, 'SUCCESS': 25, 'WARNING': 30, 'ERROR': 40}}, {}] /usr/local/lib/python3.7/site-packages/django/template/context.py:83: KeyError =================================================================== 1 failed, 4 passed in 1.28 seconds ==================================================================== |
KeyErrorが出ました。これは、localhost:8000アクセス後、Djangoから「products」が返却されなかったことを示しています。
views.pyを修正しましょう。
1 2 3 4 5 6 7 8 9 10 11 |
app/shop/views.py from django.shortcuts import render from shop.models import Product def all_products(request): # 全てのプロダクトを取得 products = Product.objects.all() return render(request, 'shop/product_list.html', {'products': products}) |
テストを実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
=================================== FAILURES =================================== _______________________ ViewTest.test_retrieve_products ________________________ ... assert 'dog' in [product.name for product in response.context['products']] assert 'rabbit' in [product.name for product in response.context['products']] > assert 'cat' not in [product.name for product in response.context['products']] E AssertionError: assert 'cat' not in ['cat', 'dog', 'rabbit'] c = <Category: Black Urban Cushion> response = <HttpResponse status_code=200, "text/html; charset=utf-8"> self = <test_views.ViewTest testMethod=test_retrieve_products> shop/tests/test_views.py:74: AssertionError ========================= 1 failed, 4 passed in 1.27s ========================== |
「assert ‘cat’ not in [product.name for product in response.context[‘products’]]」のアサーションでエラーが発生しています。
実は、「cat」プロダクトのavailableプロパティを「False」に指定しているので、商品一覧に表示させたくないのです。
views.pyの「products = Product.objects.all()」の箇所を修正する必要があります。
availableプロパティがTrueのもののみ取得する方法はいくつかあります。filterを使ったり、カスタムマネージャーを作ったりなどです。
filterを使うほうが簡単ですが、せっかくなので、カスタムマネージャを作成する方法をご紹介します(便利なので)。
カスタムマネージャーを使うと、データ検索の条件を制御できます。
例えば、Prodcut.objects.all()の場合は、「全てのデータを取得する」というクエリをDBに投げて、データを取得しています。
つまり、「availableがTrueのデータだけ取得する」というカスタムマネージャーも作れるというわけです。
カスタムマネージャーは、models.pyに記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
app/shop/models.py from django.db import models # new class ValidManager(models.Manager): def get_queryset(self): return super(ValidManager, self).get_queryset().filter(available=True) class Category(models.Model): .... class Product(models.Model): name = models.CharField(max_length=250, unique=True) slug = models.SlugField(max_length=250, unique=True) description = models.TextField(blank=True) category = models.ForeignKey(Category, on_delete=models.CASCADE) price = models.DecimalField(max_digits=10, decimal_places=2) image = models.ImageField(upload_to='product', blank=True) stock = models.IntegerField() available = models.BooleanField(default=True) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) # デフォルトのマネージャー objects = models.Manager() # new # カスタムマネージャー valid_objects = ValidManager() #new class Meta: ordering = ('name',) verbose_name = 'product' verbose_name_plural = 'products' def __str__(self): return '{}'.format(self.name) |
「ValidManager」がカスタムマネージャーです。models.Managerを継承したクラスで、「get_queryset」をオーバーライドすることで、自由にクエリを定義できます。
そして、Productモデルにカスタムマネージャーを定義しました。
気を付ける点として、デフォルトマネージャーも併せて設定しなければなりません。デフォルトマネージャーを設定していない場合は、「Prodcut.objects.all()」など、objectsマネージャーを使った処理が一切できなくなります。
続いて、views.pyを修正します。
1 2 3 4 5 6 7 8 9 10 |
app/shop/views.py from django.shortcuts import render from shop.models import Product def all_products(request): products = Product.valid_objects.all() return render(request, 'shop/product_list.html', {'products': products}) |
「Prodcut.objects.all()」を「Product.valid_objects.all()」に変更しました。
簡単ですね。
テストを実行しましょう。
1 2 3 |
make test ============================== 5 passed in 1.22s =============================== |
パスしましたね。
テンプレートの作成
最後にテンプレートを作成しましょう。
ここからは、HTMLの設定であるため、テストコードの作成は省きます。
1 2 3 4 5 6 7 8 |
# イメージを格納するためのフォルダ作成 mkdir -p app/static/media # テンプレートを作成 touch app/shop/templates/base.html touch app/shop/templates/header.html touch app/shop/templates/navbar.html touch app/shop/templates/footer.html |
base.html
base.html は、shoppingcart アプリで共通で使用される html ファイルです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
app/shop/templates/base.html {% load staticfiles %} <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="description" content="{% block metadescription %}{% endblock %}"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <title>{% block title %}{% endblock %}</title> </head> <body> <div> {% include 'header.html' %} {% include 'navbar.html' %} {% block content %} {% endblock %} </div> {% include 'footer.html' %} <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script> </body> </html> |
BootStrapの機能も使えるようにしておきました。
header.html
header.html は、ヘッダの HTML 要素をまとめたファイルです。
1 2 3 4 5 6 7 8 |
app/shop/templates/header.html {% load staticfiles %} <header> <center> <img src="{% static 'img/logo.png' %}" alt="Perfect Cushion Store"> </center> </header> |
navbar.html
navbar.html は、ナビゲーションバーの設定を行うファイルです。
1 2 3 4 5 6 7 8 |
app/shop/templates/navbar.html <nav> <ul> <li><a href="{% url 'shop:all_product' %}">All Products</a></li> <li>Your Cart()</li> </ul> </nav> |
footer.html
footer.html は、ページのフッターを作成します。
1 2 3 4 5 |
app/shop/templates/footer.html <div> <p>© ShoppingCart, With, Django</p> </div> |
product_list.html
最後に商品一覧を表示させるページを作成さます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
app/shop/templates/shop/product_list.html {% extends "base.html" %} {% load staticfiles %} {% block metadescription %} {% if category %} {{ category.description|truncatewords:155 }} {% else %} Welcome to the cushion store where you can buy comfy and awesome cushions. {% endif %} {% endblock %} {% block title %} {% if category %} {{ category.name }} - Perfect Cushion Store {% else %} See Our Cushion Collection - Perfect Cushion Store {% endif %} {% endblock %} {% block content %} <div> <img src="{% static 'img/banner.jpg' %}" alt="Our Products Collection"> </div> <br> <div> <h1>Our Products Collection</h1> <p>Finding the perfect cushion for your room can instantly add to the levels of comfort and sense of style throughout your home.</p> </div> <div> <div> {% for product in products %} <div> <div> <a href=""><img src="{{product.image.url}}" alt="{{product.name}}"></a> <div> <h4>{{product.name}}</h4> <p>£{{product.price}}</p> </div> </div> </div> {% endfor %} </div> </div> {% endblock %} |
とりあえず暫定ですが、HTMlのコーディングは以上です。
動作確認
動作確認のために、何点か設定事項がありますので、もう少々お付き合いください。
管理画面への登録
ProductモデルとCategoryモデルをadmin.pyに登録し、Djangoの管理画面上で確認できるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
app/shop/admin.py from django.contrib import admin from .models import Category, Product class CategoryAdmin(admin.ModelAdmin): list_display = ['name', 'slug'] prepopulated_field = {'slug':('name',)} admin.site.register(Category, CategoryAdmin) class ProductAdmin(admin.ModelAdmin): list_display = ['name', 'price', 'stock', 'available', 'created', 'updated'] list_editable = ['price', 'stock', 'available'] prepopulated_field = {'slug':('name',)} list_per_page = 20 admin.site.register(Product, ProductAdmin) |
静的ファイル設定
静的ファイルの設定を行います。
例えば、管理画面から画像を投稿したとき、保存される場所を設定します。
まずは、settings.pyを開いて、末尾に以下を設定してください。
※細かい設定内容については省きます
1 2 3 4 5 6 7 8 9 10 |
app/app/settings.py STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') STATICFILES_DIRS = ( os.path.join(BASE_DIR, 'static'), ) MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'static', 'media') |
続いて、app/app/urls.pyに以下の設定をしてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
app/app/urls.py from django.contrib import admin from django.urls import include, path from django.conf import settings from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), path('', include('shop.urls')), ] if settings.DEBUG: # staticファイルとmediaファイルのドキュメントルートを設定 urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) |
管理ユーザーの作成
以下のコマンドを実行して、管理ユーザーを作成してください。※管理画面へのアクセスに必要です
1 2 3 4 5 6 7 8 |
make admin Username (leave blank to use 'user'): admin Email address: Password: Password (again): Superuser created successfully. |
ロゴとバナー画像の格納
app/static/imgフォルダ配下に「logo.png」、「banner.jpg」を格納してください。


データ投入
次のコマンドを実行して、dockerコンテナを立ち上げてください。
1 |
docker-compose up |
「localhost:8000/admin」にアクセスし、Category -> Productの順に商品データを投入してください。


データ投入完了後、「app/static」配下に「media/product」フォルダが作成され、その中にapple.jpegファイルが保存されていると思います。

画面確認
「localhost:8000」にアクセスしてください。問題なく表示されたでしょうか?


まだまだ不格好な画面ですが、次回以降に直していきたいと思います。
参考書籍
次回
次回は、商品の投稿ページを作成します。
コメントを残す
コメントを投稿するにはログインしてください。