plain.flags
Local feature flags via database models.
Custom flags are written as subclasses of Flag
.
You define the flag's "key" and initial value,
and the results will be stored in the database for future reference.
# app/flags.py
from plain.flags import Flag
class FooEnabled(Flag):
def __init__(self, user):
self.user = user
def get_key(self):
return self.user
def get_value(self):
# Initially all users will have this feature disabled
# and we'll enable them manually in the admin
return False
Use flags in HTML templates:
{% if flags.FooEnabled(request.user) %}
<p>Foo is enabled for you!</p>
{% else %}
<p>Foo is disabled for you.</p>
{% endif %}
Or in Python:
import flags
print(flags.FooEnabled(user).value)
Installation
INSTALLED_PACKAGES = [
...
"plain.flags",
]
Create a flags.py
at the top of your app
(or point settings.FLAGS_MODULE
to a different location).
Advanced usage
Ultimately you can do whatever you want inside of get_key
and get_value
.
class OrganizationFeature(Flag):
url_param_name = ""
def __init__(self, request=None, organization=None):
# Both of these are optional, but will usually both be given
self.request = request
self.organization = organization
def get_key(self):
if (
self.url_param_name
and self.request
and self.url_param_name in self.request.GET
):
return None
if not self.organization:
# Don't save the flag result for PRs without an organization
return None
return self.organization
def get_value(self):
if self.url_param_name and self.request:
if self.request.GET.get(self.url_param_name) == "1":
return True
if self.request.GET.get(self.url_param_name) == "0":
return False
if not self.organization:
return False
# All organizations will start with False,
# and I'll override in the DB for the ones that should be True
return False
class AIEnabled(OrganizationFeature):
pass
1from functools import cached_property
2
3from plain.models.forms import ModelForm
4from plain.staff.cards import Card
5from plain.staff.views import (
6 StaffModelDetailView,
7 StaffModelListView,
8 StaffModelUpdateView,
9 StaffModelViewset,
10 register_viewset,
11)
12
13from .models import Flag, FlagResult
14
15
16class UnusedFlagsCard(Card):
17 title = "Unused Flags"
18
19 @cached_property
20 def flag_errors(self):
21 return Flag.check(databases=["default"])
22
23 def get_number(self):
24 return len(self.flag_errors)
25
26 def get_text(self):
27 return "\n".join(str(e.msg) for e in self.flag_errors)
28
29
30@register_viewset
31class FlagStaff(StaffModelViewset):
32 class ListView(StaffModelListView):
33 model = Flag
34 fields = ["name", "enabled", "created_at__date", "used_at__date", "uuid"]
35 search_fields = ["name", "description"]
36 cards = [UnusedFlagsCard]
37 nav_section = "Feature flags"
38
39 class DetailView(StaffModelDetailView):
40 model = Flag
41
42
43class FlagResultForm(ModelForm):
44 class Meta:
45 model = FlagResult
46 fields = ["key", "value"]
47
48
49@register_viewset
50class FlagResultStaff(StaffModelViewset):
51 class ListView(StaffModelListView):
52 model = FlagResult
53 title = "Flag results"
54 fields = [
55 "flag",
56 "key",
57 "value",
58 "created_at__date",
59 "updated_at__date",
60 "uuid",
61 ]
62 search_fields = ["flag__name", "key"]
63 nav_section = "Feature flags"
64
65 def get_initial_queryset(self):
66 return self.model.objects.all().select_related("flag")
67
68 class DetailView(StaffModelDetailView):
69 model = FlagResult
70 title = "Flag result"
71
72 class UpdateView(StaffModelUpdateView):
73 model = FlagResult
74 title = "Update flag result"
75 form_class = FlagResultForm