diff --git a/docs/changelog.rst b/docs/changelog.rst index 3f2c6ec48..46832ac26 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,14 @@ Changelog for django-SHOP ========================= +1.2 +=== +* Add support for Django-3.0. +* For full-text searching, replace Haystack against elasticsearch-dsl. +* Drop support for Python 2.7 and 3.4. +* Drop support for Django-1.11 and 2.0. + + 1.1.4 ===== * Fix rendering bug in Product Gallery plugin. @@ -443,7 +451,7 @@ Changelog for django-SHOP * Added method ``post_process_cart_item`` to the Cart Modifiers. * In ``CartItem`` the ``product_code`` is mandatory now. It moves from being optionally kept in dict ``CartItem.extra`` into the ``CartItem`` model itself. This simplifies a lot of boilerplate code, - otherwise required by the merchant implementation. Please read :ref:`release-notes/0.10` for details. + otherwise required by the merchant implementation. * In :class:`shop.models.product.BaseProduct` added a hook method ``get_product_variant(self, **kwargs)`` which can be overridden by products with variations to return a product variant. @@ -470,8 +478,7 @@ Changelog for django-SHOP * Minimum required version of django-filer is now 1.2.5. * Minimum required version of djangocms-cascade is now 0.10.2. * Minimum required version of djangoshop-stripe is now 0.2.0. -* Changed the default address models to be more generic. Please read the - :ref:`release-notes/0.9` if you are upgrading from 0.9.0 or 0.9.1. +* Changed the default address models to be more generic. * Fixed :py:meth:`shop.money.fields.decontruct` to avoid repetitive useless generation of migration files. * Using cached_property decoration for methods ``unit_price`` and ``line_total`` in diff --git a/docs/index.rst b/docs/index.rst index 589024d12..cf040a5f5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,12 +11,6 @@ Django-SHOP documentation features -Upgrading -========= -If you are upgrading from an earlier version, please be sure to read the -:ref:`release-notes`. - - Tutorial ======== @@ -92,12 +86,15 @@ Django/Python compatibility table =========== === === ==== ==== === === === === === django-SHOP Django Python ----------- ------------------------- ------------------ -\ 1.8 1.9 1.10 1.11 2.0 2.7 3.4 3.5 3.6 -=========== === === ==== ==== === === === === === -0.10.x ✓ ✓ ⨯ ⨯ ⨯ ✓ ✓ ✓ ⨯ -0.11.x ⨯ ✓ ✓ ⨯ ⨯ ✓ ✓ ✓ ✓ -0.12.x ⨯ ⨯ ⨯ ✓ ⨯ ✓ ✓ ✓ ✓ -0.13.x ⨯ ⨯ ⨯ ✓ ⨯ ✓ ✓ ✓ ✓ +\ 1.8 1.9 1.10 1.11 2.0 2.7 3.4 3.5 3.6 3.7 3.8 +=========== === === ==== ==== === === === === === === === +0.10.x ✓ ✓ ⨯ ⨯ ⨯ ✓ ✓ ✓ ⨯ +0.11.x ⨯ ✓ ✓ ⨯ ⨯ ✓ ✓ ✓ ✓ +0.12.x ⨯ ⨯ ⨯ ✓ ⨯ ✓ ✓ ✓ ✓ +0.13.x ⨯ ⨯ ⨯ ✓ ⨯ ✓ ✓ ✓ ✓ +1.0.x ⨯ ⨯ ⨯ ✓ ⨯ ✓ ✓ ✓ ✓ +1.1.x ⨯ ⨯ ⨯ ✓ ⨯ ✓ ✓ ✓ ✓ +1.2.x ⨯ ⨯ ⨯ ✓ ⨯ ⨯ ⨯ ✓ ✓ =========== === === ==== ==== === === === === === @@ -108,7 +105,6 @@ Development and Community :maxdepth: 1 changelog - release-notes/index faq contributing authors diff --git a/docs/reference/catalog.rst b/docs/reference/catalog.rst index 662d7d59f..b211831bb 100644 --- a/docs/reference/catalog.rst +++ b/docs/reference/catalog.rst @@ -4,20 +4,25 @@ Catalog ======= -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. +The catalog presumably is that part, where customers of our e-commerce site 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 -implementation, but some sites may require categories implemented independently of the CMS. +**django-CMS** pages in combination with a `django-CMS apphook`_. This works perfectly well for most +implementations, but some sites may require categories implemented independently of the CMS. Using an external **django-SHOP** plugin for managing categories is a very conceivable solution, 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 in this -document, we proceed using CMS pages as categories. +plugin can make sense, if an e-commerce site requires hundreds of hierarchical levels and/or +these category implementations can provide functionality, which is not available in **django-CMS** +pages. If you are going to use externally implemented categories, please refer to their +documentation, since in this document, we proceed using standard CMS pages as product categories. + +It should be emphasized, that nowadays the classical hierarchy of categories is no longer +contemporary. Instead many merchants tag their products with different attributes. This provides a +better browsing experience, since customers usually filter by product characteristics, rather than +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 @@ -29,11 +34,15 @@ into a serializer, which in this documentation is referred as ``ProductSerialize Catalog List View ================= -In this documentation, the catalog list view is implemented as a **django-CMS** page. Depending on +In this documentation, the *Catalog List View* is implemented as a **django-CMS** page. Depending on whether the e-commerce aspect of that site is the most prominent part or just a niche of the CMS, select an appropriate location in the page tree and create a new page. This will become the root of our catalog list. +.. note:: + If required, we can add as many catalog list views as we want, and distribute them accross the + CMS page tree. + But first we must :ref:`reference/create-CatalogListApp`. @@ -42,9 +51,9 @@ 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`_. The -class ``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`_. For +this, we must inherit from :class:`shop.cms_apphooks.CatalogListCMSApp` and add that class +declaration to a file named ``cms_apps.py``, located in the root folder of our merchant's project: .. code-block:: python :caption: myshop/cms_apps.py @@ -57,17 +66,19 @@ folder of the merchant's project: def get_urls(self, page=None, language=None, **kwargs): from shop.views.catalog import AddToCartView, ProductListView, ProductRetrieveView + filter_backends = [CMSPagesFilterBackend] + filter_backends.extend(api_settings.DEFAULT_FILTER_BACKENDS) 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), + filter_backends=filter_backends, )), ] apphook_pool.register(CatalogListApp) -In the page tree editor of **django-CMS**, create a new page at an appropriate location. As the +In the page tree editor of **django-CMS**, we create a new page at an appropriate node. 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. @@ -82,10 +93,10 @@ Change into the **Advanced Settings** of the CMS page, which shall act as the ca 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**. +followed by a Row and then by a Column plugin. As the child of this column, choose the *Catalog List +View* plugin from section **Shop**. -Finally we publish the page, it probably doesn't contain any products yet. To fill it we first have +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 @@ -100,7 +111,7 @@ Assign Products to CMS Pages 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`. +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: @@ -129,15 +140,15 @@ product might be declared as: objects = BaseProductManager() -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 +An important part of this product model is the field ``cms_pages = ManyToManyField(...)``. +Mapping a relationship between CMS pages and products, 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. +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``. +automatically for many-to-many fields by the Django framework. Instead, we must refer to the +mapping table :class:`shop.models.defaults.mapping.ProductPage` explicitely, using the ``though`` +parameter, when declaring the field ``cms_pages``. .. _reference/product-summary-serializer: @@ -147,12 +158,13 @@ Product Summary Serializer 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. +all products in our catalog. Since this catalog list can be rendered either by the server using the +class :class:`shop.rest.renderers.CMSPageRenderer`, or by the client using the AngularJS directive +``shop-catalog-list``, 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 view class +:class:`shop.views.catalog.ProductListView`, whenever we switch from the server-side rendered +catalog list into infinite scroll mode, which for technical reasons can only be rendered by the +client. 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 @@ -179,21 +191,22 @@ into a HTML snippet, which here we name ``media``. Using method 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]_, + [#app_label]_ [#product-model-name]_ [#field-name]_, otherwise +* look for a template named :samp:`{app_label}/products/catalog-product-{field-name}.html`` + [#app_label]_ [#field-name]_, otherwise * use the template ``shop/product/catalog-product-media.html``. -.. [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 +.. [#app_label] :samp:`{app_label}` is the app label of the project in lowercase. +.. [#product-model-name] :samp:`{product-model-name}` is the class name of the product model in lowercase. +.. [#field-name] :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. .. 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 + Otherwise we would have to refer to the original, often much heavier image and thumbnail it on the fly, which would be pretty inefficient. To test if that serializer works properly, we can examine the raw content of the declared fields by @@ -203,14 +216,16 @@ representation of the context as JSON. .. _thumb: https://easy-thumbnails.readthedocs.io/en/latest/usage/#thumbnail-tag +.. _reference/customized-product-serializer: + Customizing the Product Summary Serializer .......................................... -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: +In case we need serialized content from other fields of our product model, let's add them to a +customized product serializer class: For this we use the `serializer fields`_ from the Django's +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 @@ -262,7 +277,7 @@ A product detail view is rendered by the :class:`shop.views.catalog.ProductRetri *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. +bigger effort, rather than handcrafting 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: @@ -294,8 +309,8 @@ a video, or whatever else is available from the **django-CMS** plugin system. .. 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. + other than the product's code, its name and price. So all relevant information must be added to + the product's detail view using the **django-CMS** structure editor. Customizing the Product Detail Serializer @@ -330,7 +345,7 @@ detail view, there is an extra endpoint ending in ``.../add-to-cart``. Its URL p :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. -Each product's detail page shall implement an element containing the AngularJS directive +Each product's detail page shall implement a HTML 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. @@ -345,6 +360,54 @@ For products with a **django-CMS** placeholder field, the merchant can also use page. Products of type "Commodity" make use of this plugin. +Products with variations +------------------------ + +In some situations, it might be neccessary to use a custom endpoint for adding a product to the +cart. This for instance is required, when the product to be added contains variations. We then +rewrite our ``CatalogListApp`` to use this url pattern: + +.. code-block:: python + + class CatalogListApp(CatalogListCMSApp): + def get_urls(self, page=None, language=None, **kwargs): + ... + return [ + ... + url(r'^(?P[\w-]+)/add-product-to-cart', AddToCartView.as_view( + serializer_class=AddProductWithVariationsSerializer, + )), + ... + ] + +We then create a special serializer for that view: + +.. code-block:: python + + from shop.models.cart import CartModel + from shop.serializers.defaults.catalog import AddToCartSerializer + + class AddProductWithVariationsSerializer(AddToCartSerializer): + def get_instance(self, context, data, extra_args): + product = context['product'] + cart = CartModel.objects.get_from_request(context['request']) + variant = product.get_product_variant(product_code=data['product_code']) + is_in_cart = bool(product.is_in_cart(cart, product_code=variant.product_code)) + instance = { + 'product': product.id, + 'product_code': variant.product_code, + 'unit_price': variant.unit_price, + 'is_in_cart': is_in_cart, + } + return instance + +This serializer is adopted to a product with variations. Each variation of the product provides +its own product code and a price. Additionally we want to know, whether the same variation of +that product is already in the cart (increasing the quantity), or if it has to be considered as +different product (adding a new one to the cart). For indicating this state, the serializer returns +a flag, named ``is_in_cart``. + + Admin Integration ================= diff --git a/docs/reference/search.rst b/docs/reference/search.rst index c2bb00f9e..21410c069 100644 --- a/docs/reference/search.rst +++ b/docs/reference/search.rst @@ -34,14 +34,12 @@ Configuration Download and install the latest version of the Elasticsearch binary. During development, all tests have been performed with version 7.5. After unzipping the file, start Elasticsearch in daemon mode: -.. code-block:: shell - - ./path/to/elasticsearch-version/bin/elasticsearch -d +:samp:` ./path/to/elasticsearch-{version}/bin/elasticsearch -d ` -Check if the server answers on HTTP requests. Pointing a browser onto port http://localhost:9200/ -should return something similar to this: +Check if the server answers on HTTP requests. Pointing a browser on +`http://localhost:9200/ `_ should return something similar to this: -.. code-block:: shell +.. code-block:: $ curl http://localhost:9200/ { @@ -53,8 +51,11 @@ should return something similar to this: }, } -Install ``elasticsearch-dsl`` and ``django-elasticsearch-dsl`` using the ``pip`` command or an -alternative Python package manager. +Install ``elasticsearch-dsl`` and ``django-elasticsearch-dsl`` using + +.. code-block:: shell + + pipenv install django-elasticsearch-dsl In ``settings.py``, check that ``'django_elasticsearch_dsl'`` has been added to ``INSTALLED_APPS``. Configure the connection to the Elasticsearch database: @@ -78,11 +79,11 @@ Therefore it is quite important to spot the fields in the product models, which information customers might search for. Elasticsearch uses the term ``Document`` to describe a searchable entity. In **django-SHOP**, we -can define one or more product models, each declaring their own fields. Since in our shop we want -to search over all products, regardless of their specific model definition, we need a mapping from -those fields onto the representation used to create the reverse index. For this purpose, -**django-SHOP** is shipped with a generic document class named ``ProductDocument``. It contains -three index fields: ``product_name``, ``product_code`` and ``body``. +can define one or more product models, each declaring their own fields. Since in our e-commerce +site, we want to search over all products, regardless of their specific model definition, we need a +mapping from those fields onto the representation used to create the reverse index. For this +purpose, **django-SHOP** is shipped with a generic document class named ``ProductDocument``. It +contains three index fields: ``product_name``, ``product_code`` and ``body``. Product Name @@ -91,8 +92,9 @@ Product Name The product's name often is declared as a ``CharField`` in our product's model. Depending on the nature of the product, it could also be declared for different languages. Using django-parler's ``TranslatableField``, the product name then is stored in a Django model related to the product -model. We also want to ensure, that this name is indexed only for a specific language. This -information is stored inside the ``Document`` field ``product_name``. +model. We also want to ensure, that this name is indexed only for a specific language. + +This information is stored inside the ``Document`` field: ``product_name``. Product Code @@ -101,20 +103,21 @@ Product Code 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. +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 ---------- Depending on our product's model declaration, we can have many additional fields containing -information, which may be relevant to be searched for. Therefore the merchant must declare a Django +information, which may be relevant for search. Therefore the merchant must declare a Django template for each product type. This template then is used to render the content of those fields as -plain text. This text is never seen by the customer, but rather used to feed our full text search +plain text. This text is never seen by humans, but rather is used to feed our full text search engine when building the reverse index. First Elasticsearch strips all HTML tags from that text. In the second step, this text is tokenized and stemmed by Elasticsearch analyzers. In -**django-SHOP** we can specify one analyzer for each language. +**django-SHOP** we shall specify one analyzer for each natural language. Example @@ -129,29 +132,29 @@ Say, we have a product using this simplified model representation: class Author(models.Model): name = models.CharField( - _("Author Name"), + "Author Name", max_length=255, ) class Editor(models.Model): name = models.CharField( - _("Editor"), + "Editor", max_length=255, ) class Book(BaseProduct): product_name = models.CharField( - _("Book Title"), + "Book Title", max_length=255, ) product_code = models.CharField( - _("Product code"), + "Product code", max_length=255, ) caption = HTMLField( - help_text=_("Short description"), + help_text="Short description", ) authors = models.ManyToManyField(Author) @@ -162,7 +165,7 @@ Say, we have a product using this simplified model representation: ) 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 +``product_code``. Usually we also want other fields beeing indexed, if they 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: @@ -176,12 +179,12 @@ This template file then shall contain a structure similar to this: {{ product.editor.name }} 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 +The rendered content is passed directly to the search engine and serves to feed the Elasticsearch +database with a reverse index. Before importing, 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`. +as "or", "the", "is", "and" etc. This list of words is named "The Reverse Index" and is then stored +in the ``body`` field inside entities of type :class:`shop.search.documents.ProductDocument`. .. note:: If the above template file can not be found, **django-SHOP** falls back onto @@ -193,11 +196,12 @@ the :class:`shop.search.documents.ProductDocument`. Populate the Database --------------------- -To build the index in Elasticsearch, invoke: +To build the index in Elasticsearch, invoke ``./manage.py search_index --rebuild``. If German and +English are configured, then the output may look like: .. code-block:: shell - ./manage.py search_index --rebuild + $ ./manage.py search_index --rebuild Deleting index 'awesome_bookstore.de.products' Deleting index 'awesome_bookstore.en.products' Creating index 'awesome_bookstore.de.products' @@ -207,16 +211,25 @@ To build the index in Elasticsearch, invoke: 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. +a browser onto: + +`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. + + +.. _reference/search-view: +Search View +=========== -Showing Search Results -====================== +In order to show search results, we need a database filled with a reverse index. This is what we +have done in the previous section. This populated search database can be used for two kind of +purposes: -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. +Generic search over all products and as an additional "search-as-you-type" filter, while rendering +the catalog's list view. Search Apphook @@ -228,23 +241,23 @@ page we choose "*Search Results*" or something similar meaningful. Since we want 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. +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**, we 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 + The apphook *Catalog Search* must be registered by the merchant implementation. It's just as simple as registering :class:`shop.cms_apphooks.CatalogSearchApp` using the - :method:`menus.menu_pool.menu_pool.apphook_pool.register`. + :meth:`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: +As leaf child of that column, choose the **Search Results** plugin from section **Shop**. This +CMS 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. @@ -269,22 +282,26 @@ Adopting the Templates 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``. +be replaced or extended by a customized template in the merchant implementation, namely +:samp:`{app_label}/templates/{app_label}/search/results.html` [#app_label]_. In our bookstore +example this template would be named +``awesome_bookstore/templates/awesome_bookstore/search/results.html``. 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: +each product type can provide its own way how to be rendered. That snippet template is looked up +following these rules: * :samp:`{app_label}/templates/{app_label}/products/search-{product-model-name}-media.html` -* :samp:`{app_label}/templates/{app_label}/products/search-product-media.html` + [#app_label]_, [#product-model-name]_ +* :samp:`{app_label}/templates/{app_label}/products/search-product-media.html` [#app_label]_ * :samp:`shop/templates/shop/products/search-product-media.html` This means that the template to render the products's detail view is selected automatically depending on its product type. -.. [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. +.. [#app_label] *app_label* is the app label of the project in lowercase. +.. [#product-model-name] *product-model-name* is the class name of the product model in lowercase. .. _reference/search-autocompletion-catalog: @@ -298,9 +315,9 @@ catalog's list view. Here, loading a new page which uses a layout able to render 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. +the list of items in the default 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 want to pick out the correct one by entering some search terms. 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. @@ -308,6 +325,7 @@ This could be implemented as: .. code-block:: python :caption: awesome_bookstore/cms_apps.py + :emphasize-lines: 10 from cms.apphook_pool import apphook_pool from shop.cms_apphooks import CatalogListCMSApp @@ -318,18 +336,21 @@ This could be implemented as: from shop.search.mixins import ProductSearchViewMixin from shop.views.catalog import AddToCartView, ProductListView, ProductRetrieveView - ProductSearchListView = type('SearchView', (ProductSearchViewMixin, ProductListView), {}) + bases = (ProductSearchViewMixin, ProductListView) + ProductSearchListView = type('SearchView', bases, {}) + filter_backends = [CMSPagesFilterBackend] + filter_backends.extend(api_settings.DEFAULT_FILTER_BACKENDS) 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), + filter_backends=filter_backends, )), ] apphook_pool.register(CatalogListApp) -In this apphook, we created the class ``ProductSearchListView`` on the fly. It actually just adds +In this apphook, we create 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. @@ -338,13 +359,13 @@ the Elasticsearch database if we filter the product against a given query reques 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 +To facilitate the placement of the search input field, **django-SHOP** ships with the reusable +AngularJS directive named ``shopProductSearch``. It is declared inside the module ``shop/js/search-form.js``. A HTML snipped with a submission form using this directive can be found in the shop's templates -folder at ``shop/navbar/search-form.html``. If you override it, make sure that the form element -uses the directive ``shop-product-search`` as attribute: +folder at ``shop/navbar/search-form.html``. If you override it, make sure that the ```` +tag uses the directive ``shop-product-search`` as attribute: .. code-block:: django diff --git a/docs/reference/serializers.rst b/docs/reference/serializers.rst index 3d7ee7df3..63e84e588 100644 --- a/docs/reference/serializers.rst +++ b/docs/reference/serializers.rst @@ -97,16 +97,18 @@ template is constructed using the following rules: #. Search for a subfolder named ``products``. #. Search for a template named "*label*-*product_type*-*postfix*.html". These three subfieds are determined using the following rule: + - *label*: the component of the shop, for instance ``catalog``, ``cart``, ``order``. - *product_type*: the class name in lower case of the product's Django model, for instance ``smartcard``, ``smartphone`` or if no such template can be found, just ``product``. - *postfix*: This is an arbitrary name passed in by the rendering function. As in the example above, this is the string ``media``. -.. note:: It might seem "un-restful" to render HTML snippets by a REST serializer and deliver them - via JSON to the client. However, we somehow must re-size the images assigned to our product to - fit into the layout of our list view. The easiest way to do this in a configurable manner is - to use the easythumbnails_ library and its templatetag ``{% thumbnail product.sample_image ... %}``. +.. note:: It might seem *un*-RESTful to render HTML snippets by a serializer and deliver them via + JSON to the client. However, we somehow must re-size the images assigned to our product to + fit into the layout of our list view. The easiest way to do this in a configurable manner is + to use the easy-thumbnails_ library and its templatetag + ``{% thumbnail product.sample_image ... %}``. The template to render the media snippet could look like: @@ -160,7 +162,7 @@ of the shop framework, but must be created and added to the project as the Catalog List View -~~~~~~~~~~~~~~~~~ +................. The urlpattern matching the regular expression ``^$`` routes onto the catalog list view class :class:`shop.views.catalog.CMSPageProductListView` passing in a special serializer class, for @@ -182,7 +184,7 @@ for restricting selected products on the current catalog list view. Catalog Detail View -~~~~~~~~~~~~~~~~~~~ +................... The urlpattern matching the regular expression ``^(?P[\w-]+)$`` routes onto the class :class:`shop.views.catalog.ProductRetrieveView` passing in a special serializer class, @@ -198,7 +200,7 @@ given ``serializer_class`` it can accept these fields: Add Product to Cart -~~~~~~~~~~~~~~~~~~~ +................... The product detail view requires another serializer, the so called ``AddToCartSerializer``. This serializer is responsible for controlling the number of items being added to the cart and gives @@ -293,3 +295,5 @@ business logic from the underlying request-response-cycle. .. _directives: https://docs.angularjs.org/guide/directive .. _apphook: http://django-cms.readthedocs.org/en/stable/introduction/apphooks.html +.. _easy-thumbnails: https://easy-thumbnails.readthedocs.io/en/stable/ +.. _Pillow: https://pillow.readthedocs.io/en/stable/