wagtail-flags 2.0.4 2017-06-12

wagtail-flags on PyPI  

Feature flags for Wagtail sites


AuthorCFPB
LicenseCC0

Feature flags allow you to toggle functionality in both Django settings and the Wagtail or Django admin based on configurable conditions.

Feature flags in the Wagtail admin

Dependencies

  • Django 1.8+
  • Wagtail 1.7+
  • Python 2.7+, 3.5+

Installation

  1. Install wagtail-flags using pip:
pip install wagtail-flags
  1. Add flags as an installed app in your Django settings.py:

python  INSTALLED_APPS = (      ...      'flags',      ...  )

Concepts

Feature flags in Wagtail-Flags are identified by simple strings that are enabled when the conditions they are associated with are met. These flags can be used to wrap code and template content that should only be used when a flag is enabled or disabled.

Conditions determine whether a flag is enabled or disabled by comparing a defined expected value of some kind with the value at the time the flag is checked. In many cases, the flag is checked during a request, and some piece of the request’s metadata is what is compared. For example, a feature flag that is enabled for a specific Wagtail Site would be enabled if the request’s site matches the condition’s site.

Usage

Overview

To use Wagtail-Flags you first need to define the flag, use the flag in code, and define conditions for the flag to be enabled.

First, define the flag in Django settings.py:

FLAGS = {
    'MY_FLAG': {}
}

Then use the flag in a Django template (mytemplate.html):

{% load feature_flags %}
{% flag_enabled 'MY_FLAG' as my_flag %}

{% if my_flag %}
  <div class="flagged-banner">
    I’m the result of a feature flag.
  </div>
{% endif %}

Configure a URL for that template (urls.py):

from django.conf.urls import url
from django.views.generic.base import TemplateView

urlpatterns = [
    url(r'^/mypage$', TemplateView.as_view(template_name='mytemplate.html'),
]

Then in the Wagtail admin add conditions for the flag in “Settings”, “Flags”:

Creating conditions in the Wagtail admin

Then visiting the URL /mypage?enable_my_flag=True should show you the flagged <div> in the template.

Adding flags

Defining flags

Flags are defined in Django settings with the conditions in which they are enabled.

FLAGS = {
  'FLAG_WITH_EMPTY_CONDITIONS': {}
  'MY_FLAG': {
    'condition name': 'value flag is expected to match to be enabled',
    'user': 'lady.liberty'
  }
}

The set of conditions can be none (flag will never be enabled), one (only condition that has to be met for the flag to be enabled), or many (all have to be met for the flag to be enabled).

Additional conditions can be added in the Django or Wagtail admin for any defined flag (illustrated in Usage). Conditions added in the Django or Wagtail admin can be changed without restarting Django, conditions defined in settings.py cannot.

Built-in conditions

Wagtail-Flags comes with the following conditions built-in:

boolean

A simple boolean true/false intended to enable or disable a flag explicitly. The state of the flag evaluates to the value of the boolean condition.

FLAGS = {'MY_FLAG': {'boolean': True}}
user

Allows a flag to be enabled for the username given as the condition’s value.

FLAGS = {'MY_FLAG': {'user': 'jane.doe'}}
anonymous

Allows a flag to be either enabled or disabled depending on the condition’s boolean value.

FLAGS = {'MY_FLAG': {'anonymous: False}}
parameter

Allows a flag to be enabled based on a GET parameter with the name given as the condition’s value.

FLAGS = {'MY_FLAG': {'parameter': 'my_flag_param'}}
path

Allows a flag to be enabled if the request’s path matches the condition value.

FLAGS = {'MY_FLAG': {'path': '/flagged/path'}}
site

Allows a flag to be enabled for a Wagtail site that matches the hostname and port in the condition value.

FLAGS = {'MY_FLAG': {'site': 'staging.mysite.com'}}

API

Flag state

from flags.state import (
    flag_state,
    flag_enabled,
    flag_disabled,
)

flag_state(flag_name, **kwargs)

Return the value for the flag (True or False) by passing kwargs to its conditions.

flag_enabled(flag_name, **kwargs)

Returns True if a flag is enabled by passing kwargs to its conditions, otherwise returns False.

if flag_enabled('MY_FLAG', request=a_request):
    print("My feature flag is enabled")

flag_disabled(flag_name, **kwargs)

Returns True if a flag is disabled by passing kwargs to its conditions, otherwise returns False.

if flag_disabled('MY_FLAG', request=a_request):
    print(My feature flag is disabled)

Flag decorators

Decorators are provided for use with Django views and conditions that take a request argument. The default behavior is to return a 404 if a callable fallback is not given.

from flags.decorators import (
    flag_check,
    flag_required,
)

flag_check(flag_name, state, fallback=None, **kwargs)

Check that a given flag has the given state. If the state does not match, perform the fallback.

Note, because flags that do not exist are taken to be False by default, @flag_check('MY_FLAG', False) and @flag_check('MY_FLAG', None) will both succeed if MY_FLAG does not exist.

from flags.decorators import flag_check

@flag_check('MY_FLAG', True)
def view_requiring_flag(request):
    return HttpResponse('flag was set')

@flag_check('MY_OTHER_FLAG', False)
def view_when_flag_is_not_set(request):
    return HttpResponse('flag was set')

def other_view(request):
    return HttpResponse('flag was not set')

@flag_check('MY_FLAG_WITH_FALLBACK', True, fallback=other_view)
def view_with_fallback(request):
    return HttpResponse('flag was set')

flag_required(flag_name, fallback_view=None, pass_if_set=True)

Require the given flag to be enabled.

from flags.decorators import flag_required

@flag_required('MY_FLAG')
def view_requiring_flag(request):
    return HttpResponse('flag was set')

def other_view(request):
    return HttpResponse('flag was not set')

@flag_required('MY_FLAG_WITH_FALLBACK', fallback_view=other_view)
def view_with_fallback(request):
    return HttpResponse('flag was set')

Flagged URLs

from flags.urls import flagged_url, flagged_urls

flagged_url(flag_name, regex, view, kwargs=None, name=None, state=True, fallback=None)

Make a URL depend on the state of a feature flag. flagged_url() can be used in place of Django’s url().

fallback can be a a set of include()ed patterns, but the regular expressions in the fallback includes must match the regular expression for the URL or includes exactly.

urlpatterns = [
    flagged_url('MY_FLAG', r'^an-url$', view_requiring_flag, state=True),
    flagged_url('MY_FLAG_WITH_FALLBACK', r'^another-url$', view_with_fallback,
                state=True, fallback=other_view)
    flagged_url('MY_FLAGGED_INCLUDE', r'^myapp$', include('myapp.urls'),
                state=True, fallback=other_view)
    flagged_url('MY_NEW_APP_FLAG', r'^mynewapp$', include('mynewapp.urls'),
                state=True, fallback=include('myoldapp.urls'))
]

flagged_urls(flag_name, state=True, fallback=None)

Flag multiple URLs in the same context. Returns function that can be used in place of Django’s url() that wraps flagged_url(). Can take an optional fallback view that will apply to all urls.

with flagged_urls('MY_FLAG') as url:
    flagged_url_patterns = [
        url(r'^an-url$', view_requiring_flag),
    ]

urlpatterns = urlpatterns + flagged_url_patterns

Django templates

Wagtail-Flags provides a template tag library that can be used to evaluate flags in Django templates.

{% load feature_flags %}

flag_enabled

Returns True if a flag is enabled by passing the current request to its conditions, otherwise returns False.

{% flag_enabled 'MY_FLAG' as my_flag %}
{% if my_flag %}
  <div class="m-global-banner">
    I’m the result of a feature flag.
  </div>
{% endif %}

flag_disabled

Returns True if a flag is disabled by passing the current request to its conditions, otherwise returns False.

{% flag_disabled 'MY_FLAG' as my_flag %}
{% if my_flag %}
  <div class="m-global-banner">
    I’m the result of a feature flag that is not enabled.
  </div>
{% endif %}

Jinja2 templates

Wagtail-Flags provides template functions that can be added to a Jinja2 environment and subsequently used in templates.

from flags.template_functions import (
    flag_enabled,
    flag_disabled
)

...

env.globals.update(
    flag_enabled=flag_enabled,
    flag_disabled=flag_disabled
)

flag_enabled

Returns True if a flag is enabled by for the given request, otherwise returns False.

{% if flag_enabled('MY_FLAG', request) %}
  <div class="m-global-banner">
    I’m the result of a feature flag.
  </div>
{% endif %}

flag_disabled

Returns True if a flag is disabled by passing the current request to its conditions, otherwise returns False. Returns True if a flag is disabled by for the given request, otherwise returns False.

{% if flag_disabled('MY_FLAG', request) %}
  <div class="m-global-banner">
    I’m the result of a feature flag that is not enabled.
  </div>
{% endif %}

Conditions

Conditions are functions that take a configured value and possible keyword arguments and determines whether the given arguments are equivalent to the value. Conditions are registered with a unique name that is exposed to users in Django settings and the Django and Wagtail admin.

from flags import conditions

conditions.register(condition_name, fn=None)

Register a new condition, either as a decorator:

from flags import conditions

@conditions.register('path')
def path_condition(path, request=None, **kwargs):
    return request.path.startswith(path)

Or as a function call:

def path_condition(path, request=None, **kwargs):
    return request.path.startswith(path)

conditions.register('path', fn=path_condition)

conditions.RequiredForCondition

Exception intended to be raised when a condition is not given a keyword argument it requires for evaluation.

@conditions.register('path')
def path_condition(path, request=None, **kwargs):
    if request is None:
        raise conditions.RequiredForCondition(
            "request is required for condition 'path'")

    return request.path.startswith(path)

Getting help

Please add issues to the issue tracker.

Getting involved

General instructions on how to contribute can be found in CONTRIBUTING.

Credits and references

  1. Forked from cfgov-refresh