Plain is headed towards 1.0! Subscribe for development updates →

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