Skip to content

Commit

Permalink
Exposed JSON endpoint for Waffle flag/switch/sample state (#452)
Browse files Browse the repository at this point in the history
Adds a JSON endpoint that returns the state of all flags/switches/samples.

In addition to the state, the endpoint also includes the last-modified time, which can be useful to identify stale flags that may need to be cleaned up.
  • Loading branch information
shaungallagher authored Jul 23, 2022
1 parent c5d5646 commit 5d8f55c
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 10 deletions.
1 change: 1 addition & 0 deletions docs/usage/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ JavaScript.
mixins
templates
javascript
json
cli
62 changes: 62 additions & 0 deletions docs/usage/json.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
.. _usage-json:

=====================
Waffle Status as JSON
=====================

Although :doc:`WaffleJS<javascript>` returns the status of all
:ref:`flags <types-flag>`, :ref:`switches <types-switch>`, and
:ref:`samples <types-sample>`, it does so by exposing a Javascript
object, rather than returning the data in a directly consumable format.

In cases where a directly consumable format is preferrable,
Waffle also exposes this data as JSON via the ``waffle_status`` view.


Using the view
--------------

Using the ``waffle_status`` view requires adding Waffle to your URL
configuration. For example, in your ``ROOT_URLCONF``::

urlpatterns = patterns('',
(r'^', include('waffle.urls')),
)

This adds a route called ``waffle_status``, which will return the current
status of each flag, switch, and sample as JSON, with the following structure:

.. code-block:: json
{
"flags": {
"flag_active": {
"is_active": true,
"last_modified": "2020-01-01T12:00:00.000"
},
"flag_inactive": {
"is_active": false,
"last_modified": "2020-01-01T12:00:00.000"
}
},
"switches": {
"switch_active": {
"is_active": true,
"last_modified": "2020-01-01T12:00:00.000"
},
"switch_inactive": {
"is_active": false,
"last_modified": "2020-01-01T12:00:00.000"
}
},
"samples": {
"sample_active": {
"is_active": true,
"last_modified": "2020-01-01T12:00:00.000"
},
"sample_inactive": {
"is_active": false,
"last_modified": "2020-01-01T12:00:00.000"
}
}
}
14 changes: 11 additions & 3 deletions waffle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,32 @@


def flag_is_active(request, flag_name, read_only=False):
flag = get_waffle_model('FLAG_MODEL').get(flag_name)
flag = get_waffle_flag_model().get(flag_name)
return flag.is_active(request, read_only=read_only)


def switch_is_active(switch_name):
switch = get_waffle_model('SWITCH_MODEL').get(switch_name)
switch = get_waffle_switch_model().get(switch_name)
return switch.is_active()


def sample_is_active(sample_name):
sample = get_waffle_model('SAMPLE_MODEL').get(sample_name)
sample = get_waffle_sample_model().get(sample_name)
return sample.is_active()


def get_waffle_flag_model():
return get_waffle_model('FLAG_MODEL')


def get_waffle_switch_model():
return get_waffle_model('SWITCH_MODEL')


def get_waffle_sample_model():
return get_waffle_model('SAMPLE_MODEL')


def get_waffle_model(setting_name):
"""
Returns the waffle Flag model that is active in this project.
Expand Down
37 changes: 36 additions & 1 deletion waffle/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.urls import reverse

from waffle import get_waffle_flag_model
from waffle import get_waffle_flag_model, get_waffle_sample_model, get_waffle_switch_model
from waffle.models import Sample, Switch
from waffle.tests.base import TestCase

Expand All @@ -14,6 +14,41 @@ def test_wafflejs(self):
for control in response['cache-control'].split(',')]
self.assertIn('max-age=0', cache_control)

def test_waffle_status(self):
response = self.client.get(reverse('waffle_status'))
self.assertEqual(200, response.status_code)
self.assertEqual('application/json', response['content-type'])
cache_control = [control.strip()
for control in response['cache-control'].split(',')]
self.assertIn('max-age=0', cache_control)

def test_waffle_status_response(self):
get_waffle_flag_model().objects.create(name='test_flag_active', everyone=True)
get_waffle_flag_model().objects.create(name='test_flag_inactive', everyone=False)
get_waffle_switch_model().objects.create(name='test_switch_active', active=True)
get_waffle_switch_model().objects.create(name='test_switch_inactive', active=False)
get_waffle_sample_model().objects.create(name='test_sample_active', percent=100)
get_waffle_sample_model().objects.create(name='test_sample_inactive', percent=0)

response = self.client.get(reverse('waffle_status'))
self.assertEqual(200, response.status_code)
content = response.json()

assert 'test_flag_active' in content['flags'].keys()
assert content['flags']['test_flag_active']['is_active']
assert 'test_flag_inactive' in content['flags'].keys()
assert not content['flags']['test_flag_inactive']['is_active']

assert 'test_switch_active' in content['switches'].keys()
assert content['switches']['test_switch_active']['is_active']
assert 'test_switch_inactive' in content['switches'].keys()
assert not content['switches']['test_switch_inactive']['is_active']

assert 'test_sample_active' in content['samples'].keys()
assert content['samples']['test_sample_active']['is_active']
assert 'test_sample_inactive' in content['samples'].keys()
assert not content['samples']['test_sample_inactive']['is_active']

def test_flush_all_flags(self):
"""Test the 'FLAGS_ALL' list gets invalidated correctly."""
get_waffle_flag_model().objects.create(name='myflag1', everyone=True)
Expand Down
3 changes: 2 additions & 1 deletion waffle/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from django.urls import path

from waffle.views import wafflejs
from waffle.views import wafflejs, waffle_json

urlpatterns = [
path('wafflejs', wafflejs, name='wafflejs'),
path('waffle_status', waffle_json, name='waffle_status'),
]
49 changes: 44 additions & 5 deletions waffle/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from django.http import HttpResponse
from django.http import HttpResponse, JsonResponse
from django.template import loader
from django.views.decorators.cache import never_cache

from waffle import get_waffle_flag_model
from waffle.models import Sample, Switch
from waffle import get_waffle_flag_model, get_waffle_switch_model, get_waffle_sample_model
from waffle.utils import get_setting


Expand All @@ -17,10 +16,10 @@ def _generate_waffle_js(request):
flags = get_waffle_flag_model().get_all()
flag_values = [(f.name, f.is_active(request)) for f in flags]

switches = Switch.get_all()
switches = get_waffle_switch_model().get_all()
switch_values = [(s.name, s.is_active()) for s in switches]

samples = Sample.get_all()
samples = get_waffle_sample_model().get_all()
sample_values = [(s.name, s.is_active()) for s in samples]

return loader.render_to_string('waffle/waffle.js', {
Expand All @@ -31,3 +30,43 @@ def _generate_waffle_js(request):
'switch_default': get_setting('SWITCH_DEFAULT'),
'sample_default': get_setting('SAMPLE_DEFAULT'),
})


@never_cache
def waffle_json(request):
return JsonResponse(_generate_waffle_json(request))


def _generate_waffle_json(request):
flags = get_waffle_flag_model().get_all()
flag_values = {
f.name: {
'is_active': f.is_active(request),
'last_modified': f.modified,
}
for f in flags
}

switches = get_waffle_switch_model().get_all()
switch_values = {
s.name: {
'is_active': s.is_active(),
'last_modified': s.modified,
}
for s in switches
}

samples = get_waffle_sample_model().get_all()
sample_values = {
s.name: {
'is_active': s.is_active(),
'last_modified': s.modified,
}
for s in samples
}

return {
'flags': flag_values,
'switches': switch_values,
'samples': sample_values,
}

0 comments on commit 5d8f55c

Please sign in to comment.