.. image:: https://raw.githubusercontent.com/ababic/django-cogwheels/master/docs/source/_static/django-cogwheels-logo.png
    :alt: Django Cogwheels

.. image:: https://travis-ci.com/ababic/django-cogwheels.svg?branch=master
    :alt: Build Status
    :target: https://travis-ci.com/ababic/django-cogwheels

.. image:: https://img.shields.io/pypi/v/django-cogwheels.svg
    :alt: PyPi Version
    :target: https://pypi.python.org/pypi/django-cogwheels

.. image:: https://codecov.io/gh/ababic/django-cogwheels/branch/master/graph/badge.svg
    :alt: Code coverage
    :target: https://codecov.io/gh/ababic/django-cogwheels

=========================
What is django-cogwheels?
=========================

The aim of ``django-cogwheels`` is to create a standardised, well-tested approach for allowing users of an app to override default behaviour, by overriding things in their project's Django settings.

There are other apps out there that try to solve this problem, but it was important for me to create a solution that would cater well for deprecation of settings, as this is something I find myself having to do regularly in apps I maintain. It was also important for me to create something that:

- Is super easy to set up
- Properly accounts for different audiences (the 'app developer' and 'app user')
- Will work as well for 100 apps setting as it will for 5
- Only makes things complicated when absolutely necessary

**Give your users the flexibility they deserve, and allow them to:**

- Override basic python type values such as: strings, integers, booleans, decimals and floats.
- Override structured python type values such as: lists, tuples and dictionaries.
- Use custom Django models in place of the ones you provide.
- Use custom python classes, objects or entire modules in place of the ones you provide.

Goodness for app developers!
============================

- A stable, documented, standardised approach for implementing overridable app-specific settings.
- Clearly define and communicate the deprecation status of app settings, giving you the flexibility to rename, replace or flag settings for removal over your project's lifecycle. User overrides defined using old setting names remain available to you, allowing you to continue to support them during the deprecation period.
- Helpful, consistent error messages when default values provided for models, modules or other overridable object settings are invalid.
- Cached imports for speedy access to models, modules and other importable python objects.
- Plays nicely with Django's test framework (subscribes to Django's ``setting_changed`` signal, so that cached values are cleared when ``override_settings`` is used).


Goodness for app users!
=======================

- Helpful, consistent error messages when their Model, Class, method or module override settings are incorrectly formatted, or cannot be imported.
- Helpful, consistent deprecation warnings when they are overriding a setting that has been renamed, replaced or flagged for removal.


Quick start guide
=================

1.  Install the package using pip: 

    .. code-block:: console

        pip install django-cogwheels

2.  ``cd`` into your project's root app directory:
    
    .. code-block:: console

        cd your-django-project/yourproject/

3.  Create a ``conf`` app using the cogwheels Django app template:

    .. code-block:: console

        django-admin.py startapp conf --template=https://github.com/ababic/cogwheels-conf-app/zipball/master

4.  Open up ``yourproject/conf/defaults.py``, and add your setting values like so:

    .. code-block:: python

        # You can add settings for any type of value
        MAX_ITEMS_PER_ORDER = 5

        # For settings that refer to models, use a string in the format 'app_name.Model'
        ORDER_ITEM_MODEL = 'yourproject.SimpleOrderItem'

        # For settings that refer to a python module, use an 'import path' string, like so:
        DISCOUNTS_BACKEND = 'yourproject.discount_backends.simple'

        # For settings that refer to classes, methods, or other python objects from a
        # python module, use an 'object import path' string, like so:
        ORDER_FORM_CLASS = 'yourproject.forms.OrderForm'

        
5.  To use setting values in your app, simply import the settings helper, and reference the relevant setting as an attribute, like this:

    .. code-block:: console

        >>> from yourproject.conf import settings

        >>> settings.MAX_ITEMS_PER_ORDER
        5 

        >>> settings.ORDER_ITEM_MODEL
        'yourproject.SimpleOrderItem'

        >>> settings.DISCOUNTS_BACKEND
        'yourproject.discount_backends.simple'

        >>> settings.ORDER_FORM_CLASS
        'yourproject.forms.OrderForm'


6.  For settings that refer to Django models, you can use the settings helper's special ``models`` attribute to access model classes themselves, rather than just the string value. For example: 

    .. code-block:: console

        >>> from yourproject.conf import settings

        >>> model = settings.models.ORDER_ITEM_MODEL
        yourproject.models.SimpleOrderItem

        >>> obj = model(id=1, product='test product', quantity=15)
        >>> obj.save()

        >>> print(model.objects.all())
        <QuerySet [<SimpleOrderItem: SimpleOrderItem object (1)>]>

    Behind the scenes, Django's ``django.apps.apps.get_model()`` method is called, and the result is cached so that repeat requests for the same model are handled quickly and efficiently.


7.  For settings that refer to python modules, you can use the settings helper's special ``modules`` attribute to access the modules themselves, instead of an import path string: 
    
    .. code-block:: console

        >>> from yourproject.conf import settings

        >>> module = settings.modules.DISCOUNTS_BACKEND
        <module 'yourproject.discount_backends.simple' from '/system/path/to/your-django-project/yourproject/discount_backends/simple.py'>


    Behind the scenes, python's ``importlib.import_module()`` method is called, and the result is cached so that repeat requests for same module are handled quickly and efficiently.


8.  For settings that refer to classes, functions, or other importable python objects, you can use the settings helper's special ``objects`` attribute to access those objects, instead of an import path string: 

    .. code-block:: console

        >>> from yourproject.conf import settings

        >>> form_class = settings.objects.ORDER_FORM_CLASS
        yourproject.formsOrderForm

        >>> form = form_class(data={})
        >>> form.is_valid()
        False

    Behind the scenes, python's ``importlib.import_module()`` method is called, and the result is cached so that repeat requests for same object are handled quickly and efficiently.


9.  Users of your app can now override any of the default values by adding alternative values to their project's Django settings module. For example: 

    .. code-block:: python

        # userproject/settings/base.py

        YOURAPP_MAX_ITEMS_PER_ORDER = 2

        YOURAPP_ORDER_ITEM_MODEL = 'userproject_orders.CustomOrderItem'

        YOURAPP_DISCOUNTS_BACKEND = 'userproject.discounts.custom_discount_backend'

        YOURAPP_ORDER_FORM_CLASS = 'userproject.orders.forms.CustomOrderForm'

10. You may noticed that the above variable names are all prefixed with ``YOURAPP_``. This prefix will differ for your app, depending on the package name. 

    This 'namespacing' of settings is important. Not only does it helps users of your app to remember which app their override settings are for, but it also helps to prevent setting name clashes between apps.

    You can find out what the prefix is for your app by doing:
    
    .. code-block:: console

        >>> from yourproject.conf import settings
        >>> settings.get_prefix()
        'YOURPROJECT_'

    You can change this prefix to whatever you like by setting a ``prefix`` attribute on your settings helper class, like so:

    .. code-block:: python

        # yourapp/conf/settings.py
        
        class MyAppSettingsHelper(BaseAppSettingsHelper):
            prefix = 'CUSTOM'  # No need for a trailing underscore here

    .. code-block:: console

        >>> from yourproject.conf import settings
        >>> settings.get_prefix()
        'CUSTOM_'


Frequently asked questions
==========================


1. Are there any example implmentations of ``django-cogwheels`` that I can look at?
-----------------------------------------------------------------------------------

Sure thing.

``wagtailmenus`` uses cogwheels to manage it's app settings. See:
https://github.com/rkhleics/wagtailmenus/tree/master/wagtailmenus

You might also want to check out the ``tests`` app within cogwheels itself, which includes lots of examples:
https://github.com/ababic/django-cogwheels/tree/master/cogwheels/tests


2. Do ``defaults.py`` and ``settings.py`` have to live in a ``conf`` app?
-------------------------------------------------------------------------

No. This is just a recommendation. Everyone has their own preferences for how they structure their projects, and that's all well and good. So long as you keep ``defaults.py`` and ``settings.py`` in the same directory, things should work just fine out of the box. 

If you want ``defaults.py`` and ``settings.py`` to live in separate places, ``cogwheels`` supports that too. But, you'll have to set the ``defaults_path`` attribute on your settings helper class, so that it knows where to find the default values. For example:

.. code-block:: python

        # yourapp/some_directory/settings.py

        class MyAppSettingsHelper(BaseAppSettingsHelper):
            defaults_path = 'yourapp.some_other_place.defaults'


3. You mentioned support for setting deprecation. How does that work?
---------------------------------------------------------------------

More complete documentation will be added soon. In the meantime, if you're curious about what deprecation definitions look like, you may want to check out the ``tests`` app's setting helper definition: https://github.com/ababic/django-cogwheels/blob/master/cogwheels/tests/conf/settings.py 
        

4. How do specify validation rules for certain settings?
--------------------------------------------------------

The only validation that ``cogwheels`` performs is on setting values that are supposed to reference Django models and other importables, and this validation is only triggered when you use ``settings.models.SETTING_NAME``, ``settings.modules.SETTING_NAME`` or ``settings.objects.SETTING_NAME`` in your code to import and access the object. 

**There's currently no way to configure ``cogwheels`` to apply validation to other setting values.**

I do intend to support such a thing future versions, but I can't make any promises as to when.

If this puts you off, keep in mind that it's not in anybody's interest for developers to purposefully use inappropriate override values for settings. So long as your documentation explains the rules/boundaries for expected values well enough, issues should be very rare.


5. What's that last line in ``settings.py`` all about?
------------------------------------------------------

Ahh, yes. The ``sys.modules[__name__] = MyAppSettingsHelper()`` bit. I understand that some developers might think this dirty/hacky/unpythonic/whatever. I have to admit, I was unsure about it for a while, too.

I'll agree that it is somewhat 'uncommon' to see this code in use. Perhaps because it's not particularly useful in a lot situations, or perhaps because using such features incorrectly can break things in strange, hard-to-debug ways. But, support for this hack is not going anywhere, and in `cogwheels` case, it's useful, as it removes the need to instantiate things in ``__init__.py`` (which I dislike for a number of reasons). 

If you're still not reassured, perhaps Guido van Rossum (Founder of Python) can put your mind at rest?
https://mail.python.org/pipermail/python-ideas/2012-May/014969.html
        

Compatibility
=============

The current version is tested for compatiblily with the following: 

- Django versions 1.11 to 2.1
- Python versions 3.4 to 3.7
