diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 9c721dee4..d1cb287a3 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2720,6 +2720,14 @@ class WaffleFlagAdmin(FlagAdmin): fields = "__all__" +class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin): + list_display = ["name", "portfolio"] + + +class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin): + list_display = ["name", "portfolio"] + + admin.site.unregister(LogEntry) # Unregister the default registration admin.site.register(LogEntry, CustomLogEntryAdmin) @@ -2743,6 +2751,8 @@ admin.site.register(models.DomainRequest, DomainRequestAdmin) admin.site.register(models.TransitionDomain, TransitionDomainAdmin) admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin) admin.site.register(models.Portfolio, PortfolioAdmin) +admin.site.register(models.DomainGroup, DomainGroupAdmin) +admin.site.register(models.Suborganization, SuborganizationAdmin) # Register our custom waffle implementations admin.site.register(models.WaffleFlag, WaffleFlagAdmin) diff --git a/src/registrar/migrations/0105_suborganization_domaingroup.py b/src/registrar/migrations/0105_suborganization_domaingroup.py new file mode 100644 index 000000000..471f9379c --- /dev/null +++ b/src/registrar/migrations/0105_suborganization_domaingroup.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.10 on 2024-06-21 18:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0104_create_groups_v13"), + ] + + operations = [ + migrations.CreateModel( + name="Suborganization", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("name", models.CharField(help_text="Suborganization", max_length=1000, unique=True)), + ("portfolio", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="registrar.portfolio")), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="DomainGroup", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("name", models.CharField(help_text="Domain group", unique=True)), + ("domains", models.ManyToManyField(blank=True, to="registrar.domaininformation")), + ("portfolio", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="registrar.portfolio")), + ], + options={ + "unique_together": {("name", "portfolio")}, + }, + ), + ] diff --git a/src/registrar/migrations/0106_create_groups_v14.py b/src/registrar/migrations/0106_create_groups_v14.py new file mode 100644 index 000000000..816a49ac8 --- /dev/null +++ b/src/registrar/migrations/0106_create_groups_v14.py @@ -0,0 +1,37 @@ +# This migration creates the create_full_access_group and create_cisa_analyst_group groups +# It is dependent on 0079 (which populates federal agencies) +# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS +# in the user_group model then: +# [NOT RECOMMENDED] +# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions +# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups +# step 3: fake run the latest migration in the migrations list +# [RECOMMENDED] +# Alternatively: +# step 1: duplicate the migration that loads data +# step 2: docker-compose exec app ./manage.py migrate + +from django.db import migrations +from registrar.models import UserGroup +from typing import Any + + +# For linting: RunPython expects a function reference, +# so let's give it one +def create_groups(apps, schema_editor) -> Any: + UserGroup.create_cisa_analyst_group(apps, schema_editor) + UserGroup.create_full_access_group(apps, schema_editor) + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0105_suborganization_domaingroup"), + ] + + operations = [ + migrations.RunPython( + create_groups, + reverse_code=migrations.RunPython.noop, + atomic=True, + ), + ] diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index b2cffaf32..376739826 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -17,6 +17,8 @@ from .transition_domain import TransitionDomain from .verified_by_staff import VerifiedByStaff from .waffle_flag import WaffleFlag from .portfolio import Portfolio +from .domain_group import DomainGroup +from .suborganization import Suborganization __all__ = [ @@ -38,6 +40,8 @@ __all__ = [ "VerifiedByStaff", "WaffleFlag", "Portfolio", + "DomainGroup", + "Suborganization", ] auditlog.register(Contact) @@ -58,3 +62,5 @@ auditlog.register(TransitionDomain) auditlog.register(VerifiedByStaff) auditlog.register(WaffleFlag) auditlog.register(Portfolio) +auditlog.register(DomainGroup) +auditlog.register(Suborganization) diff --git a/src/registrar/models/domain_group.py b/src/registrar/models/domain_group.py new file mode 100644 index 000000000..16b1fd203 --- /dev/null +++ b/src/registrar/models/domain_group.py @@ -0,0 +1,23 @@ +from django.db import models +from .utility.time_stamped_model import TimeStampedModel + + +class DomainGroup(TimeStampedModel): + """ + Organized group of domains. + """ + + class Meta: + unique_together = [("name", "portfolio")] + + name = models.CharField( + unique=True, + help_text="Domain group", + ) + + portfolio = models.ForeignKey("registrar.Portfolio", on_delete=models.PROTECT) + + domains = models.ManyToManyField("registrar.DomainInformation", blank=True) + + def __str__(self) -> str: + return f"{self.name}" diff --git a/src/registrar/models/suborganization.py b/src/registrar/models/suborganization.py new file mode 100644 index 000000000..b1e010953 --- /dev/null +++ b/src/registrar/models/suborganization.py @@ -0,0 +1,22 @@ +from django.db import models +from .utility.time_stamped_model import TimeStampedModel + + +class Suborganization(TimeStampedModel): + """ + Suborganization under an organization (portfolio) + """ + + name = models.CharField( + unique=True, + max_length=1000, + help_text="Suborganization", + ) + + portfolio = models.ForeignKey( + "registrar.Portfolio", + on_delete=models.PROTECT, + ) + + def __str__(self) -> str: + return f"{self.name}"