mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-05 17:28:31 +02:00
Merge branch 'za/1501-users-delete-domain-records' into za/1484-domain-manager-delete
This commit is contained in:
commit
99ee850238
7 changed files with 250 additions and 37 deletions
|
@ -631,7 +631,6 @@ class DomainApplication(TimeStampedModel):
|
|||
|
||||
# Update submission_date to today
|
||||
self.submission_date = timezone.now().date()
|
||||
|
||||
self.save()
|
||||
|
||||
self._send_status_update_email(
|
||||
|
|
|
@ -57,6 +57,9 @@ class DomainHelper:
|
|||
# If blank ok is true, just return the domain
|
||||
return domain
|
||||
|
||||
if domain.startswith("www."):
|
||||
domain = domain[4:]
|
||||
|
||||
if domain.endswith(".gov"):
|
||||
domain = domain[:-4]
|
||||
|
||||
|
|
|
@ -38,11 +38,6 @@
|
|||
>
|
||||
<span class="usa-sr-only">Action</span>
|
||||
</th>
|
||||
{% comment %}
|
||||
#1510
|
||||
{% if has_deletable_applications %}
|
||||
<th></th>
|
||||
{% endif %} {% endcomment %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -117,8 +112,9 @@
|
|||
<th th scope="row" role="rowheader" data-label="Domain name">
|
||||
{% if application.requested_domain is None %}
|
||||
New domain request
|
||||
<br>
|
||||
<span class="text-base font-body-xs">({{ application.created_at }})</span>
|
||||
{# Add a breakpoint #}
|
||||
<div aria-hidden="true"></div>
|
||||
<span class="text-base font-body-xs">({{ application.created_at }} UTC)</span>
|
||||
{% else %}
|
||||
{{ application.requested_domain.name }}
|
||||
{% endif %}
|
||||
|
@ -132,19 +128,29 @@
|
|||
</td>
|
||||
<td data-label="Status">{{ application.get_status_display }}</td>
|
||||
<td>
|
||||
{% if application.status == "started" or application.status == "action needed" or application.status == "withdrawn" %}
|
||||
<a href="{% url 'edit-application' application.pk %}">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#edit"></use>
|
||||
</svg>
|
||||
Edit <span class="usa-sr-only">{{ application.requested_domain.name|default:"New domain request ("|add:application.created_at|add:")" }}</span>
|
||||
{% else %}
|
||||
<a href="{% url 'application-status' application.pk %}">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#settings"></use>
|
||||
</svg>
|
||||
Manage <span class="usa-sr-only">{{ application.requested_domain.name|default:"New domain request ("|add:application.created_at|add:")" }}</span>
|
||||
{% endif %}
|
||||
{% with prefix="New domain request ("%}
|
||||
{% with date=application.created_at|date:"DATETIME_FORMAT"%}
|
||||
{% with name_default=prefix|add:date|add:" UTC)"%}
|
||||
{% if application.status == application.ApplicationStatus.STARTED or application.status == application.ApplicationStatus.ACTION_NEEDED or application.status == application.ApplicationStatus.WITHDRAWN %}
|
||||
<a href="{% url 'edit-application' application.pk %}">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#edit"></use>
|
||||
</svg>
|
||||
{% if application.requested_domain is not None%}
|
||||
Edit <span class="usa-sr-only">{{ application.requested_domain.name }}</span>
|
||||
{% else %}
|
||||
Edit <span class="usa-sr-only">{{ name_default }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a href="{% url 'application-status' application.pk %}">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#settings"></use>
|
||||
</svg>
|
||||
Manage <span class="usa-sr-only">{{ application.requested_domain.name|default:name_default }}</span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
</a>
|
||||
</td>
|
||||
{% if has_deletable_applications %}
|
||||
|
@ -161,7 +167,17 @@
|
|||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||
</svg>
|
||||
Delete <span class="usa-sr-only">{{ application.requested_domain.name|default:"New domain request ("|add:application.created_at|add:")" }}</span>
|
||||
{% with prefix="New domain request ("%}
|
||||
{% with date=application.created_at|date:"DATETIME_FORMAT"%}
|
||||
{% with name_default=prefix|add:date|add:" UTC)"%}
|
||||
{% if application.requested_domain is not None %}
|
||||
Delete <span class="usa-sr-only">{{ application.requested_domain.name }}</span>
|
||||
{% else %}
|
||||
Delete <span class="usa-sr-only">{{ name_default }}</span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
</a>
|
||||
|
||||
<div
|
||||
|
@ -173,17 +189,17 @@
|
|||
>
|
||||
<form method="POST" action="{% url "application-delete" pk=application.id %}">
|
||||
{% if application.requested_domain is None %}
|
||||
{% with prefix="New domain request (" %}
|
||||
{% if application.created_at %}
|
||||
{% with formatted_date=application.created_at|date:"DATETIME_FORMAT" %}
|
||||
{% with modal_content=prefix|add:formatted_date|add:")" %}
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete" heading_value="New domain request?" modal_description="This will remove "|add:modal_content|add:" from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
||||
{% endwith %}
|
||||
{% if application.created_at %}
|
||||
{% with prefix="(created " %}
|
||||
{% with formatted_date=application.created_at|date:"DATETIME_FORMAT" %}
|
||||
{% with modal_content=prefix|add:formatted_date|add:" UTC)" %}
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete this domain request?" modal_description="This will remove the domain request "|add:modal_content|add:" from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete New domain request?" modal_description="This will remove the domain request from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete New domain request?" modal_description="This will remove the domain request from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% with modal_heading_value=application.requested_domain.name|add:"?" %}
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete" heading_value=modal_heading_value modal_description="This will remove the domain request from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
<h2 class="usa-modal__heading" id="modal-1-heading">
|
||||
{{ modal_heading }}
|
||||
{% if heading_value is not None %}
|
||||
<br>
|
||||
{# Add a breakpoint #}
|
||||
<div aria-hidden="true"></div>
|
||||
{{ heading_value }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
|
|
@ -89,6 +89,10 @@ class LoggedInTests(TestWithUser):
|
|||
super().setUp()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
Contact.objects.all().delete()
|
||||
|
||||
def test_home_lists_domain_applications(self):
|
||||
response = self.client.get("/")
|
||||
self.assertNotContains(response, "igorville.gov")
|
||||
|
@ -96,8 +100,8 @@ class LoggedInTests(TestWithUser):
|
|||
application = DomainApplication.objects.create(creator=self.user, requested_domain=site)
|
||||
response = self.client.get("/")
|
||||
|
||||
# count = 5 because of screenreader content
|
||||
self.assertContains(response, "igorville.gov", count=5)
|
||||
# count = 7 because of screenreader content
|
||||
self.assertContains(response, "igorville.gov", count=7)
|
||||
|
||||
# clean up
|
||||
application.delete()
|
||||
|
@ -182,6 +186,132 @@ class LoggedInTests(TestWithUser):
|
|||
# clean up
|
||||
application.delete()
|
||||
|
||||
def test_home_deletes_domain_application_and_orphans(self):
|
||||
"""Tests if delete for DomainApplication deletes orphaned Contact objects"""
|
||||
|
||||
# Create the site and contacts to delete (orphaned)
|
||||
contact = Contact.objects.create(
|
||||
first_name="Henry",
|
||||
last_name="Mcfakerson",
|
||||
)
|
||||
contact_shared = Contact.objects.create(
|
||||
first_name="Relative",
|
||||
last_name="Aether",
|
||||
)
|
||||
|
||||
# Create two non-orphaned contacts
|
||||
contact_2 = Contact.objects.create(
|
||||
first_name="Saturn",
|
||||
last_name="Mars",
|
||||
)
|
||||
|
||||
# Attach a user object to a contact (should not be deleted)
|
||||
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
||||
|
||||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
application = DomainApplication.objects.create(
|
||||
creator=self.user,
|
||||
requested_domain=site,
|
||||
status=DomainApplication.ApplicationStatus.WITHDRAWN,
|
||||
authorizing_official=contact,
|
||||
submitter=contact_user,
|
||||
)
|
||||
application.other_contacts.set([contact_2])
|
||||
|
||||
# Create a second application to attach contacts to
|
||||
site_2 = DraftDomain.objects.create(name="teaville.gov")
|
||||
application_2 = DomainApplication.objects.create(
|
||||
creator=self.user,
|
||||
requested_domain=site_2,
|
||||
status=DomainApplication.ApplicationStatus.STARTED,
|
||||
authorizing_official=contact_2,
|
||||
submitter=contact_shared,
|
||||
)
|
||||
application_2.other_contacts.set([contact_shared])
|
||||
|
||||
# Ensure that igorville.gov exists on the page
|
||||
home_page = self.client.get("/")
|
||||
self.assertContains(home_page, "igorville.gov")
|
||||
|
||||
# Trigger the delete logic
|
||||
response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True)
|
||||
|
||||
self.assertNotContains(response, "igorville.gov")
|
||||
|
||||
# Check if the orphaned contact was deleted
|
||||
orphan = Contact.objects.filter(id=contact.id)
|
||||
self.assertFalse(orphan.exists())
|
||||
|
||||
# All non-orphan contacts should still exist and are unaltered
|
||||
try:
|
||||
current_user = Contact.objects.filter(id=contact_user.id).get()
|
||||
except Contact.DoesNotExist:
|
||||
self.fail("contact_user (a non-orphaned contact) was deleted")
|
||||
|
||||
self.assertEqual(current_user, contact_user)
|
||||
try:
|
||||
edge_case = Contact.objects.filter(id=contact_2.id).get()
|
||||
except Contact.DoesNotExist:
|
||||
self.fail("contact_2 (a non-orphaned contact) was deleted")
|
||||
|
||||
self.assertEqual(edge_case, contact_2)
|
||||
|
||||
def test_home_deletes_domain_application_and_shared_orphans(self):
|
||||
"""Test the edge case for an object that will become orphaned after a delete
|
||||
(but is not an orphan at the time of deletion)"""
|
||||
|
||||
# Create the site and contacts to delete (orphaned)
|
||||
contact = Contact.objects.create(
|
||||
first_name="Henry",
|
||||
last_name="Mcfakerson",
|
||||
)
|
||||
contact_shared = Contact.objects.create(
|
||||
first_name="Relative",
|
||||
last_name="Aether",
|
||||
)
|
||||
|
||||
# Create two non-orphaned contacts
|
||||
contact_2 = Contact.objects.create(
|
||||
first_name="Saturn",
|
||||
last_name="Mars",
|
||||
)
|
||||
|
||||
# Attach a user object to a contact (should not be deleted)
|
||||
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
||||
|
||||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
application = DomainApplication.objects.create(
|
||||
creator=self.user,
|
||||
requested_domain=site,
|
||||
status=DomainApplication.ApplicationStatus.WITHDRAWN,
|
||||
authorizing_official=contact,
|
||||
submitter=contact_user,
|
||||
)
|
||||
application.other_contacts.set([contact_2])
|
||||
|
||||
# Create a second application to attach contacts to
|
||||
site_2 = DraftDomain.objects.create(name="teaville.gov")
|
||||
application_2 = DomainApplication.objects.create(
|
||||
creator=self.user,
|
||||
requested_domain=site_2,
|
||||
status=DomainApplication.ApplicationStatus.STARTED,
|
||||
authorizing_official=contact_2,
|
||||
submitter=contact_shared,
|
||||
)
|
||||
application_2.other_contacts.set([contact_shared])
|
||||
|
||||
home_page = self.client.get("/")
|
||||
self.assertContains(home_page, "teaville.gov")
|
||||
|
||||
# Trigger the delete logic
|
||||
response = self.client.post(reverse("application-delete", kwargs={"pk": application_2.pk}), follow=True)
|
||||
|
||||
self.assertNotContains(response, "teaville.gov")
|
||||
|
||||
# Check if the orphaned contact was deleted
|
||||
orphan = Contact.objects.filter(id=contact_shared.id)
|
||||
self.assertFalse(orphan.exists())
|
||||
|
||||
def test_application_form_view(self):
|
||||
response = self.client.get("/request/", follow=True)
|
||||
self.assertContains(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import logging
|
||||
|
||||
from collections import defaultdict
|
||||
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import resolve, reverse
|
||||
|
@ -10,6 +10,7 @@ from django.contrib import messages
|
|||
|
||||
from registrar.forms import application_wizard as forms
|
||||
from registrar.models import DomainApplication
|
||||
from registrar.models.contact import Contact
|
||||
from registrar.models.user import User
|
||||
from registrar.utility import StrEnum
|
||||
from registrar.views.utility import StepsHelper
|
||||
|
@ -649,3 +650,64 @@ class DomainApplicationDeleteView(DomainApplicationPermissionDeleteView):
|
|||
def get_success_url(self):
|
||||
"""After a delete is successful, redirect to home"""
|
||||
return reverse("home")
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# Grab all orphaned contacts
|
||||
application: DomainApplication = self.get_object()
|
||||
contacts_to_delete, duplicates = self._get_orphaned_contacts(application)
|
||||
|
||||
# Delete the DomainApplication
|
||||
response = super().post(request, *args, **kwargs)
|
||||
|
||||
# Delete orphaned contacts - but only for if they are not associated with a user
|
||||
Contact.objects.filter(id__in=contacts_to_delete, user=None).delete()
|
||||
|
||||
# After a delete occurs, do a second sweep on any returned duplicates.
|
||||
# This determines if any of these three fields share a contact, which is used for
|
||||
# the edge case where the same user may be an AO, and a submitter, for example.
|
||||
if len(duplicates) > 0:
|
||||
duplicates_to_delete, _ = self._get_orphaned_contacts(application)
|
||||
Contact.objects.filter(id__in=duplicates_to_delete, user=None).delete()
|
||||
|
||||
return response
|
||||
|
||||
def _get_orphaned_contacts(self, application: DomainApplication, check_db=True):
|
||||
"""Collects all orphaned contacts"""
|
||||
contacts_to_delete = []
|
||||
|
||||
# Get each contact object on the DomainApplication object
|
||||
ao = application.authorizing_official
|
||||
submitter = application.submitter
|
||||
other_contacts = list(application.other_contacts.all())
|
||||
other_contact_ids = application.other_contacts.all().values_list("id", flat=True)
|
||||
|
||||
# Check if the desired item still exists in the DB
|
||||
if check_db:
|
||||
ao = self._get_contacts_by_id([ao.id]).first() if ao is not None else None
|
||||
submitter = self._get_contacts_by_id([submitter.id]).first() if submitter is not None else None
|
||||
other_contacts = self._get_contacts_by_id(other_contact_ids)
|
||||
|
||||
# Pair each contact with its related name
|
||||
checked_contacts = [(ao, "authorizing_official"), (submitter, "submitted_applications")]
|
||||
checked_contacts.extend((contact, "contact_applications") for contact in other_contacts)
|
||||
|
||||
for contact, related_name in checked_contacts:
|
||||
if contact is not None and not contact.has_more_than_one_join(related_name):
|
||||
contacts_to_delete.append(contact.id)
|
||||
|
||||
return (contacts_to_delete, self._get_duplicates(checked_contacts))
|
||||
|
||||
def _get_contacts_by_id(self, contact_ids):
|
||||
"""Given a list of ids, grab contacts if it exists"""
|
||||
contacts = Contact.objects.filter(id__in=contact_ids)
|
||||
return contacts
|
||||
|
||||
def _get_duplicates(self, objects):
|
||||
"""Given a list of objects, return a list of which items were duplicates"""
|
||||
# Gets the occurence count
|
||||
object_dict = defaultdict(int)
|
||||
for contact, _related in objects:
|
||||
object_dict[contact] += 1
|
||||
|
||||
duplicates = [item for item, count in object_dict.items() if count > 1]
|
||||
return duplicates
|
||||
|
|
|
@ -42,7 +42,9 @@ def _get_applications(request):
|
|||
# Let's exclude the approved applications since our
|
||||
# domain_applications context will be used to populate
|
||||
# the active applications table
|
||||
applications = DomainApplication.objects.filter(creator=request.user).exclude(status="approved")
|
||||
applications = DomainApplication.objects.filter(creator=request.user).exclude(
|
||||
status=DomainApplication.ApplicationStatus.APPROVED
|
||||
)
|
||||
|
||||
# Create a placeholder DraftDomain for each incomplete draft
|
||||
valid_statuses = [DomainApplication.ApplicationStatus.STARTED, DomainApplication.ApplicationStatus.WITHDRAWN]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue