Quickstart

Glossary

Preference

An object that deals with preference logic, such as serialization, deserialization, form display, default values, etc. After being defined, preferences can be tied via registries to one ore many preference models, which will deal with database persistence.

PreferenceModel

A model that store preferences values in database. A preference model may be tied to a particular model instance, which is the case for UserPreferenceModel, or concern the whole project, as GlobalPreferenceModel.

Create and register your own preferences

In this example, we assume you are building a blog. Some preferences will apply to your whole project, while others will belong to specific users.

First, create a dynamic_preferences_registry.py file within one of your project app. The app must be listed in settings.INSTALLED_APPS.

Let’s declare a few preferences in this file:

# blog/dynamic_preferences_registry.py

from dynamic_preferences.types import BooleanPreference, StringPreference
from dynamic_preferences.preferences import Section
from dynamic_preferences.registries import global_preferences_registry
from dynamic_preferences.users.registries import user_preferences_registry

# we create some section objects to link related preferences together

general = Section('general')
discussion = Section('discussion')

# We start with a global preference
@global_preferences_registry.register
class SiteTitle(StringPreference):
    section = general
    name = 'title'
    default = 'My site'
    required = False

@global_preferences_registry.register
class MaintenanceMode(BooleanPreference):
    name = 'maintenance_mode'
    default = False

# now we declare a per-user preference
@user_preferences_registry.register
class CommentNotificationsEnabled(BooleanPreference):
    """Do you want to be notified on comment publication ?"""
    section = discussion
    name = 'comment_notifications_enabled'
    default = True

The section attribute is a convenient way to keep your preferences in different… well… sections. While you can totally forget this attribute, it is used in various places like admin or forms to filter and separate preferences. You’ll probably find it useful if you have many different preferences. The name attribute is a unique identifier for your preference. However, You can share the same name for various preferences if you use different sections.

Important

preferences names and sections names (if you use them) are persisted in database and should be considered as primary keys. If, for some reason, you want to update a preference or section name and keep already persisted preferences sync, you’ll have to write a data migration.

Retrieve and update preferences

You can get and update preferences via a Manager, a dictionary-like object. The logic is almost exactly the same for global preferences and per-instance preferences.

from dynamic_preferences.registries import global_preferences_registry

# We instantiate a manager for our global preferences
global_preferences = global_preferences_registry.manager()

# now, we can use it to retrieve our preferences
# the lookup for a preference has the following form: <section>__<name>
assert global_preferences['general__title'] == 'My site'

# You can also access section-less preferences
assert global_preferences['maintenance_mode'] == False

# We can update our preferences values the same way
global_preferences['maintenance_mode'] = True

For per-instance preferences it’s even easier. You can access each instance preferences via the preferences attribute.

from django.contrib.auth import get_user_model

user = get_user_model().objects.get(username='bob')

assert user.preferences['discussion__comment_notifications_enabled'] == True

# Disable the notification system
user.preferences['discussion__comment_notifications_enabled'] = False

Under the hood

When you access a preference value (e.g. via global_preferences['maintenance_mode']), dynamic-preferences follows these steps:

  1. It checks for the cached value (using classic django cache mechanisms)

  2. If no cache key is found, it queries the database for the value

  3. If the value does not exists in database, a new row is added with the default preference value, and the value is returned. The cache is updated to avoid another database query the next time you want to retrieve the value.

Therefore, in the worst-case scenario, accessing a single preference value can trigger up to two database queries. Most of the time, however, dynamic-preferences will only hit the cache.

When you set a preference value (e.g. via global_preferences['maintenance_mode'] = True), dynamic-preferences follows these steps:

  1. The corresponding row is queried from the database (1 query)

  2. The new value is set and persisted in db (1 query)

  3. The cache is updated.

Updating a preference value will always trigger two database queries.

Misc methods for retrieving preferences

A few other methods are available on managers to retrieve preferences:

  • manager.all(): returns a dict containing all preferences identifiers and values

  • manager.by_name(): returns a dict containing all preferences identifiers and values.

    The preference section name (if any) is removed from the identifier

  • manager.get_by_name(name): returns a single preference value using only the preference name

Additional validation

In some situations, you’ll want to enforce custom rules for your preferences values, and raise validation errors when those rules are not matched.

You can implement that behaviour by declaring a validate method on your preference class, as follows:

from django.forms import ValidationError

# We start with a global preference
@global_preferences_registry.register
class MeaningOfLife(IntegerPreference):
    name = 'meaning_of_life'
    default = 42

    def validate(self, value):
        # ensure the meaning of life is always 42
        if value != 42:
            raise ValidationError('42 only')

Internally, the validate method is pass as a validator to the underlying form field.

About serialization

When you get or set preferences values, you interact with Python values. On the database/cache side, values are serialized before storage.

Dynamic preferences handle this for you, using each preference type (BooleanPreference, StringPreference, IntPreference, etc.). It’s totally possible to create your own preferences types and serializers, have a look at types.py and serializers.py to get started.

Admin integration

Dynamic-preferences integrates with django.contrib.admin out of the box. You can therefore use the admin interface to edit preferences values, which is particularly convenient for global preferences.

Forms

Form builder

A form builder is provided if you want to create and update preferences in custom views.

from dynamic_preferences.forms import global_preference_form_builder

# get a form for all global preferences
form_class = global_preference_form_builder()

# get a form for global preferences of the 'general' section
form_class = global_preference_form_builder(section='general')

# get a form for a specific set of preferences
# You can use the lookup notation (section__name) as follow
form_class = global_preference_form_builder(preferences=['general__title'])

# or pass explicitly the section and names as an iterable of tuples
form_class = global_preference_form_builder(preferences=[('general', 'title'), ('another_section', 'another_name')])

Getting a form for a specific instance preferences works similarly, except that you need to provide the user instance:

from dynamic_preferences.users.forms import user_preference_form_builder

form_class = user_preference_form_builder(instance=request.user)
form_class = user_preference_form_builder(instance=request.user, section='discussion')

Form builder with DjangoFormView

Be careful about instance registry cache, and use form.update_preferences() instead of form.save() (see #120)

from django.contrib import messages
from django.urls import reverse
from django.views.generic.edit import FormView

class SectionFormView(FormView):
    template_name = 'my_template.html'
    def get_form_class(self):
        from dynamic_preferences.forms import global_preference_form_builder
        return global_preference_form_builder(section='foo')

    def get_success_url(self):
        messages.add_message(self.request, messages.INFO, 'Saved !')
        return reverse('...')

    def form_valid(self, form):
        form.update_preferences()
        form = self.get_form()
        return super(SectionFormView, self).form_valid(form)

Form fields

In various places, a dynamic form field will be created from preferences. Consider the following example:

class MyPreference(StringPreference):
    default = 'my text'

In the admin area and using the form builder, the generated form field would look like this:

from django import forms

field = forms.CharField(initial='my text')

You can customize the behaviour and instanciation of the underlying form field using the following attributes and methods on any preference class:

class dynamic_preferences.types.BasePreferenceType(registry=None)[source]

Used as a base for all other preference classes. You should subclass this one if you want to implement your own preference.

field_class = None

A form field that will be used to display and edit the preference use a class, not an instance.

Example:

from django import forms

class MyPreferenceType(BasePreferenceType):
    field_class = forms.CharField
field_kwargs = {}

Additional kwargs to be passed to the form field.

Example:

class MyPreference(StringPreference):

    field_kwargs = {
        'required': False,
        'initial': 'Hello there'
    }
get_field_kwargs()[source]

Return a dict of arguments to use as parameters for the field class instianciation.

This will use field_kwargs as a starter, and use sensible defaults for a few attributes:

  • instance.verbose_name for the field label

  • instance.help_text for the field help text

  • instance.widget for the field widget

  • instance.required defined if the value is required or not

  • instance.initial defined if the initial value

validate(value)[source]

Used to implement custom cleaning logic for use in forms and serializers. The method will be passed as a validator to the preference form field.

Example:

def validate(self, value):
    if value == '42':
        raise ValidationError('Wrong value!')

Preferences attributes

You can customize a lof of preferences behaviour some class attributes / methods.

For example, if you want to customize the verbose_name of a preference you can simply do:

class MyPreference(StringPreference):
    verbose_name = "This is my preference"

But if you need more customization, you can do:

import datetime

class MyPreference(StringPreference):

    def get_verbose_name(self):
        return "Verbose name instantiated on {0}".format(datetime.datetime.now())

Both methods are perfectly valid. You can override the following attributes:

  • field_class: the field class used to edit the preference value

  • field_kwargs: kwargs that are passed to the field class upon instantiation. Ensure to call super() since some default are provided.

  • verbose_name: used in admin and as a label for the field

  • help_text: used in admin and in the field

  • default: the default value for the preference, that will also be used as initial data for the form field

  • widget: the widget used for the form field

  • required: used to define if the value is required

Accessing global preferences within a template

Dynamic-preferences provide a context processors (remember to add them to your settings, as described in “Installation”) that will pass global preferences values to your templates:

# myapp/templates/mytemplate.html

<title>{{ global_preferences.general__title }}</title>

{% if request.user.preferences.discussion__comment_notifications_enabled %}
    You will receive an email each time a comment is published
{% else %}
    <a href='/subscribe'>Subscribe to comments notifications</a>
{% endif %}

Bundled views and urls

Example views and urls are bundled for global and per-user preferences updating. Include this in your URLconf:

urlpatterns = [
    # your project urls here
    re_path(r'^preferences/', include('dynamic_preferences.urls')),
]

Then, in your code:

from django.urls import reverse

# URL to a page that display a form to edit all global preferences
url = reverse("dynamic_preferences.global")

# URL to a page that display a form to edit global preferences of the general section
url = reverse("dynamic_preferences.global.section", kwargs={'section': 'general'})

# URL to a page that display a form to edit all preferences of the user making the request
url = reverse("dynamic_preferences.user")

# URL to a page that display a form to edit preferences listed under section 'discussion' of the user making the request
url = reverse("dynamic_preferences.user.section", kwargs={'section': 'discussion'})