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] inlines = [UserContactInline]
list_display = ( list_display = (
"username",
"email", "email",
"first_name", "first_name",
"last_name", "last_name",
@ -202,6 +203,13 @@ class ContactAdmin(ListHeaderAdmin):
search_help_text = "Search by firstname, lastname or email." 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): class DomainApplicationAdmin(ListHeaderAdmin):
"""Customize the applications listing view.""" """Customize the applications listing view."""
@ -234,7 +242,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
# Detail view # Detail view
fieldsets = [ fieldsets = [
(None, {"fields": ["status", "investigator", "creator"]}), (None, {"fields": ["status", "investigator", "creator", "approved_domain"]}),
( (
"Type of organization", "Type of organization",
{ {
@ -306,29 +314,57 @@ class DomainApplicationAdmin(ListHeaderAdmin):
# Get the original application from the database # Get the original application from the database
original_obj = models.DomainApplication.objects.get(pk=obj.pk) original_obj = models.DomainApplication.objects.get(pk=obj.pk)
if obj.status != original_obj.status: if (
status_method_mapping = { obj
models.DomainApplication.STARTED: None, and original_obj.status == models.DomainApplication.APPROVED
models.DomainApplication.SUBMITTED: obj.submit, and (
models.DomainApplication.IN_REVIEW: obj.in_review, obj.status == models.DomainApplication.REJECTED
models.DomainApplication.ACTION_NEEDED: obj.action_needed, or obj.status == models.DomainApplication.INELIGIBLE
models.DomainApplication.APPROVED: obj.approve, )
models.DomainApplication.WITHDRAWN: obj.withdraw, and not obj.domain_is_not_active()
models.DomainApplication.REJECTED: obj.reject, ):
models.DomainApplication.INELIGIBLE: obj.reject_with_prejudice, # If an admin tried to set an approved application to
} # rejected or ineligible and the related domain is already
selected_method = status_method_mapping.get(obj.status) # active, shortcut the action and throw a friendly
if selected_method is None: # error message. This action would still not go through
logger.warning("Unknown status selected in django admin") # shortcut or not as the rules are duplicated on the model,
else: # but the error would be an ugly Django error screen.
# 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) # 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: else:
# Clear the success message # Clear the success message
messages.set_level(request, messages.ERROR) 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.UserDomainRole, AuditedAdmin)
admin.site.register(models.Contact, ContactAdmin) admin.site.register(models.Contact, ContactAdmin)
admin.site.register(models.DomainInvitation, AuditedAdmin) 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.Domain, DomainAdmin)
admin.site.register(models.Host, MyHostAdmin) admin.site.register(models.Host, MyHostAdmin)
admin.site.register(models.Nameserver, 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 # TODO: implement a check -- should be performant so it can be called for
# any number of domains on a status page # any number of domains on a status page
# this is NOT as simple as checking if Domain.Status.OK is in self.statuses # 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 return False
def delete_request(self):
"""Delete from host. Possibly a duplicate of _delete_host?"""
pass
def transfer(self): def transfer(self):
"""Going somewhere. Not implemented.""" """Going somewhere. Not implemented."""
raise NotImplementedError() raise NotImplementedError()
@ -338,9 +344,6 @@ class Domain(TimeStampedModel, DomainHelper):
help_text="Very basic info about the lifecycle of this domain object", 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 # ForeignKey on UserDomainRole creates a "permissions" member for
# all of the user-roles that are in place for this domain # all of the user-roles that are in place for this domain

View file

@ -411,7 +411,7 @@ class DomainApplication(TimeStampedModel):
blank=True, blank=True,
help_text="The approved domain", help_text="The approved domain",
related_name="domain_application", related_name="domain_application",
on_delete=models.PROTECT, on_delete=models.SET_NULL,
) )
requested_domain = models.OneToOneField( requested_domain = models.OneToOneField(
@ -477,6 +477,11 @@ class DomainApplication(TimeStampedModel):
except Exception: except Exception:
return "" 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( def _send_status_update_email(
self, new_status, email_template, email_template_subject self, new_status, email_template, email_template_subject
): ):
@ -600,11 +605,22 @@ class DomainApplication(TimeStampedModel):
"emails/domain_request_withdrawn_subject.txt", "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): def reject(self):
"""Reject an application that has been submitted. """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( self._send_status_update_email(
"action needed", "action needed",
@ -612,14 +628,25 @@ class DomainApplication(TimeStampedModel):
"emails/status_change_rejected_subject.txt", "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): def reject_with_prejudice(self):
"""The applicant is a bad actor, reject with prejudice. """The applicant is a bad actor, reject with prejudice.
No email As a side effect, but we block the applicant from editing No email As a side effect, but we block the applicant from editing
any existing domains/applications and from submitting new aplications. any existing domains/applications and from submitting new aplications.
We do this by setting an ineligible status on the user, which the 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() self.creator.restrict_user()

View file

@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
class DomainInformation(TimeStampedModel): class DomainInformation(TimeStampedModel):
"""A registrant's domain information for that domain, exported from """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 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 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 the application once approved, so copying them that way we can make changes
@ -156,7 +156,7 @@ class DomainInformation(TimeStampedModel):
domain = models.OneToOneField( domain = models.OneToOneField(
"registrar.Domain", "registrar.Domain",
on_delete=models.PROTECT, on_delete=models.CASCADE,
blank=True, blank=True,
null=True, null=True,
# Access this information via Domain as "domain.domain_info" # Access this information via Domain as "domain.domain_info"