Skip to content

Commit

Permalink
merge
Browse files Browse the repository at this point in the history
  • Loading branch information
hugobessa committed May 8, 2018
2 parents 60231fc + 9b9119e commit 4267c01
Show file tree
Hide file tree
Showing 20 changed files with 601 additions and 264 deletions.
8 changes: 8 additions & 0 deletions docs/starting/configuring.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ behavior.
``False``. Set this to ``True`` to make Waffle consider undefined
flags ``True``. Defaults to ``False``.

``WAFFLE_FLAG_MODEL``
The model that will be use to keep track of flags. Defaults to ``waffle.Flag``
which allows user- and group-based flags. Can be swapped for a different Flag model
that allows flagging based on other things, such as an organization or a company
that a user belongs to. Analogous functionality to Django's extendable User models.
Needs to be set at the start of a project, as the Django migrations framework does not
support changing swappable models after the initial migration.

``WAFFLE_SWITCH_DEFAULT``
When a Switch is undefined in the database, Waffle considers it
``False``. Set this to ``True`` to make Waffle consider undefined
Expand Down
92 changes: 92 additions & 0 deletions docs/types/flag.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,98 @@ are in the group *or* if they are in the 12%.
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.


.. _types-flag-custom-model:

Custom Flag Models
======================

For many cases, the default Flag model provides all the necessary functionality. It allows
flagging individual ``User``s and ``Group``s. 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 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:

```python
# settings.py
WAFFLE_FLAG_MODEL = 'myapp.Flag'
# models.py
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_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)
```


.. _types-flag-testing:
Expand Down
64 changes: 64 additions & 0 deletions test_app/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Generated by Django 2.0.4 on 2018-04-26 07:42

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='Company',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='CompanyAwareFlag',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='The human/computer readable name.', max_length=100, unique=True, verbose_name='Name')),
('everyone', models.NullBooleanField(help_text='Flip this flag on (Yes) or off (No) for everyone, overriding all other settings. Leave as Unknown to use normally.', verbose_name='Everyone')),
('percent', models.DecimalField(blank=True, decimal_places=1, help_text='A number between 0.0 and 99.9 to indicate a percentage of users for whom this flag will be active.', max_digits=3, null=True, verbose_name='Percent')),
('testing', models.BooleanField(default=False, help_text='Allow this flag to be set for a session for user testing', verbose_name='Testing')),
('superusers', models.BooleanField(default=True, help_text='Flag always active for superusers?', verbose_name='Superusers')),
('staff', models.BooleanField(default=False, help_text='Flag always active for staff?', verbose_name='Staff')),
('authenticated', models.BooleanField(default=False, help_text='Flag always active for authenticated users?', verbose_name='Authenticated')),
('languages', models.TextField(blank=True, default='', help_text='Activate this flag for users with one of these languages (comma-separated list)', verbose_name='Languages')),
('rollout', models.BooleanField(default=False, help_text='Activate roll-out mode?', verbose_name='Rollout')),
('note', models.TextField(blank=True, help_text='Note where this Flag is used.', verbose_name='Note')),
('created', models.DateTimeField(db_index=True, default=django.utils.timezone.now, help_text='Date when this Flag was created.', verbose_name='Created')),
('modified', models.DateTimeField(default=django.utils.timezone.now, help_text='Date when this Flag was last modified.', verbose_name='Modified')),
('companies', models.ManyToManyField(blank=True, help_text='Activate this flag for these companies.', to='test_app.Company')),
('groups', models.ManyToManyField(blank=True, help_text='Activate this flag for these user groups.', to='auth.Group', verbose_name='Groups')),
('users', models.ManyToManyField(blank=True, help_text='Activate this flag for these users.', to=settings.AUTH_USER_MODEL, verbose_name='Users')),
],
options={
'verbose_name': 'Flag',
'verbose_name_plural': 'Flags',
'abstract': False,
},
),
migrations.CreateModel(
name='CompanyUser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('username', models.CharField(max_length=100)),
('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='test_app.Company')),
],
options={
'abstract': False,
},
),
]
Empty file added test_app/migrations/__init__.py
Empty file.
73 changes: 73 additions & 0 deletions test_app/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from django.contrib.auth.base_user import AbstractBaseUser
from django.db import models
from django.db.models import CASCADE
from django.utils.translation import ugettext_lazy as _

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

cache = get_cache()


class Company(models.Model):
name = models.CharField(
max_length=100,
)


class CompanyUser(AbstractBaseUser):
company = models.ForeignKey(
Company,
on_delete=CASCADE
)

username = models.CharField(
max_length=100,
)


class CompanyAwareFlag(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(CompanyAwareFlag, self).get_flush_keys(flush_keys)
companies_cache_key = get_setting(CompanyAwareFlag.FLAG_COMPANIES_CACHE_KEY,
CompanyAwareFlag.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(CompanyAwareFlag, 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_key = keyfmt(
get_setting(CompanyAwareFlag.FLAG_COMPANIES_CACHE_KEY, CompanyAwareFlag.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
29 changes: 26 additions & 3 deletions waffle/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
from __future__ import unicode_literals

from django.core.exceptions import ImproperlyConfigured

from waffle.utils import get_cache, get_setting, keyfmt
from waffle.helpers import waffle_flag_call, waffle_switch_call
from django.apps import apps as django_apps

VERSION = (0, 13, 0)
__version__ = '.'.join(map(str, VERSION))


def flag_is_active(request, flag_name):
from .models import Flag

flag = Flag.get(flag_name)
flag = get_waffle_flag_model().get(flag_name)
return flag.is_active(request)


Expand All @@ -26,3 +27,25 @@ def sample_is_active(sample_name):

sample = Sample.get(sample_name)
return sample.is_active()


def get_waffle_flag_model():
"""
Returns the waffle Flag model that is active in this project.
"""
# Add backwards compatibility by not requiring adding of WAFFLE_FLAG_MODEL
# for everyone who upgrades.
# At some point it would be helpful to require this to be defined explicitly,
# but no for now, to remove pain form upgrading.
flag_model_name = get_setting('FLAG_MODEL', 'waffle.Flag')

try:
return django_apps.get_model(flag_model_name)
except ValueError:
raise ImproperlyConfigured("WAFFLE_FLAG_MODEL must be of the form 'app_label.model_name'")
except LookupError:
raise ImproperlyConfigured(
"WAFFLE_FLAG_MODEL refers to model '{}' that has not been installed".format(
flag_model_name
)
)

0 comments on commit 4267c01

Please sign in to comment.