{% endfor %}
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py
index 8f812b815..246222fe7 100644
--- a/src/registrar/tests/test_views.py
+++ b/src/registrar/tests/test_views.py
@@ -88,8 +88,9 @@ 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 = 6 because it is also in screenreader content, and in the delete modal
+ self.assertContains(response, "igorville.gov", count=6)
# clean up
application.delete()
diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py
index 63adbf3d9..5043ef245 100644
--- a/src/registrar/views/application.py
+++ b/src/registrar/views/application.py
@@ -1,4 +1,5 @@
import logging
+from django.forms import ValidationError
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect, render
@@ -576,7 +577,20 @@ class ApplicationWithdrawn(DomainApplicationPermissionWithdrawView):
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
+ if status not in [DomainApplication.ApplicationStatus.WITHDRAWN, DomainApplication.ApplicationStatus.STARTED]:
+ return False
+
+ return True
+
def get_success_url(self):
return reverse("home")
\ No newline at end of file
From 2042fd1146223cb9cab91ea1dd01e90527d191b6 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 5 Jan 2024 09:58:33 -0700
Subject: [PATCH 004/120] Increase padding
---
src/registrar/templates/home.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html
index 33708c8f6..b5bef3b71 100644
--- a/src/registrar/templates/home.html
+++ b/src/registrar/templates/home.html
@@ -9,7 +9,7 @@
{% if user.is_authenticated %}
{# the entire logged in page goes here #}
-
{% if application.status == "started" or application.status == "withdrawn" %}
{% endif %}
+ {% endif %}
{% endfor %}
diff --git a/src/registrar/views/index.py b/src/registrar/views/index.py
index a4a8e3d90..5996c27f6 100644
--- a/src/registrar/views/index.py
+++ b/src/registrar/views/index.py
@@ -18,11 +18,19 @@ def index(request):
domains = Domain.objects.filter(id__in=domain_ids)
context["domains"] = domains
- modal_button = (
- ''
- )
- context["modal_button"] = modal_button
+ # Determine if the user will see applications that they can delete
+ valid_statuses = [DomainApplication.ApplicationStatus.STARTED, DomainApplication.ApplicationStatus.WITHDRAWN]
+ has_deletable_applications = applications.filter(status__in=valid_statuses)
+ context["has_deletable_applications"] = has_deletable_applications
+
+ if has_deletable_applications:
+ modal_button = (
+ ''
+ )
+
+
+ context["modal_button"] = modal_button
return render(request, "home.html", context)
From 90760e6d43d2d4c1e5af47f9811e0315da775a48 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 5 Jan 2024 12:32:08 -0700
Subject: [PATCH 006/120] Add unit tests
---
src/registrar/tests/test_views.py | 80 +++++++++++++++++++
src/registrar/views/application.py | 4 +-
src/registrar/views/index.py | 1 -
.../views/utility/permission_views.py | 2 +-
4 files changed, 83 insertions(+), 4 deletions(-)
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py
index 246222fe7..cfc03d70d 100644
--- a/src/registrar/tests/test_views.py
+++ b/src/registrar/tests/test_views.py
@@ -94,6 +94,86 @@ class LoggedInTests(TestWithUser):
# 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_home_lists_domains(self):
response = self.client.get("/")
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py
index 5043ef245..d50502db4 100644
--- a/src/registrar/views/application.py
+++ b/src/registrar/views/application.py
@@ -1,5 +1,4 @@
import logging
-from django.forms import ValidationError
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect, render
@@ -578,6 +577,7 @@ class ApplicationWithdrawn(DomainApplicationPermissionWithdrawView):
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):
@@ -593,4 +593,4 @@ class DomainApplicationDeleteView(DomainApplicationPermissionDeleteView):
return True
def get_success_url(self):
- return reverse("home")
\ No newline at end of file
+ return reverse("home")
diff --git a/src/registrar/views/index.py b/src/registrar/views/index.py
index 5996c27f6..43f251d47 100644
--- a/src/registrar/views/index.py
+++ b/src/registrar/views/index.py
@@ -31,6 +31,5 @@ def index(request):
'name="delete-application">Yes, delete request'
)
-
context["modal_button"] = modal_button
return render(request, "home.html", context)
diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py
index cf6bd930d..587eb0b5c 100644
--- a/src/registrar/views/utility/permission_views.py
+++ b/src/registrar/views/utility/permission_views.py
@@ -129,4 +129,4 @@ class DomainApplicationPermissionDeleteView(DomainApplicationPermission, DeleteV
"""Abstract view for deleting a DomainApplication."""
model = DomainApplication
- object: DomainApplication
\ No newline at end of file
+ object: DomainApplication
From 693058c02a2d7a4430fd5c861319508735ad59b3 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 5 Jan 2024 12:49:39 -0700
Subject: [PATCH 007/120] Add padding for mobile view
---
src/registrar/assets/sass/_theme/_tables.scss | 7 +++++++
src/registrar/templates/home.html | 2 +-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/src/registrar/assets/sass/_theme/_tables.scss b/src/registrar/assets/sass/_theme/_tables.scss
index 6a52a5d08..65fb8b487 100644
--- a/src/registrar/assets/sass/_theme/_tables.scss
+++ b/src/registrar/assets/sass/_theme/_tables.scss
@@ -29,6 +29,13 @@
th.action-col-custom-width {
width: 27% !important;
}
+
+ // Fix margins in mobile view
+ @media screen {
+ td.tablet-margin-top-5 {
+ margin-top: 40px !important;
+ }
+ }
}
.dotgov-table {
diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html
index 62bbc3ac6..da9212bea 100644
--- a/src/registrar/templates/home.html
+++ b/src/registrar/templates/home.html
@@ -147,7 +147,7 @@
{% if has_deletable_applications %}
-
{% endif %}
diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py
index c76d285a7..80ff1eff1 100644
--- a/src/registrar/views/application.py
+++ b/src/registrar/views/application.py
@@ -143,93 +143,13 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
except DomainApplication.DoesNotExist:
logger.debug("Application id %s did not have a DomainApplication" % id)
- draft_domain = self._create_default_draft_domain()
-
- # Check added for linting purposes
- if self.request.user and isinstance(self.request.user, User):
- self._application = DomainApplication.objects.create(
- creator=self.request.user,
- requested_domain=draft_domain,
- )
- else:
- # TODO - Need some sort of front end display for this
- raise ValueError("Invalid type for user")
+ self._application = DomainApplication.objects.create(
+ creator=self.request.user
+ )
self.storage["application_id"] = self._application.id
return self._application
- def _create_default_draft_domain(self):
- "Set a default draft name for if the user exits without completing"
- default_draft_text = "New domain request"
-
- # Does the user have any incomplete drafts?
- existing_applications = DomainApplication.objects.filter(
- Q(requested_domain=None) | Q(requested_domain__is_incomplete=True),
- creator=self.request.user,
- )
-
- name_field = "requested_domain__name"
-
- incomplete_drafts = (
- existing_applications.exclude(requested_domain=None)
- .filter(requested_domain__name__icontains=default_draft_text)
- .order_by(name_field)
- )
-
- incomplete_draft_names = incomplete_drafts.values_list(name_field, flat=True)
-
- proposed_draft_number = incomplete_drafts.count() + 1
- draft_number = 1
- for application in existing_applications:
- if application.requested_domain is not None and application.requested_domain.name is not None:
- name = application.requested_domain.name
-
- # If we already have a list of draft numbers, base the
- # subsequent number off of the last numbered field.
- # This is to avoid a scenario in which drafts 1, 2, 3 and exist
- # and 2 is deleted - meaning we would get two duplicate "3"s if we added another
- if name in incomplete_draft_names:
- # Get the last numbered draft
- last_draft = incomplete_drafts.last()
- last_draft_number = last_draft.requested_domain.draft_number
-
- smallest_number = self._find_smallest_missing_number(incomplete_drafts)
- smallest_name = f"New domain request {smallest_number}"
- if smallest_name not in incomplete_draft_names:
- draft_number = smallest_number
- elif proposed_draft_number == last_draft_number:
- # If the draft number we are trying to create matches the last draft number,
- # simply add one to that number
- draft_number = last_draft_number + 1
-
- draft_domain = DraftDomain(
- # Save a blank string rather then None due to DB requirements
- name="",
- draft_number=draft_number,
- is_incomplete=True,
- )
- # Generate a default name based off of a draft_number
- draft_domain.name = draft_domain.get_default_request_name()
- draft_domain.save()
-
- return draft_domain
-
- def _find_smallest_missing_number(self, incomplete_drafts):
- draft_numbers = []
- for draft in incomplete_drafts:
- number = draft.requested_domain.draft_number
- if number is not None:
- draft_numbers.append(number)
-
- draft_numbers = sorted(draft_numbers)
- smallest_missing = 1
- for number in draft_numbers:
- if number == smallest_missing:
- smallest_missing += 1
- elif number > smallest_missing:
- break
- return smallest_missing
-
@property
def storage(self):
# marking session as modified on every access
@@ -409,7 +329,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
# Build the submit button that we'll pass to the modal.
modal_button = '"
# Concatenate the modal header that we'll pass to the modal.
- if self.application.requested_domain and not self.application.requested_domain.is_incomplete:
+ if self.application.requested_domain:
modal_heading = "You are about to submit a domain request for " + str(self.application.requested_domain)
else:
modal_heading = "You are about to submit an incomplete request"
@@ -556,14 +476,6 @@ class DotgovDomain(ApplicationWizard):
context["federal_type"] = self.application.federal_type
return context
- def post(self, request, *args, **kwargs):
- """Override for the post method to mark the DraftDomain as complete"""
- # Set the DraftDomain to "complete"
- print(f"what is the request at this time? {request}")
- self.application.requested_domain.is_incomplete = False
- response = super().post(request, *args, **kwargs)
- return response
-
class Purpose(ApplicationWizard):
template_name = "application_purpose.html"
diff --git a/src/registrar/views/index.py b/src/registrar/views/index.py
index f9a658942..3c0f7c723 100644
--- a/src/registrar/views/index.py
+++ b/src/registrar/views/index.py
@@ -1,6 +1,8 @@
+from django.utils import timezone
from django.shortcuts import render
from registrar.models import DomainApplication, Domain, UserDomainRole
+from registrar.models.draft_domain import DraftDomain
def index(request):
@@ -12,6 +14,22 @@ def index(request):
# the active applications table
applications = DomainApplication.objects.filter(creator=request.user).exclude(status="approved")
+
+ valid_statuses = [DomainApplication.ApplicationStatus.STARTED, DomainApplication.ApplicationStatus.WITHDRAWN]
+
+ # Create a placeholder DraftDomain for each incomplete draft
+ deletable_applications = applications.filter(status__in=valid_statuses, requested_domain=None)
+ for application in applications:
+ if application in deletable_applications:
+ created_at = application.created_at.strftime("%b. %d, %Y, %I:%M %p UTC")
+ _name = f"New domain request ({created_at})"
+ default_draft_domain = DraftDomain(
+ name=_name,
+ is_complete=False
+ )
+
+ application.requested_domain = default_draft_domain
+
# Pass the final context to the application
context["domain_applications"] = applications
@@ -22,17 +40,16 @@ def index(request):
context["domains"] = domains
# Determine if the user will see applications that they can delete
- valid_statuses = [DomainApplication.ApplicationStatus.STARTED, DomainApplication.ApplicationStatus.WITHDRAWN]
- has_deletable_applications = applications.filter(status__in=valid_statuses).exists()
+ has_deletable_applications = deletable_applications.exists()
context["has_deletable_applications"] = has_deletable_applications
-
if has_deletable_applications:
+
+ # Add the delete modal button to the context
modal_button = (
''
)
-
context["modal_button"] = modal_button
return render(request, "home.html", context)
From f623d00fc229d102df05a682dc3207f77238ca22 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 16 Jan 2024 14:16:48 -0700
Subject: [PATCH 052/120] Index.py refactor
---
src/registrar/views/index.py | 66 ++++++++++++++++++++++--------------
1 file changed, 40 insertions(+), 26 deletions(-)
diff --git a/src/registrar/views/index.py b/src/registrar/views/index.py
index 3c0f7c723..0a682b855 100644
--- a/src/registrar/views/index.py
+++ b/src/registrar/views/index.py
@@ -9,41 +9,21 @@ def index(request):
"""This page is available to anyone without logging in."""
context = {}
if request.user.is_authenticated:
- # 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")
-
- valid_statuses = [DomainApplication.ApplicationStatus.STARTED, DomainApplication.ApplicationStatus.WITHDRAWN]
-
- # Create a placeholder DraftDomain for each incomplete draft
- deletable_applications = applications.filter(status__in=valid_statuses, requested_domain=None)
- for application in applications:
- if application in deletable_applications:
- created_at = application.created_at.strftime("%b. %d, %Y, %I:%M %p UTC")
- _name = f"New domain request ({created_at})"
- default_draft_domain = DraftDomain(
- name=_name,
- is_complete=False
- )
-
- application.requested_domain = default_draft_domain
-
- # Pass the final context to the application
+ # Get all domain applications the user has access to
+ applications, deletable_applications = _get_applications(request)
context["domain_applications"] = applications
- 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)
-
+ # 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 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 = (
'