From db68aa9f70be89bf988d962ffd78e9256f74a23c Mon Sep 17 00:00:00 2001 From: Rob Hudson Date: Thu, 19 Sep 2024 16:28:14 -0700 Subject: [PATCH] =?UTF-8?q?Import=20config=20waffles=20to=20django=20waffl?= =?UTF-8?q?es=20=F0=9F=A7=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commands/import_waffle_switches.py | 29 ++++++ bedrock/firefox/firefox_details.py | 27 +----- bedrock/firefox/templatetags/helpers.py | 25 +---- bedrock/firefox/tests/test_firefox_details.py | 86 ----------------- bedrock/mozorg/context_processors.py | 17 ---- .../mozorg/tests/test_context_processors.py | 43 --------- bedrock/mozorg/tests/test_views.py | 21 ---- bedrock/settings/base.py | 10 +- docs/funnelcake.rst | 96 ------------------- docs/index.rst | 1 - requirements/dev.txt | 5 + requirements/prod.in | 11 ++- requirements/prod.txt | 5 + 13 files changed, 51 insertions(+), 325 deletions(-) create mode 100644 bedrock/base/management/commands/import_waffle_switches.py delete mode 100644 bedrock/mozorg/tests/test_context_processors.py delete mode 100644 docs/funnelcake.rst diff --git a/bedrock/base/management/commands/import_waffle_switches.py b/bedrock/base/management/commands/import_waffle_switches.py new file mode 100644 index 00000000000..4e0260461f5 --- /dev/null +++ b/bedrock/base/management/commands/import_waffle_switches.py @@ -0,0 +1,29 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from django.core.management.base import BaseCommand + +from waffle.models import Switch + +from bedrock.base.models import ConfigValue +from bedrock.utils.management.decorators import alert_sentry_on_exception + + +@alert_sentry_on_exception +class Command(BaseCommand): + def handle(self, *args, **options): + prefix = "SWITCH_" + + for config in ConfigValue.objects.all(): + # Ignore funnelcakes and other yummy things. + if not config.name.startswith(prefix): + continue + + # Remove prefix. + name = config.name[len(prefix) :] + # Set active to boolean. + active = config.value == "on" + + switch, created = Switch.objects.update_or_create(name=name, defaults={"active": active}) + print(f"{'Created new' if created else 'Updated'} switch: {name} = {'on' if active else 'off'}") diff --git a/bedrock/firefox/firefox_details.py b/bedrock/firefox/firefox_details.py index 35c6063e73b..7047339b58e 100644 --- a/bedrock/firefox/firefox_details.py +++ b/bedrock/firefox/firefox_details.py @@ -2,8 +2,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -# coding=utf-8 - import re from collections import OrderedDict from operator import itemgetter @@ -11,11 +9,8 @@ from django.conf import settings -from everett.manager import ListOf from product_details import ProductDetails -from bedrock.base.waffle import config - # TODO: port this to django-mozilla-product-details class _ProductDetails(ProductDetails): @@ -225,8 +220,6 @@ def get_download_url( locale, force_direct=False, force_full_installer=False, - force_funnelcake=False, - funnelcake_id=None, locale_in_transition=False, ): """ @@ -239,9 +232,6 @@ def get_download_url( always True for non-release URLs. :param force_full_installer: Force the installer download to not be the stub installer (for aurora). - :param force_funnelcake: Force the download version for en-US Windows to be - 'latest', which bouncer will translate to the funnelcake build. - :param funnelcake_id: ID for the the funnelcake build. :param locale_in_transition: Include the locale in the transition URL :return: string url """ @@ -253,7 +243,6 @@ def get_download_url( force_direct = True if channel != "release" else force_direct stub_platforms = ["win", "win64"] esr_channels = ["esr", "esr_next"] - include_funnelcake_param = False # support optional MSI installer downloads # bug 1493205 @@ -261,21 +250,11 @@ def get_download_url( if is_msi: platform = platform[:-4] - # Bug 1345467 - Only allow specifically configured funnelcake builds - if funnelcake_id: - fc_platforms = config(f"FUNNELCAKE_{funnelcake_id}_PLATFORMS", default="", parser=ListOf(str)) - fc_locales = config(f"FUNNELCAKE_{funnelcake_id}_LOCALES", default="", parser=ListOf(str)) - include_funnelcake_param = platform in fc_platforms and _locale in fc_locales - # Check if direct download link has been requested # if not just use transition URL if not force_direct: # build a link to the transition page transition_url = self.download_base_url_transition - if funnelcake_id: - # include funnelcake in scene 2 URL - transition_url += f"?f={funnelcake_id}" - if locale_in_transition: transition_url = f"/{locale}{transition_url}" @@ -294,11 +273,7 @@ def get_download_url( prod_name = "firefox-esr-next" elif platform in stub_platforms and not is_msi and not force_full_installer: # Use the stub installer for approved platforms - # append funnelcake id to version if we have one - if include_funnelcake_param: - suffix = f"stub-f{funnelcake_id}" - else: - suffix = "stub" + suffix = "stub" elif channel == "nightly" and locale != "en-US": # Nightly uses a different product name for localized builds, # and is the only one ಠ_ಠ diff --git a/bedrock/firefox/templatetags/helpers.py b/bedrock/firefox/templatetags/helpers.py index e32406bbf12..24e28c73a3c 100644 --- a/bedrock/firefox/templatetags/helpers.py +++ b/bedrock/firefox/templatetags/helpers.py @@ -24,8 +24,6 @@ def desktop_builds( locale=None, force_direct=False, force_full_installer=False, - force_funnelcake=False, - funnelcake_id=False, locale_in_transition=False, classified=False, ): @@ -65,8 +63,6 @@ def desktop_builds( locale, force_direct=force_direct, force_full_installer=force_full_installer, - force_funnelcake=force_funnelcake, - funnelcake_id=funnelcake_id, locale_in_transition=locale_in_transition, ) @@ -83,8 +79,6 @@ def desktop_builds( locale, force_direct=True, force_full_installer=force_full_installer, - force_funnelcake=force_funnelcake, - funnelcake_id=funnelcake_id, ) if download_link_direct == download_link: download_link_direct = False @@ -120,7 +114,6 @@ def download_firefox( locale=None, force_direct=False, force_full_installer=False, - force_funnelcake=False, alt_copy=None, button_class="mzp-t-xl", locale_in_transition=False, @@ -136,8 +129,6 @@ def download_firefox( :param force_direct: Force the download URL to be direct. :param force_full_installer: Force the installer download to not be the stub installer (for aurora). - :param force_funnelcake: Force the download version for en-US Windows to be - 'latest', which bouncer will translate to the funnelcake build. :param alt_copy: Specifies alternate copy to use for download buttons. :param button_class: Classes to add to the download button, contains size mzp-t-xl by default :param locale_in_transition: Include the page locale in transitional download link. @@ -149,7 +140,6 @@ def download_firefox( show_ios = platform in ["all", "ios"] alt_channel = "" if channel == "release" else channel locale = locale or get_locale(ctx["request"]) - funnelcake_id = ctx.get("funnelcake_id", False) dom_id = dom_id or f"download-button-{'desktop' if platform == 'all' else platform}-{channel}" # Gather data about the build for each platform @@ -157,7 +147,7 @@ def download_firefox( if show_desktop: version = firefox_desktop.latest_version(channel) - builds = desktop_builds(channel, builds, locale, force_direct, force_full_installer, force_funnelcake, funnelcake_id, locale_in_transition) + builds = desktop_builds(channel, builds, locale, force_direct, force_full_installer, locale_in_transition) if show_android: version = firefox_android.latest_version(channel) @@ -208,16 +198,10 @@ def download_firefox_thanks(ctx, dom_id=None, locale=None, alt_copy=None, button channel = "release" locale = locale or get_locale(ctx["request"]) - funnelcake_id = ctx.get("funnelcake_id", False) dom_id = dom_id or "download-button-thanks" transition_url = "/firefox/download/thanks/" version = firefox_desktop.latest_version(channel) - # if there's a funnelcake param in the page URL e.g. ?f=123 - if funnelcake_id: - # include param in transitional URL e.g. /firefox/download/thanks/?f=123 - transition_url += f"?f={funnelcake_id}" - if locale_in_transition: transition_url = f"/{locale}{transition_url}" @@ -228,8 +212,6 @@ def download_firefox_thanks(ctx, dom_id=None, locale=None, alt_copy=None, button locale, force_direct=True, force_full_installer=False, - force_funnelcake=False, - funnelcake_id=funnelcake_id, ) data = { @@ -262,8 +244,9 @@ def download_firefox_desktop_list(ctx, channel="release", dom_id=None, locale=No dom_id = dom_id or f"download-platform-list-{channel}" locale = locale or get_locale(ctx["request"]) - # Make sure funnelcake_id is not passed as builds are often Windows only. - builds = desktop_builds(channel, None, locale, True, force_full_installer, False, False, False, True) + builds = desktop_builds( + channel, builds=None, locale=locale, force_direct=True, force_full_installer=force_full_installer, locale_in_transition=False, classified=True + ) recommended_builds = [] traditional_builds = [] diff --git a/bedrock/firefox/tests/test_firefox_details.py b/bedrock/firefox/tests/test_firefox_details.py index 14c3d14ee7d..43d53ae71ec 100644 --- a/bedrock/firefox/tests/test_firefox_details.py +++ b/bedrock/firefox/tests/test_firefox_details.py @@ -655,28 +655,6 @@ def test_get_download_url_nightly_l10n(self): ], ) - def test_get_download_url_scene2_funnelcake(self): - scene2 = self.firefox_desktop.download_base_url_transition - url = self.firefox_desktop.get_download_url("release", "45.0", "win", "en-US") - self.assertEqual(url, scene2) - url = self.firefox_desktop.get_download_url("release", "45.0", "win", "en-US", funnelcake_id="64") - self.assertEqual(url, scene2 + "?f=64") - - def test_get_download_url_scene2_with_locale(self): - scene2 = self.firefox_desktop.download_base_url_transition - url = self.firefox_desktop.get_download_url("release", "45.0", "win", "de", locale_in_transition=True) - self.assertEqual(url, "/de" + scene2) - - url = self.firefox_desktop.get_download_url( - "release", - "45.0", - "win", - "fr", - locale_in_transition=True, - funnelcake_id="64", - ) - self.assertEqual(url, "/fr" + scene2 + "?f=64") - def get_download_url_ssl(self): """ SSL-enabled links should always be used except Windows stub installers. @@ -810,70 +788,6 @@ def test_latest_major_version_no_int(self): ): assert self.firefox_desktop.latest_major_version("release") == 0 - @patch.dict(os.environ, FUNNELCAKE_64_LOCALES="en-US", FUNNELCAKE_64_PLATFORMS="win") - def test_funnelcake_direct_links_en_us_win_only(self): - """ - Ensure funnelcake params are included for Windows en-US builds only. - """ - url = self.firefox_desktop.get_download_url("release", "45.0", "win", "en-US", force_direct=True, funnelcake_id="64") - assert "product=firefox-stub-f64" in url - - url = self.firefox_desktop.get_download_url("release", "45.0", "win64", "en-US", force_direct=True, funnelcake_id="64") - assert "product=firefox-stub-f64" not in url - - url = self.firefox_desktop.get_download_url("release", "45.0", "win", "de", force_direct=True, funnelcake_id="64") - assert "product=firefox-stub-f64" not in url - - url = self.firefox_desktop.get_download_url("release", "45.0", "osx", "en-US", force_direct=True, funnelcake_id="64") - assert "product=firefox-stub-f64" not in url - - def test_no_funnelcake_direct_links_if_not_configured(self): - """ - Ensure funnelcake params are included for Linux and OSX en-US builds only. - """ - url = self.firefox_desktop.get_download_url("release", "45.0", "win", "en-US", force_direct=True, funnelcake_id="64") - assert "-f64" not in url - - url = self.firefox_desktop.get_download_url( - "release", - "45.0", - "win", - "en-US", - force_direct=True, - force_full_installer=True, - funnelcake_id="64", - ) - assert "-f64" not in url - - url = self.firefox_desktop.get_download_url( - "release", - "45.0", - "win", - "en-US", - force_direct=True, - force_funnelcake=True, - funnelcake_id="64", - ) - assert "-f64" not in url - - url = self.firefox_desktop.get_download_url("release", "45.0", "osx", "de", force_direct=True, funnelcake_id="64") - assert "-f64" not in url - - url = self.firefox_desktop.get_download_url("release", "45.0", "osx", "en-US", force_direct=True, funnelcake_id="64") - assert "-f64" not in url - - url = self.firefox_desktop.get_download_url("release", "45.0", "osx", "fr", force_direct=True, funnelcake_id="64") - assert "-f64" not in url - - url = self.firefox_desktop.get_download_url("release", "45.0", "linux", "de", force_direct=True, funnelcake_id="64") - assert "-f64" not in url - - url = self.firefox_desktop.get_download_url("release", "45.0", "linux", "en-US", force_direct=True, funnelcake_id="64") - assert "-f64" not in url - - url = self.firefox_desktop.get_download_url("release", "45.0", "linux", "fr", force_direct=True, funnelcake_id="64") - assert "-f64" not in url - def test_stub_installer_win_only(self): """ Ensure that builds not in the setting don't get stub. diff --git a/bedrock/mozorg/context_processors.py b/bedrock/mozorg/context_processors.py index 2604f75c48a..d55dad26f86 100644 --- a/bedrock/mozorg/context_processors.py +++ b/bedrock/mozorg/context_processors.py @@ -6,7 +6,6 @@ from datetime import datetime from django.conf import settings -from django.urls import reverse # match 1 - 4 digits only FC_RE = re.compile(r"^\d{1,4}$") @@ -26,21 +25,5 @@ def current_year(request): return {"current_year": datetime.today().year} -def funnelcake_param(request): - """If a query param for a funnelcake is sent, add it to the context.""" - fc_id = request.GET.get("f", None) - context = {} - - if fc_id and FC_RE.match(fc_id): - # special case for installer-help page - # bug 933852 - installer_help_url = reverse("firefox.installer-help") - if installer_help_url in request.path_info: - fc_id = str(int(fc_id) + 1) - context["funnelcake_id"] = fc_id - - return context - - def contrib_numbers(request): return settings.CONTRIBUTE_NUMBERS diff --git a/bedrock/mozorg/tests/test_context_processors.py b/bedrock/mozorg/tests/test_context_processors.py deleted file mode 100644 index cc20972b14f..00000000000 --- a/bedrock/mozorg/tests/test_context_processors.py +++ /dev/null @@ -1,43 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. - -from django.test.client import RequestFactory - -from bedrock.base.urlresolvers import reverse -from bedrock.mozorg.context_processors import funnelcake_param -from bedrock.mozorg.tests import TestCase - - -class TestFunnelcakeParam(TestCase): - def setUp(self): - self.rf = RequestFactory() - - def _funnelcake(self, url="/", **kwargs): - return funnelcake_param(self.rf.get(url, kwargs)) - - def test_funnelcake_param_noop(self): - """Should return an empty dict normally.""" - assert self._funnelcake() == {} - - def test_funnelcake_param_f(self): - """Should inject funnelcake into context.""" - assert self._funnelcake(f="5") == {"funnelcake_id": "5"} - assert self._funnelcake(f="234") == {"funnelcake_id": "234"} - - def test_funnelcake_param_bad(self): - """Should not inject bad funnelcake into context.""" - assert self._funnelcake(f="5dude") == {} - assert self._funnelcake(f="123456") == {} - - def test_funnelcake_param_increment_installer_help(self): - """FC param should be +1 on the firefox/installer-help/ page. - - Bug 933852. - """ - url = reverse("firefox.installer-help") - ctx = self._funnelcake(url, f="20") - assert ctx["funnelcake_id"] == "21" - - ctx = self._funnelcake(url, f="10") - assert ctx["funnelcake_id"] == "11" diff --git a/bedrock/mozorg/tests/test_views.py b/bedrock/mozorg/tests/test_views.py index 51d31eddba3..71331430cd1 100644 --- a/bedrock/mozorg/tests/test_views.py +++ b/bedrock/mozorg/tests/test_views.py @@ -3,7 +3,6 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. import json -import os from unittest.mock import Mock, patch from django.core import mail @@ -19,26 +18,6 @@ from bedrock.mozorg.tests import TestCase -class TestViews(TestCase): - @patch.dict(os.environ, FUNNELCAKE_5_LOCALES="en-US", FUNNELCAKE_5_PLATFORMS="win") - def test_download_button_funnelcake(self): - """The download button should have the funnelcake ID.""" - with self.activate_locale("en-US"): - resp = self.client.get(reverse("firefox.download.thanks"), {"f": "5"}) - assert b"product=firefox-stub-f5&" in resp.content - - def test_download_button_bad_funnelcake(self): - """The download button should not have a bad funnelcake ID.""" - with self.activate_locale("en-US"): - resp = self.client.get(reverse("firefox.download.thanks"), {"f": "5dude"}) - assert b"product=firefox-stub&" in resp.content - assert b"product=firefox-stub-f5dude&" not in resp.content - - resp = self.client.get(reverse("firefox.download.thanks"), {"f": "999999999"}) - assert b"product=firefox-stub&" in resp.content - assert b"product=firefox-stub-f999999999&" not in resp.content - - class TestRobots(TestCase): def setUp(self): self.rf = RequestFactory() diff --git a/bedrock/settings/base.py b/bedrock/settings/base.py index 6ab551876c3..3a2a2e4e4d7 100644 --- a/bedrock/settings/base.py +++ b/bedrock/settings/base.py @@ -742,6 +742,7 @@ def get_app_name(hostname): # third-party apps "django_jinja_markdown", "django_jinja", + "waffle", "watchman", # Wagtail CMS and related, necessary apps "wagtail.contrib.redirects", @@ -860,7 +861,6 @@ def _is_bedrock_custom_app(app_name): "bedrock.mozorg.context_processors.canonical_path", "bedrock.mozorg.context_processors.contrib_numbers", "bedrock.mozorg.context_processors.current_year", - "bedrock.mozorg.context_processors.funnelcake_param", "bedrock.firefox.context_processors.latest_firefox_versions", ], "extensions": [ @@ -1270,14 +1270,6 @@ def before_send(event, hint): # Django-CSP settings are in settings/__init__.py, where they are # set according to site mode -# Bug 1345467: Funnelcakes are now explicitly configured in the environment. -# Set experiment specific variables like the following: -# -# FUNNELCAKE_103_PLATFORMS=win,win64 -# FUNNELCAKE_103_LOCALES=de,fr,en-US -# -# where "103" in the variable name is the funnelcake ID. - # Countries that need to see cookie banner # See https://www.gov.uk/eu-eea diff --git a/docs/funnelcake.rst b/docs/funnelcake.rst deleted file mode 100644 index f16a67a6ccf..00000000000 --- a/docs/funnelcake.rst +++ /dev/null @@ -1,96 +0,0 @@ -.. This Source Code Form is subject to the terms of the Mozilla Public -.. License, v. 2.0. If a copy of the MPL was not distributed with this -.. file, You can obtain one at https://mozilla.org/MPL/2.0/. - -.. _funnelcake: - -=============================== -Funnel cakes and Partner Builds -=============================== - -Funnel cakes ------------- - -In addition to being `an American delicacy `_ -funnel cakes are what we call special builds of Firefox. They can come with -extensions preinstalled and/or a custom first-run experience. - - "The whole funnelcake system is so marred by history at this point I don't - know if anyone fully understands what it's supposed to do in all situations" - - pmac - -Funnelcakes are configured by the Release Engineering team. You can see the -configs in the `funnelcake git repo `_ - -Currently bedrock only supports funnelcakes for "stub installer platforms". Which -means they are windows only. However, funnelcakes can be made for all platforms -so `bedrock support may expand `_. - -We signal to bedrock that we want a funnelcake when linking to the download -page by appending the query variable `f` with a value equal to the funnelcake -number being requested. - -.. code-block:: text - - https://www.mozilla.org/en-US/firefox/download/thanks/?f=137 - -Bedrock checks to see if the funnelcake is configured (this is handled in the -`www-config repo `_) - -.. code-block:: bash - - FUNNELCAKE_135_LOCALES=en-US - FUNNELCAKE_135_PLATFORMS=win,win64 - -Bedrock then converts that into a request to download a file like so: - -Windows: - -.. code-block:: text - - https://download.mozilla.org/?product=firefox-stub-f137&os=win&lang=en-US - -Mac (You can see the mac one does not pass the funnelcake number along.): - -.. code-block:: text - - https://download.mozilla.org/?product=firefox-latest-ssl&os=osx&lang=en-US - -Someone in Release Engineering needs to set up the redirects on their side to -take the request from here. - -Places things can go wrong -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -As with many technical things, the biggest potential problems are with people: - -- Does it have executive approval? -- Did legal sign off? -- Has it had a security review? - -On the technical side: - -- Is the switch enabled? -- Is the variable being passed? - -Partner builds --------------- - -Bedrock does not have an automated way of handling these, so you'll have to -craft your own download button: - -.. code-block:: html - - - Download - ------------- - -Bugs that might have useful info: - -- https://bugzilla.mozilla.org/show_bug.cgi?id=1450463 -- https://bugzilla.mozilla.org/show_bug.cgi?id=1495050 - -PRs that might have useful code: - -- https://github.com/mozilla/bedrock/pull/5555 diff --git a/docs/index.rst b/docs/index.rst index 0323e62ec43..8657bb539b2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -41,7 +41,6 @@ Contents send-to-device download-buttons mozilla-accounts - funnelcake abtest vpn-subscriptions attribution diff --git a/requirements/dev.txt b/requirements/dev.txt index 1ff70d6c82f..1fced800535 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -502,6 +502,7 @@ django==4.2.16 \ # django-storages # django-taggit # django-treebeard + # django-waffle # django-watchman # djangorestframework # laces @@ -588,6 +589,10 @@ django-treebeard==4.7.1 \ # via # -r requirements/prod.txt # wagtail +django-waffle==4.1.0 \ + --hash=sha256:5979a2f3dd674ef7086480525b39651fc2045427f6d8e6a614192656d3402c5b \ + --hash=sha256:e49d7d461d89f3bd8e53f20efe39310acca8f275c9888495e68e195345bf18b1 + # via -r requirements/prod.txt django-watchman==1.3.0 \ --hash=sha256:33b5fc734d689b83cb96fc17beda624ae2955f4cede0856897d990c363eac962 \ --hash=sha256:5f04300bd7fbdd63b8a883b2730ed1e4d9b0f9991133b33a1281134b81f466eb diff --git a/requirements/prod.in b/requirements/prod.in index 16806eb8dbf..8ee9f390b03 100644 --- a/requirements/prod.in +++ b/requirements/prod.in @@ -20,10 +20,11 @@ django-jinja==2.11.0 django-jsonview==2.0.0 django-memoize==2.3.1 django-mozilla-product-details==1.0.3 +django-rq==2.10.2 django-storages[google]==1.14.4 +django-waffle==4.1.0 django-watchman==1.3.0 Django==4.2.16 -django-rq==2.10.2 docutils==0.21.2 envcat==0.1.1 everett==3.3.0 @@ -50,11 +51,11 @@ qrcode==7.4.2 querystringsafe-base64==1.1.1 # Pinned to maintain stub attribution signatures https://github.com/mozilla/bedrock/issues/11156 requests>=2.32.3 # min secure version rich-text-renderer==0.2.8 -sentry-sdk==2.14.0 sentry-processor==0.0.1 +sentry-sdk==2.14.0 supervisor==4.2.5 timeago==1.0.16 -whitenoise==6.7.0 -Wagtail==6.1.3 -wagtail-localize==1.9.1 wagtail-localize-smartling==0.3.0 +wagtail-localize==1.9.1 +Wagtail==6.1.3 +whitenoise==6.7.0 diff --git a/requirements/prod.txt b/requirements/prod.txt index a8d91e599b2..eeb02c1fdb3 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -329,6 +329,7 @@ django==4.2.16 \ # django-storages # django-taggit # django-treebeard + # django-waffle # django-watchman # djangorestframework # laces @@ -405,6 +406,10 @@ django-treebeard==4.7.1 \ --hash=sha256:846e462904b437155f76e04907ba4e48480716855f88b898df4122bdcfbd6e98 \ --hash=sha256:995c7120153ab999898fe3043bbdcd8a0fc77cc106eb94de7350e9d02c885135 # via wagtail +django-waffle==4.1.0 \ + --hash=sha256:5979a2f3dd674ef7086480525b39651fc2045427f6d8e6a614192656d3402c5b \ + --hash=sha256:e49d7d461d89f3bd8e53f20efe39310acca8f275c9888495e68e195345bf18b1 + # via -r requirements/prod.in django-watchman==1.3.0 \ --hash=sha256:33b5fc734d689b83cb96fc17beda624ae2955f4cede0856897d990c363eac962 \ --hash=sha256:5f04300bd7fbdd63b8a883b2730ed1e4d9b0f9991133b33a1281134b81f466eb