3851 - Make suborg non unique portfolio unique (#3888)

* made changes

* added comma

* migrations

* fixing for tests

* removed an import

* possible change

* removed unique value

* redid the migration

* just migrating the suborg

* with sandbox fixes

* added comma

* added unique true for the suborg

* added test

* cleanup commented out section

* ran docker app black

* added unique portfolio

* this worked

* added the federal agency non duplicates

* updated to allow unique values

* another alternative for non unique federal agencies

* using clean

* updated message

* comment

* updates to the model

* adjusted constraints

* update the filter

* fixed failing tests

* made changes to suborg

* added the pk field

* removed random debugger html

* removed extra logic in a test

* formatted file

* ran formatter

* edited logic for checking federal agency to include non-federal agency

* ran the formatter

* removed the soft delete constraint

* added migration

* added exclude pk

* added test back

* fixed the tests

* formatted files

* reverted changes to the test file

* debugging line

* updated logic
This commit is contained in:
asaki222 2025-06-25 17:08:20 -04:00 committed by GitHub
parent 57a717c838
commit 263c6ef631
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 74 additions and 2 deletions

View file

@ -206,7 +206,9 @@ class Command(BaseCommand):
# == Handle domains and requests == # # == Handle domains and requests == #
for portfolio_org_name, portfolio in portfolios_to_use_dict.items(): for portfolio_org_name, portfolio in portfolios_to_use_dict.items():
federal_agency = agencies_dict.get(portfolio_org_name) federal_agency = agencies_dict.get(portfolio_org_name)
suborgs = portfolio.portfolio_suborganizations.in_bulk(field_name="name") suborgs = {}
for suborg in portfolio.portfolio_suborganizations.all():
suborgs[suborg.name] = suborg
if parse_domains: if parse_domains:
updated_domains = self.update_domains(portfolio, federal_agency, suborgs, debug) updated_domains = self.update_domains(portfolio, federal_agency, suborgs, debug)

View file

@ -0,0 +1,23 @@
# Generated by Django 4.2.20 on 2025-06-24 14:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0151_alter_suborganization_options"),
]
operations = [
migrations.AlterField(
model_name="portfolio",
name="organization_name",
field=models.CharField(blank=True, null=True, unique=True),
),
migrations.AlterField(
model_name="suborganization",
name="name",
field=models.CharField(max_length=1000, verbose_name="Suborganization"),
),
]

View file

@ -7,6 +7,7 @@ from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
from django.db.models import Q from django.db.models import Q
from .utility.time_stamped_model import TimeStampedModel from .utility.time_stamped_model import TimeStampedModel
from django.core.exceptions import ValidationError
class Portfolio(TimeStampedModel): class Portfolio(TimeStampedModel):
@ -36,6 +37,7 @@ class Portfolio(TimeStampedModel):
organization_name = models.CharField( organization_name = models.CharField(
null=True, null=True,
blank=True, blank=True,
unique=True,
) )
organization_type = models.CharField( organization_type = models.CharField(
@ -135,6 +137,19 @@ class Portfolio(TimeStampedModel):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def clean(self):
# Checks if federal agency already exists in the portfolio table
if (
self.federal_agency != FederalAgency.get_non_federal_agency()
and Portfolio.objects.exclude(pk=self.pk).filter(federal_agency=self.federal_agency).exists()
):
raise ValidationError({"federal_agency": "Portfolio with this federal agency already exists"})
# Checks if organization name already exists in the portfolio table (not case sensitive)
if Portfolio.objects.exclude(pk=self.pk).filter(organization_name__iexact=self.organization_name).exists():
raise ValidationError({"organization_name": "Portfolio with this name already exists"})
@property @property
def federal_type(self): def federal_type(self):
"""Returns the federal_type value on the underlying federal_agency field""" """Returns the federal_type value on the underlying federal_agency field"""

View file

@ -2,6 +2,7 @@ from django.db import models
from registrar.models.domain_request import DomainRequest from registrar.models.domain_request import DomainRequest
from .utility.time_stamped_model import TimeStampedModel from .utility.time_stamped_model import TimeStampedModel
from django.core.exceptions import ValidationError
class Suborganization(TimeStampedModel): class Suborganization(TimeStampedModel):
@ -13,7 +14,6 @@ class Suborganization(TimeStampedModel):
ordering = ["name"] ordering = ["name"]
name = models.CharField( name = models.CharField(
unique=True,
max_length=1000, max_length=1000,
verbose_name="Suborganization", verbose_name="Suborganization",
) )
@ -39,3 +39,17 @@ class Suborganization(TimeStampedModel):
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.name}" return f"{self.name}"
def clean(self):
if (
Suborganization.objects.exclude(pk=self.pk)
.filter(
portfolio=self.portfolio,
name__iexact=self.name,
)
.exists()
):
raise ValidationError({"name": "Name already exists in Portfolio"})
def save(self, *args, **kwargs):
super().save(*args, **kwargs)

View file

@ -73,6 +73,8 @@ from registrar.models.utility.portfolio_helper import UserPortfolioPermissionCho
from django.contrib.sessions.backends.db import SessionStore from django.contrib.sessions.backends.db import SessionStore
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib import messages from django.contrib import messages
from django.db import transaction
from django.core.exceptions import ValidationError
from unittest.mock import ANY, call, patch, Mock from unittest.mock import ANY, call, patch, Mock
@ -4183,6 +4185,15 @@ class TestPortfolioAdmin(TestCase):
suborg_names = [li.text for li in soup.find_all("li")] suborg_names = [li.text for li in soup.find_all("li")]
self.assertEqual(suborg_names, ["Sub1", "Sub2", "Sub3", "Sub4", "Sub5"]) self.assertEqual(suborg_names, ["Sub1", "Sub2", "Sub3", "Sub4", "Sub5"])
def test_can_have_dup_suborganizatons(self):
portfolio = Portfolio.objects.create(organization_name="Test portfolio too", creator=self.superuser)
Suborganization.objects.create(name="Sub1", portfolio=portfolio)
Suborganization.objects.create(name="Sub1", portfolio=portfolio)
with self.assertRaises(ValidationError):
test_dup = Suborganization(name="sub1", portfolio=portfolio)
test_dup.full_clean()
@less_console_noise_decorator @less_console_noise_decorator
def test_domains_display(self): def test_domains_display(self):
"""Tests the custom domains field which displays all related domains""" """Tests the custom domains field which displays all related domains"""
@ -4339,6 +4350,13 @@ class TestPortfolioAdmin(TestCase):
federal_agency.delete() federal_agency.delete()
portfolio.delete() portfolio.delete()
@less_console_noise_decorator
def test_duplicate_portfolio(self):
with self.assertRaises(ValidationError):
with transaction.atomic():
portfolio_dup = Portfolio(organization_name="Test portfolio", creator=self.superuser)
portfolio_dup.full_clean()
class TestTransferUser(WebTest): class TestTransferUser(WebTest):
"""User transfer custom admin page""" """User transfer custom admin page"""