diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 485751b3c..d06714de2 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -15,7 +15,9 @@ from django.contrib.contenttypes.models import ContentType from django.urls import reverse from dateutil.relativedelta import relativedelta # type: ignore from epplibwrapper.errors import ErrorCode, RegistryError +from waffle.admin import FlagAdmin from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website +from waffle.models import Sample, Switch from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes from registrar.views.utility.mixins import OrderableFieldsMixin from django.contrib.admin.views.main import ORDER_VAR @@ -2157,8 +2159,19 @@ class UserGroupAdmin(AuditedAdmin): def user_group(self, obj): return obj.name +class WaffleFlagAdmin(FlagAdmin): + class Meta: + """Contains meta information about this class""" + model = models.WaffleFlag + fields = "__all__" admin.site.unregister(LogEntry) # Unregister the default registration + +# Unregister samples and switches from django-waffle, as we currently don't use these. +# TODO - address this +admin.site.unregister(Sample) +admin.site.unregister(Switch) + admin.site.register(LogEntry, CustomLogEntryAdmin) admin.site.register(models.User, MyUserAdmin) # Unregister the built-in Group model @@ -2180,3 +2193,6 @@ admin.site.register(models.PublicContact, PublicContactAdmin) admin.site.register(models.DomainRequest, DomainRequestAdmin) admin.site.register(models.TransitionDomain, TransitionDomainAdmin) admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin) + +# Register our custom waffle flag implementation +admin.site.register(models.WaffleFlag, WaffleFlagAdmin) \ No newline at end of file diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 885d95b75..96663adf9 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -22,7 +22,6 @@ from base64 import b64decode from cfenv import AppEnv # type: ignore from pathlib import Path from typing import Final - from botocore.config import Config # # # ### @@ -326,7 +325,10 @@ SERVER_EMAIL = "root@get.gov" # region: Waffle feature flags-----------------------------------------------------------### # If Waffle encounters a reference to a flag that is not in the database, should Waffle create the flag? -# WAFFLE_CREATE_MISSING_FLAGS +WAFFLE_CREATE_MISSING_FLAGS = False + +# The model that will be used to keep track of flags. Extends AbstractUserFlag. +WAFFLE_FLAG_MODEL = "registrar.WaffleFlag" # endregion diff --git a/src/registrar/migrations/0089_user_verification_type.py b/src/registrar/migrations/0089_user_verification_type copy.py similarity index 100% rename from src/registrar/migrations/0089_user_verification_type.py rename to src/registrar/migrations/0089_user_verification_type copy.py diff --git a/src/registrar/migrations/0090_waffleflag.py b/src/registrar/migrations/0090_waffleflag.py new file mode 100644 index 000000000..d8e6fb229 --- /dev/null +++ b/src/registrar/migrations/0090_waffleflag.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.10 on 2024-04-30 16:25 + +from django.conf import settings +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('registrar', '0089_user_verification_type copy'), + ] + + operations = [ + migrations.CreateModel( + name='WaffleFlag', + fields=[ + ('id', models.BigAutoField(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.BooleanField(blank=True, help_text='Flip this flag on (Yes) or off (No) for everyone, overriding all other settings. Leave as Unknown to use normally.', null=True, 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')), + ('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, + }, + ), + ] diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index d3bbb3ae5..aac9e09b6 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -15,6 +15,7 @@ from .user_group import UserGroup from .website import Website from .transition_domain import TransitionDomain from .verified_by_staff import VerifiedByStaff +from .waffle_flag import WaffleFlag __all__ = [ "Contact", @@ -33,6 +34,7 @@ __all__ = [ "Website", "TransitionDomain", "VerifiedByStaff", + "WaffleFlag", ] auditlog.register(Contact) @@ -51,3 +53,4 @@ auditlog.register(UserGroup, m2m_fields=["permissions"]) auditlog.register(Website) auditlog.register(TransitionDomain) auditlog.register(VerifiedByStaff) +auditlog.register(WaffleFlag) \ No newline at end of file diff --git a/src/registrar/models/waffle_flag.py b/src/registrar/models/waffle_flag.py new file mode 100644 index 000000000..827947cb3 --- /dev/null +++ b/src/registrar/models/waffle_flag.py @@ -0,0 +1,37 @@ +from waffle.models import AbstractUserFlag +import logging + +logger = logging.getLogger(__name__) + + +class WaffleFlag(AbstractUserFlag): + """ + Custom implementation of django-waffles 'Flag' object. + Read more here: https://waffle.readthedocs.io/en/stable/types/flag.html + + Use this class when dealing with feature flags, such as profile_feature. + """ + + class Meta: + """Contains meta information about this class""" + verbose_name = "waffle flag" + verbose_name_plural = "Waffle flags" + + @classmethod + def create_waffle_flags(cls): + """ + Creates a pre-defined list of flags for our migrations. + """ + logger.info("Creating default waffle flags...") + try: + # Flags can be activated through the command line or through django admin. + # To keep the scope of this function minimal and simple, if we require additional + # config on these flag, it should be done in a seperate function or as a command. + flag_names = [ + "profile_feature", + "dns_hosting_feature", + ] + flags = [cls(name=flag_name) for flag_name in flag_names] + cls.objects.bulk_create(flags) + except Exception as e: + logger.error(f"An error occurred when attempting to create WaffleFlags: {e}") \ No newline at end of file diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index 814ea2656..9a801f274 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -72,7 +72,7 @@ {% if not IS_PRODUCTION %} {% include "includes/non-production-alert.html" %} - {% if profile_feature_flag %} + {% if has_profile_feature_flag %}