Adds a `features` app with a swappable waffle Flag (WAFFLE_FLAG_MODEL) that gains a m2m to Club, so a feature can be rolled out club by club. Two things worth calling out: - `everyone` keeps waffle's contract of overriding *all* other targeting, so club targeting is only consulted when `everyone is None`. This keeps `everyone = False` usable as a hard kill-switch. - m2m edits don't call save(), so waffle's per-flag cache would go stale when clubs are added or removed. A m2m_changed receiver flushes it from both directions, and get_flush_keys() drops the club-set key alongside waffle's own. Note for future flag tests: waffle's cache is not rolled back with the test transaction, so tests touching flags must clear it (see features/tests.py). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
97 lines
3.4 KiB
Python
97 lines
3.4 KiB
Python
from django.core.cache import cache
|
|
from django.test import RequestFactory, TestCase
|
|
from waffle import flag_is_active, get_waffle_flag_model
|
|
|
|
from club.models import Club
|
|
|
|
Flag = get_waffle_flag_model()
|
|
|
|
|
|
class ClubScopedFlagTests(TestCase):
|
|
def setUp(self):
|
|
# waffle caches flags by name, and its cache is NOT rolled back with the
|
|
# test transaction -- a flag row recreated under the same name in the next
|
|
# test would otherwise be shadowed by the previous test's cached object.
|
|
cache.clear()
|
|
self.addCleanup(cache.clear)
|
|
self.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
|
self.other = Club.objects.create(name="Rival FC", slug="rival-fc")
|
|
self.flag = Flag.objects.create(name="shop")
|
|
|
|
def request_for(self, club):
|
|
request = RequestFactory().get("/")
|
|
request.club = club
|
|
request.user = None
|
|
return request
|
|
|
|
def active_for(self, club):
|
|
return flag_is_active(self.request_for(club), "shop")
|
|
|
|
def test_off_for_every_club_by_default(self):
|
|
self.assertFalse(self.active_for(self.club))
|
|
self.assertFalse(self.active_for(self.other))
|
|
|
|
def test_on_only_for_the_targeted_club(self):
|
|
self.flag.clubs.add(self.club)
|
|
|
|
self.assertTrue(self.active_for(self.club))
|
|
self.assertFalse(self.active_for(self.other))
|
|
|
|
def test_removing_a_club_turns_it_off_again(self):
|
|
self.flag.clubs.add(self.club)
|
|
self.flag.clubs.remove(self.club)
|
|
|
|
self.assertFalse(self.active_for(self.club))
|
|
|
|
def test_everyone_true_overrides_club_targeting(self):
|
|
self.flag.everyone = True
|
|
self.flag.save()
|
|
|
|
self.assertTrue(self.active_for(self.other)) # not targeted, still on
|
|
|
|
def test_everyone_false_beats_club_targeting(self):
|
|
# waffle's contract: `everyone` overrides ALL other settings, so a flag
|
|
# switched off for everyone must stay off even for a targeted club.
|
|
self.flag.clubs.add(self.club)
|
|
self.flag.everyone = False
|
|
self.flag.save()
|
|
|
|
self.assertFalse(self.active_for(self.club))
|
|
|
|
def test_no_club_on_the_request_is_not_active(self):
|
|
# e.g. the base domain / control panel, where there is no tenant.
|
|
self.flag.clubs.add(self.club)
|
|
|
|
self.assertFalse(flag_is_active(self.request_for(None), "shop"))
|
|
|
|
def test_is_active_for_club_without_a_request(self):
|
|
self.flag.clubs.add(self.club)
|
|
|
|
self.assertTrue(self.flag.is_active_for_club(self.club))
|
|
self.assertFalse(self.flag.is_active_for_club(self.other))
|
|
|
|
def test_is_active_for_club_respects_everyone(self):
|
|
self.flag.everyone = False
|
|
self.flag.save()
|
|
self.assertFalse(self.flag.is_active_for_club(self.club))
|
|
|
|
self.flag.everyone = True
|
|
self.flag.save()
|
|
self.assertTrue(self.flag.is_active_for_club(self.club))
|
|
|
|
def test_cache_is_flushed_when_club_targeting_changes(self):
|
|
# The M2M does not call save(), so without the flush signal waffle would
|
|
# keep answering from a stale cached set.
|
|
self.assertFalse(self.active_for(self.club)) # primes the cache
|
|
|
|
self.flag.clubs.add(self.club)
|
|
|
|
self.assertTrue(self.active_for(self.club))
|
|
|
|
def test_cache_is_flushed_on_reverse_edit(self):
|
|
self.assertFalse(self.active_for(self.club))
|
|
|
|
self.club.flags.add(self.flag)
|
|
|
|
self.assertTrue(self.active_for(self.club))
|