From d84a7890224ca1ec52c2e5097e8c0c1104589cdd Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Thu, 26 Dec 2024 17:10:37 -0800
Subject: [PATCH 01/68] Renewal form
---
src/registrar/config/urls.py | 5 ++
src/registrar/fixtures/fixtures_domains.py | 2 +-
src/registrar/models/domain.py | 5 ++
src/registrar/templates/domain_base.html | 9 +-
src/registrar/templates/domain_detail.html | 4 +-
src/registrar/templates/domain_sidebar.html | 10 ++-
src/registrar/templatetags/custom_filters.py | 1 +
src/registrar/views/__init__.py | 1 +
src/registrar/views/domain.py | 92 ++++++++++++++++++--
9 files changed, 117 insertions(+), 12 deletions(-)
diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py
index 66708c571..2bf7b9e5f 100644
--- a/src/registrar/config/urls.py
+++ b/src/registrar/config/urls.py
@@ -345,6 +345,11 @@ urlpatterns = [
views.DomainSecurityEmailView.as_view(),
name="domain-security-email",
),
+ path(
+ "domain//renewal",
+ views.DomainRenewalView.as_view(),
+ name="domain-renewal",
+ ),
path(
"domain//users/add",
views.DomainAddUserView.as_view(),
diff --git a/src/registrar/fixtures/fixtures_domains.py b/src/registrar/fixtures/fixtures_domains.py
index 4606024d0..4d4115180 100644
--- a/src/registrar/fixtures/fixtures_domains.py
+++ b/src/registrar/fixtures/fixtures_domains.py
@@ -44,7 +44,7 @@ class DomainFixture(DomainRequestFixture):
cls._approve_domain_requests(users)
@staticmethod
- def _generate_fake_expiration_date(days_in_future=365):
+ def _generate_fake_expiration_date(days_in_future=40):
"""Generates a fake expiration date between 1 and 365 days in the future."""
current_date = timezone.now().date() # nosec
return current_date + timedelta(days=random.randint(1, days_in_future)) # nosec
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 6eb2fac07..3fa6a61b2 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -1167,6 +1167,11 @@ class Domain(TimeStampedModel, DomainHelper):
threshold_date = now + timedelta(days=60)
return now < self.expiration_date <= threshold_date
+ ###dummy method for testing for domain renewal form fail or success banner
+
+ def update_expiration(self, success=True):
+ return success
+
def state_display(self, request=None):
"""Return the display status of the domain."""
if self.is_expired() and (self.state != self.State.UNKNOWN):
diff --git a/src/registrar/templates/domain_base.html b/src/registrar/templates/domain_base.html
index 9f7e8d2e6..de8e88791 100644
--- a/src/registrar/templates/domain_base.html
+++ b/src/registrar/templates/domain_base.html
@@ -1,5 +1,7 @@
{% extends "base.html" %}
{% load static %}
+{% load static url_helpers %}
+
{% block title %}{{ domain.name }} | {% endblock %}
@@ -53,8 +55,11 @@
{% endif %}
{% block domain_content %}
-
+ {% if request.path|endswith:"renewal"%}
+
Renew {{domain.name}}
+ {%else%}
Domain Overview
+ {% endif%}
{% endblock %} {# domain_content #}
{% endif %}
@@ -62,4 +67,4 @@
-{% endblock %} {# content #}
+{% endblock %} {# content #}
\ No newline at end of file
diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html
index a5b8e52cb..b168f7e82 100644
--- a/src/registrar/templates/domain_detail.html
+++ b/src/registrar/templates/domain_detail.html
@@ -50,7 +50,9 @@
{% if domain.get_state_help_text %}
{% if has_domain_renewal_flag and domain.is_expiring and is_domain_manager %}
- This domain will expire soon. Renew to maintain access.
+ This domain will expire soon.
+ {% url 'domain-renewal' pk=domain.id as url %}
+ Renew to maintain access.
{% elif has_domain_renewal_flag and domain.is_expiring and is_portfolio_user %}
This domain will expire soon. Contact one of the listed domain managers to renew the domain.
{% else %}
diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html
index 289f544ce..dc97f5ca1 100644
--- a/src/registrar/templates/domain_sidebar.html
+++ b/src/registrar/templates/domain_sidebar.html
@@ -79,8 +79,14 @@
{% with url_name="domain-users" %}
{% include "includes/domain_sidenav_item.html" with item_text="Domain managers" %}
{% endwith %}
-
+
+ {% if has_domain_renewal_flag and is_domain_manager and domain.is_expiring %}
+ {% with url_name="domain-renewal" %}
+ {% include "includes/domain_sidenav_item.html" with item_text="Renewal form" %}
+ {% endwith %}
+ {% endif %}
+
{% endif %}
-
+
\ No newline at end of file
diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py
index 6140130c8..6f3894ea5 100644
--- a/src/registrar/templatetags/custom_filters.py
+++ b/src/registrar/templatetags/custom_filters.py
@@ -200,6 +200,7 @@ def is_domain_subpage(path):
"domain-users-add",
"domain-request-delete",
"domain-user-delete",
+ "domain-renewal",
"invitation-cancel",
]
return get_url_name(path) in url_names
diff --git a/src/registrar/views/__init__.py b/src/registrar/views/__init__.py
index a80b16b1a..4e3faced1 100644
--- a/src/registrar/views/__init__.py
+++ b/src/registrar/views/__init__.py
@@ -14,6 +14,7 @@ from .domain import (
DomainInvitationCancelView,
DomainDeleteUserView,
PrototypeDomainDNSRecordView,
+ DomainRenewalView,
)
from .user_profile import UserProfileView, FinishProfileSetupView
from .health import *
diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py
index cb3da1f83..99a173517 100644
--- a/src/registrar/views/domain.py
+++ b/src/registrar/views/domain.py
@@ -12,7 +12,7 @@ from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.db import IntegrityError
from django.http import HttpResponseRedirect
-from django.shortcuts import redirect
+from django.shortcuts import redirect, render
from django.urls import reverse
from django.views.generic.edit import FormMixin
from django.conf import settings
@@ -307,6 +307,90 @@ class DomainView(DomainBaseView):
self._update_session_with_domain()
+class DomainRenewalView(DomainBaseView):
+ """Domain detail overview page."""
+
+ template_name = "domain_renewal.html"
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+
+ default_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value]
+ context["hidden_security_emails"] = default_emails
+
+ security_email = self.object.get_security_email()
+ user = self.request.user
+ if security_email is None or security_email in default_emails:
+ context["security_email"] = None
+ context["user"] = user
+ return context
+
+ def can_access_domain_via_portfolio(self, pk):
+ """Most views should not allow permission to portfolio users.
+ If particular views allow permissions, they will need to override
+ this function."""
+ portfolio = self.request.session.get("portfolio")
+ if self.request.user.has_any_domains_portfolio_permission(portfolio):
+ if Domain.objects.filter(id=pk).exists():
+ domain = Domain.objects.get(id=pk)
+ if domain.domain_info.portfolio == portfolio:
+ return True
+ return False
+
+ def in_editable_state(self, pk):
+ """Override in_editable_state from DomainPermission
+ Allow detail page to be viewable"""
+
+ requested_domain = None
+ if Domain.objects.filter(id=pk).exists():
+ requested_domain = Domain.objects.get(id=pk)
+
+ # return true if the domain exists, this will allow the detail page to load
+ if requested_domain:
+ return True
+ return False
+
+ def _get_domain(self, request):
+ """
+ override get_domain for this view so that domain overview
+ always resets the cache for the domain object
+ """
+ self.session = request.session
+ self.object = self.get_object()
+ self._update_session_with_domain()
+
+ def post(self, request, pk):
+ domain = Domain.objects.filter(id=pk).first()
+
+ # Check if the checkbox is checked
+ is_policy_acknowledged = request.POST.get("is_policy_acknowledged", None)
+ if is_policy_acknowledged != "on":
+ print("!!! Checkbox is NOT acknowledged")
+ messages.error(
+ request, "Check the box if you read and agree to the requirements for operating a .gov domain."
+ )
+ return render(
+ request,
+ "domain_renewal.html",
+ {
+ "domain": domain,
+ "form": request.POST,
+ },
+ )
+
+ print("*** Checkbox is acknowledged")
+ if "submit_button" in request.POST:
+ print("*** Submit button clicked")
+ updated_expiration = domain.update_expiration(success=True)
+ print("*** Updated expiration result:", updated_expiration)
+
+ if updated_expiration is True:
+ messages.success(request, "This domain has been renewed for one year")
+ else:
+ messages.error(request, "This domain has not been renewed")
+ return HttpResponseRedirect(reverse("domain", kwargs={"pk": pk}))
+
+
class DomainOrgNameAddressView(DomainFormBaseView):
"""Organization view"""
@@ -807,11 +891,7 @@ class DomainDNSSECView(DomainFormBaseView):
has_dnssec_records = self.object.dnssecdata is not None
# Create HTML for the modal button
- modal_button = (
- ''
- )
+ modal_button = ''
context["modal_button"] = modal_button
context["has_dnssec_records"] = has_dnssec_records
From b5658a357b6aa9b49c7c37393ae1f8893dbebbeb Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Thu, 26 Dec 2024 17:15:07 -0800
Subject: [PATCH 02/68] Fix linter
---
src/registrar/models/domain.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 3fa6a61b2..715f1b9da 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -1167,7 +1167,7 @@ class Domain(TimeStampedModel, DomainHelper):
threshold_date = now + timedelta(days=60)
return now < self.expiration_date <= threshold_date
- ###dummy method for testing for domain renewal form fail or success banner
+ # Dummy method for testing for domain renewal form fail or success banner
def update_expiration(self, success=True):
return success
From 8b9eb93b682a3f8b714c42558b90db6b94bc7c57 Mon Sep 17 00:00:00 2001
From: asaki222
Date: Mon, 30 Dec 2024 11:07:33 -0500
Subject: [PATCH 03/68] added back domain renewal form
---
src/registrar/templates/domain_renewal.html | 127 ++++++++++++++++++++
1 file changed, 127 insertions(+)
create mode 100644 src/registrar/templates/domain_renewal.html
diff --git a/src/registrar/templates/domain_renewal.html b/src/registrar/templates/domain_renewal.html
new file mode 100644
index 000000000..c179941d6
--- /dev/null
+++ b/src/registrar/templates/domain_renewal.html
@@ -0,0 +1,127 @@
+{% extends "domain_base.html" %}
+{% load static url_helpers %}
+{% load custom_filters %}
+
+{% block domain_content %}
+ {% block breadcrumb %}
+ {% if portfolio %}
+
+
+ {% endif %}
+ {% endblock breadcrumb %}
+
+ {{ block.super }}
+
+
Confirm the following information for accuracy
+
Review these details below. We
+ require that you maintain accurate information for the domain.
+ The details you provide will only be used to support eh administration of .gov and won't be made public.
+
+
If you would like to retire your domain instead, please
+ contact us.
+
Required fields are marked with an asterisk (*).
+
+
+
+ {% url 'user-profile' as url %}
+ {% include "includes/summary_item.html" with title='Your Contact Information' value=user edit_link=url editable=is_editable contact='true' %}
+
+
+
+ {% if analyst_action != 'edit' or analyst_action_location != domain.pk %}
+ {% if is_portfolio_user and not is_domain_manager %}
+
+
+
+ You don't have access to manage {{domain.name}}. If you need to make updates, contact one of the listed domain managers.
+
+
+
+ {% endif %}
+ {% endif %}
+
+ {% url 'domain-security-email' pk=domain.id as url %}
+ {% if security_email is not None and security_email not in hidden_security_emails%}
+ {% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url editable=is_editable %}
+ {% else %}
+ {% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url editable=is_editable %}
+ {% endif %}
+ {% url 'domain-users' pk=domain.id as url %}
+ {% if portfolio %}
+ {% include "includes/summary_item.html" with title='Domain managers' domain_permissions=True value=domain edit_link=url editable=is_editable %}
+ {% else %}
+ {% include "includes/summary_item.html" with title='Domain managers' list=True users=True value=domain.permissions.all edit_link=url editable=is_editable %}
+ {% endif %}
+
+
+
Review these details below. We
+ require that you maintain accurate information for the domain.
+ The details you provide will only be used to support eh administration of .gov and won't be made public.
+
+
If you would like to retire your domain instead, please
+ contact us.
+
Required fields are marked with an asterisk (*).
+
+
+
+ {% url 'user-profile' as url %}
+ {% include "includes/summary_item.html" with title='Your Contact Information' value=user edit_link=url editable=is_editable contact='true' %}
+
+
+
+ {% if analyst_action != 'edit' or analyst_action_location != domain.pk %}
+ {% if is_portfolio_user and not is_domain_manager %}
+
+
+
+ You don't have access to manage {{domain.name}}. If you need to make updates, contact one of the listed domain managers.
+
+
+
+ {% endif %}
+ {% endif %}
+
+ {% url 'domain-security-email' pk=domain.id as url %}
+ {% if security_email is not None and security_email not in hidden_security_emails%}
+ {% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url editable=is_editable %}
+ {% else %}
+ {% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url editable=is_editable %}
+ {% endif %}
+ {% url 'domain-users' pk=domain.id as url %}
+ {% if portfolio %}
+ {% include "includes/summary_item.html" with title='Domain managers' domain_permissions=True value=domain edit_link=url editable=is_editable %}
+ {% else %}
+ {% include "includes/summary_item.html" with title='Domain managers' list=True users=True value=domain.permissions.all edit_link=url editable=is_editable %}
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %} {# domain_content #}
\ No newline at end of file
From e23d81f7e7d4096e76450b0cca84908846484872 Mon Sep 17 00:00:00 2001
From: asaki222
Date: Mon, 30 Dec 2024 12:44:52 -0500
Subject: [PATCH 05/68] added renew method
---
src/registrar/models/domain.py | 6 +++++-
src/registrar/views/domain.py | 11 ++++++-----
2 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 715f1b9da..e170c8668 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -322,28 +322,32 @@ class Domain(TimeStampedModel, DomainHelper):
"""
# If no date is specified, grab the registry_expiration_date
+ print("checking if there is a date")
try:
exp_date = self.registry_expiration_date
except KeyError:
# if no expiration date from registry, set it to today
logger.warning("current expiration date not set; setting to today")
exp_date = date.today()
-
+ print("we got the date", exp_date)
# create RenewDomain request
request = commands.RenewDomain(name=self.name, cur_exp_date=exp_date, period=epp.Period(length, unit))
try:
# update expiration date in registry, and set the updated
# expiration date in the registrar, and in the cache
+ print("we are in the second try")
self._cache["ex_date"] = registry.send(request, cleaned=True).res_data[0].ex_date
self.expiration_date = self._cache["ex_date"]
self.save()
except RegistryError as err:
# if registry error occurs, log the error, and raise it as well
+ print("registry error")
logger.error(f"registry error renewing domain: {err}")
raise (err)
except Exception as e:
# exception raised during the save to registrar
+ print("this is the last error")
logger.error(f"error updating expiration date in registrar: {e}")
raise (e)
diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py
index 99a173517..1c1996c65 100644
--- a/src/registrar/views/domain.py
+++ b/src/registrar/views/domain.py
@@ -381,12 +381,13 @@ class DomainRenewalView(DomainBaseView):
print("*** Checkbox is acknowledged")
if "submit_button" in request.POST:
print("*** Submit button clicked")
- updated_expiration = domain.update_expiration(success=True)
- print("*** Updated expiration result:", updated_expiration)
-
- if updated_expiration is True:
+ # updated_expiration = domain.update_expiration(success=True)
+ # print("*** Updated expiration result:", updated_expiration)
+ try:
+ domain.renew_domain()
messages.success(request, "This domain has been renewed for one year")
- else:
+ except Exception as e:
+ print(f'An error occured: {e}')
messages.error(request, "This domain has not been renewed")
return HttpResponseRedirect(reverse("domain", kwargs={"pk": pk}))
From a128086d6f5e2cf51dda942dfc92c7dceb92a45d Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Tue, 31 Dec 2024 10:13:53 -0800
Subject: [PATCH 06/68] Update print statements and exp timing
---
src/registrar/models/domain.py | 16 +++++++++-------
src/registrar/views/domain.py | 5 +++--
2 files changed, 12 insertions(+), 9 deletions(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index e170c8668..b7145ec0c 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -322,32 +322,34 @@ class Domain(TimeStampedModel, DomainHelper):
"""
# If no date is specified, grab the registry_expiration_date
- print("checking if there is a date")
+ print("*** Checking if there is a date")
try:
exp_date = self.registry_expiration_date
except KeyError:
# if no expiration date from registry, set it to today
- logger.warning("current expiration date not set; setting to today")
- exp_date = date.today()
- print("we got the date", exp_date)
+ logger.warning("*** Current expiration date not set; setting to 35 days ")
+ # exp_date = date.today()
+ exp_date = date.today() - timedelta(days=35)
+ print(exp_date)
+ print("*** The exp_date is", exp_date)
# create RenewDomain request
request = commands.RenewDomain(name=self.name, cur_exp_date=exp_date, period=epp.Period(length, unit))
try:
# update expiration date in registry, and set the updated
# expiration date in the registrar, and in the cache
- print("we are in the second try")
+ print("** In renew_domain in 2nd try statement")
self._cache["ex_date"] = registry.send(request, cleaned=True).res_data[0].ex_date
self.expiration_date = self._cache["ex_date"]
self.save()
except RegistryError as err:
# if registry error occurs, log the error, and raise it as well
- print("registry error")
+ print("*** Registry error")
logger.error(f"registry error renewing domain: {err}")
raise (err)
except Exception as e:
# exception raised during the save to registrar
- print("this is the last error")
+ print("*** In renew_domain, in the last Exception statement")
logger.error(f"error updating expiration date in registrar: {e}")
raise (e)
diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py
index 1c1996c65..dd8e9928b 100644
--- a/src/registrar/views/domain.py
+++ b/src/registrar/views/domain.py
@@ -384,11 +384,12 @@ class DomainRenewalView(DomainBaseView):
# updated_expiration = domain.update_expiration(success=True)
# print("*** Updated expiration result:", updated_expiration)
try:
+ print("*** Did we get into the try statement")
domain.renew_domain()
messages.success(request, "This domain has been renewed for one year")
except Exception as e:
- print(f'An error occured: {e}')
- messages.error(request, "This domain has not been renewed")
+ print(f"An error occured: {e}")
+ messages.error(request, "*** This domain has not been renewed")
return HttpResponseRedirect(reverse("domain", kwargs={"pk": pk}))
From c145ecbfa29d925af2806343eb8d8534677d8ac5 Mon Sep 17 00:00:00 2001
From: asaki222
Date: Tue, 31 Dec 2024 13:55:18 -0500
Subject: [PATCH 07/68] put back the fixtures command
---
src/registrar/fixtures/fixtures_domains.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/fixtures/fixtures_domains.py b/src/registrar/fixtures/fixtures_domains.py
index 4d4115180..4606024d0 100644
--- a/src/registrar/fixtures/fixtures_domains.py
+++ b/src/registrar/fixtures/fixtures_domains.py
@@ -44,7 +44,7 @@ class DomainFixture(DomainRequestFixture):
cls._approve_domain_requests(users)
@staticmethod
- def _generate_fake_expiration_date(days_in_future=40):
+ def _generate_fake_expiration_date(days_in_future=365):
"""Generates a fake expiration date between 1 and 365 days in the future."""
current_date = timezone.now().date() # nosec
return current_date + timedelta(days=random.randint(1, days_in_future)) # nosec
From e5f9696bac942a946c92430ff36dc6eba489b608 Mon Sep 17 00:00:00 2001
From: asaki222
Date: Fri, 3 Jan 2025 10:43:35 -0500
Subject: [PATCH 08/68] added tests
---
src/registrar/tests/test_views_domain.py | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index ba237e1e7..767445810 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -529,6 +529,21 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
)
self.assertContains(detail_page, "Renew to maintain access")
+ @override_flag("domain_renewal", active=True)
+ def test_domain_renewal_sidebar_and_form(self):
+ self.client.force_login(self.user)
+ with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
+ Domain, "is_expired", self.custom_is_expired
+ ):
+ detail_page = self.client.get(
+ reverse("domain", kwargs={"pk": self.expiringdomain.id}),
+ )
+ self.assertContains(detail_page, "Renewal form")
+ response = self.client.get(reverse("domain-renewal",kwargs={"pk": self.expiringdomain.id}))
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, f"Renew {self.expiringdomain.name}")
+
+
class TestDomainManagers(TestDomainOverview):
@classmethod
From 4feb29b269a39417e35fae2589b7026fbee757c3 Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Fri, 3 Jan 2025 10:47:42 -0800
Subject: [PATCH 09/68] Add in tests for making edit is clickable and goes to
right route
---
src/registrar/tests/test_views_domain.py | 57 +++++++++++++++++++++++-
1 file changed, 55 insertions(+), 2 deletions(-)
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index 767445810..281b7291e 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -539,10 +539,63 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
reverse("domain", kwargs={"pk": self.expiringdomain.id}),
)
self.assertContains(detail_page, "Renewal form")
- response = self.client.get(reverse("domain-renewal",kwargs={"pk": self.expiringdomain.id}))
+ response = self.client.get(reverse("domain-renewal", kwargs={"pk": self.expiringdomain.id}))
self.assertEqual(response.status_code, 200)
self.assertContains(response, f"Renew {self.expiringdomain.name}")
-
+
+ @override_flag("domain_renewal", active=True)
+ def test_domain_renewal_form_your_contact_info_edit(self):
+ with less_console_noise():
+ # Start on the Renewal page for the domain
+ renewal_page = self.app.get(reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id}))
+
+ # Verify we see "Your Contact Information" on the renewal form
+ self.assertContains(renewal_page, "Your Contact Information")
+
+ # Verify that the "Edit" button for Your Contact is there and links to correct URL
+ edit_button_url = reverse("user-profile")
+ self.assertContains(renewal_page, f'href="{edit_button_url}"')
+
+ # Simulate clicking on edit button
+ edit_page = renewal_page.click(href=edit_button_url, index=1)
+ self.assertEqual(edit_page.status_code, 200)
+ self.assertContains(edit_page, "Review the details below and update any required information")
+
+ @override_flag("domain_renewal", active=True)
+ def test_domain_renewal_form_security_contact_edit(self):
+ with less_console_noise():
+ # Start on the Renewal page for the domain
+ renewal_page = self.app.get(reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id}))
+
+ # Verify we see "Security email" on the renewal form
+ self.assertContains(renewal_page, "Security email")
+
+ # Verify that the "Edit" button for Security email is there and links to correct URL
+ edit_button_url = reverse("domain-security-email", kwargs={"pk": self.domain_with_ip.id})
+ self.assertContains(renewal_page, f'href="{edit_button_url}"')
+
+ # Simulate clicking on edit button
+ edit_page = renewal_page.click(href=edit_button_url, index=1)
+ self.assertEqual(edit_page.status_code, 200)
+ self.assertContains(edit_page, "A security contact should be capable of evaluating")
+
+ @override_flag("domain_renewal", active=True)
+ def test_domain_renewal_form_domain_manager_edit(self):
+ with less_console_noise():
+ # Start on the Renewal page for the domain
+ renewal_page = self.app.get(reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id}))
+
+ # Verify we see "Domain managers" on the renewal form
+ self.assertContains(renewal_page, "Domain managers")
+
+ # Verify that the "Edit" button for Domain managers is there and links to correct URL
+ edit_button_url = reverse("domain-users", kwargs={"pk": self.domain_with_ip.id})
+ self.assertContains(renewal_page, f'href="{edit_button_url}"')
+
+ # Simulate clicking on edit button
+ edit_page = renewal_page.click(href=edit_button_url, index=1)
+ self.assertEqual(edit_page.status_code, 200)
+ self.assertContains(edit_page, "Domain managers can update all information related to a domain")
class TestDomainManagers(TestDomainOverview):
From 3f3ba22a119db0568861440f88dc0632d2ae4bfd Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Fri, 3 Jan 2025 11:28:48 -0800
Subject: [PATCH 10/68] Update text to click on link
---
src/registrar/tests/test_views_domain.py | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index 281b7291e..02d7aa9ac 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -535,11 +535,24 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
Domain, "is_expired", self.custom_is_expired
):
+ # Grab the detail page
detail_page = self.client.get(
reverse("domain", kwargs={"pk": self.expiringdomain.id}),
)
+
+ # Make sure we see the link as a domain manager
+ self.assertContains(detail_page, "Renew to maintain access")
+
+ # Make sure we can see Renewal form on the sidebar since it's expiring
self.assertContains(detail_page, "Renewal form")
- response = self.client.get(reverse("domain-renewal", kwargs={"pk": self.expiringdomain.id}))
+
+ # Grab link to the renewal page
+ renewal_form_url = reverse("domain-renewal", kwargs={"pk": self.expiringdomain.id})
+ self.assertContains(detail_page, f'href="{renewal_form_url}"')
+
+ # Simulate clicking the link
+ response = self.client.get(renewal_form_url)
+
self.assertEqual(response.status_code, 200)
self.assertContains(response, f"Renew {self.expiringdomain.name}")
From dca70f4195237db300508421ecc59b0a43bab908 Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Sat, 4 Jan 2025 14:17:55 -0800
Subject: [PATCH 11/68] Unit test for submit and recieving error message
---
src/registrar/tests/test_views_domain.py | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index 02d7aa9ac..50b8f31f6 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -9,6 +9,7 @@ from api.tests.common import less_console_noise_decorator
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from .common import MockEppLib, MockSESClient, create_user # type: ignore
from django_webtest import WebTest # type: ignore
+from django.contrib.messages import get_messages
import boto3_mocking # type: ignore
from registrar.utility.errors import (
@@ -610,6 +611,25 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
self.assertEqual(edit_page.status_code, 200)
self.assertContains(edit_page, "Domain managers can update all information related to a domain")
+ @override_flag("domain_renewal", active=True)
+ def test_ack_checkbox_not_checked(self):
+
+ # Grab the renewal URL
+ renewal_url = reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id})
+
+ # Test clicking the checkbox
+ response = self.client.post(renewal_url, data={"submit_button": "next"})
+
+ # Verify the error message is displayed
+ # Retrieves messages obj (used in the post call)
+ messages = list(get_messages(response.wsgi_request))
+ # Check we only get 1 error message
+ self.assertEqual(len(messages), 1)
+ # Check that the 1 error msg also is the right text
+ self.assertEqual(
+ str(messages[0]),
+ "Check the box if you read and agree to the requirements for operating a .gov domain.",
+ )
class TestDomainManagers(TestDomainOverview):
@classmethod
From bad8a8c98a75c1df161321c6a49d4b087d19e291 Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Sat, 4 Jan 2025 14:48:21 -0800
Subject: [PATCH 12/68] Fix padding on form and addin subtext for security
email
---
src/registrar/templates/domain_renewal.html | 7 ++++---
src/registrar/templates/includes/summary_item.html | 8 +++++---
src/registrar/tests/test_views_domain.py | 1 +
3 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/src/registrar/templates/domain_renewal.html b/src/registrar/templates/domain_renewal.html
index eda7a8bdd..4bf69dbca 100644
--- a/src/registrar/templates/domain_renewal.html
+++ b/src/registrar/templates/domain_renewal.html
@@ -26,6 +26,7 @@
{{ block.super }}
Confirm the following information for accuracy
+
HELLO
Review these details below. We
require that you maintain accurate information for the domain.
The details you provide will only be used to support eh administration of .gov and won't be made public.
@@ -55,9 +56,9 @@
{% url 'domain-security-email' pk=domain.id as url %}
{% if security_email is not None and security_email not in hidden_security_emails%}
- {% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url editable=is_editable %}
+ {% include "includes/summary_item.html" with title='Security email' value=security_email custom_text_for_value_none='We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain.' edit_link=url editable=is_editable %}
{% else %}
- {% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url editable=is_editable %}
+ {% include "includes/summary_item.html" with title='Security email' value='None provided' custom_text_for_value_none='We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain.' edit_link=url editable=is_editable %}
{% endif %}
{% url 'domain-users' pk=domain.id as url %}
{% if portfolio %}
@@ -67,7 +68,7 @@
{% endif %}
-
+
{% url 'user-profile' as url %}
{% include "includes/summary_item.html" with title='Your Contact Information' value=user edit_link=url editable=is_editable contact='true' %}
@@ -66,62 +78,50 @@
{% include "includes/summary_item.html" with title='Domain managers' list=True users=True value=domain.permissions.all edit_link=url editable=is_editable %}
{% endif %}
-
+
+
+
+
+
+
{% endblock %} {# domain_content #}
\ No newline at end of file
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index ee8adf903..0a4096fbf 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -454,7 +454,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
self.user.save()
- def todays_expiration_date(self):
+ def expiration_date_one_year_out(self):
todays_date = datetime.today()
new_expiration_date = todays_date.replace(year=todays_date.year + 1)
return new_expiration_date
@@ -466,7 +466,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
return True
def custom_renew_domain(self):
- self.domain_with_ip.expiration_date = self.todays_expiration_date()
+ self.domain_with_ip.expiration_date = self.expiration_date_one_year_out()
self.domain_with_ip.save()
@override_flag("domain_renewal", active=True)
@@ -659,7 +659,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
self.assertRedirects(response, reverse("domain", kwargs={"pk": self.domain_with_ip.id}))
# Check for the updated expiration
- formatted_new_expiration_date = self.todays_expiration_date().strftime("%b. %-d, %Y")
+ formatted_new_expiration_date = self.expiration_date_one_year_out().strftime("%b. %-d, %Y")
redirect_response = self.client.get(reverse("domain", kwargs={"pk": self.domain_with_ip.id}), follow=True)
self.assertContains(redirect_response, formatted_new_expiration_date)
diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py
index b849459f2..593fc9093 100644
--- a/src/registrar/views/domain.py
+++ b/src/registrar/views/domain.py
@@ -12,11 +12,11 @@ from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.db import IntegrityError
from django.http import HttpResponseRedirect
-from django.shortcuts import redirect, render
+from django.shortcuts import redirect, render, get_object_or_404
from django.urls import reverse
from django.views.generic.edit import FormMixin
from django.conf import settings
-from registrar.forms.domain import DomainSuborganizationForm
+from registrar.forms.domain import DomainSuborganizationForm, DomainRenewalForm
from registrar.models import (
Domain,
DomainRequest,
@@ -364,30 +364,32 @@ class DomainRenewalView(DomainBaseView):
self._update_session_with_domain()
def post(self, request, pk):
- domain = Domain.objects.filter(id=pk).first()
+ domain = get_object_or_404(Domain, id=pk)
- # Check if the checkbox is checked
- is_policy_acknowledged = request.POST.get("is_policy_acknowledged", None)
- if is_policy_acknowledged != "on":
- messages.error(
- request, "Check the box if you read and agree to the requirements for operating a .gov domain."
- )
- return render(
- request,
- "domain_renewal.html",
- {
- "domain": domain,
- "form": request.POST,
- },
- )
+ form = DomainRenewalForm(request.POST)
- if "submit_button" in request.POST:
- try:
- domain.renew_domain()
- messages.success(request, "This domain has been renewed for one year.")
- except Exception as e:
- messages.error(request, "This domain has not been renewed for one year, error was %s" % e)
- return HttpResponseRedirect(reverse("domain", kwargs={"pk": pk}))
+ if form.is_valid():
+ # check for key in the post request data
+ if "submit_button" in request.POST:
+ try:
+ domain.renew_domain()
+ messages.success(request, "This domain has been renewed for one year.")
+ except Exception as e:
+ messages.error(
+ request,
+ "This domain has not been renewed for one year, please email help@get.gov if this problem persists.",
+ )
+ return HttpResponseRedirect(reverse("domain", kwargs={"pk": pk}))
+
+ # if not valid, render the template with error messages
+ return render(
+ request,
+ "domain_renewal.html",
+ {
+ "domain": domain,
+ "form": form,
+ },
+ )
class DomainOrgNameAddressView(DomainFormBaseView):
From e196e02d9284c55057ec1dc6ce4736c6b0f3e0ba Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Tue, 7 Jan 2025 17:28:35 -0800
Subject: [PATCH 20/68] Address linter errors
---
src/registrar/views/domain.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py
index 593fc9093..4b26caa5f 100644
--- a/src/registrar/views/domain.py
+++ b/src/registrar/views/domain.py
@@ -374,10 +374,11 @@ class DomainRenewalView(DomainBaseView):
try:
domain.renew_domain()
messages.success(request, "This domain has been renewed for one year.")
- except Exception as e:
+ except Exception:
messages.error(
request,
- "This domain has not been renewed for one year, please email help@get.gov if this problem persists.",
+ "This domain has not been renewed for one year, "
+ "please email help@get.gov if this problem persists.",
)
return HttpResponseRedirect(reverse("domain", kwargs={"pk": pk}))
From aad1ee976b5735633d93caa1a1b9724a46206206 Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Tue, 7 Jan 2025 17:40:37 -0800
Subject: [PATCH 21/68] Fix checkbox unchecked test
---
src/registrar/tests/test_views_domain.py | 14 +++-----------
1 file changed, 3 insertions(+), 11 deletions(-)
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index 0a4096fbf..ef4d415df 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -631,19 +631,11 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
# Grab the renewal URL
renewal_url = reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id})
- # Test clicking the checkbox
+ # Test that the checkbox is not checked
response = self.client.post(renewal_url, data={"submit_button": "next"})
- # Verify the error message is displayed
- # Retrieves messages obj (used in the post call)
- messages = list(get_messages(response.wsgi_request))
- # Check we only get 1 error message
- self.assertEqual(len(messages), 1)
- # Check that the 1 error msg also is the right text
- self.assertEqual(
- str(messages[0]),
- "Check the box if you read and agree to the requirements for operating a .gov domain.",
- )
+ error_message = "Check the box if you read and agree to the requirements for operating a .gov domain."
+ self.assertContains(response, error_message)
@override_flag("domain_renewal", active=True)
def test_ack_checkbox_checked(self):
From 33e36590a4e0ff9b21836d22848618bc456b0533 Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Tue, 7 Jan 2025 17:43:54 -0800
Subject: [PATCH 22/68] Remove unused import for linter
---
src/registrar/tests/test_views_domain.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index ef4d415df..79240286a 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -11,7 +11,6 @@ from api.tests.common import less_console_noise_decorator
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from .common import MockEppLib, MockSESClient, create_user # type: ignore
from django_webtest import WebTest # type: ignore
-from django.contrib.messages import get_messages
import boto3_mocking # type: ignore
from registrar.utility.errors import (
From 628c4c0d0e4015a248bcc2ee33d583bbf5bcdb69 Mon Sep 17 00:00:00 2001
From: asaki222
Date: Wed, 8 Jan 2025 13:56:48 -0500
Subject: [PATCH 23/68] expired domain renewal form
---
src/registrar/templates/domain_detail.html | 6 ++++++
src/registrar/templates/domain_sidebar.html | 2 +-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html
index b168f7e82..becf46d5b 100644
--- a/src/registrar/templates/domain_detail.html
+++ b/src/registrar/templates/domain_detail.html
@@ -50,11 +50,17 @@
{% if domain.get_state_help_text %}
{% if has_domain_renewal_flag and domain.is_expiring and is_domain_manager %}
+ This domain has expired, but it is still online.
+ {% url 'domain-renewal' pk=domain.id as url %}
+ Renew to maintain access.
+ {% elif has_domain_renewal_flag and domain.is_expired and is_domain_manager %}
This domain will expire soon.
{% url 'domain-renewal' pk=domain.id as url %}
Renew to maintain access.
{% elif has_domain_renewal_flag and domain.is_expiring and is_portfolio_user %}
This domain will expire soon. Contact one of the listed domain managers to renew the domain.
+ {% elif has_domain_renewal_flag and domain.is_expired and is_portfolio_user %}
+ This domain has expired, but it is still online. Contact one of the listed domain managers to renew the domain.
{% else %}
{{ domain.get_state_help_text }}
{% endif %}
diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html
index 9d71ebf63..c9feb7200 100644
--- a/src/registrar/templates/domain_sidebar.html
+++ b/src/registrar/templates/domain_sidebar.html
@@ -80,7 +80,7 @@
{% include "includes/domain_sidenav_item.html" with item_text="Domain managers" %}
{% endwith %}
- {% if has_domain_renewal_flag and is_domain_manager and domain.is_expiring %}
+ {% if has_domain_renewal_flag and is_domain_manager and (domain.is_expiring or domain.is_expired) %}
{% with url_name="domain-renewal" %}
{% include "includes/domain_sidenav_item.html" with item_text="Renewal form" %}
{% endwith %}
From e8fede74ac05885d3b83d90b5cff372f870a5c89 Mon Sep 17 00:00:00 2001
From: asaki222
Date: Wed, 8 Jan 2025 15:25:06 -0500
Subject: [PATCH 24/68] updated code to fix parsing error
---
src/registrar/templates/domain_sidebar.html | 10 +--
src/registrar/tests/test_views_domain.py | 77 +++++++++++++++------
2 files changed, 61 insertions(+), 26 deletions(-)
diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html
index c9feb7200..336a9fe7c 100644
--- a/src/registrar/templates/domain_sidebar.html
+++ b/src/registrar/templates/domain_sidebar.html
@@ -80,10 +80,12 @@
{% include "includes/domain_sidenav_item.html" with item_text="Domain managers" %}
{% endwith %}
- {% if has_domain_renewal_flag and is_domain_manager and (domain.is_expiring or domain.is_expired) %}
- {% with url_name="domain-renewal" %}
- {% include "includes/domain_sidenav_item.html" with item_text="Renewal form" %}
- {% endwith %}
+ {% if has_domain_renewal_flag and is_domain_manager%}
+ {% if domain.is_expiring or domain.is_expired %}
+ {% with url_name="domain-renewal" %}
+ {% include "includes/domain_sidenav_item.html" with item_text="Renewal form" %}
+ {% endwith %}
+ {% endif %}
{% endif %}
{% endif %}
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index ee8adf903..406e96ed4 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -440,15 +440,15 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
username="usertest",
)
- self.expiringdomain, _ = Domain.objects.get_or_create(
- name="expiringdomain.gov",
+ self.domaintorenew, _ = Domain.objects.get_or_create(
+ name="domainrenewal.gov",
)
UserDomainRole.objects.get_or_create(
- user=self.user, domain=self.expiringdomain, role=UserDomainRole.Roles.MANAGER
+ user=self.user, domain=self.domaintorenew, role=UserDomainRole.Roles.MANAGER
)
- DomainInformation.objects.get_or_create(creator=self.user, domain=self.expiringdomain)
+ DomainInformation.objects.get_or_create(creator=self.user, domain=self.domaintorenew)
self.portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test org", creator=self.user)
@@ -459,9 +459,12 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
new_expiration_date = todays_date.replace(year=todays_date.year + 1)
return new_expiration_date
- def custom_is_expired(self):
+ def custom_is_expired_false(self):
return False
+ def custom_is_expired_true(self):
+ return True
+
def custom_is_expiring(self):
return True
@@ -473,11 +476,11 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
def test_expiring_domain_on_detail_page_as_domain_manager(self):
self.client.force_login(self.user)
with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
- Domain, "is_expired", self.custom_is_expired
+ Domain, "is_expired", self.custom_is_expired_false
):
- self.assertEquals(self.expiringdomain.state, Domain.State.UNKNOWN)
+ self.assertEquals(self.domaintorenew.state, Domain.State.UNKNOWN)
detail_page = self.client.get(
- reverse("domain", kwargs={"pk": self.expiringdomain.id}),
+ reverse("domain", kwargs={"pk": self.domaintorenew.id}),
)
self.assertContains(detail_page, "Expiring soon")
@@ -508,17 +511,17 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
],
)
- expiringdomain2, _ = Domain.objects.get_or_create(name="bogusdomain2.gov")
+ domaintorenew2, _ = Domain.objects.get_or_create(name="bogusdomain2.gov")
DomainInformation.objects.get_or_create(
- creator=non_dom_manage_user, domain=expiringdomain2, portfolio=self.portfolio
+ creator=non_dom_manage_user, domain=domaintorenew2, portfolio=self.portfolio
)
non_dom_manage_user.refresh_from_db()
self.client.force_login(non_dom_manage_user)
with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
- Domain, "is_expired", self.custom_is_expired
+ Domain, "is_expired", self.custom_is_expired_false
):
detail_page = self.client.get(
- reverse("domain", kwargs={"pk": expiringdomain2.id}),
+ reverse("domain", kwargs={"pk": domaintorenew2.id}),
)
self.assertContains(detail_page, "Contact one of the listed domain managers to renew the domain.")
@@ -527,29 +530,29 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
def test_expiring_domain_on_detail_page_in_org_model_as_a_domain_manager(self):
portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test org2", creator=self.user)
- expiringdomain3, _ = Domain.objects.get_or_create(name="bogusdomain3.gov")
+ domaintorenew3, _ = Domain.objects.get_or_create(name="bogusdomain3.gov")
- UserDomainRole.objects.get_or_create(user=self.user, domain=expiringdomain3, role=UserDomainRole.Roles.MANAGER)
- DomainInformation.objects.get_or_create(creator=self.user, domain=expiringdomain3, portfolio=portfolio)
+ UserDomainRole.objects.get_or_create(user=self.user, domain=domaintorenew3, role=UserDomainRole.Roles.MANAGER)
+ DomainInformation.objects.get_or_create(creator=self.user, domain=domaintorenew3, portfolio=portfolio)
self.user.refresh_from_db()
self.client.force_login(self.user)
with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
- Domain, "is_expired", self.custom_is_expired
+ Domain, "is_expired", self.custom_is_expired_false
):
detail_page = self.client.get(
- reverse("domain", kwargs={"pk": expiringdomain3.id}),
+ reverse("domain", kwargs={"pk": domaintorenew3.id}),
)
self.assertContains(detail_page, "Renew to maintain access")
@override_flag("domain_renewal", active=True)
- def test_domain_renewal_form_and_sidebar(self):
+ def test_domain_renewal_form_and_sidebar_expiring(self):
self.client.force_login(self.user)
with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
- Domain, "is_expired", self.custom_is_expired
+ Domain, "is_expiring", self.custom_is_expiring
):
# Grab the detail page
detail_page = self.client.get(
- reverse("domain", kwargs={"pk": self.expiringdomain.id}),
+ reverse("domain", kwargs={"pk": self.domaintorenew.id}),
)
# Make sure we see the link as a domain manager
@@ -559,14 +562,44 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
self.assertContains(detail_page, "Renewal form")
# Grab link to the renewal page
- renewal_form_url = reverse("domain-renewal", kwargs={"pk": self.expiringdomain.id})
+ renewal_form_url = reverse("domain-renewal", kwargs={"pk": self.domaintorenew.id})
self.assertContains(detail_page, f'href="{renewal_form_url}"')
# Simulate clicking the link
response = self.client.get(renewal_form_url)
self.assertEqual(response.status_code, 200)
- self.assertContains(response, f"Renew {self.expiringdomain.name}")
+ self.assertContains(response, f"Renew {self.domaintorenew.name}")
+
+ @override_flag("domain_renewal", active=True)
+ def test_domain_renewal_form_and_sidebar_expired(self):
+
+ self.client.force_login(self.user)
+
+ with patch.object(Domain, "is_expired", self.custom_is_expired_true), patch.object(
+ Domain, "is_expired", self.custom_is_expired_true
+ ):
+ # Grab the detail page
+ detail_page = self.client.get(
+ reverse("domain", kwargs={"pk": self.domaintorenew.id}),
+ )
+
+ print("puglesss", self.domaintorenew.is_expired)
+ # Make sure we see the link as a domain manager
+ self.assertContains(detail_page, "Renew to maintain access")
+
+ # Make sure we can see Renewal form on the sidebar since it's expired
+ self.assertContains(detail_page, "Renewal form")
+
+ # Grab link to the renewal page
+ renewal_form_url = reverse("domain-renewal", kwargs={"pk": self.domaintorenew.id})
+ self.assertContains(detail_page, f'href="{renewal_form_url}"')
+
+ # Simulate clicking the link
+ response = self.client.get(renewal_form_url)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, f"Renew {self.domaintorenew.name}")
@override_flag("domain_renewal", active=True)
def test_domain_renewal_form_your_contact_info_edit(self):
From c7af6645d4a8ffac6a3b33b307f97ed6871aded2 Mon Sep 17 00:00:00 2001
From: asaki222
Date: Wed, 8 Jan 2025 15:31:59 -0500
Subject: [PATCH 25/68] ran app black .
---
src/registrar/tests/test_views_domain.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index 406e96ed4..575dc0faf 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -464,7 +464,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
def custom_is_expired_true(self):
return True
-
+
def custom_is_expiring(self):
return True
@@ -570,12 +570,12 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
self.assertEqual(response.status_code, 200)
self.assertContains(response, f"Renew {self.domaintorenew.name}")
-
+
@override_flag("domain_renewal", active=True)
def test_domain_renewal_form_and_sidebar_expired(self):
-
+
self.client.force_login(self.user)
-
+
with patch.object(Domain, "is_expired", self.custom_is_expired_true), patch.object(
Domain, "is_expired", self.custom_is_expired_true
):
From b048ff96de5565042e126ffd64cbdc3750a5face Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Wed, 8 Jan 2025 18:26:56 -0500
Subject: [PATCH 26/68] handle multiple domains in email notifications
---
src/registrar/admin.py | 3 +-
.../templates/emails/domain_invitation.txt | 41 +++++----
.../emails/domain_invitation_subject.txt | 2 +-
src/registrar/utility/email_invitations.py | 50 +++++++----
src/registrar/views/domain.py | 5 +-
src/registrar/views/portfolios.py | 85 ++++++++++++-------
6 files changed, 119 insertions(+), 67 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 0f0c76ee3..06731db38 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1480,8 +1480,9 @@ class DomainInvitationAdmin(ListHeaderAdmin):
send_domain_invitation_email(
email=requested_email,
requestor=requestor,
- domain=domain,
+ domains=domain,
is_member_of_different_org=member_of_a_different_org,
+ requested_user=requested_user
)
if requested_user is not None:
# Domain Invitation creation for an existing User
diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt
index 068040205..4959f7c23 100644
--- a/src/registrar/templates/emails/domain_invitation.txt
+++ b/src/registrar/templates/emails/domain_invitation.txt
@@ -1,36 +1,46 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
-Hi.
+{% if requested_user and requested_user.first_name %}
+Hi, {{ requested_user.first_name }}.
+{% else %}
+Hi,
+{% endif %}
-{{ requestor_email }} has added you as a manager on {{ domain.name }}.
+{{ requestor_email }} has invited you to manage:
+{% for domain in domains %}
+{{ domain.name }}
+{% endfor %}
-You can manage this domain on the .gov registrar .
+To manage domain information, visit the .gov registrar .
----------------------------------------------------------------
+{% if not requested_user %}
YOU NEED A LOGIN.GOV ACCOUNT
-You’ll need a Login.gov account to manage your .gov domain. Login.gov provides
-a simple and secure process for signing in to many government services with one
-account.
+You’ll need a Login.gov account to access the .gov registrar. That account needs to be
+associated with the following email address: {{ invitee_email_address }}
-If you don’t already have one, follow these steps to create your
-Login.gov account .
+Login.gov provides a simple and secure process for signing in to many government
+services with one account. If you don’t already have one, follow these steps to create
+your Login.gov account .
+{% endif %}
DOMAIN MANAGEMENT
-As a .gov domain manager, you can add or update information about your domain.
-You’ll also serve as a contact for your .gov domain. Please keep your contact
-information updated.
+As a .gov domain manager, you can add or update information like name servers. You’ll
+also serve as a contact for the domains you manage. Please keep your contact
+information updated.
Learn more about domain management .
SOMETHING WRONG?
-If you’re not affiliated with {{ domain.name }} or think you received this
-message in error, reply to this email.
+If you’re not affiliated with the .gov domains mentioned in this invitation or think you
+received this message in error, reply to this email.
THANK YOU
-.Gov helps the public identify official, trusted information. Thank you for using a .gov domain.
+.Gov helps the public identify official, trusted information. Thank you for using a .gov
+domain.
----------------------------------------------------------------
@@ -38,5 +48,6 @@ The .gov team
Contact us:
Learn about .gov
-The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA)
+The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency
+(CISA)
{% endautoescape %}
diff --git a/src/registrar/templates/emails/domain_invitation_subject.txt b/src/registrar/templates/emails/domain_invitation_subject.txt
index 319b80176..9663346d0 100644
--- a/src/registrar/templates/emails/domain_invitation_subject.txt
+++ b/src/registrar/templates/emails/domain_invitation_subject.txt
@@ -1 +1 @@
-You’ve been added to a .gov domain
\ No newline at end of file
+You've been invited to manage {% if domains|length > 1 %}.gov domains{% else %}{{ domains.0.name }}{% endif %}
\ No newline at end of file
diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py
index 9455c5927..630da7ce5 100644
--- a/src/registrar/utility/email_invitations.py
+++ b/src/registrar/utility/email_invitations.py
@@ -1,5 +1,7 @@
from django.conf import settings
from registrar.models import DomainInvitation
+from registrar.models.domain import Domain
+from registrar.models.user import User
from registrar.utility.errors import (
AlreadyDomainInvitedError,
AlreadyDomainManagerError,
@@ -13,7 +15,7 @@ import logging
logger = logging.getLogger(__name__)
-def send_domain_invitation_email(email: str, requestor, domain, is_member_of_different_org):
+def send_domain_invitation_email(email: str, requestor, domains: Domain | list[Domain], is_member_of_different_org, requested_user=None):
"""
Sends a domain invitation email to the specified address.
@@ -22,8 +24,9 @@ def send_domain_invitation_email(email: str, requestor, domain, is_member_of_dif
Args:
email (str): Email address of the recipient.
requestor (User): The user initiating the invitation.
- domain (Domain): The domain object for which the invitation is being sent.
+ domains (Domain or list of Domain): The domain objects for which the invitation is being sent.
is_member_of_different_org (bool): if an email belongs to a different org
+ requested_user (User | None): The recipient if the email belongs to a user in the registrar
Raises:
MissingEmailError: If the requestor has no email associated with their account.
@@ -32,13 +35,19 @@ def send_domain_invitation_email(email: str, requestor, domain, is_member_of_dif
OutsideOrgMemberError: If the requested_user is part of a different organization.
EmailSendingError: If there is an error while sending the email.
"""
+ print('send_domain_invitation_email')
+ # Normalize domains
+ if isinstance(domains, Domain):
+ domains = [domains]
+
# Default email address for staff
requestor_email = settings.DEFAULT_FROM_EMAIL
# Check if the requestor is staff and has an email
if not requestor.is_staff:
if not requestor.email or requestor.email.strip() == "":
- raise MissingEmailError(email=email, domain=domain)
+ domain_names = ", ".join([domain.name for domain in domains])
+ raise MissingEmailError(email=email, domain=domain_names)
else:
requestor_email = requestor.email
@@ -51,18 +60,19 @@ def send_domain_invitation_email(email: str, requestor, domain, is_member_of_dif
):
raise OutsideOrgMemberError
- # Check for an existing invitation
- try:
- invite = DomainInvitation.objects.get(email=email, domain=domain)
- if invite.status == DomainInvitation.DomainInvitationStatus.RETRIEVED:
- raise AlreadyDomainManagerError(email)
- elif invite.status == DomainInvitation.DomainInvitationStatus.CANCELED:
- invite.update_cancellation_status()
- invite.save()
- else:
- raise AlreadyDomainInvitedError(email)
- except DomainInvitation.DoesNotExist:
- pass
+ # Check for an existing invitation for each domain
+ for domain in domains:
+ try:
+ invite = DomainInvitation.objects.get(email=email, domain=domain)
+ if invite.status == DomainInvitation.DomainInvitationStatus.RETRIEVED:
+ raise AlreadyDomainManagerError(email)
+ elif invite.status == DomainInvitation.DomainInvitationStatus.CANCELED:
+ invite.update_cancellation_status()
+ invite.save()
+ else:
+ raise AlreadyDomainInvitedError(email)
+ except DomainInvitation.DoesNotExist:
+ pass
# Send the email
try:
@@ -71,12 +81,18 @@ def send_domain_invitation_email(email: str, requestor, domain, is_member_of_dif
"emails/domain_invitation_subject.txt",
to_address=email,
context={
- "domain": domain,
+ "domains": domains,
"requestor_email": requestor_email,
+ "invitee_email_address": email,
+ "requested_user": requested_user,
},
)
except EmailSendingError as err:
- raise EmailSendingError(f"Could not send email invitation to {email} for domain {domain}.") from err
+ print('point of failure test')
+ domain_names = ", ".join([domain.name for domain in domains])
+ raise EmailSendingError(
+ f"Could not send email invitation to {email} for domains: {domain_names}"
+ ) from err
def send_portfolio_invitation_email(email: str, requestor, portfolio):
diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py
index b8464464e..f7938a301 100644
--- a/src/registrar/views/domain.py
+++ b/src/registrar/views/domain.py
@@ -1204,7 +1204,7 @@ class DomainAddUserView(DomainFormBaseView):
send_domain_invitation_email(
email=email,
requestor=requestor,
- domain=self.object,
+ domains=self.object,
is_member_of_different_org=member_of_different_org,
)
DomainInvitation.objects.get_or_create(email=email, domain=self.object)
@@ -1215,8 +1215,9 @@ class DomainAddUserView(DomainFormBaseView):
send_domain_invitation_email(
email=email,
requestor=requestor,
- domain=self.object,
+ domains=self.object,
is_member_of_different_org=member_of_different_org,
+ requested_user=requested_user,
)
UserDomainRole.objects.create(
user=requested_user,
diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py
index 60b30ad60..0cca1280c 100644
--- a/src/registrar/views/portfolios.py
+++ b/src/registrar/views/portfolios.py
@@ -8,13 +8,14 @@ from django.utils.safestring import mark_safe
from django.contrib import messages
from registrar.forms import portfolio as portfolioForms
from registrar.models import Portfolio, User
+from registrar.models.domain import Domain
from registrar.models.domain_invitation import DomainInvitation
from registrar.models.portfolio_invitation import PortfolioInvitation
from registrar.models.user_domain_role import UserDomainRole
from registrar.models.user_portfolio_permission import UserPortfolioPermission
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from registrar.utility.email import EmailSendingError
-from registrar.utility.email_invitations import send_portfolio_invitation_email
+from registrar.utility.email_invitations import send_domain_invitation_email, send_portfolio_invitation_email
from registrar.utility.errors import MissingEmailError
from registrar.utility.enums import DefaultUserValues
from registrar.views.utility.mixins import PortfolioMemberPermission
@@ -33,6 +34,8 @@ from django.views.generic import View
from django.views.generic.edit import FormMixin
from django.db import IntegrityError
+from registrar.views.utility.portfolio_helper import get_org_membership
+
logger = logging.getLogger(__name__)
@@ -237,6 +240,7 @@ class PortfolioMemberDomainsEditView(PortfolioMemberDomainsEditPermissionView, V
removed_domains = request.POST.get("removed_domains")
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
member = portfolio_permission.user
+ portfolio = portfolio_permission.portfolio
added_domain_ids = self._parse_domain_ids(added_domains, "added domains")
if added_domain_ids is None:
@@ -248,7 +252,7 @@ class PortfolioMemberDomainsEditView(PortfolioMemberDomainsEditPermissionView, V
if added_domain_ids or removed_domain_ids:
try:
- self._process_added_domains(added_domain_ids, member)
+ self._process_added_domains(added_domain_ids, member, request.user, portfolio)
self._process_removed_domains(removed_domain_ids, member)
messages.success(request, "The domain assignment changes have been saved.")
return redirect(reverse("member-domains", kwargs={"pk": pk}))
@@ -263,7 +267,7 @@ class PortfolioMemberDomainsEditView(PortfolioMemberDomainsEditPermissionView, V
except Exception as e:
messages.error(
request,
- "An unexpected error occurred: {str(e)}. If the issue persists, "
+ f"An unexpected error occurred: {str(e)}. If the issue persists, "
f"please contact {DefaultUserValues.HELP_EMAIL}.",
)
logger.error(f"An unexpected error occurred: {str(e)}")
@@ -287,16 +291,26 @@ class PortfolioMemberDomainsEditView(PortfolioMemberDomainsEditPermissionView, V
logger.error(f"Invalid data for {domain_type}")
return None
- def _process_added_domains(self, added_domain_ids, member):
+ def _process_added_domains(self, added_domain_ids, member, requestor, portfolio):
"""
Processes added domains by bulk creating UserDomainRole instances.
"""
if added_domain_ids:
+ print('_process_added_domains')
+ added_domains = Domain.objects.filter(id__in=added_domain_ids)
+ member_of_a_different_org, _ = get_org_membership(portfolio, member.email, member)
+ send_domain_invitation_email(
+ email=member.email,
+ requestor=requestor,
+ domains=added_domains,
+ is_member_of_different_org=member_of_a_different_org,
+ requested_user=member,
+ )
# Bulk create UserDomainRole instances for added domains
UserDomainRole.objects.bulk_create(
[
- UserDomainRole(domain_id=domain_id, user=member, role=UserDomainRole.Roles.MANAGER)
- for domain_id in added_domain_ids
+ UserDomainRole(domain=domain, user=member, role=UserDomainRole.Roles.MANAGER)
+ for domain in added_domains
],
ignore_conflicts=True, # Avoid duplicate entries
)
@@ -443,6 +457,7 @@ class PortfolioInvitedMemberDomainsEditView(PortfolioMemberDomainsEditPermission
removed_domains = request.POST.get("removed_domains")
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk)
email = portfolio_invitation.email
+ portfolio = portfolio_invitation.portfolio
added_domain_ids = self._parse_domain_ids(added_domains, "added domains")
if added_domain_ids is None:
@@ -454,7 +469,7 @@ class PortfolioInvitedMemberDomainsEditView(PortfolioMemberDomainsEditPermission
if added_domain_ids or removed_domain_ids:
try:
- self._process_added_domains(added_domain_ids, email)
+ self._process_added_domains(added_domain_ids, email, request.user, portfolio)
self._process_removed_domains(removed_domain_ids, email)
messages.success(request, "The domain assignment changes have been saved.")
return redirect(reverse("invitedmember-domains", kwargs={"pk": pk}))
@@ -469,7 +484,7 @@ class PortfolioInvitedMemberDomainsEditView(PortfolioMemberDomainsEditPermission
except Exception as e:
messages.error(
request,
- "An unexpected error occurred: {str(e)}. If the issue persists, "
+ f"An unexpected error occurred: {str(e)}. If the issue persists, "
f"please contact {DefaultUserValues.HELP_EMAIL}.",
)
logger.error(f"An unexpected error occurred: {str(e)}.")
@@ -493,34 +508,41 @@ class PortfolioInvitedMemberDomainsEditView(PortfolioMemberDomainsEditPermission
logger.error(f"Invalid data for {domain_type}.")
return None
- def _process_added_domains(self, added_domain_ids, email):
+ def _process_added_domains(self, added_domain_ids, email, requestor, portfolio):
"""
Processes added domain invitations by updating existing invitations
or creating new ones.
"""
- if not added_domain_ids:
- return
-
- # Update existing invitations from CANCELED to INVITED
- existing_invitations = DomainInvitation.objects.filter(domain_id__in=added_domain_ids, email=email)
- existing_invitations.update(status=DomainInvitation.DomainInvitationStatus.INVITED)
-
- # Determine which domains need new invitations
- existing_domain_ids = existing_invitations.values_list("domain_id", flat=True)
- new_domain_ids = set(added_domain_ids) - set(existing_domain_ids)
-
- # Bulk create new invitations
- DomainInvitation.objects.bulk_create(
- [
- DomainInvitation(
- domain_id=domain_id,
+ if added_domain_ids:
+ added_domains = Domain.objects.filter(id__in=added_domain_ids)
+ member_of_a_different_org, _ = get_org_membership(portfolio, email, None)
+ send_domain_invitation_email(
email=email,
- status=DomainInvitation.DomainInvitationStatus.INVITED,
+ requestor=requestor,
+ domains=added_domains,
+ is_member_of_different_org=member_of_a_different_org,
)
- for domain_id in new_domain_ids
- ]
- )
+ # Update existing invitations from CANCELED to INVITED
+ existing_invitations = DomainInvitation.objects.filter(domain__in=added_domains, email=email)
+ existing_invitations.update(status=DomainInvitation.DomainInvitationStatus.INVITED)
+
+ # Determine which domains need new invitations
+ existing_domain_ids = existing_invitations.values_list("domain_id", flat=True)
+ new_domain_ids = set(added_domain_ids) - set(existing_domain_ids)
+
+ # Bulk create new invitations
+ DomainInvitation.objects.bulk_create(
+ [
+ DomainInvitation(
+ domain_id=domain_id,
+ email=email,
+ status=DomainInvitation.DomainInvitationStatus.INVITED,
+ )
+ for domain_id in new_domain_ids
+ ]
+ )
+
def _process_removed_domains(self, removed_domain_ids, email):
"""
Processes removed domain invitations by updating their status to CANCELED.
@@ -755,8 +777,9 @@ class PortfolioAddMemberView(PortfolioMembersPermissionView, FormMixin):
if not requested_user or not permission_exists:
send_portfolio_invitation_email(email=requested_email, requestor=requestor, portfolio=portfolio)
portfolio_invitation = form.save()
- portfolio_invitation.retrieve()
- portfolio_invitation.save()
+ if requested_user is not None:
+ portfolio_invitation.retrieve()
+ portfolio_invitation.save()
messages.success(self.request, f"{requested_email} has been invited.")
else:
if permission_exists:
From acd20e89141f7a581fdb62281fc3d5c082d3b780 Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Wed, 8 Jan 2025 21:16:01 -0800
Subject: [PATCH 27/68] Fix uncheck error
---
src/registrar/templates/domain_renewal.html | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/registrar/templates/domain_renewal.html b/src/registrar/templates/domain_renewal.html
index bf529a04d..7867bfce8 100644
--- a/src/registrar/templates/domain_renewal.html
+++ b/src/registrar/templates/domain_renewal.html
@@ -103,7 +103,9 @@
class="usa-checkbox__input"
id="renewal-checkbox"
type="checkbox"
- name="{{ form.is_policy_acknowledged.name }}">
+ name="{{ form.is_policy_acknowledged.name }}"
+ {% if form.is_policy_acknowledged.value %}checked{% endif %}
+ >
{% url 'user-profile' as url %}
- {% include "includes/summary_item.html" with title='Your Contact Information' value=request.user edit_link=url editable=is_editable contact='true' %}
+ {% include "includes/summary_item.html" with title='Your contact information' value=request.user edit_link=url editable=is_editable contact='true' %}
{% if analyst_action != 'edit' or analyst_action_location != domain.pk %}
{% if is_portfolio_user and not is_domain_manager %}
From 25ba5b2a5172c4f7e4cd79eac25663ec0a80b4a6 Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Fri, 10 Jan 2025 09:56:58 -0800
Subject: [PATCH 56/68] Fix test capitalization
---
src/registrar/tests/test_views_domain.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index 56bfe7306..d92da17dd 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -607,7 +607,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
renewal_page = self.app.get(reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id}))
# Verify we see "Your contact information" on the renewal form
- self.assertContains(renewal_page, "Your Contact Information")
+ self.assertContains(renewal_page, "Your contact information")
# Verify that the "Edit" button for Your contact is there and links to correct URL
edit_button_url = reverse("user-profile")
From 19114c7e7214474e2afcb49a57140846e40640c6 Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Fri, 10 Jan 2025 11:02:31 -0800
Subject: [PATCH 57/68] Fix checkbox issue
---
src/registrar/templates/domain_renewal.html | 7 +++----
src/registrar/views/domain.py | 3 +++
2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/registrar/templates/domain_renewal.html b/src/registrar/templates/domain_renewal.html
index f81389efb..02c9666e3 100644
--- a/src/registrar/templates/domain_renewal.html
+++ b/src/registrar/templates/domain_renewal.html
@@ -94,14 +94,13 @@
{% csrf_token %}
- {% if form.is_policy_acknowledged.errors %}
-
{{ form.is_policy_acknowledged.errors|join:" " }}
- {% endif %}
+
-
+
Required fields are marked with an asterisk (*).
-
+
{% url 'user-profile' as url %}
{% include "includes/summary_item.html" with title='Your contact information' value=request.user edit_link=url editable=is_editable contact='true' %}
From 9241790e48b8618087804c154b197de3ede3bd73 Mon Sep 17 00:00:00 2001
From: asaki222
Date: Fri, 10 Jan 2025 16:04:11 -0500
Subject: [PATCH 62/68] added exec info true
---
src/registrar/models/domain.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 6a135036d..6bd8278a1 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -326,7 +326,7 @@ class Domain(TimeStampedModel, DomainHelper):
exp_date = self.registry_expiration_date
except KeyError:
# if no expiration date from registry, set it to today
- logger.warning("current expiration date not set; setting to today")
+ logger.warning("current expiration date not set; setting to today", exc_info=True)
exp_date = date.today()
# create RenewDomain request
request = commands.RenewDomain(name=self.name, cur_exp_date=exp_date, period=epp.Period(length, unit))
From 797561df45c3783576d34b684bf92fe88930563a Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Mon, 13 Jan 2025 08:58:58 -0800
Subject: [PATCH 63/68] Remove duplicated function
---
src/registrar/views/domain.py | 12 ------------
1 file changed, 12 deletions(-)
diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py
index 2c741055f..4b2edba06 100644
--- a/src/registrar/views/domain.py
+++ b/src/registrar/views/domain.py
@@ -316,18 +316,6 @@ class DomainRenewalView(DomainView):
template_name = "domain_renewal.html"
- def can_access_domain_via_portfolio(self, pk):
- """Most views should not allow permission to portfolio users.
- If particular views allow permissions, they will need to override
- this function."""
- portfolio = self.request.session.get("portfolio")
- if self.request.user.has_any_domains_portfolio_permission(portfolio):
- if Domain.objects.filter(id=pk).exists():
- domain = Domain.objects.get(id=pk)
- if domain.domain_info.portfolio == portfolio:
- return True
- return False
-
def post(self, request, pk):
domain = get_object_or_404(Domain, id=pk)
From de07b109c216512485365d8dd736d4783926a00a Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Tue, 14 Jan 2025 12:15:59 -0600
Subject: [PATCH 64/68] fix auditlog errors
---
src/registrar/admin.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 849cb6100..f13437af1 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -2782,8 +2782,10 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
try:
# Retrieve and order audit log entries by timestamp in descending order
- audit_log_entries = LogEntry.objects.filter(object_id=object_id).order_by("-timestamp")
-
+ audit_log_entries = LogEntry.objects.filter(
+ object_id=object_id, content_type__model="domainrequest"
+ ).order_by("-timestamp")
+
# Process each log entry to filter based on the change criteria
for log_entry in audit_log_entries:
entry = self.process_log_entry(log_entry)
From 0c8a0ad1d805868a7317c56406ec7fa129c1e5d0 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Tue, 14 Jan 2025 14:35:11 -0600
Subject: [PATCH 65/68] linter fixes
---
src/registrar/admin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index f13437af1..bb42b66c6 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -2785,7 +2785,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
audit_log_entries = LogEntry.objects.filter(
object_id=object_id, content_type__model="domainrequest"
).order_by("-timestamp")
-
+
# Process each log entry to filter based on the change criteria
for log_entry in audit_log_entries:
entry = self.process_log_entry(log_entry)
From b5970ecb373f9d8f1f779d5670f999f507402e62 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Wed, 15 Jan 2025 06:34:46 -0500
Subject: [PATCH 66/68] updates in response to PR suggestions
---
src/registrar/admin.py | 3 +-
.../templates/emails/domain_invitation.txt | 10 +----
src/registrar/tests/test_admin.py | 5 ++-
src/registrar/utility/email_invitations.py | 5 ++-
src/registrar/views/portfolios.py | 8 ++--
.../views/utility/invitation_helper.py | 39 +++++++++----------
6 files changed, 35 insertions(+), 35 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index cdcc0400e..eb4e1737a 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1520,7 +1520,7 @@ class DomainInvitationAdmin(BaseInvitationAdmin):
"""
if not change:
domain = obj.domain
- domain_org = domain.domain_info.portfolio
+ domain_org = getattr(domain.domain_info, "portfolio", None)
requested_email = obj.email
# Look up a user with that email
requested_user = get_requested_user(requested_email)
@@ -1536,6 +1536,7 @@ class DomainInvitationAdmin(BaseInvitationAdmin):
and not flag_is_active(request, "multiple_portfolios")
and domain_org is not None
and not member_of_this_org
+ and not member_of_a_different_org
):
send_portfolio_invitation_email(email=requested_email, requestor=requestor, portfolio=domain_org)
portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create(
diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt
index 4959f7c23..a077bff26 100644
--- a/src/registrar/templates/emails/domain_invitation.txt
+++ b/src/registrar/templates/emails/domain_invitation.txt
@@ -1,15 +1,9 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
-{% if requested_user and requested_user.first_name %}
-Hi, {{ requested_user.first_name }}.
-{% else %}
-Hi,
-{% endif %}
+Hi,{% if requested_user and requested_user.first_name %} {{ requested_user.first_name }}.{% endif %}
{{ requestor_email }} has invited you to manage:
-{% for domain in domains %}
-{{ domain.name }}
+{% for domain in domains %}{{ domain.name }}
{% endfor %}
-
To manage domain information, visit the .gov registrar .
----------------------------------------------------------------
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index c9f7a9032..210a1a8e6 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -334,7 +334,10 @@ class TestDomainInvitationAdmin(TestCase):
Should not send a out portfolio invitation.
Should trigger success message for the domain invitation.
Should retrieve the domain invitation.
- Should not create a portfolio invitation."""
+ Should not create a portfolio invitation.
+
+ NOTE: This test may need to be reworked when the multiple_portfolio flag is fully fleshed out.
+ """
user = User.objects.create_user(email="test@example.com", username="username")
diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py
index 1491b65a5..48c796340 100644
--- a/src/registrar/utility/email_invitations.py
+++ b/src/registrar/utility/email_invitations.py
@@ -48,7 +48,10 @@ def normalize_domains(domains):
def get_requestor_email(requestor, domains):
- """Get the requestor's email or raise an error if it's missing."""
+ """Get the requestor's email or raise an error if it's missing.
+
+ If the requestor is staff, default email is returned.
+ """
if requestor.is_staff:
return settings.DEFAULT_FROM_EMAIL
diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py
index b9249ca6d..c4f60ca35 100644
--- a/src/registrar/views/portfolios.py
+++ b/src/registrar/views/portfolios.py
@@ -262,7 +262,7 @@ class PortfolioMemberDomainsEditView(PortfolioMemberDomainsEditPermissionView, V
"A database error occurred while saving changes. If the issue persists, "
f"please contact {DefaultUserValues.HELP_EMAIL}.",
)
- logger.error("A database error occurred while saving changes.")
+ logger.error("A database error occurred while saving changes.", exc_info=True)
return redirect(reverse("member-domains-edit", kwargs={"pk": pk}))
except Exception as e:
messages.error(
@@ -270,7 +270,7 @@ class PortfolioMemberDomainsEditView(PortfolioMemberDomainsEditPermissionView, V
f"An unexpected error occurred: {str(e)}. If the issue persists, "
f"please contact {DefaultUserValues.HELP_EMAIL}.",
)
- logger.error(f"An unexpected error occurred: {str(e)}")
+ logger.error(f"An unexpected error occurred: {str(e)}", exc_info=True)
return redirect(reverse("member-domains-edit", kwargs={"pk": pk}))
else:
messages.info(request, "No changes detected.")
@@ -479,7 +479,7 @@ class PortfolioInvitedMemberDomainsEditView(PortfolioMemberDomainsEditPermission
"A database error occurred while saving changes. If the issue persists, "
f"please contact {DefaultUserValues.HELP_EMAIL}.",
)
- logger.error("A database error occurred while saving changes.")
+ logger.error("A database error occurred while saving changes.", exc_info=True)
return redirect(reverse("invitedmember-domains-edit", kwargs={"pk": pk}))
except Exception as e:
messages.error(
@@ -487,7 +487,7 @@ class PortfolioInvitedMemberDomainsEditView(PortfolioMemberDomainsEditPermission
f"An unexpected error occurred: {str(e)}. If the issue persists, "
f"please contact {DefaultUserValues.HELP_EMAIL}.",
)
- logger.error(f"An unexpected error occurred: {str(e)}.")
+ logger.error(f"An unexpected error occurred: {str(e)}.", exc_info=True)
return redirect(reverse("invitedmember-domains-edit", kwargs={"pk": pk}))
else:
messages.info(request, "No changes detected.")
diff --git a/src/registrar/views/utility/invitation_helper.py b/src/registrar/views/utility/invitation_helper.py
index 771300406..5c730d0c3 100644
--- a/src/registrar/views/utility/invitation_helper.py
+++ b/src/registrar/views/utility/invitation_helper.py
@@ -1,8 +1,6 @@
from django.contrib import messages
from django.db import IntegrityError
-from registrar.models.portfolio_invitation import PortfolioInvitation
-from registrar.models.user import User
-from registrar.models.user_portfolio_permission import UserPortfolioPermission
+from registrar.models import PortfolioInvitation, User, UserPortfolioPermission
from registrar.utility.email import EmailSendingError
import logging
@@ -20,32 +18,33 @@ logger = logging.getLogger(__name__)
# any view, and were initially developed for domain.py, portfolios.py and admin.py
-def get_org_membership(requestor_org, requested_email, requested_user):
+def get_org_membership(org, email, user):
"""
- Verifies if an email belongs to a different organization as a member or invited member.
- Verifies if an email belongs to this organization as a member or invited member.
- User does not belong to any org can be deduced from the tuple returned.
+ Determines if an email/user belongs to a different organization or this organization
+ as either a member or an invited member.
- Returns a tuple (member_of_a_different_org, member_of_this_org).
+ This function returns a tuple (member_of_a_different_org, member_of_this_org),
+ which provides:
+ - member_of_a_different_org: True if the user/email is associated with an organization other than the given org.
+ - member_of_this_org: True if the user/email is associated with the given org.
+
+ Note: This implementation assumes single portfolio ownership for a user.
+ If the "multiple portfolios" feature is enabled, this logic may not account for
+ situations where a user or email belongs to multiple organizations.
"""
- # COMMENT: this code does not take into account when multiple portfolios flag is set to true
-
- # COMMENT: shouldn't this code be based on the organization of the domain, not the org
- # of the requestor? requestor could have multiple portfolios
-
- # Check for existing permissions or invitations for the requested user
- existing_org_permission = UserPortfolioPermission.objects.filter(user=requested_user).first()
- existing_org_invitation = PortfolioInvitation.objects.filter(email=requested_email).first()
+ # Check for existing permissions or invitations for the user
+ existing_org_permission = UserPortfolioPermission.objects.filter(user=user).first()
+ existing_org_invitation = PortfolioInvitation.objects.filter(email=email).first()
# Determine membership in a different organization
- member_of_a_different_org = (existing_org_permission and existing_org_permission.portfolio != requestor_org) or (
- existing_org_invitation and existing_org_invitation.portfolio != requestor_org
+ member_of_a_different_org = (existing_org_permission and existing_org_permission.portfolio != org) or (
+ existing_org_invitation and existing_org_invitation.portfolio != org
)
# Determine membership in the same organization
- member_of_this_org = (existing_org_permission and existing_org_permission.portfolio == requestor_org) or (
- existing_org_invitation and existing_org_invitation.portfolio == requestor_org
+ member_of_this_org = (existing_org_permission and existing_org_permission.portfolio == org) or (
+ existing_org_invitation and existing_org_invitation.portfolio == org
)
return member_of_a_different_org, member_of_this_org
From f196dce6077c8c420c8cd517be28596327a74932 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Wed, 15 Jan 2025 07:08:22 -0500
Subject: [PATCH 67/68] lint
---
src/registrar/tests/test_admin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 210a1a8e6..2a7a52a13 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -335,7 +335,7 @@ class TestDomainInvitationAdmin(TestCase):
Should trigger success message for the domain invitation.
Should retrieve the domain invitation.
Should not create a portfolio invitation.
-
+
NOTE: This test may need to be reworked when the multiple_portfolio flag is fully fleshed out.
"""
From ac3286d7801fd165500f3b09415ed7153ae0c21c Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Wed, 15 Jan 2025 07:26:01 -0500
Subject: [PATCH 68/68] fixed bug in portfolio invitation change view query
---
src/registrar/admin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index eb4e1737a..2e1b15dfb 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1594,7 +1594,7 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin):
# Search
search_fields = [
"email",
- "portfolio__name",
+ "portfolio__organization_name",
]
# Filters