============================
How to create a custom range
============================

Oscar ships with a range model that represents a set of products from your
catalogue.  Using the dashboard, this can be configured to be:

1.  The whole catalogue
2.  A subset of products selected by ID/SKU (CSV uploads can be used to do this)
3.  A subset of product categories

Often though, a shop may need merchant-specific ranges such as:

*  All products subject to reduced-rate VAT
*  All books by a Welsh author
*  DVDs that have an exclamation mark in the title

These are contrived but you get the picture.

Custom range interface
----------------------

A custom range must:

* have a ``name`` attribute
* have a ``contains_product`` method that takes a product instance and return a
  boolean
* have a ``num_products`` method that returns the number of products in the
  range or ``None`` if such a query would be too expensive.
* have an ``all_products`` method that returns a queryset of all products in the
  range.

Example::

    class ExclamatoryProducts(object):
        name = "Products including a '!'"

        def contains_product(self, product):
            return "!" in product.title

        def num_products(self):
            return self.all_products().count()

        def all_products(self):
            return Product.objects.filter(title__icontains="!")

Create range instance
---------------------

To make this range available to be used in offers, do the following::

    from oscar.apps.offer.custom import create_range

    create_range(ExclamatoryProducts)

Now you should see this range in the dashboard for ranges and offers.  Custom
ranges are not editable in the dashboard but can be deleted.

Deploying custom ranges
-----------------------

To avoid manual steps in each of your test/stage/production environments, use
Django's `data migrations`_ to create ranges.

.. _`data migrations`: https://docs.djangoproject.com/en/stable/topics/migrations/#data-migrations
