diff --git a/docs/reference/catalog.rst b/docs/reference/catalog.rst index 6e8d3ea17..288fc71b6 100644 --- a/docs/reference/catalog.rst +++ b/docs/reference/catalog.rst @@ -4,8 +4,9 @@ Catalog ======= -The catalog presumably is that part, where customers of our e-commerce site spend the most time. -Often it even makes sense, to start the :ref:`reference/catalog-list` on the main landing page. +The catalog presumably is that part, where customers of our e-commerce site hopefully spend most of +their time. Often it even makes sense, to start the :ref:`reference/catalog-list` on the main +landing page. In this documentation we presume that categories of products are built up using specially tagged CMS pages in combination with a `django-CMS apphook`_. This works perfectly well for most @@ -15,8 +16,8 @@ Using an external **django-SHOP** plugin for managing categories is a very conce and we will see separate implementations for this feature request. Using such an external category plugin can make sense, if this e-commerce site requires hundreds of hierarchical levels and/or these categories require a set of attributes which are not available in CMS pages. If you are -going to use externally implemented categories, please refer to their documentation, since here we -proceed using CMS pages as categories. +going to use externally implemented categories, please refer to their documentation, since in this +document, we proceed using CMS pages as categories. A nice aspect of **django-SHOP** is, that it doesn't require the programmer to write any special Django Views in order to render the catalog. Instead all merchant dependent business logic goes @@ -41,245 +42,308 @@ But first we must :ref:`reference/create-CatalogListApp`. Create the ``CatalogListApp`` ----------------------------- -To retrieve a list of product models, the Catalog List View requires a `django-CMS apphook`_. This -``CatalogListApp`` must be added into a file named ``cms_apps.py`` and located in the root folder -of the merchant's project: +To retrieve a list of product models, the Catalog List View requires a `django-CMS apphook`_. The +class ``CatalogListApp`` must be added into a file named ``cms_apps.py`` and located in the root +folder of the merchant's project: .. code-block:: python :caption: myshop/cms_apps.py from cms.apphook_pool import apphook_pool from shop.cms_apphooks import CatalogListCMSApp + from shop.rest.filters import CMSPagesFilterBackend class CatalogListApp(CatalogListCMSApp): def get_urls(self, page=None, language=None, **kwargs): - return ['myshop.urls.products'] + from shop.views.catalog import AddToCartView, ProductListView, ProductRetrieveView + + return [ + url(r'^(?P[\w-]+)/add-to-cart', AddToCartView.as_view()), + url(r'^(?P[\w-]+)', ProductRetrieveView.as_view()), + url(r'^', ProductListView.as_view( + filter_backends=[CMSPagesFilterBackend] + list(api_settings.DEFAULT_FILTER_BACKENDS), + )), + ] apphook_pool.register(CatalogListApp) -as all apphooks, it requires a file defining its urlpatterns: +In the page tree editor of **django-CMS**, create a new page at an appropriate location. As the +page title and slug we should use something describing our product catalog in a way, both meaningful +to the customers as well as to search engines. -.. code-block:: python - :caption: myshop/urls/products.py - :name: apphook-urlpatterns +As template, select one with a placeholder large enough to display the figures of the catalog's +list . + +Change into the **Advanced Settings** of the CMS page, which shall act as the catalog list. As +**Application**, select "*Catalog List*" from the drop-down menu. This selects the apphook +``CatalogListApp``, we just created. - from django.conf.urls import url - from shop.views.catalog import ProductListView +.. note:: After adding or modifying a CMS apphook, we must restart the server. - urlpatterns = [ - url(r'^$', ProductListView.as_view()), - # other patterns - ] +Then we go into the page's **Preview** mode and open the **Structure menu** on the right side of the +**django-CMS** toolbar. Now locate the placeholder named **Main Content**. Add a Container plugin, +followed by a Row and then a Column plugin. As the child of this column, choose the **Catalog List +View** plugin from section **Shop**. -By default the ``ProductListView`` renders the catalog list of products assigned to the current CMS -page. In this example, only model attributes for fields declared in the default -``ProductSummarySerializer`` are available in the render context for the used CMS template, as well -as for a representation in JSON suitable for client side rendered list views. This allows us to -reuse this Django View (``ProductListView``) whenever the catalog list switches into infinite scroll -mode, where it only requires the product's summary, composed of plain old JavaScript objects. +Finally we publish the page, it probably doesn't contain any products yet. To fill it we first have +to :ref:`reference/assign-products-to-cms-page`. +Remember to repeat this procedure, and add one CMS pages per category, in order to create a +structure of pages for our e-commerce site. -.. _reference/customized-product-serializer: -Customized Product Serializer -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. _reference/assign-products-to-cms-page: -In case we need :ref:`reference/additional-serializer-fields`, let's add them to this class using -the `serializer fields`_ from the Django RESTFramework library. This can be useful for product -serializers which shall provide more information on our catalog list view. +Assign Products to CMS Pages +---------------------------- -For this customized serializer, we normally only require a few attributes from our model, therefore -we can write it as: +Here the :class:`shop.views.catalog.ProductListView` is configured to render the catalog list of +products assigned to one or more CMS pages. For this purpose we use the filter backend +:class:`shop.rest.filters.CMSPagesFilterBackend`. In order to decide to which CMS page a product is +assigned to, our Product Model must inherit from :class:`shop.models.product.CMSPageReferenceMixin`. +This is because we must add a reference to the CMS pages our products are assigned to. A typical +product might be declared as: .. code-block:: python - from shop.serializers.bases import ProductSerializer + from shop.models.product import BaseProduct, BaseProductManager, CMSPageReferenceMixin - class CustomizedProductSerializer(ProductSerializer): - class Meta: - model = Product - fields = [all-the-fields-required-for-the-list-view] + class MyProduct(CMSPageReferenceMixin, BaseProduct): + product_name = models.CharField( + _("Product Name"), + max_length=255, + ) -Additionally, we have to rewrite the URL pattern from above as: + slug = models.SlugField( + _("Slug"), + unique=True, + ) -.. code-block:: python + # other fields making up our product - from django.conf.urls import url - from shop.views.catalog import ProductListView - from myshop.serializers import CustomizedProductSerializer + cms_pages = models.ManyToManyField( + 'cms.Page', + through=ProductPage, + help_text="Choose page this product shall appear on.", + ) - urlpatterns = [ - url(ProductListView.as_view( - serializer_class=CustomizedProductSerializer, - )), - # other patterns - ] + objects = BaseProductManager() -Here the ``CustomizedProductSerializer`` is used to create a more specialized representation of our -product model. +An important part of this product model is the category ``cms_pages = ManyToManyField(...)``. +Mapping a relationship between the CMS pages and itself, the merchant can emulate categories by +assigning a product to one ore more CMS pages. Products added to those CMS pages, then shall be +visible in the **Catalog List View** plugin. +As we work with deferred models, we can not use the mapping table, which normally is generated +automatically for Many-to-Many fields by the Django framework. Instead, we must reference the +mapping table :class:`shop.models.defaults.mapping.ProductPage` using the ``though`` parameter, when +declaring the field ``cms_pages``. -Add the Catalog to the CMS --------------------------- -In the page list editor of **django-CMS**, create a new page at an appropriate location of the -page tree. As the page title and slug we should use something describing our product catalog in a -way, both meaningful to the customers as well as to search engines. +.. _reference/product-summary-serializer: -Next, we change into **Advanced Settings** mode. +Product Summary Serializer +-------------------------- -As a template we use one with a big placeholder, since it must display our list of products. +In order to render the list view, we need to identify the fields common to all offered products. +This is because when rendering a list view, we usually want do have a consistent representation for +all products in our catalog. Since this catalog list can be rendered either by the server (using +``CMSPageRenderer``), or by the AngularJS directive ``shop-catalog-list`` on the client (using +``JSONRenderer``), we must provide some functionality to serialize a summary representation for all +the products we want to list. This separation is important, so that we can reuse the same Django +View (``ProductListView``), whenever we switch from the server-side rendered catalog list into +infinite scroll mode. + +For this purpose, we have to declare a product summary serializer using the configuration directive +``SHOP_PRODUCT_SUMMARY_SERIALIZER``. Remember that **django-SHOP** does not impose which fields a +product must offer, it's up to the merchant to declare this product summary serializer as well. +A typical implementation might look like: + +.. code-block:: + + class ProductSummarySerializer(ProductSerializer): + media = serializers.SerializerMethodField( + help_text="Returns a rendered HTML snippet containing a sample image among other elements", + ) -As **Application**, select "*Catalog List*". This selects the apphook we created in the previous -section. + class Meta(ProductSerializer.Meta): + fields = ['id', 'product_name', 'product_url', 'product_model', 'price', 'media'] -Then we save the page, change into **Structure** mode and locate the placeholder named -**Main Content**. Add a Container plugin, followed by a Row and then a Column plugin. As the -child of this column choose the **Catalog List View** plugin from section **Shop**. + def get_media(self, product): + return self.render_html(product, 'media') -Finally we publish the page. If we have assigned products to that CMS page, they should be rendered -now. +Here we assume that our product models have a very limited set of common fields. They may for +instance have a field to store a caption text and an image. Those two fields then can be rendered +into a HTML snippet, which here we name ``media``. Using method +:meth:`shop.serializers.bases.ProductSerializer.render_html()`, this snipped is rendered by the +serializer itself, looking for a Django template following these rules: +* look for a template named :samp:`{app_label}/products/catalog-{product-model-name}-{field-name}.html` + [1]_ [2]_ [3]_, otherwise +* look for a template named :samp:`{app_label}/products/catalog-product-{field-name}.html`` [1]_ [3]_, + otherwise +* use the template ``shop/product/catalog-product-media.html``. -.. _reference/catalog-detail: +.. [1] :samp:`{app_label}` is the app label of the project in lowercase. +.. [2] :samp:`{product-model-name}` is the class name of the product model in lowercase. +.. [3] :samp:`{field-name}` can be any lowercased identifier, but by convenience shall be the name + of the serializer field. In this example we use ``media`` as field name. -Catalog Detail View -=================== +.. note:: + When rendering images, we have to create a thumbnailed version and put its URL into a + ```` tag. This means that we then have to know the thumbnailed size of the + final image, so that the templatetag `thumb`_ from the easythumbnail library knows what to do. + Otherwise we would have to refer to the original, often much bigger image and thumbnail it + on the fly, which would be pretty inefficient. -The product's detail pages are the only ones we typically do not control with **django-CMS** -placeholders. This is because we often have thousands of products and creating a CMS page for each -of them, would be kind of overkill. It only makes sense for shops selling up to a dozen of different -products. +To test if that serializer works properly, we can examine the raw content of the declared fields by +appending ``?format=api`` to the URL of our catalog view. This then renders a human readable +representation of the context as JSON. -Therefore, the template used to render the products's detail view is selected automatically by the -``ProductRetrieveView`` [1]_ following these rules: -* look for a template named ``/catalog/-detail.html`` [2]_ [3]_, - otherwise -* look for a template named ``/catalog/product-detail.html`` [2]_, otherwise -* use the template ``shop/catalog/product-detail.html``. +Customizing the Product Summary Serializer +.......................................... -.. [1] This is the View class responsible for rendering the product's detail view. -.. [2] ```` is the app label of the project in lowercase. -.. [3] ```` is the class name of the product model in lowercase. +In case we need :ref:`reference/additional-serializer-fields`, let's add them to a customized +product serializer class using the `serializer fields`_ from the Django RESTFramework library. This +can be useful for product serializers which shall provide additional information on our catalog list +view. If we have to map fields from our product model, just add them to the list of fields in the +``Meta``-class. For example as: +.. code-block:: python -Use CMS Placeholders on Detail View ------------------------------------ + from shop.serializers.bases import ProductSerializer -If we require CMS functionality for each product's detail page, its quite simple to achieve. To the -class implementing our product model, add a `django-CMS Placeholder field`_ named ``placeholder``. -Then add the templatetag ``{% render_placeholder product.placeholder %}`` to the template -implementing the detail view of our product. This placeholder then shall be used to add arbitrary -content to the product's detail page. This for instance can be an additional text paragraphs, -some images, a carousel or whatever is available from the **django-CMS** plugin system. + class CustomizedProductSerializer(ProductSerializer): + class Meta: + model = CustomProductModel + fields = [all-the-fields-required-for-the-list-view] +Additionally, we have to rewrite the apphook from above as: -Route requests on Detail View ------------------------------ +.. code-block:: python -The ``ProductsListApp``, which we previously have registered into **django-CMS**, is able to route -requests on all of its sub-URLs. This is done by expanding the current list of urlpatterns: + class CatalogListApp(CatalogListCMSApp): + def get_urls(self, page=None, language=None, **kwargs): + ... -.. code-block:: python - :caption: myshop/urls/products.py - :name: productlist-urlpatterns + return [ + ... + url(r'^', ProductListView.as_view( + filter_backends=..., + serializer_class=CustomizedProductSerializer, + )), + ] - from django.conf.urls import url - from shop.views.catalog import ProductRetrieveView +By specifiying an alternative product sumary serializer, we can create a more specialized +representation of our product models. - urlpatterns = [ - # previous patterns - url(r'^(?P[\w-]+)$', ProductRetrieveView.as_view()), - # more patterns - ] +A nice aspect of this is, that we can create one apphook per product model. This can be useful, if +we want to render a different kind of catalog list per product type. Say, our shop offers two +product models, ``Book`` and ``Magazine`` and both of these models have their own list serializers. +Then by restricting our ``ProductListView`` to one product model using its customized serializer, +we can build two different list views, one for books and one for magazines. If we want to restrict +our list view to magazines only, we simply pass ``limit_choices_to = Q(instance_of=Book)`` to the +above ``as_view()``-method. -If we need additional business logic regarding our product, we can create a customized serializer -class, named for instance ``CustomizedProductDetailSerializer``. This class then may access the -various attributes of our product model, recombine them and/or merge them into a serializable -representation, as described in :ref:`reference/customized-product-serializer`. -Additionally, we have to rewrite the URL pattern from above as: +.. _reference/catalog-detail: -.. code-block:: python +Catalog Detail View +=================== - from myshop.serializers import CustomizedProductDetailSerializer +The apphook ``CatalogListApp`` as show above, is also responsible for routing to the product's +detail view. This is why our product declares a ``SlugField``. The product's slug then is appended +to the URL of the CMS page, also referred as category. This approach generates nicely spelled URLs. - urlpatterns = [ - # previous patterns - url(r'^(?P[\w-]+)$', ProductRetrieveView.as_view( - serializer_class=CustomizedProductDetailSerializer, - )), - # more patterns - ] +A product detail view is rendered by the :class:`shop.views.catalog.ProductRetrieveView` and is +*not* managed by **django-CMS**. Instead, this product detail view behaves like a normal Django +view, with its own context objects and rendered by a specifc template. This is because we often +have thousands of different products and creating one CMS page for each of them, would be a far +bigger effort, rather than creating a specific template for each product type. +When rendering a product's detail page, the ``ProductRetrieveView`` looks for a template suitable +for the given product type, following these rules: +* look for a template named :samp:`{app_label}>/catalog/{product-model-name}-detail.html` [4]_ [5]_, + otherwise +* look for a template named :samp:`{app_label}/catalog/product-detail.html` [4]_, otherwise +* use the template samp:`shop/catalog/product-detail.html`. -.. _reference/additional-serializer-fields: +This means that the template to render the products's detail view is selected automatically by the +:class:`shop.views.catalog.ProductRetrieveView`. When rendered as HTML, this view adds the product +model to the context, so that the rendering templates can refer to this context variable. -Additional Product Serializer Fields -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. [4] *app_label* is the app label of the project in lowercase. +.. [5] *product-model-name* is the class name of the product model in lowercase. -Sometimes such a serializer field shall return a HTML snippet; this for instance is required for -image source (````) tags, which must thumbnailed by the server when rendered using -the appropriate `templatetags from the easythumbnail`_ library. For these use cases add a field -of type ``media = SerializerMethodField()`` with an appropriate method ``get_media()`` to our -serializer class. This method then may forward the given product to a the built-in renderer: -.. code-block:: python +Use CMS Placeholders in the Detail View +--------------------------------------- - class ProductDetailSerializer(BaseProductDetailSerializer): - # other attributes +Sometime we want to add any kind of **django-CMS** plugins to our product's detail pages. To achieve +this, we need to add a `django-CMS Placeholder field`_ named ``placeholder``, to the class +implementing our product model. Then we add the templatetag +``{% render_placeholder product.placeholder %}`` to the template implementing the detail view of +that product. Now this placeholder can be used to add any arbitrary content to the product's detail +page. This for instance can be a CMS plugin to add text paragraphs, additional images, a carousel, +a video, or whatever else is available from the **django-CMS** plugin system. - def get_media(self, product): - return self.render_html(product, 'media') +.. note:: + The built-in product model :class:`shop.models.defaults.commodity.Commodity` makes heavy + use of that placeholder field. The commodity model actually doesn't offer any other fields, + other than the product's code, name and price. So all relevant information must be added to the + product's detail view using the **django-CMS** structure editor. -This HTML renderer method looks up for a template following these rules: -* look for a template named ``/product/catalog--.html`` - [4]_ [5]_ [6]_, otherwise -* look for a template named ``/product/catalog-product-.html`` [4]_ [6]_, - otherwise -* use the template ``shop/product/catalog-product-.html`` [6]_. +Customizing the Product Detail Serializer +----------------------------------------- -.. [4] ```` is the app label of the project in lowercase. -.. [5] ```` is the class name of the product model in lowercase. -.. [6] ```` is the attribute name of the just declared field in lowercase. +If we need additional business logic regarding our product, we can create a customized serializer +class, named for instance ``CustomizedProductDetailSerializer``. This class then may access the +various attributes of our product model, recombine them and/or merge them into a serializable +representation, as described in :ref:`reference/customized-product-serializer`. +Additionally, we have to rewrite the apphook from above as: -Emulate Categories ------------------- +.. code-block:: python -Since we want to use CMS pages to emulate categories, the product model must declare a relationship -between the CMS pages and itself. This usually is done by adding a Many-to-Many field named -``cms_pages`` to our Product model. + class CatalogListApp(CatalogListCMSApp): + def get_urls(self, page=None, language=None, **kwargs): + ... -As we work with deferred models, we can not use the mapping table, which normally is generated -automatically for Many-to-Many fields by the Django framework. Instead, this mapping table must -be created manually and referenced using the ``though`` parameter, when declaring the field: + return [ + ... + url(r'^', ProductRetrieveView.as_view( + serializer_class=CustomProductDetailSerializer, + )), + ] -.. code-block:: python - from shop.models.product import BaseProductManager, BaseProduct - from shop.models.related import BaseProductPage +Add Product to Cart +=================== - class ProductPage(BaseProductPage): - """Materialize many-to-many relation with CMS pages""" +By looking at the URL routings above, the savvy reader may have noticed, that for each product's +detail view, there is an extra endpoint ending in ``.../add-to-cart``. Its URL points onto the class +:class:`shop.views.catalog.AddToCartView`. This view handles the communication between the control +form for adding the given product to the cart on the client, and the REST endpoints on the server. - class Product(BaseProduct): - # other model fields - cms_pages = models.ManyToManyField( - 'cms.Page', - through=ProductPage - ) +Each product's detail page shall implement an element containing the AngularJS directive +``shop-add-to-cart``. This directive fetches the availability, price and cart status, and fills out +the "add to cart" form. If the customer submits that form data, the item is added either to the +cart, or the watch-list. - objects = ProductManager() +To help integration, **django-SHOP** offers a HTML snippet for this purpose. It can be included as +``shop/templates/shop/catalog/product-add2cart.html`` or, if we must handle the current availability +``shop/templates/shop/catalog/available-product-add2cart.html``. It's up to the merchant to use and +extend these templates to fit the representation for his own products. -In this example the class ``ProductPage`` is responsible for storing the mapping information -between our Product objects and the CMS pages. +For products with a **django-CMS** placeholder field, the merchant can also use the plugin named +"*Add Product to Cart*". This plugin then shall be added into the structure of the product's detail +page. Products of type "Commodity" make use of this plugin. Admin Integration -~~~~~~~~~~~~~~~~~ +================= To simplify the declaration of the admin backend used to manage our Product model, **django-SHOP** is shipped with a special mixin class, which shall be added to the product's admin class: @@ -294,23 +358,22 @@ is shipped with a special mixin class, which shall be added to the product's adm class ProductAdmin(CMSPageAsCategoryMixin, admin.ModelAdmin): fields = [ 'product_name', 'slug', 'product_code', - 'unit_price', 'active', 'description' + 'unit_price', 'active', 'description', + # other model fields ] # other admin declarations This then adds a horizontal filter widget to the product models. Here the merchant must select each CMS page, where the currently edited product shall appear on. -If we are using the method ``render_html()`` to render HTML snippets, these are cached by -**django-SHOP**, if caching is configured and enabled for that project. Caching these snippets is -highly recommended and gives a noticeable performance boost, specially while rendering catalog list -views. +If caching is configured and enabled, HTML snippets rendered by the method ``render_html()`` are +cached by **django-SHOP**. Caching these snippets is highly recommended and gives a noticeable +performance boost, specially while rendering catalog list views. Since we would have to wait until they expire naturally by reaching their expire time, -**django-SHOP** offers a mixin class to be added to the Product admin class, to expire all HTML -snippets of a product altogether, whenever a product in saved in the backend. Simply add -:class:`shop.admin.product.InvalidateProductCacheMixin` to the ``ProductAdmin`` class described -above. +**django-SHOP** offers the mixin class :class:`shop.admin.product.InvalidateProductCacheMixin`. This +should be added to the ``ProductAdmin`` class. It then expires all HTML snippets of a product, +whenever a product in saved by the backend. .. note:: Due to the way keys are handled in many caching systems, the ``InvalidateProductCacheMixin`` only makes sense if used in combination with the redis_cache_ backend. @@ -320,58 +383,3 @@ above. .. _serializer fields: http://www.django-rest-framework.org/api-guide/fields/ .. _templatetags from the easythumbnail: https://easy-thumbnails.readthedocs.org/en/stable/usage/#templates .. _redis_cache: http://django-redis-cache.readthedocs.org/en/stable/ - - -DEPRECATED DOCS -=============== - - -Connect the Serializers with the View classes -============================================= - -Now that we declared the serializers for the product's list- and detail view, the final step is to -access them through a CMS page. Remember, since we've chosen to use CMS pages as categories, we had -to set a special **django-CMS** apphook_: - -.. code-block:: python - :caption: myshop/cms_apps.py - :name: - :linenos: - - from cms.app_base import CMSApp - from cms.apphook_pool import apphook_pool - - class ProductsListApp(CMSApp): - name = _("Products List") - urls = ['myshop.urls.products'] - - apphook_pool.register(ProductsListApp) - -This apphook points onto a list of boilerplate code containing these urlpattern: - -.. code-block:: python - :caption: myshop/urls/products.py - :linenos: - - from django.conf.urls import url - from rest_framework.settings import api_settings - from shop.rest.filters import CMSPagesFilterBackend - from shop.rest.serializers import AddToCartSerializer - from shop.views.catalog import (CMSPageProductListView, - ProductRetrieveView, AddToCartView) - - urlpatterns = [ - url(r'^$', CMSPageProductListView.as_view( - serializer_class=ProductSummarySerializer, - )), - url(r'^(?P[\w-]+)$', ProductRetrieveView.as_view( - serializer_class=ProductDetailSerializer - )), - url(r'^(?P[\w-]+)/add-to-cart', AddToCartView.as_view()), - ] - -These URL patterns connect the product serializers with the catalog views in order to assign them -an endpoint. Additional note: The filter class ``CMSPagesFilterBackend`` is used to restrict -products to specific CMS pages, hence it can be regarded as the product categoriser. - -.. _apphook: http://docs.django-cms.org/en/latest/introduction/05-apphooks.html diff --git a/docs/reference/search.rst b/docs/reference/search.rst index d06d5322e..c2bb00f9e 100644 --- a/docs/reference/search.rst +++ b/docs/reference/search.rst @@ -53,8 +53,8 @@ should return something similar to this: }, } -Install ``elasticsearch-dsl`` and ``django-elasticsearch-dsl`` using the ``pip`` command or another -tool of your choice. +Install ``elasticsearch-dsl`` and ``django-elasticsearch-dsl`` using the ``pip`` command or an +alternative Python package manager. In ``settings.py``, check that ``'django_elasticsearch_dsl'`` has been added to ``INSTALLED_APPS``. Configure the connection to the Elasticsearch database: @@ -98,11 +98,11 @@ information is stored inside the ``Document`` field ``product_name``. Product Code ------------ -The product's code remains the same for all languages. However, in case a product is offerend in -different variants, each of them may declare their own code. This means, that the same product can -be found through one or more product codes. Moreover, since product code are unique identifiers, -we usually do not want to apply stemming, they are stored as a list of keywords inside an -Elasticsearch ``Document`` entity. +The product's code is the unique identifier of a product and is independant of the language. +However, in case a product is offerend in different variants, each of them may declare their own +product code. This means, that the same product can be found through one or more product codes. +Moreover, since product code are unique identifiers, we usually do not want to apply stemming, they +are stored as a list of keywords inside an Elasticsearch ``Document`` entity. Body Field @@ -161,10 +161,11 @@ Say, we have a product using this simplified model representation: on_delete=models.CASCADE, ) -By default, **django-SHOP** only indexes the fields ``product_name`` and ``product_code``. However, -we also want all other fields beeing indexed. If the merchant's project is named -``awesome_bookstore``, then inside the project's template folder, we must create a file named -``awesome_bookstore/search/indexes/book.txt``. This template file then shall contain: +By default, **django-SHOP**'s search functionality indexes only the fields ``product_name`` and +``product_code``. Usually we also want other fields beeing indexed, if the contain relevant +information. If say, the merchant's implementation is named ``awesome_bookstore``, then inside the +project's template folder, we must create a file named ``awesome_bookstore/search/indexes/book.txt``. +This template file then shall contain a structure similar to this: .. code-block:: text :caption: awesome_bookstore/search/indexes/book.txt @@ -174,15 +175,19 @@ we also want all other fields beeing indexed. If the merchant's project is named {{ author.name }}{% endfor %} {{ product.editor.name }} -When building the index, this template is rendered for each product in our bookstore. The rendered -content then cleaned up, tokenized, stemmed, filtered and used to build the reverse index for the -Elasticsearch database. The reverse index then is stored in the ``body`` field inside the -:class:`shop.search.documents.ProductDocument`. +When building the index, this template is rendered for each product offered by our bookstore. +The rendered content is not intended to be shown to humans, it rather serves to create a reverse +index in order to feed the Elasticsearch database. Before that, it is cleaned up, removing all HTML +tags. Afterwards it is tokenized into a list of separate words. These words then are stemmed, which +means that they are reduced to their basic meaning. The final step is to remove common words, such +as "and". This list of words is named reverse index and is then stored in the ``body`` field inside +the :class:`shop.search.documents.ProductDocument`. -If the above template file can not be found, **django-SHOP** falls back onto -``awesome_bookstore/search/indexes/product.txt``. If that template file is missing too, then -the file ``shop/search/indexes/product.txt`` is used. Note that the template file always is in -lowercase. +.. note:: + If the above template file can not be found, **django-SHOP** falls back onto + ``awesome_bookstore/search/indexes/product.txt``. If that template file is missing too, then + the file ``shop/search/indexes/product.txt`` is used. Note that the template file always is in + lowercase. Populate the Database @@ -193,186 +198,145 @@ To build the index in Elasticsearch, invoke: .. code-block:: shell ./manage.py search_index --rebuild + Deleting index 'awesome_bookstore.de.products' + Deleting index 'awesome_bookstore.en.products' + Creating index 'awesome_bookstore.de.products' + Creating index 'awesome_bookstore.en.products' + Indexing 986 'Product' objects + Indexing 986 'Product' objects -Depending on the number of products in the database, this may take some time. +Depending on the number of products in the database, this may take some time. Note, that only +products tagged as "active" are indexed. To check, if the product can be found in the index, point +a browser on http://localhost:9200/awesome_bookstore.en.products/_search?q=django&pretty . If our +awesome bookstore offers books whose title or caption text contains the word "Django", then these +books are listed as "hits" in the JSON response from Elasticsearch. +Showing Search Results +====================== +The populated search database can be used for two kind of purposes: Generic search over all products +and as an additional "search-as-you-type" filter, while rendering the catalog's list view. -Building the Index ------------------- - - -Search Serializers -================== - -`Haystack for Django REST Framework`_ is a small library aiming to simplify using Haystack with -Django REST Framework. It takes the search results returned by Haystack, treating them the similar -to Django database models when serializing their fields. The serializer used to render the content -for this demo site, may look like: - -.. code-block:: python - :caption: myshop/serializers.py - :name: serializers - - from rest_framework import serializers - from shop.search.serializers import ProductSearchSerializer as ProductSearchSerializerBase - from .search_indexes import SmartCardIndex, SmartPhoneIndex - - class ProductSearchSerializer(ProductSearchSerializerBase): - media = serializers.SerializerMethodField() - - class Meta(ProductSearchSerializerBase.Meta): - fields = ProductSearchSerializerBase.Meta.fields + ('media',) - index_classes = (SmartCardIndex, SmartPhoneIndex) - - def get_media(self, search_result): - return search_result.search_media - -This serializer is part of the project, since we must adopt it to whatever content we want to -display on our site, whenever a visitor enters some text into the search field. - - -.. _reference/search-view: - -Search View -=========== - -In the Search View we link the serializer together with a `djangoCMS apphook`_. This -``CatalogSearchApp`` can be added to the same file, we already used to declare the -``CatalogListApp`` used to render the catalog view: - -.. code-block:: python - :caption: myshop/cms_apps.py - :name: search-app - - from cms.apphook_pool import apphook_pool - from shop.cms_apphooks import CatalogSearchCMSApp - - class CatalogSearchApp(CatalogSearchCMSApp): - def get_urls(self, page=None, language=None, **kwargs): - return ['myshop.urls.search'] - - apphook_pool.register(CatalogSearchApp) - -as all apphooks, it requires a file defining its urlpatterns: - -.. code-block:: python - :caption: myshop/urls/search.py - - from django.conf.urls import url - from shop.search.views import SearchView - from myshop.serializers import ProductSearchSerializer - - urlpatterns = [ - url(r'^', SearchView.as_view( - serializer_class=ProductSearchSerializer, - )), - ] - - -Display Search Results ----------------------- +Search Apphook +-------------- As with all other pages in **django-SHOP**, the page displaying our search results is a normal CMS -page too. It is suggested to create this page on the root level of the page tree. +page too. It is suggested to create this page on the root level of the page tree. As title for this +page we choose "*Search Results*" or something similar meaningful. Since we want to hide this page +from the menu navigation, we must disable its Menu visibility using the appropriate checkbox in the +CMS page tree admin. + +We now change into the *Advanced Setting* of the page.There we set the page **ID** to +``shop-search-product``. This identifier is required, so that the search functionality knows where +to render the search results. As **Application**, select *Catalog Search* from the drop-down menu. +This selects the `django-CMS apphook`_ provided by **django-SHOP** for its catalog search. + +.. note:: + The apphook *Catalog Search* must be registered by the merchant implementation. Its just as + simple as registering :class:`shop.cms_apphooks.CatalogSearchApp` using the + :method:`menus.menu_pool.menu_pool.apphook_pool.register`. + +As a template use one with a placeholder large enough to render the search results. The default +template shipped with **django-SHOP** usually is a good fit. + +Now save the page and change into **Structure** mode. There locate the placeholder named +**Main Content** and add a Bootstrap Container plugin, followed by a Row and then a Column plugin. +As child of that column, choose the **Search Results** plugin from section **Shop**. This plugin +offers three pagination options: + +* **Manual Paginator**: If searching generates too many results, add a paginator on the bottom of + the page. The customer may scroll through those pages manually. +* **Manual Infinite**: If searching generates too many results, add a button on the bottom of + the page. The customer load more results clicking on that button. +* **Auto Infinite**: If searching generates too many results, and the customer scrolls to the + bottom of the page, more results are loaded automatically. + +As with all other placeholders in **django-CMS**, you may add as many plugins together with the +**Search Results** plugin. + +Finally publish the page and enter some text into the search field. This should render a list of +found products. -As the page title use "*Search*" or whatever is appropriate as expression. Then we change into -the *Advanced Setting* od the page. +|product-search-results| -As a template use one with a big placeholder, since it must display our search results. Our default -template usually is a good fit. +.. |product-search-results| image:: /_static/product-search-results.png -As the page **Id** field, enter ``shop-search-product``. Some default HTML snippets, prepared for -inclusion in other templates, use this hard coded string. -Set the input field **Soft root** to checked. This hides our search results page from the menu list. +Adopting the Templates +...................... -As **Application**, select "*Search*". This selects the apphook we created in the previous section. +Search results are displayed using a wrapper template responsible for rendering a list of found +items. The default template can be found in ``shop/templates/shop/search/results.html``. It can +be replaced or extended by a customized template in the merchant implementation. In our bookstore +this template would be named ``awesome_bookstore/templates/awesome_bookstore/search/results.html``. -Then save the page, change into **Structure** mode and locate the placeholder named -**Main Content**. Add a Bootstrap Container plugin, followed by a Row and then a Column plugin. As -the child of this column, choose the **Search Results** plugin from section **Shop**. +Since each of the found items may be from a different product type, we can provide a snippet +template for each of them. This allows us to display the given list in a polymorphic way, so that +each product type is rendered differently. That snippet template is looked up following these rules: -Finally publish the page and enter some text into the search field. It should render a list of -found products. +* :samp:`{app_label}/templates/{app_label}/products/search-{product-model-name}-media.html` +* :samp:`{app_label}/templates/{app_label}/products/search-product-media.html` +* :samp:`shop/templates/shop/products/search-product-media.html` -|product-search-results| +This means that the template to render the products's detail view is selected automatically +depending on its product type. -.. |product-search-results| image:: /_static/product-search-results.png +.. [1] *app_label* is the app label of the project in lowercase. +.. [2] *product-model-name* is the class name of the product model in lowercase. .. _reference/search-autocompletion-catalog: Autocompletion in Catalog List View -=================================== +----------------------------------- As we have seen in the previous example, the Product Search View is suitable to search for any item -in the product database. However, the site visitor sometimes might just refine the list of items -shown in the catalog's list view. Here, loading a new page which uses a layout able to render every -kind of product usually differs from the catalog's list layout, and hence may by inappropriate. +in the product database. Sometimes the site visitor might just refine the list of items shown in the +catalog's list view. Here, loading a new page which uses a layout able to render every kind of +product usually differs from the catalog's list layout, and hence may by inappropriate. Instead, when someone enters some text into the search field, **django-SHOP** starts to narrow down the list of items in the catalog's list view by typing query terms into the search field. This is specially useful in situations where hundreds of products are displayed together on the same page and the customer needs to pick out the correct one by entering some search terms. -To extend the existing Catalog List View for autocompletion, locate the file containing the -urlpatterns, which are used by the apphook ``ProductsListApp``. In doubt, consult the file -``myshop/cms_apps.py``. This apphook names a file with urlpatterns. Locate that file and add the -following entry: - -In order to use the Product Search View, our Product Model must inherit from -:class:`shop.models.product.CMSPageReferenceMixin`. This is because we must add a reference to the -CMS pages our products are assigned to, into the search index database. Such a product may for -instance be declared as: +To extend the existing Catalog List View for autocompletion, locate the file ``cms_apps.py`` in +the merchant implementation. There we add a special search filter to our existing product list view. +This could be implemented as: .. code-block:: python + :caption: awesome_bookstore/cms_apps.py - from shop.models.product import BaseProduct, BaseProductManager, CMSPageReferenceMixin - - class MyProduct(CMSPageReferenceMixin, BaseProduct): - ... - - objects = BaseProductManager() - - ... - -We normally want to use the same URL to render the catalog's list view, as well as the -autocomplete view, and hence must route onto the same view class. However the search- and the -catalog's list view classes have different bases and a completely different implementation. - -The normal List View uses a Django queryset to iterate over the products, while the autocomplete -View uses a Haystack Search queryset. Therefore we wrap both View classes into -:class:`shop.search.views.CMSPageCatalogWrapper` and use it in our URL routing such as: - -.. code-block:: python + from cms.apphook_pool import apphook_pool + from shop.cms_apphooks import CatalogListCMSApp + from shop.rest.filters import CMSPagesFilterBackend - from django.conf.urls import url - from shop.search.views import CMSPageCatalogWrapper - from myshop.serializers import CatalogSearchSerializer + class CatalogListApp(CatalogListCMSApp): + def get_urls(self, page=None, language=None, **kwargs): + from shop.search.mixins import ProductSearchViewMixin + from shop.views.catalog import AddToCartView, ProductListView, ProductRetrieveView - urlpatterns = [ - url(r'^$', CMSPageCatalogWrapper.as_view( - search_serializer_class=CatalogSearchSerializer, - )), - # other patterns - ] + ProductSearchListView = type('SearchView', (ProductSearchViewMixin, ProductListView), {}) + return [ + url(r'^(?P[\w-]+)/add-to-cart', AddToCartView.as_view()), + url(r'^(?P[\w-]+)', ProductRetrieveView.as_view()), + url(r'^', ProductSearchListView.as_view( + filter_backends=[CMSPagesFilterBackend] + list(api_settings.DEFAULT_FILTER_BACKENDS), + )), + ] -The view class ``CMSPageCatalogWrapper`` is a functional wrapper around the catalog's products list -view and the search view. Depending on whether the request contains a search query starting with -``q=``, either the search view or the normal products list view is invoked. + apphook_pool.register(CatalogListApp) -The ``CatalogSearchSerializer`` used here is very similar to the ``ProductSearchSerializer``, we -have seen in the previous section. The only difference is, that instead of the ``search_media`` -field is uses the ``catalog_media`` field, which renders the result items media in a layout -appropriate for the catalog's list view. Therefore this kind of search, normally is used in -combination with auto-completion, because here we reuse the same layout for the product's list view. +In this apphook, we created the class ``ProductSearchListView`` on the fly. It actually just adds +the mixin :class:`shop.search.mixins.ProductSearchViewMixin` to the existing +:ref:`reference/catalog-list`. This class extends the internal filters by one, which also consults +the Elasticsearch database if we filter the product against a given query request. The Client Side ---------------- +=============== To facilitate the placement of the search input field, **django-SHOP** ships with a reusable AngularJS directive ``shopProductSearch``, which is declared inside the module @@ -400,5 +364,4 @@ bootstrapping our Angular application: .. _Elasticsearch: https://www.elastic.co/ .. _elasticsearch-dsl: https://elasticsearch-dsl.readthedocs.io/en/latest/ .. _django-elasticsearch-dsl: https://django-elasticsearch-dsl.readthedocs.io/en/latest/ -.. _normalized: https://www.elastic.co/guide/en/elasticsearch/guide/current/token-normalization.html -.. _djangoCMS apphook: http://docs.django-cms.org/en/stable/how_to/apphooks.html +.. _django-CMS apphook: http://docs.django-cms.org/en/stable/how_to/apphooks.html