check is_active on rejected and ineligible, delete domain and domain info and remove references, intercept and show friendly error in admin

This commit is contained in:
Rachid Mrad 2023-09-05 15:19:39 -04:00
parent 482d900f57
commit 324bea9868
No known key found for this signature in database
GPG key ID: EF38E4CEC4A8F3CF
4 changed files with 100 additions and 34 deletions

View file

@ -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,6 +314,32 @@ class DomainApplicationAdmin(ListHeaderAdmin):
# Get the original application from the database
original_obj = models.DomainApplication.objects.get(pk=obj.pk)
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.
# 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,
@ -315,7 +349,9 @@ class DomainApplicationAdmin(ListHeaderAdmin):
models.DomainApplication.APPROVED: obj.approve,
models.DomainApplication.WITHDRAWN: obj.withdraw,
models.DomainApplication.REJECTED: obj.reject,
models.DomainApplication.INELIGIBLE: obj.reject_with_prejudice,
models.DomainApplication.INELIGIBLE: (
obj.reject_with_prejudice
),
}
selected_method = status_method_mapping.get(obj.status)
if selected_method is None:
@ -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)

View file

@ -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

View file

@ -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()

View file

@ -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"