mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-06 01:35:22 +02:00
Merge branch 'za/1501-users-delete-domain-records' into za/1484-domain-manager-delete
This commit is contained in:
commit
8082083d63
10 changed files with 267 additions and 26 deletions
|
@ -25,6 +25,22 @@
|
|||
color: color('primary-darker');
|
||||
padding-bottom: units(2px);
|
||||
}
|
||||
|
||||
// Ticket #1510
|
||||
// @include at-media('desktop') {
|
||||
// th:first-child {
|
||||
// width: 220px;
|
||||
// }
|
||||
// th:nth-child(2) {
|
||||
// width: 175px;
|
||||
// }
|
||||
// th:nth-child(3) {
|
||||
// width: 130px;
|
||||
// }
|
||||
// th:nth-child(5) {
|
||||
// width: 130px;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
.dotgov-table {
|
||||
|
|
|
@ -137,6 +137,11 @@ urlpatterns = [
|
|||
views.DomainInvitationDeleteView.as_view(http_method_names=["post"]),
|
||||
name="invitation-delete",
|
||||
),
|
||||
path(
|
||||
"application/<int:pk>/delete",
|
||||
views.DomainApplicationDeleteView.as_view(http_method_names=["post"]),
|
||||
name="application-delete",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/users/<int:user_pk>/delete",
|
||||
views.DomainDeleteUserView.as_view(http_method_names=["post"]),
|
||||
|
|
|
@ -487,7 +487,8 @@ class DotGovDomainForm(RegistrarForm):
|
|||
values = {}
|
||||
requested_domain = getattr(obj, "requested_domain", None)
|
||||
if requested_domain is not None:
|
||||
values["requested_domain"] = Domain.sld(requested_domain.name)
|
||||
domain_name = requested_domain.name
|
||||
values["requested_domain"] = Domain.sld(domain_name)
|
||||
return values
|
||||
|
||||
def clean_requested_domain(self):
|
||||
|
|
|
@ -631,6 +631,7 @@ class DomainApplication(TimeStampedModel):
|
|||
|
||||
# Update submission_date to today
|
||||
self.submission_date = timezone.now().date()
|
||||
|
||||
self.save()
|
||||
|
||||
self._send_status_update_email(
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
{% if user.is_authenticated %}
|
||||
{# the entire logged in page goes here #}
|
||||
|
||||
<div class="tablet:grid-offset-1 desktop:grid-offset-2">
|
||||
<div class="tablet:grid-col-11 desktop:grid-col-10 tablet:grid-offset-1">
|
||||
{% block messages %}
|
||||
{% include "includes/form_messages.html" %}
|
||||
{% endblock %}
|
||||
|
@ -34,7 +34,7 @@
|
|||
{% endif %}
|
||||
</p>
|
||||
|
||||
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
|
||||
<section class="section--outlined">
|
||||
<h2>Domains</h2>
|
||||
{% if domains %}
|
||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
||||
|
@ -44,7 +44,17 @@
|
|||
<th data-sortable scope="col" role="columnheader">Domain name</th>
|
||||
<th data-sortable scope="col" role="columnheader">Expires</th>
|
||||
<th data-sortable scope="col" role="columnheader">Status</th>
|
||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
||||
<th
|
||||
scope="col"
|
||||
role="columnheader"
|
||||
>
|
||||
<span class="usa-sr-only">Action</span>
|
||||
</th>
|
||||
{% comment %}
|
||||
#1510
|
||||
{% if has_deletable_applications %}
|
||||
<th></th>
|
||||
{% endif %} {% endcomment %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -97,7 +107,7 @@
|
|||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
|
||||
<section class="section--outlined">
|
||||
<h2>Domain requests</h2>
|
||||
{% if domain_applications %}
|
||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
||||
|
@ -108,13 +118,22 @@
|
|||
<th data-sortable scope="col" role="columnheader">Date submitted</th>
|
||||
<th data-sortable scope="col" role="columnheader">Status</th>
|
||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
||||
{% if has_deletable_applications %}
|
||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Delete Action</span></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for application in domain_applications %}
|
||||
<tr>
|
||||
<th th scope="row" role="rowheader" data-label="Domain name">
|
||||
{{ application.requested_domain.name|default:"New domain request" }}
|
||||
{% if application.requested_domain is None %}
|
||||
New domain request
|
||||
<br>
|
||||
<span class="text-base font-body-xs">({{ application.created_at }})</span>
|
||||
{% else %}
|
||||
{{ application.requested_domain.name }}
|
||||
{% endif %}
|
||||
</th>
|
||||
<td data-sort-value="{{ application.submission_date|date:"U" }}" data-label="Date submitted">
|
||||
{% if application.submission_date %}
|
||||
|
@ -125,22 +144,68 @@
|
|||
</td>
|
||||
<td data-label="Status">{{ application.get_status_display }}</td>
|
||||
<td>
|
||||
{% if application.status == "started" or application.status == "action needed" or application.status == "withdrawn" %}
|
||||
{% 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" }} </span>
|
||||
|
||||
{% else %}
|
||||
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}} </span>
|
||||
{% endif %}
|
||||
Manage <span class="usa-sr-only">{{ application.requested_domain.name|default:"New domain request ("|add:application.created_at|add:")" }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
{% if has_deletable_applications %}
|
||||
<td>
|
||||
{% if application.status == "started" or application.status == "withdrawn" %}
|
||||
<a
|
||||
role="button"
|
||||
id="button-toggle-delete-domain-alert-{{ forloop.counter }}"
|
||||
href="#toggle-delete-domain-alert-{{ forloop.counter }}"
|
||||
class="usa-button--unstyled text-no-underline"
|
||||
aria-controls="toggle-delete-domain-alert-{{ forloop.counter }}"
|
||||
data-open-modal
|
||||
>
|
||||
<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>
|
||||
</a>
|
||||
|
||||
<div
|
||||
class="usa-modal"
|
||||
id="toggle-delete-domain-alert-{{ forloop.counter }}"
|
||||
aria-labelledby="Are you sure you want to continue?"
|
||||
aria-describedby="Domain will be removed"
|
||||
data-force-action
|
||||
>
|
||||
<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 %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading" id="modal-1-heading">
|
||||
{{ modal_heading }}
|
||||
{% if heading_value is not None %}
|
||||
<br>
|
||||
{{ heading_value }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
<div class="usa-prose">
|
||||
<p id="modal-1-description">
|
||||
|
|
|
@ -95,11 +95,93 @@ class LoggedInTests(TestWithUser):
|
|||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
application = DomainApplication.objects.create(creator=self.user, requested_domain=site)
|
||||
response = self.client.get("/")
|
||||
# count = 2 because it is also in screenreader content
|
||||
self.assertContains(response, "igorville.gov", count=2)
|
||||
|
||||
# count = 5 because of screenreader content
|
||||
self.assertContains(response, "igorville.gov", count=5)
|
||||
|
||||
# clean up
|
||||
application.delete()
|
||||
|
||||
def test_home_deletes_withdrawn_domain_application(self):
|
||||
"""Tests if the user can delete a DomainApplication in the 'withdrawn' status"""
|
||||
|
||||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
application = DomainApplication.objects.create(
|
||||
creator=self.user, requested_domain=site, status=DomainApplication.ApplicationStatus.WITHDRAWN
|
||||
)
|
||||
|
||||
# Ensure that igorville.gov exists on the page
|
||||
home_page = self.client.get("/")
|
||||
self.assertContains(home_page, "igorville.gov")
|
||||
|
||||
# Check if the delete button exists. We can do this by checking for its id and text content.
|
||||
self.assertContains(home_page, "Delete")
|
||||
self.assertContains(home_page, "button-toggle-delete-domain-alert-1")
|
||||
|
||||
# Trigger the delete logic
|
||||
response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True)
|
||||
|
||||
self.assertNotContains(response, "igorville.gov")
|
||||
|
||||
# clean up
|
||||
application.delete()
|
||||
|
||||
def test_home_deletes_started_domain_application(self):
|
||||
"""Tests if the user can delete a DomainApplication in the 'started' status"""
|
||||
|
||||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
application = DomainApplication.objects.create(
|
||||
creator=self.user, requested_domain=site, status=DomainApplication.ApplicationStatus.STARTED
|
||||
)
|
||||
|
||||
# Ensure that igorville.gov exists on the page
|
||||
home_page = self.client.get("/")
|
||||
self.assertContains(home_page, "igorville.gov")
|
||||
|
||||
# Check if the delete button exists. We can do this by checking for its id and text content.
|
||||
self.assertContains(home_page, "Delete")
|
||||
self.assertContains(home_page, "button-toggle-delete-domain-alert-1")
|
||||
|
||||
# Trigger the delete logic
|
||||
response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True)
|
||||
|
||||
self.assertNotContains(response, "igorville.gov")
|
||||
|
||||
# clean up
|
||||
application.delete()
|
||||
|
||||
def test_home_doesnt_delete_other_domain_applications(self):
|
||||
"""Tests to ensure the user can't delete Applications not in the status of STARTED or WITHDRAWN"""
|
||||
|
||||
# Given that we are including a subset of items that can be deleted while excluding the rest,
|
||||
# subTest is appropriate here as otherwise we would need many duplicate tests for the same reason.
|
||||
draft_domain = DraftDomain.objects.create(name="igorville.gov")
|
||||
for status in DomainApplication.ApplicationStatus:
|
||||
if status not in [
|
||||
DomainApplication.ApplicationStatus.STARTED,
|
||||
DomainApplication.ApplicationStatus.WITHDRAWN,
|
||||
]:
|
||||
with self.subTest(status=status):
|
||||
application = DomainApplication.objects.create(
|
||||
creator=self.user, requested_domain=draft_domain, status=status
|
||||
)
|
||||
|
||||
# Trigger the delete logic
|
||||
response = self.client.post(
|
||||
reverse("application-delete", kwargs={"pk": application.pk}), follow=True
|
||||
)
|
||||
|
||||
# Check for a 403 error - the end user should not be allowed to do this
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
desired_application = DomainApplication.objects.filter(requested_domain=draft_domain)
|
||||
|
||||
# Make sure the DomainApplication wasn't deleted
|
||||
self.assertEqual(desired_application.count(), 1)
|
||||
|
||||
# clean up
|
||||
application.delete()
|
||||
|
||||
def test_application_form_view(self):
|
||||
response = self.client.get("/request/", follow=True)
|
||||
self.assertContains(
|
||||
|
|
|
@ -13,6 +13,7 @@ from registrar.models import DomainApplication
|
|||
from registrar.models.user import User
|
||||
from registrar.utility import StrEnum
|
||||
from registrar.views.utility import StepsHelper
|
||||
from registrar.views.utility.permission_views import DomainApplicationPermissionDeleteView
|
||||
|
||||
from .utility import (
|
||||
DomainApplicationPermissionView,
|
||||
|
@ -148,9 +149,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
|||
except DomainApplication.DoesNotExist:
|
||||
logger.debug("Application id %s did not have a DomainApplication" % id)
|
||||
|
||||
self._application = DomainApplication.objects.create(
|
||||
creator=self.request.user, # type: ignore
|
||||
)
|
||||
self._application = DomainApplication.objects.create(creator=self.request.user)
|
||||
|
||||
self.storage["application_id"] = self._application.id
|
||||
return self._application
|
||||
|
@ -159,7 +158,6 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
|||
def storage(self):
|
||||
# marking session as modified on every access
|
||||
# so that updates to nested keys are always saved
|
||||
# push to sandbox will remove
|
||||
self.request.session.modified = True
|
||||
return self.request.session.setdefault(self.prefix, {})
|
||||
|
||||
|
@ -628,3 +626,26 @@ class ApplicationWithdrawn(DomainApplicationPermissionWithdrawView):
|
|||
application.withdraw()
|
||||
application.save()
|
||||
return HttpResponseRedirect(reverse("home"))
|
||||
|
||||
|
||||
class DomainApplicationDeleteView(DomainApplicationPermissionDeleteView):
|
||||
"""Delete view for home that allows the end user to delete DomainApplications"""
|
||||
|
||||
object: DomainApplication # workaround for type mismatch in DeleteView
|
||||
|
||||
def has_permission(self):
|
||||
"""Custom override for has_permission to exclude all statuses, except WITHDRAWN and STARTED"""
|
||||
has_perm = super().has_permission()
|
||||
if not has_perm:
|
||||
return False
|
||||
|
||||
status = self.get_object().status
|
||||
valid_statuses = [DomainApplication.ApplicationStatus.WITHDRAWN, DomainApplication.ApplicationStatus.STARTED]
|
||||
if status not in valid_statuses:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_success_url(self):
|
||||
"""After a delete is successful, redirect to home"""
|
||||
return reverse("home")
|
||||
|
|
|
@ -7,15 +7,53 @@ def index(request):
|
|||
"""This page is available to anyone without logging in."""
|
||||
context = {}
|
||||
if request.user.is_authenticated:
|
||||
applications = DomainApplication.objects.filter(creator=request.user)
|
||||
# Let's exclude the approved applications since our
|
||||
# domain_applications context will be used to populate
|
||||
# the active applications table
|
||||
context["domain_applications"] = applications.exclude(status="approved")
|
||||
# Get all domain applications the user has access to
|
||||
applications, deletable_applications = _get_applications(request)
|
||||
|
||||
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
|
||||
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
||||
domains = Domain.objects.filter(id__in=domain_ids)
|
||||
context["domain_applications"] = applications
|
||||
|
||||
# Get all domains the user has access to
|
||||
domains = _get_domains(request)
|
||||
context["domains"] = domains
|
||||
|
||||
# Determine if the user will see applications that they can delete
|
||||
has_deletable_applications = deletable_applications.exists()
|
||||
context["has_deletable_applications"] = has_deletable_applications
|
||||
|
||||
# If they can delete applications, add the delete button to the context
|
||||
if has_deletable_applications:
|
||||
# Add the delete modal button to the context
|
||||
modal_button = (
|
||||
'<button type="submit" '
|
||||
'class="usa-button usa-button--secondary" '
|
||||
'name="delete-application">Yes, delete request</button>'
|
||||
)
|
||||
context["modal_button"] = modal_button
|
||||
|
||||
return render(request, "home.html", context)
|
||||
|
||||
|
||||
def _get_applications(request):
|
||||
"""Given the current request,
|
||||
get all DomainApplications that are associated with the UserDomainRole object.
|
||||
|
||||
Returns a tuple of all applications, and those that are deletable by the user.
|
||||
"""
|
||||
# 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")
|
||||
|
||||
# Create a placeholder DraftDomain for each incomplete draft
|
||||
valid_statuses = [DomainApplication.ApplicationStatus.STARTED, DomainApplication.ApplicationStatus.WITHDRAWN]
|
||||
deletable_applications = applications.filter(status__in=valid_statuses)
|
||||
|
||||
return (applications, deletable_applications)
|
||||
|
||||
|
||||
def _get_domains(request):
|
||||
"""Given the current request,
|
||||
get all domains that are associated with the UserDomainRole object"""
|
||||
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
|
||||
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
||||
return Domain.objects.filter(id__in=domain_ids)
|
||||
|
|
|
@ -126,6 +126,14 @@ class DomainInvitationPermissionDeleteView(DomainInvitationPermission, DeleteVie
|
|||
object: DomainInvitation # workaround for type mismatch in DeleteView
|
||||
|
||||
|
||||
class DomainApplicationPermissionDeleteView(DomainApplicationPermission, DeleteView, abc.ABC):
|
||||
|
||||
"""Abstract view for deleting a DomainApplication."""
|
||||
|
||||
model = DomainApplication
|
||||
object: DomainApplication
|
||||
|
||||
|
||||
class UserDomainRolePermissionView(UserDomainRolePermission, DetailView, abc.ABC):
|
||||
|
||||
"""Abstract base view for UserDomainRole that enforces permissions.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue