Flags

Flags are the most robust, flexible method of rolling out a feature with Waffle. Flags can be used to enable a feature for specific users, groups, users meeting certain criteria (such as being authenticated, or superusers) or a certain percentage of visitors.

How Flags Work

Flags compare the current request to their criteria to decide whether they are active. Consider this simple example:

if flag_is_active(request, 'foo'):
    pass

The flag_is_active function takes two arguments, the request, and the name of a flag. Assuming this flag (foo) is defined in the database, Waffle will make roughly the following decisions:

  • Is WAFFLE_OVERRIDE active and if so does this request specify a value for this flag? If so, use that value.
  • If not, is the flag set to globally on or off (the Everyone setting)? If so, use that value.
  • If not, is the flag in Testing mode, and does the request specify a value for this flag? If so, use that value and set a testing cookie.
  • If not, does the current user meet any of our criteria? If so, the flag is active.
  • If not, does the user have an existing cookie set for this flag? If so, use that value.
  • If not, randomly assign a value for this user based on the Percentage and set a cookie.

Flag Attributes

Flags can be administered through the Django admin site or the command line. They have the following attributes:

Name:The name of the flag. Will be used to identify the flag everywhere.
Everyone:Globally set the Flag, overriding all other criteria. Leave as Unknown to use other criteria.
Testing:Can the flag be specified via a querystring parameter? See below.
Percent:A percentage of users for whom the flag will be active, if no other criteria applies to them.
Superusers:Is this flag always active for superusers?
Staff:Is this flag always active for staff?
Authenticated:Is this flag always active for authenticated users?
Languages:Is the LANGUAGE_CODE of the request in this list? (Comma-separated values.)
Groups:A list of group IDs for which this flag will always be active.
Users:A list of user IDs for which this flag will always be active.
Rollout:Activate Rollout mode? See below.
Note:Describe where the flag is used.

A Flag will be active if any of the criteria are true for the current user or request (i.e. they are combined with or). For example, if a Flag is active for superusers, a specific group, and 12% of visitors, then it will be active if the current user is a superuser or if they are in the group or if they are in the 12%.

Note

Users are assigned randomly when using Percentages, so in practice the actual proportion of users for whom the Flag is active will probably differ slightly from the Percentage value.

Custom Flag Models

For many cases, the default Flag model provides all the necessary functionality. It allows flagging individual Users and Groups. If you would like flags to be applied to different things, such as companies a User belongs to, you can use a custom flag model.

The functionality uses the same concepts as Django’s custom user models, and a lot of this will be immediately recognizable.

An application needs to define a WAFFLE_FLAG_MODEL settings. The default is waffle.Flag but can be pointed to an arbitrary object.

Note

It is not possible to change the Flag model and generate working migrations. Ideally, the flag model should be defined at the start of a new project. This is a limitation of the swappable Django magic. Please use magic responsibly.

The custom Flag model must inherit from waffle.models.AbstractBaseFlag. If you want the existing User and Group based flagging and would like to add more entities to it, you may extend waffle.models.AbstractUserFlag.

If you use a custom flag model to apply to models beyond Users and Groups, you must run Django’s makemigrations before running migrations as outlined in the installation docs.

If you need to reference the class that is being used as the Flag model in your project, use the get_waffle_flag_model() method. If you reference the Flag a lot, it may be convenient to add Flag = get_waffle_flag_model() right below your imports and reference the Flag model as if it had been imported directly.

Example:

# settings.py
WAFFLE_FLAG_MODEL = 'myapp.Flag'

# models.py
from waffle.models import AbstractUserFlag, CACHE_EMPTY
from waffle.utils import get_setting, keyfmt, get_cache

class Flag(AbstractUserFlag):
    FLAG_COMPANIES_CACHE_KEY = 'FLAG_COMPANIES_CACHE_KEY'
    FLAG_COMPANIES_CACHE_KEY_DEFAULT = 'flag:%s:companies'

    companies = models.ManyToManyField(
        Company,
        blank=True,
        help_text=_('Activate this flag for these companies.'),
    )

    def get_flush_keys(self, flush_keys=None):
        flush_keys = super(Flag, self).get_flush_keys(flush_keys)
        companies_cache_key = get_setting(Flag.FLAG_COMPANIES_CACHE_KEY, Flag.FLAG_COMPANIES_CACHE_KEY_DEFAULT)
        flush_keys.append(keyfmt(companies_cache_key, self.name))
        return flush_keys

    def is_active_for_user(self, user):
        is_active = super(Flag, self).is_active_for_user(user)
        if is_active:
            return is_active

        if getattr(user, 'company_id', None):
            company_ids = self._get_company_ids()
            if user.company_id in company_ids:
                return True

    def _get_company_ids(self):
        cache = get_cache()
        cache_key = keyfmt(
            get_setting(Flag.FLAG_COMPANIES_CACHE_KEY, Flag.FLAG_COMPANIES_CACHE_KEY_DEFAULT),
            self.name
        )
        cached = cache.get(cache_key)
        if cached == CACHE_EMPTY:
            return set()
        if cached:
            return cached

        company_ids = set(self.companies.all().values_list('pk', flat=True))
        if not company_ids:
            cache.add(cache_key, CACHE_EMPTY)
            return set()

        cache.add(cache_key, company_ids)
        return company_ids

# admin.py
from waffle.admin import FlagAdmin as WaffleFlagAdmin

class FlagAdmin(WaffleFlagAdmin):
    raw_id_fields = tuple(list(WaffleFlagAdmin.raw_id_fields) + ['companies'])
admin.site.register(Flag, FlagAdmin)

Testing Mode

See User testing with Waffle.

Rollout Mode

When a Flag is activated by chance, Waffle sets a cookie so the flag will not flip back and forth on subsequent visits. This can present a problem for gradually deploying new features: users can get “stuck” with the Flag turned off, even as the percentage increases.

Rollout mode addresses this by changing the TTL of “off” cookies. When Rollout mode is active, cookies setting the Flag to “off” are session cookies, while those setting the Flag to “on” are still controlled by WAFFLE_MAX_AGE.

Effectively, Rollout mode changes the Percentage from “percentage of visitors” to “percent chance that the Flag will be activated per visit.”

Auto Create Missing

When a flag is evaluated in code that is missing in the database the flag returns the WAFFLE_FLAG_DEFAULT value but does not create a flag in the database. If you’d like waffle to create missing flags in the database whenever it encounters a missing flag you can set WAFFLE_CREATE_MISSING_FLAGS to True. Missing flags will be created in the database and the value of the Everyone flag attribute will be set to WAFFLE_FLAG_DEFAULT in the auto-created database record.

Log Missing

Whether or not you enabled Auto Create Missing Flags, it can be practical to be informed that a flag was or is missing. If you’d like waffle to log a warning, error, … you can set WAFFLE_LOG_MISSING_FLAGS to any level known by Python default logger.