jazzband / django-waffle

A feature flipper for Django
https://waffle.readthedocs.io
BSD 3-Clause "New" or "Revised" License
1.12k stars 258 forks source link

Set flag active based on request or user attribute #485

Closed hugorodgerbrown closed 1 year ago

hugorodgerbrown commented 1 year ago

I will submit a PR for this, but thought I'd raise an issue first to explain my thinking.

We use waffle a lot, but there is a use case where it falls down (for us), and that is where we want to activate a flag based on a specific user attribute. An example might be "all users who have registered since XX", or "users who have performed this action".

I think this could be supported by having a list of functions set up that take a request and return a bool, that could be configured in settings and then attached to a flag. The flag_is_active functions would then use this to determine whether the outcome.

# settings.py

def waffle_new_users(request):
    return request.user.date_registered > some_arbitrary_date

WAFFLE_REQUEST_FILTERS = {
   "All new users": waffle_new_users,
   "Users with red hair": lambda r: r.user.has_red_hair
}

These could then appear on the new flag page:

Screenshot 2023-05-17 at 15 21 46

hugorodgerbrown commented 1 year ago

Closed as this is possible with a custom Flag model.

hugorodgerbrown commented 1 year ago

Adding this as an example, for future reference.

import datetime

from django.conf import settings
from django.db import models
from django.utils.timezone import now as tz_now

from waffle.models import AbstractUserFlag

class FeatureFlag(AbstractUserFlag):
    """
    Drop-in replacement for Waffle's Flag model that supports custom
    groups.

    This is a subclass of Waffle's Flag model which adds the ability to
    to target a "custom group" of users. This group is just a string,
    which is then used in the `is_active_for_user` method override to
    determine whether the flag should be active.

    To add a new custom group add a const to the `CustomGroup` class,
    and then add a matching model method with the same name as the
    const. See the `anonymous_only` and `new_registrations` methods for
    examples.

    """

    class CustomGroup(models.TextChoices):
        # when adding a new group, remember to add the appropriate
        # method with the same name to the class.
        ANONYMOUS_ONLY = "anonymous_only", "Anonymous users only"
        NEW_REGISTRATIONS = "new_registrations", "Users registered in the last 30 days"

    custom_group = models.CharField(
        max_length=255,
        blank=True,
        null=True,
        choices=CustomGroup.choices,
        help_text="Custom group to enable this flag for",
    )

    class Meta:
        verbose_name = "Feature Flag"
        verbose_name_plural = "Feature Flags (ex. Waffle)"

    def is_active_for_user(self, user):
        if self.custom_group:
            # will raise AttributeError if the custom group doesn't exist
            return getattr(self, self.custom_group)(user)
        # use the normal Waffle operation if no custom group is set.
        return super().is_active_for_user(user)

    # region: group methods
    def anonymous_only(self, user: settings.AUTH_USER_MODEL) -> bool:
        return user.is_anonymous

    def new_registrations(self, user: settings.AUTH_USER_MODEL) -> bool:
        return user.date_joined > tz_now() - datetime.timedelta(days=30)
    # endregion: group methods