From 5adf0630844fb798c74f6c899834081d471efd80 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 6 Jun 2024 16:56:48 -0600 Subject: [PATCH 01/15] Initial scaffold of models (still needs logic for auto-populating some fields) --- src/registrar/models/organization.py | 145 +++++++++++++++++++++++++++ src/registrar/models/portfolio.py | 109 ++++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 src/registrar/models/organization.py create mode 100644 src/registrar/models/portfolio.py diff --git a/src/registrar/models/organization.py b/src/registrar/models/organization.py new file mode 100644 index 000000000..3ae54719e --- /dev/null +++ b/src/registrar/models/organization.py @@ -0,0 +1,145 @@ +from django.db import models +from django_fsm import FSMField # type: ignore + +from registrar.models.domain_request import DomainRequest +from registrar.models.federal_agency import FederalAgency +from registrar.models.domain import State + +from .utility.time_stamped_model import TimeStampedModel + + +class Organization(TimeStampedModel): + """ + TODO: + """ + + class Meta: + """Contains meta information about this class""" + + indexes = [ + models.Index(fields=["user"]), + models.Index(fields=["email"]), + ] + + # federal agency - FK to fed agency table (Not nullable, should default to the Non-federal agency value in the fed agency table) + federal_agency = models.ForeignKey( + "registrar.FederalAgency", + on_delete=models.PROTECT, + help_text="Associated federal agency", + unique=False, + default=FederalAgency.objects.filter(agency="Non-Federal Agency").first() + ) + + # creator- user foreign key- stores who created this model + # should get the user who is adding it via django admin if there + # is a user (aka not done via commandline/ manual means) + # TODO: auto-populate + creator = models.ForeignKey( + "registrar.User", + on_delete=models.PROTECT, + help_text="Associated user", + unique=False + ) + + # organization type- should match organization types allowed on domain info + organization_type = models.CharField( + max_length=255, + choices=DomainRequest.OrgChoicesElectionOffice.choices, + null=True, + blank=True, + help_text='"Election" appears after the org type if it\'s an election office.', + ) + + # organization name + # TODO: org name will be the same as federal agency, if it is federal, + # otherwise it will be the actual org name. If nothing is entered for + # org name and it is a federal organization, have this field fill with + # the federal agency text name. + organization_name = models.CharField( + null=True, + blank=True, + ) + + # address_line1 + address_line1 = models.CharField( + null=True, + blank=True, + verbose_name="address line 1", + ) + # address_line2 + address_line2 = models.CharField( + null=True, + blank=True, + verbose_name="address line 2", + ) + # city + city = models.CharField( + null=True, + blank=True, + ) + # state (copied from domain.py -- imports enums from domain.py) + state = FSMField( + max_length=21, + choices=State.choices, + default=State.UNKNOWN, + # cannot change state directly, particularly in Django admin + protected=True, + # This must be defined for custom state help messages, + # as otherwise the view will purge the help field as it does not exist. + help_text=" ", + verbose_name="domain state", + ) + # zipcode + zipcode = models.CharField( + max_length=10, + null=True, + blank=True, + verbose_name="zip code", + ) + # urbanization + urbanization = models.CharField( + null=True, + blank=True, + help_text="Required for Puerto Rico only", + verbose_name="urbanization", + ) + + # security_contact_email + security_contact_email = models.EmailField( + null=True, + blank=True, + verbose_name="security contact e-mail", + max_length=320, + ) + + # def save(self, *args, **kwargs): + # # Call the parent class's save method to perform the actual save + # super().save(*args, **kwargs) + + # if self.user: + # updated = False + + # # Update first name and last name if necessary + # if not self.user.first_name or not self.user.last_name: + # self.user.first_name = self.first_name + # self.user.last_name = self.last_name + # updated = True + + # # Update phone if necessary + # if not self.user.phone: + # self.user.phone = self.phone + # updated = True + + # # Save user if any updates were made + # if updated: + # self.user.save() + + # def __str__(self): + # if self.first_name or self.last_name: + # return self.get_formatted_name() + # elif self.email: + # return self.email + # elif self.pk: + # return str(self.pk) + # else: + # return "" diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py new file mode 100644 index 000000000..801f267f4 --- /dev/null +++ b/src/registrar/models/portfolio.py @@ -0,0 +1,109 @@ +from django.db import models + +from .utility.time_stamped_model import TimeStampedModel + + +class Portfolio(TimeStampedModel): + """ + TODO: + """ + + class Meta: + """Contains meta information about this class""" + + indexes = [ + models.Index(fields=["user"]), + models.Index(fields=["email"]), + ] + + # NOTE: This will be specified in a future ticket + # class PortfolioTypes(models.TextChoices): + # TYPE1 = "foo", "foo" + # TYPE2 = "foo", "foo" + # TYPE3 = "foo", "foo" + # TYPE4 = "foo", "foo" + + + # creator- user foreign key- stores who created this model should get the user who is adding + # it via django admin if there is a user (aka not done via commandline/ manual means)""" + # TODO: auto-populate + creator = models.ForeignKey( + "registrar.User", + on_delete=models.PROTECT, + help_text="Associated user", + unique=False + ) + + # NOTE: This will be specified in a future ticket + # portfolio_type = models.TextField( + # choices=PortfolioTypes.choices, + # null=True, + # blank=True, + # ) + + # notes- text field (copy what is done on requests/domains) + notes = models.TextField( + null=True, + blank=True, + ) + + # domains- many to many to Domain field (nullable) + domains = models.ManyToManyField( + "registrar.Domain", + null=True, + blank=True, + related_name="portfolio domains", + verbose_name="portfolio domains", + # on_delete=models.PROTECT, # TODO: protect this? + ) + + # domain_requests- Many to many to Domain Request field (nullable) + domain_requests = models.ManyToManyField( + "registrar.DomainRequest", + null=True, + blank=True, + related_name="portfolio domain requests", + verbose_name="portfolio domain requests", + # on_delete=models.PROTECT, # TODO: protect this? + ) + + # organization + organization = models.OneToOneField( + "registrar.Organization", + null=True, + blank=True, + # on_delete=models.PROTECT, # TODO: protect this? + ) + + + # def save(self, *args, **kwargs): + # # Call the parent class's save method to perform the actual save + # super().save(*args, **kwargs) + + # if self.user: + # updated = False + + # # Update first name and last name if necessary + # if not self.user.first_name or not self.user.last_name: + # self.user.first_name = self.first_name + # self.user.last_name = self.last_name + # updated = True + + # # Update phone if necessary + # if not self.user.phone: + # self.user.phone = self.phone + # updated = True + + # # Save user if any updates were made + # if updated: + # self.user.save() + + # def __str__(self): + # if self.first_name or self.last_name: + # return self.get_formatted_name() + # elif self.email: + # return self.email + # elif self.pk: + # return str(self.pk) + # else: + # return "" From 85f3234fcd60ee981ab11596fe458829b65cd7c9 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 12 Jun 2024 14:49:06 -0600 Subject: [PATCH 02/15] Model corrections --- src/registrar/models/domain_information.py | 11 ++ src/registrar/models/domain_request.py | 10 + src/registrar/models/organization.py | 145 --------------- src/registrar/models/portfolio.py | 207 ++++++++++++++++----- 4 files changed, 183 insertions(+), 190 deletions(-) delete mode 100644 src/registrar/models/organization.py diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index bc35d4e30..198bf29c5 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -57,6 +57,17 @@ class DomainInformation(TimeStampedModel): help_text="Person who submitted the domain request", ) + + # portfolio + portfolio = models.OneToOneField( + "registrar.Portfolio", + on_delete=models.PROTECT, + null=True, + blank=True, + related_name="DomainRequest_portfolio", + help_text="Portfolio associated with this domain", + ) + domain_request = models.OneToOneField( "registrar.DomainRequest", on_delete=models.PROTECT, diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 8dfa419c9..a594f29f5 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -286,6 +286,16 @@ class DomainRequest(TimeStampedModel): null=True, ) + # portfolio + portfolio = models.OneToOneField( + "registrar.Portfolio", + on_delete=models.PROTECT, + null=True, + blank=True, + related_name="DomainInformation_portfolio", + help_text="Portfolio associated with this domain", + ) + # This is the domain request user who created this domain request. The contact # information that they gave is in the `submitter` field creator = models.ForeignKey( diff --git a/src/registrar/models/organization.py b/src/registrar/models/organization.py deleted file mode 100644 index 3ae54719e..000000000 --- a/src/registrar/models/organization.py +++ /dev/null @@ -1,145 +0,0 @@ -from django.db import models -from django_fsm import FSMField # type: ignore - -from registrar.models.domain_request import DomainRequest -from registrar.models.federal_agency import FederalAgency -from registrar.models.domain import State - -from .utility.time_stamped_model import TimeStampedModel - - -class Organization(TimeStampedModel): - """ - TODO: - """ - - class Meta: - """Contains meta information about this class""" - - indexes = [ - models.Index(fields=["user"]), - models.Index(fields=["email"]), - ] - - # federal agency - FK to fed agency table (Not nullable, should default to the Non-federal agency value in the fed agency table) - federal_agency = models.ForeignKey( - "registrar.FederalAgency", - on_delete=models.PROTECT, - help_text="Associated federal agency", - unique=False, - default=FederalAgency.objects.filter(agency="Non-Federal Agency").first() - ) - - # creator- user foreign key- stores who created this model - # should get the user who is adding it via django admin if there - # is a user (aka not done via commandline/ manual means) - # TODO: auto-populate - creator = models.ForeignKey( - "registrar.User", - on_delete=models.PROTECT, - help_text="Associated user", - unique=False - ) - - # organization type- should match organization types allowed on domain info - organization_type = models.CharField( - max_length=255, - choices=DomainRequest.OrgChoicesElectionOffice.choices, - null=True, - blank=True, - help_text='"Election" appears after the org type if it\'s an election office.', - ) - - # organization name - # TODO: org name will be the same as federal agency, if it is federal, - # otherwise it will be the actual org name. If nothing is entered for - # org name and it is a federal organization, have this field fill with - # the federal agency text name. - organization_name = models.CharField( - null=True, - blank=True, - ) - - # address_line1 - address_line1 = models.CharField( - null=True, - blank=True, - verbose_name="address line 1", - ) - # address_line2 - address_line2 = models.CharField( - null=True, - blank=True, - verbose_name="address line 2", - ) - # city - city = models.CharField( - null=True, - blank=True, - ) - # state (copied from domain.py -- imports enums from domain.py) - state = FSMField( - max_length=21, - choices=State.choices, - default=State.UNKNOWN, - # cannot change state directly, particularly in Django admin - protected=True, - # This must be defined for custom state help messages, - # as otherwise the view will purge the help field as it does not exist. - help_text=" ", - verbose_name="domain state", - ) - # zipcode - zipcode = models.CharField( - max_length=10, - null=True, - blank=True, - verbose_name="zip code", - ) - # urbanization - urbanization = models.CharField( - null=True, - blank=True, - help_text="Required for Puerto Rico only", - verbose_name="urbanization", - ) - - # security_contact_email - security_contact_email = models.EmailField( - null=True, - blank=True, - verbose_name="security contact e-mail", - max_length=320, - ) - - # def save(self, *args, **kwargs): - # # Call the parent class's save method to perform the actual save - # super().save(*args, **kwargs) - - # if self.user: - # updated = False - - # # Update first name and last name if necessary - # if not self.user.first_name or not self.user.last_name: - # self.user.first_name = self.first_name - # self.user.last_name = self.last_name - # updated = True - - # # Update phone if necessary - # if not self.user.phone: - # self.user.phone = self.phone - # updated = True - - # # Save user if any updates were made - # if updated: - # self.user.save() - - # def __str__(self): - # if self.first_name or self.last_name: - # return self.get_formatted_name() - # elif self.email: - # return self.email - # elif self.pk: - # return str(self.pk) - # else: - # return "" diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index 801f267f4..6721a8a20 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -1,4 +1,9 @@ from django.db import models +from django_fsm import FSMField # type: ignore + +from registrar.models.domain_request import DomainRequest +from registrar.models.federal_agency import FederalAgency +from registrar.models.domain import State from .utility.time_stamped_model import TimeStampedModel @@ -15,6 +20,10 @@ class Portfolio(TimeStampedModel): models.Index(fields=["user"]), models.Index(fields=["email"]), ] + + + # use the short names in Django admin + OrganizationChoices = DomainRequest.OrganizationChoices # NOTE: This will be specified in a future ticket # class PortfolioTypes(models.TextChoices): @@ -23,6 +32,12 @@ class Portfolio(TimeStampedModel): # TYPE3 = "foo", "foo" # TYPE4 = "foo", "foo" + # NOTE: This will be specified in a future ticket + # portfolio_type = models.TextField( + # choices=PortfolioTypes.choices, + # null=True, + # blank=True, + # ) # creator- user foreign key- stores who created this model should get the user who is adding # it via django admin if there is a user (aka not done via commandline/ manual means)""" @@ -34,69 +49,171 @@ class Portfolio(TimeStampedModel): unique=False ) - # NOTE: This will be specified in a future ticket - # portfolio_type = models.TextField( - # choices=PortfolioTypes.choices, - # null=True, - # blank=True, - # ) - # notes- text field (copy what is done on requests/domains) notes = models.TextField( null=True, blank=True, ) - # domains- many to many to Domain field (nullable) - domains = models.ManyToManyField( - "registrar.Domain", - null=True, - blank=True, - related_name="portfolio domains", - verbose_name="portfolio domains", - # on_delete=models.PROTECT, # TODO: protect this? + # # domains- many to many to Domain field (nullable) + # domains = models.ManyToManyField( + # "registrar.Domain", + # null=True, + # blank=True, + # related_name="portfolio domains", + # verbose_name="portfolio domains", + # # on_delete=models.PROTECT, # TODO: protect this? + # ) + + # # domain_requests- Many to many to Domain Request field (nullable) + # domain_requests = models.ManyToManyField( + # "registrar.DomainRequest", + # null=True, + # blank=True, + # related_name="portfolio domain requests", + # verbose_name="portfolio domain requests", + # # on_delete=models.PROTECT, # TODO: protect this? + # ) + + # # organization + # organization = models.OneToOneField( + # "registrar.Organization", + # null=True, + # blank=True, + # ) + + + # federal agency - FK to fed agency table (Not nullable, should default to the Non-federal agency value in the fed agency table) + federal_agency = models.ForeignKey( + "registrar.FederalAgency", + on_delete=models.PROTECT, + help_text="Associated federal agency", + unique=False, + default=FederalAgency.objects.filter(agency="Non-Federal Agency").first() ) - # domain_requests- Many to many to Domain Request field (nullable) - domain_requests = models.ManyToManyField( - "registrar.DomainRequest", - null=True, - blank=True, - related_name="portfolio domain requests", - verbose_name="portfolio domain requests", - # on_delete=models.PROTECT, # TODO: protect this? + # creator- user foreign key- stores who created this model + # should get the user who is adding it via django admin if there + # is a user (aka not done via commandline/ manual means) + # TODO: auto-populate + creator = models.ForeignKey( + "registrar.User", + on_delete=models.PROTECT, + help_text="Associated user", + unique=False ) - # organization - organization = models.OneToOneField( - "registrar.Organization", + # organization type- should match organization types allowed on domain info + organization_type = models.CharField( + max_length=255, + choices=OrganizationChoices.choices, null=True, blank=True, - # on_delete=models.PROTECT, # TODO: protect this? + help_text="Type of organization", ) - - # def save(self, *args, **kwargs): - # # Call the parent class's save method to perform the actual save - # super().save(*args, **kwargs) + # organization name + # TODO: org name will be the same as federal agency, if it is federal, + # otherwise it will be the actual org name. If nothing is entered for + # org name and it is a federal organization, have this field fill with + # the federal agency text name. + organization_name = models.CharField( + null=True, + blank=True, + ) - # if self.user: - # updated = False + # address_line1 + address_line1 = models.CharField( + null=True, + blank=True, + verbose_name="address line 1", + ) + # address_line2 + address_line2 = models.CharField( + null=True, + blank=True, + verbose_name="address line 2", + ) + # city + city = models.CharField( + null=True, + blank=True, + ) + # state (copied from domain.py -- imports enums from domain.py) + state = FSMField( + max_length=21, + choices=State.choices, + default=State.UNKNOWN, + # cannot change state directly, particularly in Django admin + protected=True, + # This must be defined for custom state help messages, + # as otherwise the view will purge the help field as it does not exist. + help_text=" ", + verbose_name="domain state", + ) + # zipcode + zipcode = models.CharField( + max_length=10, + null=True, + blank=True, + verbose_name="zip code", + ) + # urbanization + urbanization = models.CharField( + null=True, + blank=True, + help_text="Required for Puerto Rico only", + verbose_name="urbanization", + ) - # # Update first name and last name if necessary - # if not self.user.first_name or not self.user.last_name: - # self.user.first_name = self.first_name - # self.user.last_name = self.last_name - # updated = True + # security_contact_email + security_contact_email = models.EmailField( + null=True, + blank=True, + verbose_name="security contact e-mail", + max_length=320, + ) - # # Update phone if necessary - # if not self.user.phone: - # self.user.phone = self.phone - # updated = True + def save(self, *args, **kwargs): + # Call the parent class's save method to perform the actual save + super().save(*args, **kwargs) - # # Save user if any updates were made - # if updated: - # self.user.save() + # TODO: + # ---- auto-populate creator ---- + # creator- user foreign key- stores who created this model + # should get the user who is adding it via django admin if there + # is a user (aka not done via commandline/ manual means) + + # ---- update organization name ---- + # org name will be the same as federal agency, if it is federal, + # otherwise it will be the actual org name. If nothing is entered for + # org name and it is a federal organization, have this field fill with + # the federal agency text name. + is_federal = self.organization_type == self.OrganizationChoices.FEDERAL + if is_federal: + self.organization_name = DomainRequest.OrganizationChoicesVerbose(self.organization_type) + #NOTE: Is this what is meant by "federal agency text name?" + + + # ----------------------------------- + # if self.user: + # updated = False + + # # Update first name and last name if necessary + # if not self.user.first_name or not self.user.last_name: + # self.user.first_name = self.first_name + # self.user.last_name = self.last_name + # updated = True + + # # Update phone if necessary + # if not self.user.phone: + # self.user.phone = self.phone + # updated = True + + # # Save user if any updates were made + # if updated: + # self.user.save() + # ----------------------------------- # def __str__(self): # if self.first_name or self.last_name: From c9c6890e7d6a7ef62299e658b23c65b52d988f0c Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 12 Jun 2024 15:47:13 -0600 Subject: [PATCH 03/15] model updates --- src/registrar/admin.py | 17 +++++++ src/registrar/models/domain_information.py | 16 +++---- src/registrar/models/domain_request.py | 16 +++---- src/registrar/models/portfolio.py | 53 +--------------------- 4 files changed, 35 insertions(+), 67 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 6aabfdd52..578146624 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2464,6 +2464,23 @@ class VerifiedByStaffAdmin(ListHeaderAdmin): super().save_model(request, obj, form, change) +class PortfolioAdmin(ListHeaderAdmin): + # list_display = ("email", "requestor", "truncated_notes", "created_at") + # search_fields = ["email"] + # search_help_text = "Search by email." + # readonly_fields = [ + # "requestor", + # ] + + # change_form_template = "django/admin/email_clipboard_change_form.html" + + def save_model(self, request, obj, form, change): + # Set the creator field to the current admin user + obj.creator = request.user if request.user.is_authenticated else None + super().save_model(request, obj, form, change) + + + class FederalAgencyAdmin(ListHeaderAdmin): list_display = ["agency"] search_fields = ["agency"] diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 198bf29c5..8235981f5 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -59,14 +59,14 @@ class DomainInformation(TimeStampedModel): # portfolio - portfolio = models.OneToOneField( - "registrar.Portfolio", - on_delete=models.PROTECT, - null=True, - blank=True, - related_name="DomainRequest_portfolio", - help_text="Portfolio associated with this domain", - ) + # portfolio = models.OneToOneField( + # "registrar.Portfolio", + # on_delete=models.PROTECT, + # null=True, + # blank=True, + # related_name="DomainRequest_portfolio", + # help_text="Portfolio associated with this domain", + # ) domain_request = models.OneToOneField( "registrar.DomainRequest", diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index a594f29f5..8c0734ba9 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -287,14 +287,14 @@ class DomainRequest(TimeStampedModel): ) # portfolio - portfolio = models.OneToOneField( - "registrar.Portfolio", - on_delete=models.PROTECT, - null=True, - blank=True, - related_name="DomainInformation_portfolio", - help_text="Portfolio associated with this domain", - ) + # portfolio = models.OneToOneField( + # "registrar.Portfolio", + # on_delete=models.PROTECT, + # null=True, + # blank=True, + # related_name="DomainInformation_portfolio", + # help_text="Portfolio associated with this domain", + # ) # This is the domain request user who created this domain request. The contact # information that they gave is in the `submitter` field diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index 6721a8a20..cbd39f3c7 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -10,34 +10,13 @@ from .utility.time_stamped_model import TimeStampedModel class Portfolio(TimeStampedModel): """ - TODO: + Portfolio is used for organizing domains/domain-requests into + manageable groups. """ - - class Meta: - """Contains meta information about this class""" - - indexes = [ - models.Index(fields=["user"]), - models.Index(fields=["email"]), - ] - # use the short names in Django admin OrganizationChoices = DomainRequest.OrganizationChoices - - # NOTE: This will be specified in a future ticket - # class PortfolioTypes(models.TextChoices): - # TYPE1 = "foo", "foo" - # TYPE2 = "foo", "foo" - # TYPE3 = "foo", "foo" - # TYPE4 = "foo", "foo" - # NOTE: This will be specified in a future ticket - # portfolio_type = models.TextField( - # choices=PortfolioTypes.choices, - # null=True, - # blank=True, - # ) # creator- user foreign key- stores who created this model should get the user who is adding # it via django admin if there is a user (aka not done via commandline/ manual means)""" @@ -54,34 +33,6 @@ class Portfolio(TimeStampedModel): null=True, blank=True, ) - - # # domains- many to many to Domain field (nullable) - # domains = models.ManyToManyField( - # "registrar.Domain", - # null=True, - # blank=True, - # related_name="portfolio domains", - # verbose_name="portfolio domains", - # # on_delete=models.PROTECT, # TODO: protect this? - # ) - - # # domain_requests- Many to many to Domain Request field (nullable) - # domain_requests = models.ManyToManyField( - # "registrar.DomainRequest", - # null=True, - # blank=True, - # related_name="portfolio domain requests", - # verbose_name="portfolio domain requests", - # # on_delete=models.PROTECT, # TODO: protect this? - # ) - - # # organization - # organization = models.OneToOneField( - # "registrar.Organization", - # null=True, - # blank=True, - # ) - # federal agency - FK to fed agency table (Not nullable, should default to the Non-federal agency value in the fed agency table) federal_agency = models.ForeignKey( From de926b1abbea873095b47284a440e30fb84d9bb3 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 12 Jun 2024 17:05:22 -0600 Subject: [PATCH 04/15] migrations --- src/registrar/admin.py | 22 ++- ...io_domaininformation_portfolio_and_more.py | 174 ++++++++++++++++++ .../migrations/0102_create_groups_v13.py | 37 ++++ src/registrar/models/__init__.py | 3 + src/registrar/models/domain_information.py | 16 +- src/registrar/models/domain_request.py | 16 +- src/registrar/models/portfolio.py | 93 ++-------- 7 files changed, 261 insertions(+), 100 deletions(-) create mode 100644 src/registrar/migrations/0101_portfolio_domaininformation_portfolio_and_more.py create mode 100644 src/registrar/migrations/0102_create_groups_v13.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 578146624..9a1110af4 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2465,18 +2465,29 @@ class VerifiedByStaffAdmin(ListHeaderAdmin): class PortfolioAdmin(ListHeaderAdmin): - # list_display = ("email", "requestor", "truncated_notes", "created_at") - # search_fields = ["email"] - # search_help_text = "Search by email." + # NOTE: these are just placeholders. Not part of ACs (haven't been defined yet). Update in future tickets. + list_display = ("organization_name", "federal_agency", "creator") + search_fields = ["organization_name"] + search_help_text = "Search by organization name." # readonly_fields = [ # "requestor", # ] - # change_form_template = "django/admin/email_clipboard_change_form.html" - def save_model(self, request, obj, form, change): + # ---- update creator ---- # Set the creator field to the current admin user obj.creator = request.user if request.user.is_authenticated else None + + # ---- update organization name ---- + # org name will be the same as federal agency, if it is federal, + # otherwise it will be the actual org name. If nothing is entered for + # org name and it is a federal organization, have this field fill with + # the federal agency text name. + is_federal = obj.organization_type == DomainRequest.OrganizationChoices.FEDERAL + if is_federal: + obj.organization_name = obj.organization_type + #NOTE: What is meant by "federal agency text name?" + super().save_model(request, obj, form, change) @@ -2551,6 +2562,7 @@ admin.site.register(models.PublicContact, PublicContactAdmin) admin.site.register(models.DomainRequest, DomainRequestAdmin) admin.site.register(models.TransitionDomain, TransitionDomainAdmin) admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin) +admin.site.register(models.Portfolio, PortfolioAdmin) # Register our custom waffle implementations admin.site.register(models.WaffleFlag, WaffleFlagAdmin) diff --git a/src/registrar/migrations/0101_portfolio_domaininformation_portfolio_and_more.py b/src/registrar/migrations/0101_portfolio_domaininformation_portfolio_and_more.py new file mode 100644 index 000000000..fd2138801 --- /dev/null +++ b/src/registrar/migrations/0101_portfolio_domaininformation_portfolio_and_more.py @@ -0,0 +1,174 @@ +# Generated by Django 4.2.10 on 2024-06-12 22:15 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import registrar.models.portfolio + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0100_domainrequest_action_needed_reason"), + ] + + operations = [ + migrations.CreateModel( + name="Portfolio", + 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)), + ("notes", models.TextField(blank=True, null=True)), + ( + "organization_type", + models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of organization", + max_length=255, + null=True, + ), + ), + ("organization_name", models.CharField(blank=True, null=True)), + ("address_line1", models.CharField(blank=True, null=True, verbose_name="address line 1")), + ("address_line2", models.CharField(blank=True, null=True, verbose_name="address line 2")), + ("city", models.CharField(blank=True, null=True)), + ( + "state_territory", + models.CharField( + blank=True, + choices=[ + ("AL", "Alabama (AL)"), + ("AK", "Alaska (AK)"), + ("AS", "American Samoa (AS)"), + ("AZ", "Arizona (AZ)"), + ("AR", "Arkansas (AR)"), + ("CA", "California (CA)"), + ("CO", "Colorado (CO)"), + ("CT", "Connecticut (CT)"), + ("DE", "Delaware (DE)"), + ("DC", "District of Columbia (DC)"), + ("FL", "Florida (FL)"), + ("GA", "Georgia (GA)"), + ("GU", "Guam (GU)"), + ("HI", "Hawaii (HI)"), + ("ID", "Idaho (ID)"), + ("IL", "Illinois (IL)"), + ("IN", "Indiana (IN)"), + ("IA", "Iowa (IA)"), + ("KS", "Kansas (KS)"), + ("KY", "Kentucky (KY)"), + ("LA", "Louisiana (LA)"), + ("ME", "Maine (ME)"), + ("MD", "Maryland (MD)"), + ("MA", "Massachusetts (MA)"), + ("MI", "Michigan (MI)"), + ("MN", "Minnesota (MN)"), + ("MS", "Mississippi (MS)"), + ("MO", "Missouri (MO)"), + ("MT", "Montana (MT)"), + ("NE", "Nebraska (NE)"), + ("NV", "Nevada (NV)"), + ("NH", "New Hampshire (NH)"), + ("NJ", "New Jersey (NJ)"), + ("NM", "New Mexico (NM)"), + ("NY", "New York (NY)"), + ("NC", "North Carolina (NC)"), + ("ND", "North Dakota (ND)"), + ("MP", "Northern Mariana Islands (MP)"), + ("OH", "Ohio (OH)"), + ("OK", "Oklahoma (OK)"), + ("OR", "Oregon (OR)"), + ("PA", "Pennsylvania (PA)"), + ("PR", "Puerto Rico (PR)"), + ("RI", "Rhode Island (RI)"), + ("SC", "South Carolina (SC)"), + ("SD", "South Dakota (SD)"), + ("TN", "Tennessee (TN)"), + ("TX", "Texas (TX)"), + ("UM", "United States Minor Outlying Islands (UM)"), + ("UT", "Utah (UT)"), + ("VT", "Vermont (VT)"), + ("VI", "Virgin Islands (VI)"), + ("VA", "Virginia (VA)"), + ("WA", "Washington (WA)"), + ("WV", "West Virginia (WV)"), + ("WI", "Wisconsin (WI)"), + ("WY", "Wyoming (WY)"), + ("AA", "Armed Forces Americas (AA)"), + ("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"), + ("AP", "Armed Forces Pacific (AP)"), + ], + max_length=2, + null=True, + verbose_name="state / territory", + ), + ), + ("zipcode", models.CharField(blank=True, max_length=10, null=True, verbose_name="zip code")), + ( + "urbanization", + models.CharField( + blank=True, help_text="Required for Puerto Rico only", null=True, verbose_name="urbanization" + ), + ), + ( + "security_contact_email", + models.EmailField(blank=True, max_length=320, null=True, verbose_name="security contact e-mail"), + ), + ( + "creator", + models.ForeignKey( + help_text="Associated user", + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "federal_agency", + models.ForeignKey( + default=registrar.models.portfolio.get_default_federal_agency, + help_text="Associated federal agency", + on_delete=django.db.models.deletion.PROTECT, + to="registrar.federalagency", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="domaininformation", + name="portfolio", + field=models.OneToOneField( + blank=True, + help_text="Portfolio associated with this domain", + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="DomainRequest_portfolio", + to="registrar.portfolio", + ), + ), + migrations.AddField( + model_name="domainrequest", + name="portfolio", + field=models.OneToOneField( + blank=True, + help_text="Portfolio associated with this domain", + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="DomainInformation_portfolio", + to="registrar.portfolio", + ), + ), + ] diff --git a/src/registrar/migrations/0102_create_groups_v13.py b/src/registrar/migrations/0102_create_groups_v13.py new file mode 100644 index 000000000..6ddea8260 --- /dev/null +++ b/src/registrar/migrations/0102_create_groups_v13.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", "0101_portfolio_domaininformation_portfolio_and_more"), + ] + + 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 f084a5d8b..401b8012d 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -16,6 +16,7 @@ from .website import Website from .transition_domain import TransitionDomain from .verified_by_staff import VerifiedByStaff from .waffle_flag import WaffleFlag +from .portfolio import Portfolio __all__ = [ @@ -36,6 +37,7 @@ __all__ = [ "TransitionDomain", "VerifiedByStaff", "WaffleFlag", + "Portfolio" ] auditlog.register(Contact) @@ -55,3 +57,4 @@ auditlog.register(Website) auditlog.register(TransitionDomain) auditlog.register(VerifiedByStaff) auditlog.register(WaffleFlag) +auditlog.register(Portfolio) diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 8235981f5..198bf29c5 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -59,14 +59,14 @@ class DomainInformation(TimeStampedModel): # portfolio - # portfolio = models.OneToOneField( - # "registrar.Portfolio", - # on_delete=models.PROTECT, - # null=True, - # blank=True, - # related_name="DomainRequest_portfolio", - # help_text="Portfolio associated with this domain", - # ) + portfolio = models.OneToOneField( + "registrar.Portfolio", + on_delete=models.PROTECT, + null=True, + blank=True, + related_name="DomainRequest_portfolio", + help_text="Portfolio associated with this domain", + ) domain_request = models.OneToOneField( "registrar.DomainRequest", diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 8c0734ba9..a594f29f5 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -287,14 +287,14 @@ class DomainRequest(TimeStampedModel): ) # portfolio - # portfolio = models.OneToOneField( - # "registrar.Portfolio", - # on_delete=models.PROTECT, - # null=True, - # blank=True, - # related_name="DomainInformation_portfolio", - # help_text="Portfolio associated with this domain", - # ) + portfolio = models.OneToOneField( + "registrar.Portfolio", + on_delete=models.PROTECT, + null=True, + blank=True, + related_name="DomainInformation_portfolio", + help_text="Portfolio associated with this domain", + ) # This is the domain request user who created this domain request. The contact # information that they gave is in the `submitter` field diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index cbd39f3c7..43605b27d 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -3,10 +3,12 @@ from django_fsm import FSMField # type: ignore from registrar.models.domain_request import DomainRequest from registrar.models.federal_agency import FederalAgency -from registrar.models.domain import State from .utility.time_stamped_model import TimeStampedModel +def get_default_federal_agency(): + """returns non-federal agency""" + return FederalAgency.objects.filter(agency="Non-Federal Agency").first() class Portfolio(TimeStampedModel): """ @@ -16,11 +18,10 @@ class Portfolio(TimeStampedModel): # use the short names in Django admin OrganizationChoices = DomainRequest.OrganizationChoices - + StateTerritoryChoices = DomainRequest.StateTerritoryChoices # creator- user foreign key- stores who created this model should get the user who is adding # it via django admin if there is a user (aka not done via commandline/ manual means)""" - # TODO: auto-populate creator = models.ForeignKey( "registrar.User", on_delete=models.PROTECT, @@ -40,18 +41,7 @@ class Portfolio(TimeStampedModel): on_delete=models.PROTECT, help_text="Associated federal agency", unique=False, - default=FederalAgency.objects.filter(agency="Non-Federal Agency").first() - ) - - # creator- user foreign key- stores who created this model - # should get the user who is adding it via django admin if there - # is a user (aka not done via commandline/ manual means) - # TODO: auto-populate - creator = models.ForeignKey( - "registrar.User", - on_delete=models.PROTECT, - help_text="Associated user", - unique=False + default=get_default_federal_agency ) # organization type- should match organization types allowed on domain info @@ -64,7 +54,7 @@ class Portfolio(TimeStampedModel): ) # organization name - # TODO: org name will be the same as federal agency, if it is federal, + # NOTE: org name will be the same as federal agency, if it is federal, # otherwise it will be the actual org name. If nothing is entered for # org name and it is a federal organization, have this field fill with # the federal agency text name. @@ -90,17 +80,13 @@ class Portfolio(TimeStampedModel): null=True, blank=True, ) - # state (copied from domain.py -- imports enums from domain.py) - state = FSMField( - max_length=21, - choices=State.choices, - default=State.UNKNOWN, - # cannot change state directly, particularly in Django admin - protected=True, - # This must be defined for custom state help messages, - # as otherwise the view will purge the help field as it does not exist. - help_text=" ", - verbose_name="domain state", + # state (copied from domain_request.py -- imports enums from domain_request.py) + state_territory = models.CharField( + max_length=2, + choices=StateTerritoryChoices.choices, + null=True, + blank=True, + verbose_name="state / territory", ) # zipcode zipcode = models.CharField( @@ -123,55 +109,4 @@ class Portfolio(TimeStampedModel): blank=True, verbose_name="security contact e-mail", max_length=320, - ) - - def save(self, *args, **kwargs): - # Call the parent class's save method to perform the actual save - super().save(*args, **kwargs) - - # TODO: - # ---- auto-populate creator ---- - # creator- user foreign key- stores who created this model - # should get the user who is adding it via django admin if there - # is a user (aka not done via commandline/ manual means) - - # ---- update organization name ---- - # org name will be the same as federal agency, if it is federal, - # otherwise it will be the actual org name. If nothing is entered for - # org name and it is a federal organization, have this field fill with - # the federal agency text name. - is_federal = self.organization_type == self.OrganizationChoices.FEDERAL - if is_federal: - self.organization_name = DomainRequest.OrganizationChoicesVerbose(self.organization_type) - #NOTE: Is this what is meant by "federal agency text name?" - - - # ----------------------------------- - # if self.user: - # updated = False - - # # Update first name and last name if necessary - # if not self.user.first_name or not self.user.last_name: - # self.user.first_name = self.first_name - # self.user.last_name = self.last_name - # updated = True - - # # Update phone if necessary - # if not self.user.phone: - # self.user.phone = self.phone - # updated = True - - # # Save user if any updates were made - # if updated: - # self.user.save() - # ----------------------------------- - - # def __str__(self): - # if self.first_name or self.last_name: - # return self.get_formatted_name() - # elif self.email: - # return self.email - # elif self.pk: - # return str(self.pk) - # else: - # return "" + ) \ No newline at end of file From a15772115be3131ccf905b4452e52226ec83e876 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 12 Jun 2024 22:36:08 -0600 Subject: [PATCH 05/15] update migration --- .../0101_portfolio_domaininformation_portfolio_and_more.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/migrations/0101_portfolio_domaininformation_portfolio_and_more.py b/src/registrar/migrations/0101_portfolio_domaininformation_portfolio_and_more.py index fd2138801..02ff5a1e8 100644 --- a/src/registrar/migrations/0101_portfolio_domaininformation_portfolio_and_more.py +++ b/src/registrar/migrations/0101_portfolio_domaininformation_portfolio_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-06-12 22:15 +# Generated by Django 4.2.10 on 2024-06-12 23:06 from django.conf import settings from django.db import migrations, models From 2a90335499c052e7d259fd2ca9e57c2d2b081991 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 13 Jun 2024 16:32:45 -0600 Subject: [PATCH 06/15] updated migrations --- ...=> 0102_portfolio_domaininformation_portfolio_and_more.py} | 4 ++-- .../{0102_create_groups_v13.py => 0103_create_groups_v13.py} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/registrar/migrations/{0101_portfolio_domaininformation_portfolio_and_more.py => 0102_portfolio_domaininformation_portfolio_and_more.py} (98%) rename src/registrar/migrations/{0102_create_groups_v13.py => 0103_create_groups_v13.py} (95%) diff --git a/src/registrar/migrations/0101_portfolio_domaininformation_portfolio_and_more.py b/src/registrar/migrations/0102_portfolio_domaininformation_portfolio_and_more.py similarity index 98% rename from src/registrar/migrations/0101_portfolio_domaininformation_portfolio_and_more.py rename to src/registrar/migrations/0102_portfolio_domaininformation_portfolio_and_more.py index 02ff5a1e8..3882e503a 100644 --- a/src/registrar/migrations/0101_portfolio_domaininformation_portfolio_and_more.py +++ b/src/registrar/migrations/0102_portfolio_domaininformation_portfolio_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-06-12 23:06 +# Generated by Django 4.2.10 on 2024-06-13 22:29 from django.conf import settings from django.db import migrations, models @@ -9,7 +9,7 @@ import registrar.models.portfolio class Migration(migrations.Migration): dependencies = [ - ("registrar", "0100_domainrequest_action_needed_reason"), + ("registrar", "0101_domaininformation_cisa_representative_first_name_and_more"), ] operations = [ diff --git a/src/registrar/migrations/0102_create_groups_v13.py b/src/registrar/migrations/0103_create_groups_v13.py similarity index 95% rename from src/registrar/migrations/0102_create_groups_v13.py rename to src/registrar/migrations/0103_create_groups_v13.py index 6ddea8260..7504ff331 100644 --- a/src/registrar/migrations/0102_create_groups_v13.py +++ b/src/registrar/migrations/0103_create_groups_v13.py @@ -25,7 +25,7 @@ def create_groups(apps, schema_editor) -> Any: class Migration(migrations.Migration): dependencies = [ - ("registrar", "0101_portfolio_domaininformation_portfolio_and_more"), + ("registrar", "0102_portfolio_domaininformation_portfolio_and_more"), ] operations = [ From 1dfc68ce47f36b1ae02789430c2d2175fd29a84a Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 13 Jun 2024 16:39:17 -0600 Subject: [PATCH 07/15] linted --- src/registrar/admin.py | 3 +-- src/registrar/models/__init__.py | 2 +- src/registrar/models/domain_information.py | 3 +-- src/registrar/models/domain_request.py | 2 +- src/registrar/models/portfolio.py | 23 ++++++++++------------ 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 70b06abb6..9abb909d2 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2490,12 +2490,11 @@ class PortfolioAdmin(ListHeaderAdmin): is_federal = obj.organization_type == DomainRequest.OrganizationChoices.FEDERAL if is_federal: obj.organization_name = obj.organization_type - #NOTE: What is meant by "federal agency text name?" + # NOTE: What is meant by "federal agency text name?" super().save_model(request, obj, form, change) - class FederalAgencyAdmin(ListHeaderAdmin): list_display = ["agency"] search_fields = ["agency"] diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index 401b8012d..b2cffaf32 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -37,7 +37,7 @@ __all__ = [ "TransitionDomain", "VerifiedByStaff", "WaffleFlag", - "Portfolio" + "Portfolio", ] auditlog.register(Contact) diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index c76c6fe3e..c058ab209 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -57,7 +57,6 @@ class DomainInformation(TimeStampedModel): help_text="Person who submitted the domain request", ) - # portfolio portfolio = models.OneToOneField( "registrar.Portfolio", @@ -66,7 +65,7 @@ class DomainInformation(TimeStampedModel): blank=True, related_name="DomainRequest_portfolio", help_text="Portfolio associated with this domain", - ) + ) domain_request = models.OneToOneField( "registrar.DomainRequest", diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 75fe0dc0b..3e616f202 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -307,7 +307,7 @@ class DomainRequest(TimeStampedModel): blank=True, related_name="DomainInformation_portfolio", help_text="Portfolio associated with this domain", - ) + ) # This is the domain request user who created this domain request. The contact # information that they gave is in the `submitter` field diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index 43605b27d..ca5365177 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -1,47 +1,44 @@ from django.db import models -from django_fsm import FSMField # type: ignore from registrar.models.domain_request import DomainRequest from registrar.models.federal_agency import FederalAgency from .utility.time_stamped_model import TimeStampedModel + def get_default_federal_agency(): """returns non-federal agency""" return FederalAgency.objects.filter(agency="Non-Federal Agency").first() + class Portfolio(TimeStampedModel): """ Portfolio is used for organizing domains/domain-requests into manageable groups. """ - + # use the short names in Django admin OrganizationChoices = DomainRequest.OrganizationChoices StateTerritoryChoices = DomainRequest.StateTerritoryChoices - # creator- user foreign key- stores who created this model should get the user who is adding + # creator- user foreign key- stores who created this model should get the user who is adding # it via django admin if there is a user (aka not done via commandline/ manual means)""" - creator = models.ForeignKey( - "registrar.User", - on_delete=models.PROTECT, - help_text="Associated user", - unique=False - ) + creator = models.ForeignKey("registrar.User", on_delete=models.PROTECT, help_text="Associated user", unique=False) # notes- text field (copy what is done on requests/domains) notes = models.TextField( null=True, blank=True, ) - - # federal agency - FK to fed agency table (Not nullable, should default to the Non-federal agency value in the fed agency table) + + # federal agency - FK to fed agency table (Not nullable, should default + # to the Non-federal agency value in the fed agency table) federal_agency = models.ForeignKey( "registrar.FederalAgency", on_delete=models.PROTECT, help_text="Associated federal agency", unique=False, - default=get_default_federal_agency + default=get_default_federal_agency, ) # organization type- should match organization types allowed on domain info @@ -109,4 +106,4 @@ class Portfolio(TimeStampedModel): blank=True, verbose_name="security contact e-mail", max_length=320, - ) \ No newline at end of file + ) From 1c96c9ebc96fcfbd7273a6369ccaa274766dbfce Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 17 Jun 2024 11:06:18 -0600 Subject: [PATCH 08/15] fixed failing unit test --- src/registrar/tests/test_admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index e567f9329..7c435c5b0 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -2300,6 +2300,7 @@ class TestDomainRequestAdmin(MockEppLib): "rejection_reason", "action_needed_reason", "federal_agency", + 'portfolio', "creator", "investigator", "generic_org_type", From 83243ad4882bfe23cdd6a4a113c2a7a2642c1606 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 17 Jun 2024 11:09:17 -0600 Subject: [PATCH 09/15] fix migrations --- ...=> 0103_portfolio_domaininformation_portfolio_and_more.py} | 4 ++-- .../{0103_create_groups_v13.py => 0104_create_groups_v13.py} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/registrar/migrations/{0102_portfolio_domaininformation_portfolio_and_more.py => 0103_portfolio_domaininformation_portfolio_and_more.py} (98%) rename src/registrar/migrations/{0103_create_groups_v13.py => 0104_create_groups_v13.py} (95%) diff --git a/src/registrar/migrations/0102_portfolio_domaininformation_portfolio_and_more.py b/src/registrar/migrations/0103_portfolio_domaininformation_portfolio_and_more.py similarity index 98% rename from src/registrar/migrations/0102_portfolio_domaininformation_portfolio_and_more.py rename to src/registrar/migrations/0103_portfolio_domaininformation_portfolio_and_more.py index 3882e503a..15cec0b73 100644 --- a/src/registrar/migrations/0102_portfolio_domaininformation_portfolio_and_more.py +++ b/src/registrar/migrations/0103_portfolio_domaininformation_portfolio_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-06-13 22:29 +# Generated by Django 4.2.10 on 2024-06-17 17:08 from django.conf import settings from django.db import migrations, models @@ -9,7 +9,7 @@ import registrar.models.portfolio class Migration(migrations.Migration): dependencies = [ - ("registrar", "0101_domaininformation_cisa_representative_first_name_and_more"), + ("registrar", "0102_domain_dsdata_last_change"), ] operations = [ diff --git a/src/registrar/migrations/0103_create_groups_v13.py b/src/registrar/migrations/0104_create_groups_v13.py similarity index 95% rename from src/registrar/migrations/0103_create_groups_v13.py rename to src/registrar/migrations/0104_create_groups_v13.py index 7504ff331..0ce3bafa5 100644 --- a/src/registrar/migrations/0103_create_groups_v13.py +++ b/src/registrar/migrations/0104_create_groups_v13.py @@ -25,7 +25,7 @@ def create_groups(apps, schema_editor) -> Any: class Migration(migrations.Migration): dependencies = [ - ("registrar", "0102_portfolio_domaininformation_portfolio_and_more"), + ("registrar", "0103_portfolio_domaininformation_portfolio_and_more"), ] operations = [ From 6b669df5e7254bb3a9a192bbddfcbf334668481e Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 17 Jun 2024 13:50:41 -0600 Subject: [PATCH 10/15] minor cleanup --- ...0103_portfolio_domaininformation_portfolio_and_more.py | 4 ++-- src/registrar/models/domain_request.py | 2 +- src/registrar/models/portfolio.py | 8 ++++---- src/registrar/tests/test_admin.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/registrar/migrations/0103_portfolio_domaininformation_portfolio_and_more.py b/src/registrar/migrations/0103_portfolio_domaininformation_portfolio_and_more.py index 15cec0b73..3edc34b2a 100644 --- a/src/registrar/migrations/0103_portfolio_domaininformation_portfolio_and_more.py +++ b/src/registrar/migrations/0103_portfolio_domaininformation_portfolio_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-06-17 17:08 +# Generated by Django 4.2.10 on 2024-06-17 19:47 from django.conf import settings from django.db import migrations, models @@ -164,7 +164,7 @@ class Migration(migrations.Migration): name="portfolio", field=models.OneToOneField( blank=True, - help_text="Portfolio associated with this domain", + help_text="Portfolio associated with this domain request", null=True, on_delete=django.db.models.deletion.PROTECT, related_name="DomainInformation_portfolio", diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index cffe5f86c..dfb57cee0 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -310,7 +310,7 @@ class DomainRequest(TimeStampedModel): null=True, blank=True, related_name="DomainInformation_portfolio", - help_text="Portfolio associated with this domain", + help_text="Portfolio associated with this domain request", ) # This is the domain request user who created this domain request. The contact diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index ca5365177..14de445c2 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -21,11 +21,11 @@ class Portfolio(TimeStampedModel): OrganizationChoices = DomainRequest.OrganizationChoices StateTerritoryChoices = DomainRequest.StateTerritoryChoices - # creator- user foreign key- stores who created this model should get the user who is adding - # it via django admin if there is a user (aka not done via commandline/ manual means)""" + # creator - stores who created this model. If no creator is specified in DJA, + # then the creator will default to the current request user""" creator = models.ForeignKey("registrar.User", on_delete=models.PROTECT, help_text="Associated user", unique=False) - # notes- text field (copy what is done on requests/domains) + # notes - text field (copies what is done on domain requests) notes = models.TextField( null=True, blank=True, @@ -41,7 +41,7 @@ class Portfolio(TimeStampedModel): default=get_default_federal_agency, ) - # organization type- should match organization types allowed on domain info + # organization type - should match organization types allowed on domain info organization_type = models.CharField( max_length=255, choices=OrganizationChoices.choices, diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 62b7bb9d8..802974b6e 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -2291,7 +2291,7 @@ class TestDomainRequestAdmin(MockEppLib): "rejection_reason", "action_needed_reason", "federal_agency", - 'portfolio', + "portfolio", "creator", "investigator", "generic_org_type", From 2c96eebc6f69060aea58c71a1658800a9d9e53bd Mon Sep 17 00:00:00 2001 From: CuriousX Date: Mon, 17 Jun 2024 20:57:57 -0600 Subject: [PATCH 11/15] Update src/registrar/admin.py Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- src/registrar/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index bb256f742..226527e28 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2572,8 +2572,8 @@ class PortfolioAdmin(ListHeaderAdmin): # org name and it is a federal organization, have this field fill with # the federal agency text name. is_federal = obj.organization_type == DomainRequest.OrganizationChoices.FEDERAL - if is_federal: - obj.organization_name = obj.organization_type + if is_federal and obj.organization_name is None: + obj.organization_name = obj.federal_agency.agency # NOTE: What is meant by "federal agency text name?" super().save_model(request, obj, form, change) From 42efc7372283e1200235b0cc651b444e8b6dfd75 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 17 Jun 2024 21:20:05 -0600 Subject: [PATCH 12/15] Model updates and cleanup of comments --- src/registrar/admin.py | 8 ++++--- ...io_domaininformation_portfolio_and_more.py | 6 ++--- src/registrar/models/domain_information.py | 2 +- src/registrar/models/domain_request.py | 2 +- src/registrar/models/portfolio.py | 24 ++++++------------- 5 files changed, 17 insertions(+), 25 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 226527e28..25f2afe34 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2562,9 +2562,11 @@ class PortfolioAdmin(ListHeaderAdmin): # ] def save_model(self, request, obj, form, change): - # ---- update creator ---- - # Set the creator field to the current admin user - obj.creator = request.user if request.user.is_authenticated else None + + if not obj.creator is None: + # ---- update creator ---- + # Set the creator field to the current admin user + obj.creator = request.user if request.user.is_authenticated else None # ---- update organization name ---- # org name will be the same as federal agency, if it is federal, diff --git a/src/registrar/migrations/0103_portfolio_domaininformation_portfolio_and_more.py b/src/registrar/migrations/0103_portfolio_domaininformation_portfolio_and_more.py index 3edc34b2a..df0945712 100644 --- a/src/registrar/migrations/0103_portfolio_domaininformation_portfolio_and_more.py +++ b/src/registrar/migrations/0103_portfolio_domaininformation_portfolio_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-06-17 19:47 +# Generated by Django 4.2.10 on 2024-06-18 03:19 from django.conf import settings from django.db import migrations, models @@ -150,7 +150,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name="domaininformation", name="portfolio", - field=models.OneToOneField( + field=models.ForeignKey( blank=True, help_text="Portfolio associated with this domain", null=True, @@ -162,7 +162,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name="domainrequest", name="portfolio", - field=models.OneToOneField( + field=models.ForeignKey( blank=True, help_text="Portfolio associated with this domain request", null=True, diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index c058ab209..b6f2dd9a7 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -58,7 +58,7 @@ class DomainInformation(TimeStampedModel): ) # portfolio - portfolio = models.OneToOneField( + portfolio = models.ForeignKey( "registrar.Portfolio", on_delete=models.PROTECT, null=True, diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index dfb57cee0..1c4725be1 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -304,7 +304,7 @@ class DomainRequest(TimeStampedModel): ) # portfolio - portfolio = models.OneToOneField( + portfolio = models.ForeignKey( "registrar.Portfolio", on_delete=models.PROTECT, null=True, diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index 14de445c2..b3131cb87 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -21,18 +21,15 @@ class Portfolio(TimeStampedModel): OrganizationChoices = DomainRequest.OrganizationChoices StateTerritoryChoices = DomainRequest.StateTerritoryChoices - # creator - stores who created this model. If no creator is specified in DJA, + # Stores who created this model. If no creator is specified in DJA, # then the creator will default to the current request user""" creator = models.ForeignKey("registrar.User", on_delete=models.PROTECT, help_text="Associated user", unique=False) - # notes - text field (copies what is done on domain requests) notes = models.TextField( null=True, blank=True, ) - # federal agency - FK to fed agency table (Not nullable, should default - # to the Non-federal agency value in the fed agency table) federal_agency = models.ForeignKey( "registrar.FederalAgency", on_delete=models.PROTECT, @@ -41,7 +38,6 @@ class Portfolio(TimeStampedModel): default=get_default_federal_agency, ) - # organization type - should match organization types allowed on domain info organization_type = models.CharField( max_length=255, choices=OrganizationChoices.choices, @@ -50,34 +46,29 @@ class Portfolio(TimeStampedModel): help_text="Type of organization", ) - # organization name - # NOTE: org name will be the same as federal agency, if it is federal, - # otherwise it will be the actual org name. If nothing is entered for - # org name and it is a federal organization, have this field fill with - # the federal agency text name. organization_name = models.CharField( null=True, blank=True, ) - # address_line1 address_line1 = models.CharField( null=True, blank=True, verbose_name="address line 1", ) - # address_line2 + address_line2 = models.CharField( null=True, blank=True, verbose_name="address line 2", ) - # city + city = models.CharField( null=True, blank=True, ) - # state (copied from domain_request.py -- imports enums from domain_request.py) + + # (imports enums from domain_request.py) state_territory = models.CharField( max_length=2, choices=StateTerritoryChoices.choices, @@ -85,14 +76,14 @@ class Portfolio(TimeStampedModel): blank=True, verbose_name="state / territory", ) - # zipcode + zipcode = models.CharField( max_length=10, null=True, blank=True, verbose_name="zip code", ) - # urbanization + urbanization = models.CharField( null=True, blank=True, @@ -100,7 +91,6 @@ class Portfolio(TimeStampedModel): verbose_name="urbanization", ) - # security_contact_email security_contact_email = models.EmailField( null=True, blank=True, From 732cf2379b15ec83a7d13804587ef1bc79b56b44 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 18 Jun 2024 11:56:19 -0600 Subject: [PATCH 13/15] move agency function to be inside Federal Agency model --- ...3_portfolio_domaininformation_portfolio_and_more.py | 6 +++--- src/registrar/models/federal_agency.py | 5 +++++ src/registrar/models/portfolio.py | 10 +++++----- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/registrar/migrations/0103_portfolio_domaininformation_portfolio_and_more.py b/src/registrar/migrations/0103_portfolio_domaininformation_portfolio_and_more.py index df0945712..ac7a69074 100644 --- a/src/registrar/migrations/0103_portfolio_domaininformation_portfolio_and_more.py +++ b/src/registrar/migrations/0103_portfolio_domaininformation_portfolio_and_more.py @@ -1,9 +1,9 @@ -# Generated by Django 4.2.10 on 2024-06-18 03:19 +# Generated by Django 4.2.10 on 2024-06-18 17:55 from django.conf import settings from django.db import migrations, models import django.db.models.deletion -import registrar.models.portfolio +import registrar.models.federal_agency class Migration(migrations.Migration): @@ -136,7 +136,7 @@ class Migration(migrations.Migration): ( "federal_agency", models.ForeignKey( - default=registrar.models.portfolio.get_default_federal_agency, + default=registrar.models.federal_agency.FederalAgency.get_non_federal_agency, help_text="Associated federal agency", on_delete=django.db.models.deletion.PROTECT, to="registrar.federalagency", diff --git a/src/registrar/models/federal_agency.py b/src/registrar/models/federal_agency.py index cb09d12ac..521d5875a 100644 --- a/src/registrar/models/federal_agency.py +++ b/src/registrar/models/federal_agency.py @@ -230,3 +230,8 @@ class FederalAgency(TimeStampedModel): FederalAgency.objects.bulk_create(agencies) except Exception as e: logger.error(f"Error creating federal agencies: {e}") + + @classmethod + def get_non_federal_agency(cls): + """Returns the non-federal agency.""" + return FederalAgency.objects.filter(agency="Non-Federal Agency").first() \ No newline at end of file diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index b3131cb87..3382de98a 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -6,9 +6,9 @@ from registrar.models.federal_agency import FederalAgency from .utility.time_stamped_model import TimeStampedModel -def get_default_federal_agency(): - """returns non-federal agency""" - return FederalAgency.objects.filter(agency="Non-Federal Agency").first() +# def get_default_federal_agency(): +# """returns non-federal agency""" +# return FederalAgency.objects.filter(agency="Non-Federal Agency").first() class Portfolio(TimeStampedModel): @@ -20,7 +20,7 @@ class Portfolio(TimeStampedModel): # use the short names in Django admin OrganizationChoices = DomainRequest.OrganizationChoices StateTerritoryChoices = DomainRequest.StateTerritoryChoices - + # Stores who created this model. If no creator is specified in DJA, # then the creator will default to the current request user""" creator = models.ForeignKey("registrar.User", on_delete=models.PROTECT, help_text="Associated user", unique=False) @@ -35,7 +35,7 @@ class Portfolio(TimeStampedModel): on_delete=models.PROTECT, help_text="Associated federal agency", unique=False, - default=get_default_federal_agency, + default=FederalAgency.get_non_federal_agency, ) organization_type = models.CharField( From 46c2326933e09e6f79f21d7de806f8d1a8047f43 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 18 Jun 2024 12:05:04 -0600 Subject: [PATCH 14/15] linted --- src/registrar/admin.py | 2 +- src/registrar/models/federal_agency.py | 2 +- src/registrar/models/portfolio.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 25f2afe34..8085af631 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2563,7 +2563,7 @@ class PortfolioAdmin(ListHeaderAdmin): def save_model(self, request, obj, form, change): - if not obj.creator is None: + if obj.creator is not None: # ---- update creator ---- # Set the creator field to the current admin user obj.creator = request.user if request.user.is_authenticated else None diff --git a/src/registrar/models/federal_agency.py b/src/registrar/models/federal_agency.py index 521d5875a..8db415bbd 100644 --- a/src/registrar/models/federal_agency.py +++ b/src/registrar/models/federal_agency.py @@ -234,4 +234,4 @@ class FederalAgency(TimeStampedModel): @classmethod def get_non_federal_agency(cls): """Returns the non-federal agency.""" - return FederalAgency.objects.filter(agency="Non-Federal Agency").first() \ No newline at end of file + return FederalAgency.objects.filter(agency="Non-Federal Agency").first() diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index 3382de98a..a05422960 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -20,7 +20,7 @@ class Portfolio(TimeStampedModel): # use the short names in Django admin OrganizationChoices = DomainRequest.OrganizationChoices StateTerritoryChoices = DomainRequest.StateTerritoryChoices - + # Stores who created this model. If no creator is specified in DJA, # then the creator will default to the current request user""" creator = models.ForeignKey("registrar.User", on_delete=models.PROTECT, help_text="Associated user", unique=False) From 412a144c8cd4d48095c195a794b162fab0cc4014 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 18 Jun 2024 16:42:00 -0600 Subject: [PATCH 15/15] removed stray comment --- src/registrar/admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 8085af631..8a691c7fa 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2576,7 +2576,6 @@ class PortfolioAdmin(ListHeaderAdmin): is_federal = obj.organization_type == DomainRequest.OrganizationChoices.FEDERAL if is_federal and obj.organization_name is None: obj.organization_name = obj.federal_agency.agency - # NOTE: What is meant by "federal agency text name?" super().save_model(request, obj, form, change)