Skip to content

Commit

Permalink
Support specifying metadata in DatabaseFlagsSource
Browse files Browse the repository at this point in the history
This change supports specifying metadata in the DatabaseFlagsSource by using a FlagMetadata model.  It also modifies the DatabaseFlagSource to return a three-tuple instead of the dict that was deprecated in b1a752f.
  • Loading branch information
willbarton committed Jun 10, 2020
1 parent e454cdb commit 08ab63b
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 21 deletions.
8 changes: 8 additions & 0 deletions docs/releasenotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@

- Added Django 3.0 support
- Added validator support to ensure that the values that flag conditions test against are valid.
- Added metadata support for flags.

### Deprecations

- Deprecated the optional `flags.middleware.FlagConditionsMiddleware` in favor of always lazily caching flags on the request object.

### Removals

- Django Flags 4.1 deprecated support for using a single dictionary to hold key/values of conditions for a settings-based feature flag, and this has been removed. Use [a list of dictionaries or tuples instead](/settings/#flags).

## 4.2.4

### What's new?
Expand Down
9 changes: 7 additions & 2 deletions flags/admin.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from django.contrib import admin

from flags.forms import FlagStateForm
from flags.models import FlagState
from flags.forms import FlagMetadataForm, FlagStateForm
from flags.models import FlagMetadata, FlagState


class FlagMetadataAdmin(admin.ModelAdmin):
form = FlagMetadataForm


class FlagStateAdmin(admin.ModelAdmin):
form = FlagStateForm


admin.site.register(FlagMetadata, FlagMetadataAdmin)
admin.site.register(FlagState, FlagStateAdmin)
19 changes: 18 additions & 1 deletion flags/forms.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
from django import forms

from flags.conditions import get_condition, get_conditions
from flags.models import FlagState
from flags.models import FlagMetadata, FlagState
from flags.sources import get_flags


class FlagMetadataForm(forms.ModelForm):
name = forms.ChoiceField(label="Flag", required=True)
key = forms.CharField(label="Key", required=True)
value = forms.CharField(label="Value", required=True)

def __init__(self, *args, **kwargs):
super(FlagMetadataForm, self).__init__(*args, **kwargs)

self.fields["name"].choices = [
(f, f) for f in sorted(get_flags().keys())
]

class Meta:
model = FlagMetadata
fields = ("name", "key", "value")


class FlagStateForm(forms.ModelForm):
name = forms.ChoiceField(label="Flag", required=True)
condition = forms.ChoiceField(label="Condition name", required=True)
Expand Down
25 changes: 25 additions & 0 deletions flags/migrations/0014_flagmetadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 2.2.12 on 2020-05-28 20:37

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('flags', '0013_add_required_field'),
]

operations = [
migrations.CreateModel(
name='FlagMetadata',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=64)),
('key', models.CharField(max_length=64)),
('value', models.TextField()),
],
options={
'unique_together': {('name', 'key')},
},
),
]
23 changes: 16 additions & 7 deletions flags/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,21 @@ class Meta:
unique_together = ("name", "condition", "value")

def __str__(self):
required_str = " (required)" if self.required else ""
return (
"{name} is enabled when {condition} is "
"{value}{required}".format(
name=self.name,
condition=self.condition,
value=self.value,
required=" (required)" if self.required else "",
)
f"{self.name} is enabled when {self.condition} is "
f"{self.value}{required_str}"
)


class FlagMetadata(models.Model):
name = models.CharField(max_length=64)
key = models.CharField(max_length=64)
value = models.TextField()

class Meta:
app_label = "flags"
unique_together = ("name", "key")

def __str__(self):
return f"{self.name} ({self.key}: {self.value})"
21 changes: 17 additions & 4 deletions flags/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,17 +148,30 @@ def get_queryset(self):
FlagState = apps.get_model("flags", "FlagState")
return FlagState.objects.all()

def get_metadata_queryset(self):
FlagMetadata = apps.get_model("flags", "FlagMetadata")
return FlagMetadata.objects.all()

def get_flags(self):
flags = {}
for o in self.get_queryset():
for o in self.get_queryset().all():
if o.name not in flags:
flags[o.name] = []
flags[o.name].append(
flags[o.name] = {"conditions": [], "metadata": {}}
flags[o.name]["conditions"].append(
DatabaseCondition(
o.condition, o.value, required=o.required, obj=o
)
)
return flags

for o in self.get_metadata_queryset().all():
if o.name not in flags:
flags[o.name] = {"conditions": [], "metadata": {}}
flags[o.name]["metadata"][o.key] = o.value

return [
(flag, flags[flag]["conditions"], flags[flag]["metadata"])
for flag in flags
]


def get_flags(sources=None, ignore_errors=False, request=None):
Expand Down
32 changes: 30 additions & 2 deletions flags/tests/test_forms.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,38 @@
from django.test import TestCase

from flags.conditions.registry import _conditions, register
from flags.forms import FlagStateForm
from flags.forms import FlagMetadataForm, FlagStateForm


class FormTestCase(TestCase):
class FlagMetadataFormTestCase(TestCase):
def test_valid_data(self):
form = FlagMetadataForm(
{
"name": "FLAG_ENABLED",
"key": "help_text",
"value": "enable a cool thing",
}
)
self.assertTrue(form.is_valid())
metadata = form.save()
self.assertEqual(metadata.name, "FLAG_ENABLED")
self.assertEqual(metadata.key, "help_text")
self.assertEqual(metadata.value, "enable a cool thing")

def test_blank_data(self):
form = FlagMetadataForm({})
self.assertFalse(form.is_valid())
self.assertEqual(
form.errors,
{
"name": ["This field is required."],
"key": ["This field is required."],
"value": ["This field is required."],
},
)


class FlagStateFormTestCase(TestCase):
def test_valid_data(self):
form = FlagStateForm(
{"name": "FLAG_ENABLED", "condition": "boolean", "value": "True"}
Expand Down
12 changes: 11 additions & 1 deletion flags/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.test import TestCase

from flags.models import FlagState
from flags.models import FlagMetadata, FlagState


class FlagStateTestCase(TestCase):
Expand All @@ -17,3 +17,13 @@ def test_flag_str_required(self):
self.assertEqual(
str(state), "MY_FLAG is enabled when boolean is True (required)"
)


class FlagMetadataTestCase(TestCase):
def test_flag_str(self):
metadata = FlagMetadata.objects.create(
name="MY_FLAG", key="help_text", value="enable a cool thing"
)
self.assertEqual(
str(metadata), "MY_FLAG (help_text: enable a cool thing)"
)
22 changes: 18 additions & 4 deletions flags/tests/test_sources.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.http import HttpRequest
from django.test import TestCase, override_settings

from flags.models import FlagState
from flags.models import FlagMetadata, FlagState
from flags.sources import (
Condition,
DatabaseFlagsSource,
Expand Down Expand Up @@ -148,7 +148,21 @@ def test_get_flags(self):
)
source = DatabaseFlagsSource()
flags = source.get_flags()
self.assertEqual(flags, {"MY_FLAG": [Condition("boolean", "False")]})
self.assertEqual(
flags, [("MY_FLAG", [Condition("boolean", "False")], {})]
)

def test_get_flags_metadata(self):
FlagMetadata.objects.create(
name="MY_FLAG", key="help_text", value="enable a cool thing"
)
source = DatabaseFlagsSource()

flags = source.get_flags()

self.assertEqual(
flags, [("MY_FLAG", [], {"help_text": "enable a cool thing"})]
)


class ConditionTestCase(TestCase):
Expand Down Expand Up @@ -296,7 +310,7 @@ def test_uses_cached_flags_from_request(self):
request = HttpRequest()

# The initial call looks up flag conditions from the database source.
with self.assertNumQueries(1):
with self.assertNumQueries(2):
get_flags(request=request)

# Subsequent calls with a request object don't need to redo the lookup
Expand All @@ -305,5 +319,5 @@ def test_uses_cached_flags_from_request(self):
get_flags(request=request)

# But subsequent calls without a request object still redo the lookup.
with self.assertNumQueries(1):
with self.assertNumQueries(2):
get_flags()

0 comments on commit 08ab63b

Please sign in to comment.