Feature flags for Wagtail sites
Project description
Feature flags allow you to toggle functionality in both Django settings and the Wagtail or Django admin based on configurable conditions.
Dependencies
Django 1.8+
Wagtail 1.7+
Python 2.7+, 3.5+
Installation
Install wagtail-flags using pip:
pip install wagtail-flags
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”:
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.
Licensing
Credits and references
Forked from cfgov-refresh
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.