diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 4696a15bf..a46873f51 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -104,6 +104,7 @@ class MyUserAdmin(BaseUserAdmin): inlines = [UserContactInline] list_display = ( + "username", "email", "first_name", "last_name", @@ -202,6 +203,13 @@ class ContactAdmin(ListHeaderAdmin): search_help_text = "Search by firstname, lastname or email." +class DomainInformationAdmin(ListHeaderAdmin): + """Custom contact admin class to add search.""" + + search_fields = ["domain__name"] + search_help_text = "Search by domain name." + + class DomainApplicationAdmin(ListHeaderAdmin): """Customize the applications listing view.""" @@ -234,7 +242,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): # Detail view fieldsets = [ - (None, {"fields": ["status", "investigator", "creator"]}), + (None, {"fields": ["status", "investigator", "creator", "approved_domain"]}), ( "Type of organization", { @@ -306,29 +314,57 @@ class DomainApplicationAdmin(ListHeaderAdmin): # Get the original application from the database original_obj = models.DomainApplication.objects.get(pk=obj.pk) - if obj.status != original_obj.status: - status_method_mapping = { - models.DomainApplication.STARTED: None, - models.DomainApplication.SUBMITTED: obj.submit, - models.DomainApplication.IN_REVIEW: obj.in_review, - models.DomainApplication.ACTION_NEEDED: obj.action_needed, - models.DomainApplication.APPROVED: obj.approve, - models.DomainApplication.WITHDRAWN: obj.withdraw, - models.DomainApplication.REJECTED: obj.reject, - models.DomainApplication.INELIGIBLE: obj.reject_with_prejudice, - } - selected_method = status_method_mapping.get(obj.status) - if selected_method is None: - logger.warning("Unknown status selected in django admin") - else: - # This is an fsm in model which will throw an error if the - # transition condition is violated, so we roll back the - # status to what it was before the admin user changed it and - # let the fsm method set it. - obj.status = original_obj.status - selected_method() + if ( + obj + and original_obj.status == models.DomainApplication.APPROVED + and ( + obj.status == models.DomainApplication.REJECTED + or obj.status == models.DomainApplication.INELIGIBLE + ) + and not obj.domain_is_not_active() + ): + # If an admin tried to set an approved application to + # rejected or ineligible and the related domain is already + # active, shortcut the action and throw a friendly + # error message. This action would still not go through + # shortcut or not as the rules are duplicated on the model, + # but the error would be an ugly Django error screen. - super().save_model(request, obj, form, change) + # Clear the success message + messages.set_level(request, messages.ERROR) + + messages.error( + request, + "This action is not permitted, the domain " + + "is already active.", + ) + + else: + if obj.status != original_obj.status: + status_method_mapping = { + models.DomainApplication.STARTED: None, + models.DomainApplication.SUBMITTED: obj.submit, + models.DomainApplication.IN_REVIEW: obj.in_review, + models.DomainApplication.ACTION_NEEDED: obj.action_needed, + models.DomainApplication.APPROVED: obj.approve, + models.DomainApplication.WITHDRAWN: obj.withdraw, + models.DomainApplication.REJECTED: obj.reject, + models.DomainApplication.INELIGIBLE: ( + obj.reject_with_prejudice + ), + } + selected_method = status_method_mapping.get(obj.status) + if selected_method is None: + logger.warning("Unknown status selected in django admin") + else: + # This is an fsm in model which will throw an error if the + # transition condition is violated, so we roll back the + # status to what it was before the admin user changed it and + # let the fsm method set it. + obj.status = original_obj.status + selected_method() + + super().save_model(request, obj, form, change) else: # Clear the success message messages.set_level(request, messages.ERROR) @@ -382,7 +418,7 @@ admin.site.register(models.User, MyUserAdmin) admin.site.register(models.UserDomainRole, AuditedAdmin) admin.site.register(models.Contact, ContactAdmin) admin.site.register(models.DomainInvitation, AuditedAdmin) -admin.site.register(models.DomainInformation, AuditedAdmin) +admin.site.register(models.DomainInformation, DomainInformationAdmin) admin.site.register(models.Domain, DomainAdmin) admin.site.register(models.Host, MyHostAdmin) admin.site.register(models.Nameserver, MyHostAdmin) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 59563d3d8..4988ae36b 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -301,8 +301,14 @@ class Domain(TimeStampedModel, DomainHelper): # TODO: implement a check -- should be performant so it can be called for # any number of domains on a status page # this is NOT as simple as checking if Domain.Status.OK is in self.statuses + + # NOTE: This was stubbed in all along for 852 and 811 return False + def delete_request(self): + """Delete from host. Possibly a duplicate of _delete_host?""" + pass + def transfer(self): """Going somewhere. Not implemented.""" raise NotImplementedError() @@ -338,9 +344,6 @@ class Domain(TimeStampedModel, DomainHelper): help_text="Very basic info about the lifecycle of this domain object", ) - def isActive(self): - return self.state == Domain.State.CREATED - # ForeignKey on UserDomainRole creates a "permissions" member for # all of the user-roles that are in place for this domain diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index b1230b703..db962fda0 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -411,7 +411,7 @@ class DomainApplication(TimeStampedModel): blank=True, help_text="The approved domain", related_name="domain_application", - on_delete=models.PROTECT, + on_delete=models.SET_NULL, ) requested_domain = models.OneToOneField( @@ -477,6 +477,11 @@ class DomainApplication(TimeStampedModel): except Exception: return "" + def domain_is_not_active(self): + if self.approved_domain: + return not self.approved_domain.is_active() + return True + def _send_status_update_email( self, new_status, email_template, email_template_subject ): @@ -600,11 +605,22 @@ class DomainApplication(TimeStampedModel): "emails/domain_request_withdrawn_subject.txt", ) - @transition(field="status", source=[IN_REVIEW, APPROVED], target=REJECTED) + @transition( + field="status", + source=[IN_REVIEW, APPROVED], + target=REJECTED, + conditions=[domain_is_not_active], + ) def reject(self): """Reject an application that has been submitted. - As a side effect, an email notification is sent, similar to in_review""" + As a side effect this will delete the domain and domain_information + (will cascade), and send an email notification""" + + if self.status == self.APPROVED: + self.approved_domain.delete_request() + self.approved_domain.delete() + self.approved_domain = None self._send_status_update_email( "action needed", @@ -612,14 +628,25 @@ class DomainApplication(TimeStampedModel): "emails/status_change_rejected_subject.txt", ) - @transition(field="status", source=[IN_REVIEW, APPROVED], target=INELIGIBLE) + @transition( + field="status", + source=[IN_REVIEW, APPROVED], + target=INELIGIBLE, + conditions=[domain_is_not_active], + ) def reject_with_prejudice(self): """The applicant is a bad actor, reject with prejudice. No email As a side effect, but we block the applicant from editing any existing domains/applications and from submitting new aplications. We do this by setting an ineligible status on the user, which the - permissions classes test against""" + permissions classes test against. This will also delete the domain + and domain_information (will cascade)""" + + if self.status == self.APPROVED: + self.approved_domain.delete_request() + self.approved_domain.delete() + self.approved_domain = None self.creator.restrict_user() diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index b12039e73..3361185b8 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) class DomainInformation(TimeStampedModel): """A registrant's domain information for that domain, exported from - DomainApplication. We use these field from DomainApplication with few exceptation + DomainApplication. We use these field from DomainApplication with few exceptions which are 'removed' via pop at the bottom of this file. Most of design for domain management's user information are based on application, but we cannot change the application once approved, so copying them that way we can make changes @@ -156,7 +156,7 @@ class DomainInformation(TimeStampedModel): domain = models.OneToOneField( "registrar.Domain", - on_delete=models.PROTECT, + on_delete=models.CASCADE, blank=True, null=True, # Access this information via Domain as "domain.domain_info"