Skip to content

Commit

Permalink
Warn about possible issues when running 'make sandbox'
Browse files Browse the repository at this point in the history
  • Loading branch information
codeinthehole committed Sep 19, 2014
1 parent 39f57e4 commit 6342e56
Show file tree
Hide file tree
Showing 23 changed files with 1,812 additions and 0 deletions.
82 changes: 82 additions & 0 deletions docs/source/customising/apps.rst
@@ -0,0 +1,82 @@
================
Customising apps
================

The key mechanism which allows Oscar to be customised is dynamically loading of
classes; this allows any core class to be replaced and extended.

Dynamic class-loading requires replacing (or "forking") an Oscar app from
``INSTALLED_APPS`` with a custom one that has the same app label. Afterwards,
you should generally be able to override any class/model/view by just dropping
it in the right place and giving it the same name.

Forking a core app
------------------

If this is the first time you're forking an Oscar app, create
a root package under which all your forked apps will live:

.. code-block:: bash
$ mkdir yourproject
$ touch yourproject/__init__.py
Now use the :ref:`oscar_fork_app` management command to create a local version of a
core app:

.. code-block:: bash
$ ./manage.py oscar_fork_app order yourproject
Creating folder yourproject/order
Creating __init__.py and admin.py
Creating models.py and copying migrations from [...] to [...]
This will create a new app package ``yourproject/order`` with ``models.py``
and a copy of the migrations folder. The ``models.py`` will be initialised to
import all models from the corresponding Oscar ``models.py`` module:

.. code-block:: python
# yourproject/order/models.py
from oscar.apps.order.models import *
Finally, tell Django to use your new custom app:

.. code-block:: python
# settings.py
from oscar import get_core_apps
INSTALLED_APPS = [
... # all non-oscar apps
] + get_core_apps(['yourproject.order'])
The ``get_core_apps`` function returns a list of Oscar core apps. If you supply a
list of additional apps, they will be used to replace the Oscar core apps.
In the above example, ``yourproject.order`` will be returned instead of

Customising a core class
------------------------

You can now override every class (that is
:doc:`dynamically loaded </customisation/class_loading_explained>`, which is
almost every class) in the app you've replaced. That means forms,
views, strategies, etc. All you need to do is give it the same name
and place it in a module with the same name.

Suppose you want to alter the way order numbers are generated. By default,
the class ``oscar.apps.order.utils.OrderNumberGenerator`` is used. So just
create a class within your ``order`` app which
matches the module path from oscar: ``order.utils.OrderNumberGenerator``. This
could subclass the class from Oscar or not::

# yourproject/order/utils.py

from oscar.apps.order.utils import OrderNumberGenerator as CoreOrderNumberGenerator

class OrderNumberGenerator(CoreOrderNumberGenerator):

def order_number(self, basket=None):
num = super(OrderNumberGenerator, self).order_number(basket)
return "SHOP-%s" % num
82 changes: 82 additions & 0 deletions docs/source/customising/class_loading_explained.rst
@@ -0,0 +1,82 @@
===============================
Dynamic class loading explained
===============================

Dynamic class loading is the foundation for making Oscar
customisable. It is hence worth understanding how it works, because most
customisations depend on it.

It is achieved by :meth:`oscar.core.loading.get_classes` and its
single-class cousin :meth:`~oscar.core.loading.get_class`. Wherever feasible,
Oscar's codebase uses ``get_classes`` instead of a regular import statement.
That is, instead of:

.. code-block:: python
from oscar.apps.shipping.repository import Repository
Oscar uses something like:

.. code-block:: python
from oscar.core.loading import get_class
Repository = get_class('shipping.repository', 'Repository')
This is done for almost all classes: views, models, Application
instances, etc. Every class imported by ``get_class`` or ``get_classes`` can be
overridden and customised.

Why?
----

This structure enables a project to create a local ``shipping.repository``
module, and optionally subclass the class from
``oscar.app.shipping.repository``. When Oscar tries to load the
``Repository`` class, it will load the one from your local project.

This way, most classes can be overridden with minimal duplication, as only
the to-be-changed classes have to be altered. They can optionally inherit from
Oscar's implementation, which often amounts to little more than a few lines of
custom code for changes to core behaviour.

Seen on a bigger scale, this structures enables Oscar to ship with classes with
minimal assumptions about the domain, and make it easy to modify behaviour as
needed.

How it works
------------

The ``get_class`` function iterates over ``INSTALLED_APPS`` looking for a
matching app label and, if one is found, will attempt to load the custom class
from the appropriate app module. If the app isn't overridden or the class can't
be found, it will fall back to the default Oscar class.

In practice
-----------

For ``get_class`` to pick up the customised class, the Oscar apps need to be
forked. It is usually enough to call ``oscar_fork_app`` and replace the app in
``INSTALLED_APPS``.

Using ``get_class`` in your own code
------------------------------------

Generally, there is no need for ``get_class`` in your own code as the location
of the module for the class is known. Some Oscar developers nonetheless
use ``get_class`` when importing classes from Oscar. This means that if someday
the class is overridden, it will not require code changes. Care should be taken
when doing this, as this is a tricky trade-off between maintainability and
added complexity.

Testing
-------

You can test whether your overriding worked by trying to get a class from your
module:

.. code-block:: python
>>> from oscar.core.loading import get_class
>>> get_class('shipping.repository', 'Repository')
yourproject.shipping.repository.Repository # it worked!
59 changes: 59 additions & 0 deletions docs/source/customising/index.rst
@@ -0,0 +1,59 @@
=================
Customising Oscar
=================

.. toctree::
:maxdepth: 1
:hidden:

Dynamic loading <class_loading_explained>
Apps <apps>
Models <models>
URLs <urls>
Views <views>
Templates <templates>

Oscar provides e-commerce functionality that can be extended and customised to
suit your requirements. To achieve this, several techniques are employed:

Dynamic loading
---------------

Oscar loads classes dynamically (rather than using explicit import statements).
For models, Oscar leverages Django's ``get_model`` function. For classes, Oscar
provides a custom loading function:

- :doc:`/customising/class_loading_explained`
- :doc:`/customising/apps`

Core models are abstract
------------------------

Oscar's apps split models into abstract and concrete versions - these are found
in modules called ``abstract_models.py`` and ``models.py`` respectively.
Oscar's philosophy is to keep the core models lean, where all the fields are
meaningful within any e-commerce domain. Oscar then provides a mechanism for
subclassing these models within your project so domain-specific fields can be
added.

- :doc:`/customising/models`

URLs and permissions are handled by ``Application`` instances
-------------------------------------------------------------

The :class:`oscar.core.application.Application` class handles mapping URLs
to views and permissions at an per-app level. This makes Oscar's apps more
modular, and makes it easy to customise this mapping as they can be overridden
just like any other class in Oscar.

- :doc:`/customising/models`
- :doc:`/customising/views`

Templates can be overridden
---------------------------

This is a common technique relying on the fact that the template loader can be
configured to look in your project first for templates, before it uses the defaults
from Oscar.

- :doc:`/customising/templates`
89 changes: 89 additions & 0 deletions docs/source/customising/models.rst
@@ -0,0 +1,89 @@
==================
Customising models
==================

Oscar core apps come with an abstract and concrete version.

Adding a field to a model
-------------------------

Suppose you want to add a ``video_url`` field to the core product model. This
means that you want your application to use a subclass of
:class:`oscar.apps.catalogue.abstract_models.AbstractProduct` which has an
additional field.

If you haven't already, the first step is to :doc:`fork the catalogue app
</customising/apps>`. Now create a custom ``Product`` model:

.. code-block:: python
# yourproject/catalogue/models.py
from django.db import models
from oscar.apps.catalogue.abstract_models import AbstractProduct
class Product(AbstractProduct):
video_url = models.URLField()
from oscar.apps.catalogue.models import *
.. tip::

Using ``from ... import *`` is strange isn't it? Yes it is, but it needs to
be done at the bottom of the module due to the way Django registers models.
The order that model classes are imported makes a difference with only the
first one for a given class name being registered.

This should now be sufficient for Oscar to start using your custom model
whenever it looks for a product.

Migrations for custom models
----------------------------

The final step is to create a migration for this new field. When forking an
app, it's recommended that a copy of the Oscar migrations folder is created
in the new app (this is what the :ref:`oscar_fork_app` management command does).
With this in place, a new catalogue migration can be created in the standard
way:

.. code-block:: bash
$ ./manage.py schemamigration catalogue --auto
.. note::

It is possible to simply create a new catalogue migration without having
copied Oscar's migrations into your custom app (using
``./manage.py schemamigration catalogue --initial``) but this isn't recommended as any
dependencies between migrations will need to be applied manually (by adding
a ``depends_on`` attribute to the migration class).

Finally, running:

.. code-block:: bash
$ ./manage.py migrate catalogue
will update your database schema and create a new column in the product table.

Trouble-shooting
----------------

Model customisations are not picked up
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If your custom model is not being loaded as you expect, it is likely that the
core Oscar model is being imported ahead of your custom one. Django's model
registration only respects the first declaration it encounters.

In your overriding ``models.py``, ensure that you import Oscar's models *after*
your custom ones have been defined.

If that doesn't help, you have an import
from ``oscar.apps.*.models`` somewhere that is being executed before your models
are parsed. One trick for finding that import: put ``assert False`` in the relevant
Oscar's models.py, and the stack trace will show you the importing module.

If other modules need to import your models, then import from your local module,
not from Oscar directly.

0 comments on commit 6342e56

Please sign in to comment.