Django開発~ショッピングカート構築編5~ページネーションの導入の続きです。
本日は、検索機能の実装を行います。
<目次>
学習履歴
searchアプリケーションの作成
まずは、次のコマンドで”search”アプリケーションを作成しましょう。
1 |
make app app=search |
続いて、settings.pyに次の設定を行います
①shopアプリケーションを認識させる
②テンプレートファイル格納場所を指定
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 |
app/app/settings.py INSTALLED_APPS = [ # myapp 'shop', 'search', # new ① 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'shop', 'templates/'), os.path.join(BASE_DIR, 'search', 'templates/')], # new ② 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] |
Todoリストの作成
テスト駆動で開発を行っている為、最初にテストコードを書く必要があります。
その為には、検索アプリの仕様を決めなければなりません。
検索アプリの仕様は、シンプルです。
・商品一覧から検索に合致したプロダクトを別ページに表示する
・フリーテキスト入力の検索ボックスを用意する(既に用意済み)
・プロダクトの名前と概要から検索をかける
この検索アプリを実装するために必要なことを箇条書きにします。
①検索結果ページを用意する
②検索結果ページへのアクセスを可能にする
③複数のデータの中から検索をかけ、任意のデータのみ抽出できることを確認する
④navbarに設定されている検索ボックスを修正する
ひとまず、4つ思いつきました。
Todoリストは後から追加してもかまいません。
検索結果へのアクセス
まずは、searchフォルダ配下にtestsフォルダを作成し、その中にテストファイルを作成しましょう。
1 2 3 4 5 6 |
# 既存のテストファイルを削除 rm app/search/tests.py # テストフォルダ作成 mkdir app/search/tests # テストファイル作成 touch app/search/tests/test_views.py |
続いて、検索結果画面にアクセスしたときのテストを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
app/search/tests/test_views.py from django.test import TestCase from django.urls import reverse SEARCH_RESULT_URL = reverse('search:search_result') class SearchTest(TestCase): def test_access_to_search_result_view(self): response = self.client.get(SEARCH_RESULT_URL) assert response.status_code == 200 |
正常にアクセスできた場合は、HTTPリクエストコードが「200」を返すので、そのコードが帰ってくるかテストしています。
テストを実行しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
make test app=search ==================================== ERRORS ==================================== _________________ ERROR collecting search/tests/test_views.py __________________ /usr/local/lib/python3.7/site-packages/django/urls/base.py:75: in reverse extra, resolver = resolver.namespace_dict[ns] E KeyError: 'search' During handling of the above exception, another exception occurred: search/tests/test_views.py:5: in <module> SEARCH_RESULT_URL = reverse('search:search_result') /usr/local/lib/python3.7/site-packages/django/urls/base.py:86: in reverse raise NoReverseMatch("%s is not a registered namespace" % key) E django.urls.exceptions.NoReverseMatch: 'search' is not a registered namespace !!!!!!!!!!!!!!!!!!! Interrupted: 1 errors during collection !!!!!!!!!!!!!!!!!!!! =============================== 1 error in 0.39s =============================== |
「NoReverseMatch」の表示とともにエラーが出力されました。
これは、指定したURLに該当するURLがないことを示しています。
「reverse(‘search:search_result’)」は、urls.pyに定義する「namespace」と「pathのname」からURLを取得します。
searchアプリケーション配下に、urls.pyを作成してください。
1 |
touch app/search/urls.py |
このファイルにsearchアプリのviewへのパスを記述します。
1 2 3 4 5 6 7 8 9 10 11 12 |
app/search/urls.py from django.urls import path from . import views # namespace app_name = 'search' urlpatterns = [ path('', views.search_result, name='search_result'),# pathのname ] |
この「app_name」と「search_result」をreverse関数に「’search:search_result’」という形式で指定するとviews.pyのsearch_result関数へのパスが取得できます。
テストを実行しましょう。
1 2 3 4 |
make test app=search E django.urls.exceptions.NoReverseMatch: 'search' is not a registered namespace |
同じエラーが出力されました。
実は、Djangoでは、urls.pyは「2種類」あります。
「プロジェクト(app)配下のurls.py」と「アプリケーション(search)配下のurls.py」です。
ユーザーからのリクエストを受け取ると最初にプロジェクト配下のurls.pyにリクエストが渡り、続いて、アプリケーション配下のurls.pyにリクエストが渡されます。
プロジェクト配下のurls.pyを修正しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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('search/', include('search.urls')), # new 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 |
make test app=search E AttributeError: module 'search.views' has no attribute 'search_result' |
エラー内容が変わりました。
このエラーは、「views.pyにsearch_result関数が設定されていない」ため、発生しました。
search_result関数を実装しましょう。
1 2 3 4 5 6 7 |
app/search/views.py from django.shortcuts import render def search_result(request): return render(request, 'search.html', {}) |
テストを実行しましょう。
1 2 3 4 |
make test app=search django.template.exceptions.TemplateDoesNotExist: search.html |
search.htmlファイルが存在しないのでエラーがでました。
※同様のエラーが出ない場合は、一旦、Dockerコンテナを立ち上げなおしてみてください
search.htmlファイルを作成しましょう。
1 2 |
mkdir app/search/templates touch app/search/templates/search.html |
テストを実行します。
1 2 3 |
make test app=search ============================== 1 passed in 1.33s =============================== |
テストがパスしましたね。
Todoリストから②を消しましょう。※①は完了していないのでそのままです
①検索結果ページを用意する
③複数のデータの中から検索をかけ、任意のデータのみ抽出できることを確認する
④navbarに設定されている検索ボックスを修正する
検索ロジックの作成
続いては、検索ロジックを作成しましょう。
テストコードを書きます。
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 |
app/search/tests/test_views.py from django.test import Client from django.test import TestCase from django.urls import reverse # new from shop.models import Category, Product SEARCH_RESULT_URL = reverse('search:search_result') # new def sample_category(name, slug, description): return Category.objects.create( name=name, slug=slug, description=description) # new def sample_product(name, slug, description, category, price, stock, available, image="test.png"): return Product.objects.create( name=name, slug=slug, description=description, category=category, price=price, stock=stock, available=available) class SearchTest(TestCase): # new def setUp(self): self.client = Client() self.category = sample_category( name='Black Urban Cushion', slug='black-urban-cushion', description='This is a category for black urban cushion') self.product1 = sample_product( name='dog', slug='dog', description='Dogs are intelligent animals!', category=self.category, price=30.2, stock=30, available=True) self.product2 = sample_product( name='cat', slug='cat', description='cats are so cute animal!', category=self.category, price=30.2, stock=30, available=True) self.product3 = sample_product( name='rabbit', slug='rabbit', description='Rabbits are small animals!', category=self.category, price=30.2, stock=30, available=True) def test_access_to_search_result_view(self): response = self.client.get(SEARCH_RESULT_URL) assert response.status_code == 200 # new def test_search_result(self): response = self.client.get(SEARCH_RESULT_URL, {'q': 'dog'}) assert 'dog' in [product.name for product in response.context['products']] assert 'rabbit' not in [product.name for product in response.context['products']] assert 'cat' not in [product.name for product in response.context['products']] |
カテゴリとプロダクトのデータ作成用の関数を追加し、setUp関数でデータ作成しています。
setUp関数は、テストコードが実行される前に必ず実行される特別な関数です。
テストを実行してみましょう。
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 |
make test app=search =================================== FAILURES =================================== ________________________ SearchTest.test_search_result _________________________ ... _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = [{'True': True, 'False': False, 'None': None}, {'csrf_token': <SimpleLazyObject: <function csrf.<locals>._get_val at 0...t 0x7f8a4f79b590>, 'DEFAULT_MESSAGE_LEVELS': {'DEBUG': 10, 'INFO': 20, 'SUCCESS': 25, 'WARNING': 30, 'ERROR': 40}}, {}] key = 'products' 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 0x7f8a4f79b590>, '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, 1 passed in 1.31s ========================== |
key エラーになりましたね。
views.pyのsearch_resultの戻り値にproductsがないことが原因です。
views.pyを実装していきましょう。
1 2 3 4 5 6 7 8 9 |
app/search/views.py from django.shortcuts import render from shop.models import Product # new def search_result(request): products = Product.objects.all() return render(request, 'search.html', {'products': products}) |
テストを実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
make test app=search =================================== FAILURES =================================== ________________________ SearchTest.test_search_result _________________________ self = <test_views.SearchTest testMethod=test_search_result> def test_search_result(self): response = self.client.get(SEARCH_RESULT_URL, kwargs={'q': 'dog'}) assert 'dog' in [product.name for product in response.context['products']] > assert 'rabbit' not in [product.name for product in response.context['products']] E AssertionError: assert 'rabbit' not in ['cat', 'dog', 'rabbit'] response = <HttpResponse status_code=200, "text/html; charset=utf-8"> self = <test_views.SearchTest testMethod=test_search_result> search/tests/test_views.py:66: AssertionError ========================= 1 failed, 1 passed in 1.30s ========================== |
「AssertionError: assert ‘rabbit’ not in [‘cat’, ‘dog’, ‘rabbit’]」と出ました。
次は、検索処理を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
app/search/views.py from django.shortcuts import render from shop.models import Product from django.db.models import Q def search_result(request): products = None query = None if 'q' in request.GET: query = request.GET.get('q') products = Product.objects.all().filter(Q(name__contains=query) | Q(description__contains=query)) return render(request, 'search.html', {'query': query, 'products': products}) |
Djangoで検索する方法はいくつかありますが、今回は「Qオブジェクト」を使用しました。
Qオブジェクトは、データベースの検索用クエリを作成してくれます。
上記の例では、Productモデルのname属性に対して「ダブルスコア(__)」と「contains」を結合しました。
このようにすると、検索条件(q)に含まれる名前を検索できます。
その他の使い方については、こちらを確認してください。
テストを実行しましょう。
1 2 3 4 |
make test app=search ============================== 2 passed in 1.25s =============================== |
テストがパスしましたね。
これで、Todoリストの③が終了しました。
①検索結果ページを用意する
④navbarに設定されている検索ボックスを修正する
HTMLの変更
まず、navbar検索ボックスの変更をしましょう。検索ボタンを押下したときに、qクエリがURLの末尾に追加されるようにします。
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 |
app/shop/templates/navbar.html <nav class="navbar navbar-expand-lg navbar-light bg-light"> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav mr-auto"> <li class="nav-item dropdown {% if 'shop' in request.path %}active{% endif %}"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> Shop </a> <div class="dropdown-menu" aria-labelledby="navbarDropdown"> <a class="dropdown-item" href="{% url 'shop:all_product' %}">All Products</a> {% for product in products %} <a class="dropdown-item" href="{{product.get_url}}">{{product.name}}</a> {% endfor %} </div> </li> <li class="nav-item"> <a class="nav-link" href="#"><i class="fas fa-shopping-cart"></i></a> </li> </ul> <!-- 修正 --> <form class="form-inline my-2 my-lg-0" action="{% url 'search:search_result' %}" method="get"> {% csrf_token %} <!-- name="q" を追加 --> <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search" name="q"> <button class="btn btn-secondary my-2 my-sm-0" type="submit"><i class="fas fa-search"></i></button> </form> </div> </nav> |
続いて、search.htmlを作成します。
1 |
touch app/search/templates/search.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 |
app/search/templates/search.html {% extends "base.html" %} {% load staticfiles %} {% block matadescription %} We have a variety of stunning and comfy cushions. Lock for the one that suits your needs. {% endblock %} {% block title %} Search - Perfect Cushion Store {% endblock %} {% block content %} <div> <p class="text-center my_search_text">You have searchd for: <b>"{{ query }}"</b></p> </div> <div class="container"> <div class="row mx-auto"> {% for product in products %} <div class="my_bottom_margin col-9 col-sm-12 col-md-4 col-md-12 col-lg-4"> <div class="card text-center" style="min-width: 18rem;"> <a href="{{product.get_url}}"><img class="card-img-top my_image" src="{{product.image.url}}" alt="{{product.name}}"></a> <div class="card-body"> <h4>{{product.name}}</h4> <p>£{{product.price}}</p> </div> </div> </div> {% empty %} <div class="row mx-auto"> <p class="text-center my_search_text">0 results found.</p> </div> {% endfor %} </div> </div> {% endblock %} |
custom.cssに個別のスタイルを追加します。
1 2 3 4 5 6 |
static/css/custom.css .my_search_text { padding-top: 20px; } |
これで、残りのTodoリストは削除できます。
動作確認
「localhost:8000」にアクセスして、検索機能の動作確認をしましょう。


私の環境だと問題なく動作しました^^
ちなみに検索後のURLは、以下のようになっています。
1 |
http://localhost:8000/search/?csrfmiddlewaretoken=UTiWShyk0VCfG6JzxJ3nA0MnRILEAvcZiSfyHTMsIxV4pRdxJnNPor9RevTvSXnk&q=orange |
参考書籍
次回
次回は、ショッピングカートを実装します!
コメントを残す
コメントを投稿するにはログインしてください。