Merge branch 'main' into ag/3234-test-existing-script

This commit is contained in:
zandercymatics 2025-01-08 15:25:20 -07:00
commit 1e6a887514
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
12 changed files with 423 additions and 70 deletions

View file

@ -1434,6 +1434,20 @@ class DomainInvitationAdmin(ListHeaderAdmin):
# Get the filtered values # Get the filtered values
return super().changelist_view(request, extra_context=extra_context) return super().changelist_view(request, extra_context=extra_context)
def save_model(self, request, obj, form, change):
"""
Override the save_model method.
On creation of a new domain invitation, attempt to retrieve the invitation,
which will be successful if a single User exists for that email; otherwise, will
just continue to create the invitation.
"""
if not change and User.objects.filter(email=obj.email).count() == 1:
# Domain Invitation creation for an existing User
obj.retrieve()
# Call the parent save method to save the object
super().save_model(request, obj, form, change)
class PortfolioInvitationAdmin(ListHeaderAdmin): class PortfolioInvitationAdmin(ListHeaderAdmin):
"""Custom portfolio invitation admin class.""" """Custom portfolio invitation admin class."""

View file

@ -137,7 +137,7 @@ export class MembersTable extends BaseTable {
} }
// This easter egg is only for fixtures that dont have names as we are displaying their emails // This easter egg is only for fixtures that dont have names as we are displaying their emails
// All prod users will have emails linked to their account // All prod users will have emails linked to their account
if (customTableOptions.needsAdditionalColumn) MembersTable.addMemberModal(num_domains, member.email || "Samwise Gamgee", member_delete_url, unique_id, row); if (customTableOptions.needsAdditionalColumn) MembersTable.addMemberDeleteModal(num_domains, member.email || "Samwise Gamgee", member_delete_url, unique_id, row);
} }
/** /**
@ -417,25 +417,22 @@ export class MembersTable extends BaseTable {
* @param {string} submit_delete_url - `${member_type}-${member_id}/delete` * @param {string} submit_delete_url - `${member_type}-${member_id}/delete`
* @param {HTMLElement} wrapper_element - The element to which the modal is appended * @param {HTMLElement} wrapper_element - The element to which the modal is appended
*/ */
static addMemberModal(num_domains, member_email, submit_delete_url, id, wrapper_element) { static addMemberDeleteModal(num_domains, member_email, submit_delete_url, id, wrapper_element) {
let modalHeading = '';
let modalDescription = '';
if (num_domains == 0){ let modalHeading = ``;
let modalDescription = ``;
if (num_domains >= 0){
modalHeading = `Are you sure you want to delete ${member_email}?`; modalHeading = `Are you sure you want to delete ${member_email}?`;
modalDescription = `They will no longer be able to access this organization. modalDescription = `They will no longer be able to access this organization.
This action cannot be undone.`; This action cannot be undone.`;
} else if (num_domains == 1) { if (num_domains >= 1)
modalHeading = `Are you sure you want to delete ${member_email}?`; {
modalDescription = `<b>${member_email}</b> currently manages ${num_domains} domain in the organization. modalDescription = `<b>${member_email}</b> currently manages ${num_domains} domain${num_domains > 1 ? "s": ""} in the organization.
Removing them from the organization will remove all of their domains. They will no longer be able to Removing them from the organization will remove them from all of their domains. They will no longer be able to
access this organization. This action cannot be undone.`;
} else if (num_domains > 1) {
modalHeading = `Are you sure you want to delete ${member_email}?`;
modalDescription = `<b>${member_email}</b> currently manages ${num_domains} domains in the organization.
Removing them from the organization will remove all of their domains. They will no longer be able to
access this organization. This action cannot be undone.`; access this organization. This action cannot be undone.`;
} }
}
const modalSubmit = ` const modalSubmit = `
<button type="button" <button type="button"

View file

@ -351,6 +351,40 @@ div#content > h2 {
} }
} }
.module {
.margin-left-0 {
margin-left: 0;
}
.margin-top-0 {
margin-top: 0;
}
.padding-left-0 {
padding-left: 0;
}
}
.admin-list-inline {
li {
float: left;
padding-top: 0;
margin-right: 4px;
}
li:not(:last-child)::after {
content: ",";
}
}
.form-row {
.margin-y-0 {
margin-top: 0;
margin-bottom: 0;
}
.padding-y-0 {
padding-top: 0;
padding-bottom: 0;
}
}
// Fixes a display issue where the list was entirely white, or had too much whitespace // Fixes a display issue where the list was entirely white, or had too much whitespace
.select2-dropdown { .select2-dropdown {
display: inline-grid !important; display: inline-grid !important;

View file

@ -1,4 +1,4 @@
from datetime import timedelta from datetime import datetime, timedelta
from django.utils import timezone from django.utils import timezone
import logging import logging
import random import random
@ -126,7 +126,22 @@ class DomainRequestFixture:
# TODO for a future ticket: Allow for more than just "federal" here # TODO for a future ticket: Allow for more than just "federal" here
request.generic_org_type = request_dict["generic_org_type"] if "generic_org_type" in request_dict else "federal" request.generic_org_type = request_dict["generic_org_type"] if "generic_org_type" in request_dict else "federal"
if request.status != "started": if request.status != "started":
request.last_submitted_date = fake.date() # Generate fake data for first_submitted_date and last_submitted_date
# First generate a random date set to be later than 2020 (or something)
# (if we just use fake.date() we might get years like 1970 or earlier)
earliest_date_allowed = datetime(2020, 1, 1).date()
end_date = datetime.today().date() # Today's date (latest allowed date)
days_range = (end_date - earliest_date_allowed).days
first_submitted_date = earliest_date_allowed + timedelta(days=random.randint(0, days_range)) # nosec
# Generate a random positive offset to ensure last_submitted_date is later
# (Start with 1 to ensure at least 1 day difference)
offset_days = random.randint(1, 30) # nosec
last_submitted_date = first_submitted_date + timedelta(days=offset_days)
# Convert back to strings before assigning
request.first_submitted_date = first_submitted_date.strftime("%Y-%m-%d")
request.last_submitted_date = last_submitted_date.strftime("%Y-%m-%d")
request.federal_type = ( request.federal_type = (
request_dict["federal_type"] request_dict["federal_type"]
if "federal_type" in request_dict if "federal_type" in request_dict

View file

@ -110,21 +110,37 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% endcomment %} {% endcomment %}
<div class="readonly"> <div class="readonly">
{% with total_websites=field.contents|split:", " %} {% with total_websites=field.contents|split:", " %}
{% if total_websites|length == 1 %}
<p class="margin-y-0 padding-y-0">
<a href="{{ total_websites.0 }}" target="_blank">
{{ total_websites.0 }}
</a>
</p>
{% elif total_websites|length > 1 %}
<ul class="margin-top-0 margin-left-0 padding-left-0{% if total_websites|length > 5 %} admin-list-inline{% endif %}">
{% for website in total_websites %} {% for website in total_websites %}
<a href="{{ website }}" target="_blank" class="padding-top-1 current-website__{{forloop.counter}}">{{ website }}</a>{% if not forloop.last %}, {% endif %} {% comment %}White space matters: do NOT reformat the following line{% endcomment %}
{# Acts as a <br> #} <li><a href="{{ website }}" target="_blank">{{ website }}</a></li>
{% if total_websites|length < 5 %}
<div class="display-block margin-top-1"></div>
{% endif %}
{% endfor %} {% endfor %}
</ul>
{% endif %}
{% endwith %} {% endwith %}
</div> </div>
{% elif field.field.name == "alternative_domains" %} {% elif field.field.name == "alternative_domains" %}
<div class="readonly"> <div class="readonly">
{% with current_path=request.get_full_path %} {% with current_path=request.get_full_path %}
{% if original_object.alternative_domains.all|length == 1 %}
<p class="margin-y-0 padding-y-0">
<a href="{% url 'admin:registrar_website_change' original_object.alternative_domains.all.0.id %}?{{ 'return_path='|add:current_path }}" target="_blank">{{ original_object.alternative_domains.all.0 }}</a>
</p>
{% elif original_object.alternative_domains.all|length > 1 %}
<ul class="margin-top-0 margin-left-0 padding-left-0 admin-list-inline">
{% for alt_domain in original_object.alternative_domains.all %} {% for alt_domain in original_object.alternative_domains.all %}
<a href="{% url 'admin:registrar_website_change' alt_domain.id %}?{{ 'return_path='|add:current_path }}">{{ alt_domain }}</a>{% if not forloop.last %}, {% endif %} {% comment %}White space matters: do NOT reformat the following line{% endcomment %}
<li><a href="{% url 'admin:registrar_website_change' alt_domain.id %}?{{ 'return_path='|add:current_path }}" target="_blank">{{alt_domain}}</a></li>
{% endfor %} {% endfor %}
</ul>
{% endif %}
{% endwith %} {% endwith %}
</div> </div>
{% elif field.field.name == "domain_managers" or field.field.name == "invited_domain_managers" %} {% elif field.field.name == "domain_managers" or field.field.name == "invited_domain_managers" %}

View file

@ -13,6 +13,7 @@ from django.contrib.auth import get_user_model, login
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from django.utils import timezone from django.utils import timezone
from django.utils.html import strip_spaces_between_tags
from registrar.models import ( from registrar.models import (
Contact, Contact,
@ -107,6 +108,11 @@ def get_time_aware_date(date=datetime(2023, 11, 1)):
return timezone.make_aware(date) return timezone.make_aware(date)
def normalize_html(html):
"""Normalize HTML by removing newlines and extra spaces."""
return strip_spaces_between_tags(" ".join(html.split()))
class GenericTestHelper(TestCase): class GenericTestHelper(TestCase):
"""A helper class that contains various helper functions for TestCases""" """A helper class that contains various helper functions for TestCases"""
@ -1017,8 +1023,9 @@ def create_ready_domain():
# TODO in 1793: Remove the federal agency/updated federal agency fields # TODO in 1793: Remove the federal agency/updated federal agency fields
def completed_domain_request( # noqa def completed_domain_request( # noqa
has_other_contacts=True, has_other_contacts=True,
has_current_website=True, # pass empty [] if you want current_websites or alternative_domains set to None
has_alternative_gov_domain=True, current_websites=["city.com"],
alternative_domains=["city1.gov"],
has_about_your_organization=True, has_about_your_organization=True,
has_anything_else=True, has_anything_else=True,
has_cisa_representative=True, has_cisa_representative=True,
@ -1052,8 +1059,6 @@ def completed_domain_request( # noqa
phone="(555) 555 5555", phone="(555) 555 5555",
) )
domain, _ = DraftDomain.objects.get_or_create(name=name) domain, _ = DraftDomain.objects.get_or_create(name=name)
alt, _ = Website.objects.get_or_create(website="city1.gov")
current, _ = Website.objects.get_or_create(website="city.com")
other, _ = Contact.objects.get_or_create( other, _ = Contact.objects.get_or_create(
first_name="Testy", first_name="Testy",
last_name="Tester", last_name="Tester",
@ -1124,9 +1129,13 @@ def completed_domain_request( # noqa
if has_other_contacts: if has_other_contacts:
domain_request.other_contacts.add(other) domain_request.other_contacts.add(other)
if has_current_website: if len(current_websites) > 0:
for website in current_websites:
current, _ = Website.objects.get_or_create(website=website)
domain_request.current_websites.add(current) domain_request.current_websites.add(current)
if has_alternative_gov_domain: if len(alternative_domains) > 0:
for alternative_domain in alternative_domains:
alt, _ = Website.objects.get_or_create(website=alternative_domain)
domain_request.alternative_domains.add(alt) domain_request.alternative_domains.add(alt)
if has_cisa_representative: if has_cisa_representative:
domain_request.cisa_representative_first_name = "CISA-first-name" domain_request.cisa_representative_first_name = "CISA-first-name"

View file

@ -131,13 +131,11 @@ class TestDomainInvitationAdmin(TestCase):
tests have available superuser, client, and admin tests have available superuser, client, and admin
""" """
@classmethod
def setUpClass(cls):
cls.factory = RequestFactory()
cls.admin = ListHeaderAdmin(model=DomainInvitationAdmin, admin_site=AdminSite())
cls.superuser = create_superuser()
def setUp(self): def setUp(self):
self.factory = RequestFactory()
self.admin = ListHeaderAdmin(model=DomainInvitationAdmin, admin_site=AdminSite())
self.superuser = create_superuser()
self.domain = Domain.objects.create(name="example.com")
"""Create a client object""" """Create a client object"""
self.client = Client(HTTP_HOST="localhost:8080") self.client = Client(HTTP_HOST="localhost:8080")
@ -145,9 +143,6 @@ class TestDomainInvitationAdmin(TestCase):
"""Delete all DomainInvitation objects""" """Delete all DomainInvitation objects"""
DomainInvitation.objects.all().delete() DomainInvitation.objects.all().delete()
Contact.objects.all().delete() Contact.objects.all().delete()
@classmethod
def tearDownClass(self):
User.objects.all().delete() User.objects.all().delete()
@less_console_noise_decorator @less_console_noise_decorator
@ -168,6 +163,7 @@ class TestDomainInvitationAdmin(TestCase):
) )
self.assertContains(response, "Show more") self.assertContains(response, "Show more")
@less_console_noise_decorator
def test_get_filters(self): def test_get_filters(self):
"""Ensures that our filters are displaying correctly""" """Ensures that our filters are displaying correctly"""
with less_console_noise(): with less_console_noise():
@ -192,6 +188,59 @@ class TestDomainInvitationAdmin(TestCase):
self.assertContains(response, invited_html, count=1) self.assertContains(response, invited_html, count=1)
self.assertContains(response, retrieved_html, count=1) self.assertContains(response, retrieved_html, count=1)
@less_console_noise_decorator
def test_save_model_user_exists(self):
"""Test saving a domain invitation when the user exists.
Should attempt to retrieve the domain invitation."""
# Create a user with the same email
User.objects.create_user(email="test@example.com", username="username")
# Create a domain invitation instance
invitation = DomainInvitation(email="test@example.com", domain=self.domain)
admin_instance = DomainInvitationAdmin(DomainInvitation, admin_site=None)
# Create a request object
request = self.factory.post("/admin/registrar/DomainInvitation/add/")
request.user = self.superuser
# Patch the retrieve method
with patch.object(DomainInvitation, "retrieve") as mock_retrieve:
admin_instance.save_model(request, invitation, form=None, change=False)
# Assert retrieve was called
mock_retrieve.assert_called_once()
# Assert the invitation was saved
self.assertEqual(DomainInvitation.objects.count(), 1)
self.assertEqual(DomainInvitation.objects.first().email, "test@example.com")
@less_console_noise_decorator
def test_save_model_user_does_not_exist(self):
"""Test saving a domain invitation when the user does not exist.
Should not attempt to retrieve the domain invitation."""
# Create a domain invitation instance
invitation = DomainInvitation(email="nonexistent@example.com", domain=self.domain)
admin_instance = DomainInvitationAdmin(DomainInvitation, admin_site=None)
# Create a request object
request = self.factory.post("/admin/registrar/DomainInvitation/add/")
request.user = self.superuser
# Patch the retrieve method to ensure it is not called
with patch.object(DomainInvitation, "retrieve") as mock_retrieve:
admin_instance.save_model(request, invitation, form=None, change=False)
# Assert retrieve was not called
mock_retrieve.assert_not_called()
# Assert the invitation was saved
self.assertEqual(DomainInvitation.objects.count(), 1)
self.assertEqual(DomainInvitation.objects.first().email, "nonexistent@example.com")
class TestUserPortfolioPermissionAdmin(TestCase): class TestUserPortfolioPermissionAdmin(TestCase):
"""Tests for the PortfolioInivtationAdmin class""" """Tests for the PortfolioInivtationAdmin class"""

View file

@ -40,6 +40,7 @@ from .common import (
multiple_unalphabetical_domain_objects, multiple_unalphabetical_domain_objects,
MockEppLib, MockEppLib,
GenericTestHelper, GenericTestHelper,
normalize_html,
) )
from unittest.mock import ANY, patch from unittest.mock import ANY, patch
@ -1530,8 +1531,9 @@ class TestDomainRequestAdmin(MockEppLib):
self.assertContains(response, expected_url) self.assertContains(response, expected_url)
@less_console_noise_decorator @less_console_noise_decorator
def test_other_websites_has_readonly_link(self): def test_other_websites_has_one_readonly_link(self):
"""Tests if the readonly other_websites field has links""" """Tests if the readonly other_websites field has links.
Test markup for one website."""
# Create a fake domain request # Create a fake domain request
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
@ -1547,8 +1549,224 @@ class TestDomainRequestAdmin(MockEppLib):
self.assertContains(response, domain_request.requested_domain.name) self.assertContains(response, domain_request.requested_domain.name)
# Check that the page contains the link we expect. # Check that the page contains the link we expect.
expected_url = '<a href="city.com" target="_blank" class="padding-top-1 current-website__1">city.com</a>' expected_markup = """
self.assertContains(response, expected_url) <p class="margin-y-0 padding-y-0">
<a href="city.com" target="_blank">
city.com
</a>
</p>
"""
normalized_expected = normalize_html(expected_markup)
normalized_response = normalize_html(response.content.decode("utf-8"))
index = normalized_response.find(normalized_expected)
# Assert that the expected markup is found in the response
if index == -1:
self.fail(
f"Expected markup not found in the response.\n\n"
f"Expected:\n{normalized_expected}\n\n"
f"Start index of mismatch: {index}\n\n"
f"Consider checking the surrounding response for context."
)
@less_console_noise_decorator
def test_other_websites_has_few_readonly_links(self):
"""Tests if the readonly other_websites field has links.
Test markup for 5 or less websites."""
# Create a domain request with 4 current websites
domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
current_websites=["city.gov", "city2.gov", "city3.gov", "city4.gov"],
)
self.client.force_login(self.staffuser)
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain_request.requested_domain.name)
# Check that the page contains the link we expect.
expected_markup = """
<ul class="margin-top-0 margin-left-0 padding-left-0">
<li><a href="city.gov" target="_blank">city.gov</a></li>
<li><a href="city2.gov" target="_blank">city2.gov</a></li>
<li><a href="city3.gov" target="_blank">city3.gov</a></li>
<li><a href="city4.gov" target="_blank">city4.gov</a></li>
</ul>
"""
normalized_expected = normalize_html(expected_markup)
normalized_response = normalize_html(response.content.decode("utf-8"))
index = normalized_response.find(normalized_expected)
# Assert that the expected markup is found in the response
if index == -1:
self.fail(
f"Expected markup not found in the response.\n\n"
f"Expected:\n{normalized_expected}\n\n"
f"Start index of mismatch: {index}\n\n"
f"Consider checking the surrounding response for context."
)
@less_console_noise_decorator
def test_other_websites_has_lots_readonly_links(self):
"""Tests if the readonly other_websites field has links.
Test markup for 6 or more websites."""
# Create a domain requests with 6 current websites
domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
current_websites=["city.gov", "city2.gov", "city3.gov", "city4.gov", "city5.gov", "city6.gov"],
)
self.client.force_login(self.staffuser)
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain_request.requested_domain.name)
# Check that the page contains the link we expect.
expected_markup = """
<ul class="margin-top-0 margin-left-0 padding-left-0 admin-list-inline">
<li><a href="city.gov" target="_blank">city.gov</a></li>
<li><a href="city2.gov" target="_blank">city2.gov</a></li>
<li><a href="city3.gov" target="_blank">city3.gov</a></li>
<li><a href="city4.gov" target="_blank">city4.gov</a></li>
<li><a href="city5.gov" target="_blank">city5.gov</a></li>
<li><a href="city6.gov" target="_blank">city6.gov</a></li>
</ul>
"""
normalized_expected = normalize_html(expected_markup)
normalized_response = normalize_html(response.content.decode("utf-8"))
index = normalized_response.find(normalized_expected)
# Assert that the expected markup is found in the response
if index == -1:
self.fail(
f"Expected markup not found in the response.\n\n"
f"Expected:\n{normalized_expected}\n\n"
f"Start index of mismatch: {index}\n\n"
f"Consider checking the surrounding response for context."
)
@less_console_noise_decorator
def test_alternative_domains_has_one_readonly_link(self):
"""Tests if the readonly alternative_domains field has links.
Test markup for one website."""
# Create a fake domain request
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
self.client.force_login(self.staffuser)
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain_request.requested_domain.name)
# Check that the page contains the link we expect.
website = Website.objects.filter(website="city1.gov").first()
base_url = "/admin/registrar/website"
return_path = f"/admin/registrar/domainrequest/{domain_request.pk}/change/"
expected_markup = f"""
<p class="margin-y-0 padding-y-0">
<a href="{base_url}/{website.pk}/change/?return_path={return_path}" target="_blank">city1.gov</a>
</p>
"""
normalized_expected = normalize_html(expected_markup)
normalized_response = normalize_html(response.content.decode("utf-8"))
index = normalized_response.find(normalized_expected)
# Assert that the expected markup is found in the response
if index == -1:
self.fail(
f"Expected markup not found in the response.\n\n"
f"Expected:\n{normalized_expected}\n\n"
f"Start index of mismatch: {index}\n\n"
f"Consider checking the surrounding response for context."
)
@less_console_noise_decorator
def test_alternative_domains_has_lots_readonly_link(self):
"""Tests if the readonly other_websites field has links.
Test markup for 6 or more websites."""
# Create a domain request with 6 alternative domains
domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
alternative_domains=[
"altcity1.gov",
"altcity2.gov",
"altcity3.gov",
"altcity4.gov",
"altcity5.gov",
"altcity6.gov",
],
)
self.client.force_login(self.staffuser)
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain_request.requested_domain.name)
# Check that the page contains the link we expect.
website1 = Website.objects.filter(website="altcity1.gov").first()
website2 = Website.objects.filter(website="altcity2.gov").first()
website3 = Website.objects.filter(website="altcity3.gov").first()
website4 = Website.objects.filter(website="altcity4.gov").first()
website5 = Website.objects.filter(website="altcity5.gov").first()
website6 = Website.objects.filter(website="altcity6.gov").first()
base_url = "/admin/registrar/website"
return_path = f"/admin/registrar/domainrequest/{domain_request.pk}/change/"
attr = 'target="_blank"'
expected_markup = f"""
<ul class="margin-top-0 margin-left-0 padding-left-0 admin-list-inline">
<li><a href="{base_url}/{website1.pk}/change/?return_path={return_path}" {attr}>altcity1.gov</a></li>
<li><a href="{base_url}/{website2.pk}/change/?return_path={return_path}" {attr}>altcity2.gov</a></li>
<li><a href="{base_url}/{website3.pk}/change/?return_path={return_path}" {attr}>altcity3.gov</a></li>
<li><a href="{base_url}/{website4.pk}/change/?return_path={return_path}" {attr}>altcity4.gov</a></li>
<li><a href="{base_url}/{website5.pk}/change/?return_path={return_path}" {attr}>altcity5.gov</a></li>
<li><a href="{base_url}/{website6.pk}/change/?return_path={return_path}" {attr}>altcity6.gov</a></li>
</ul>
"""
normalized_expected = normalize_html(expected_markup)
normalized_response = normalize_html(response.content.decode("utf-8"))
index = normalized_response.find(normalized_expected)
# Assert that the expected markup is found in the response
if index == -1:
self.fail(
f"Expected markup not found in the response.\n\n"
f"Expected:\n{normalized_expected}\n\n"
f"Start index of mismatch: {index}\n\n"
f"Consider checking the surrounding response for context."
)
@less_console_noise_decorator @less_console_noise_decorator
def test_contact_fields_have_detail_table(self): def test_contact_fields_have_detail_table(self):

View file

@ -150,7 +150,7 @@ class TestEmails(TestCase):
def test_submission_confirmation_no_current_website_spacing(self): def test_submission_confirmation_no_current_website_spacing(self):
"""Test line spacing without current_website.""" """Test line spacing without current_website."""
domain_request = completed_domain_request( domain_request = completed_domain_request(
has_current_website=False, user=User.objects.create(username="test", email="testy@town.com") current_websites=[], user=User.objects.create(username="test", email="testy@town.com")
) )
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
domain_request.submit() domain_request.submit()
@ -164,9 +164,7 @@ class TestEmails(TestCase):
@less_console_noise_decorator @less_console_noise_decorator
def test_submission_confirmation_current_website_spacing(self): def test_submission_confirmation_current_website_spacing(self):
"""Test line spacing with current_website.""" """Test line spacing with current_website."""
domain_request = completed_domain_request( domain_request = completed_domain_request(user=User.objects.create(username="test", email="testy@town.com"))
has_current_website=True, user=User.objects.create(username="test", email="testy@town.com")
)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
domain_request.submit() domain_request.submit()
_, kwargs = self.mock_client.send_email.call_args _, kwargs = self.mock_client.send_email.call_args
@ -218,9 +216,7 @@ class TestEmails(TestCase):
@less_console_noise_decorator @less_console_noise_decorator
def test_submission_confirmation_alternative_govdomain_spacing(self): def test_submission_confirmation_alternative_govdomain_spacing(self):
"""Test line spacing with alternative .gov domain.""" """Test line spacing with alternative .gov domain."""
domain_request = completed_domain_request( domain_request = completed_domain_request(user=User.objects.create(username="test", email="testy@town.com"))
has_alternative_gov_domain=True, user=User.objects.create(username="test", email="testy@town.com")
)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
domain_request.submit() domain_request.submit()
_, kwargs = self.mock_client.send_email.call_args _, kwargs = self.mock_client.send_email.call_args
@ -234,7 +230,7 @@ class TestEmails(TestCase):
def test_submission_confirmation_no_alternative_govdomain_spacing(self): def test_submission_confirmation_no_alternative_govdomain_spacing(self):
"""Test line spacing without alternative .gov domain.""" """Test line spacing without alternative .gov domain."""
domain_request = completed_domain_request( domain_request = completed_domain_request(
has_alternative_gov_domain=False, user=User.objects.create(username="test", email="testy@town.com") alternative_domains=[], user=User.objects.create(username="test", email="testy@town.com")
) )
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
domain_request.submit() domain_request.submit()

View file

@ -338,7 +338,7 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
self.assertEqual(expected_domain_request.creator.email, creator[i]) self.assertEqual(expected_domain_request.creator.email, creator[i])
# Check action url, action label and svg icon # Check action url, action label and svg icon
# Example domain requests will test each of below three scenarios # Example domain requests will test each of below three scenarios
if creator[i] != self.user.email: if creator[i] != self.user.email or not self.user.has_edit_request_portfolio_permission(self.portfolio):
# Test case where action is View # Test case where action is View
self.assertEqual("View", action_labels[i]) self.assertEqual("View", action_labels[i])
self.assertEqual( self.assertEqual(

View file

@ -1952,7 +1952,7 @@ class DomainRequestGrowth(DomainRequestExport):
"Domain request", "Domain request",
"Domain type", "Domain type",
"Federal type", "Federal type",
"Submitted at", "First submitted date",
] ]
@classmethod @classmethod
@ -1976,7 +1976,6 @@ class DomainRequestGrowth(DomainRequestExport):
start_date_formatted = format_start_date(start_date) start_date_formatted = format_start_date(start_date)
end_date_formatted = format_end_date(end_date) end_date_formatted = format_end_date(end_date)
return Q( return Q(
status=DomainRequest.DomainRequestStatus.SUBMITTED,
last_submitted_date__lte=end_date_formatted, last_submitted_date__lte=end_date_formatted,
last_submitted_date__gte=start_date_formatted, last_submitted_date__gte=start_date_formatted,
) )

View file

@ -125,15 +125,6 @@ def serialize_domain_request(request, domain_request, user):
DomainRequest.DomainRequestStatus.WITHDRAWN, DomainRequest.DomainRequestStatus.WITHDRAWN,
] ]
# Determine if the request is deletable
if not user.is_org_user(request):
is_deletable = domain_request.status in deletable_statuses
else:
portfolio = request.session.get("portfolio")
is_deletable = (
domain_request.status in deletable_statuses and user.has_edit_request_portfolio_permission(portfolio)
) and domain_request.creator == user
# Determine action label based on user permissions and request status # Determine action label based on user permissions and request status
editable_statuses = [ editable_statuses = [
DomainRequest.DomainRequestStatus.STARTED, DomainRequest.DomainRequestStatus.STARTED,
@ -141,7 +132,22 @@ def serialize_domain_request(request, domain_request, user):
DomainRequest.DomainRequestStatus.WITHDRAWN, DomainRequest.DomainRequestStatus.WITHDRAWN,
] ]
if user.has_edit_request_portfolio_permission and domain_request.creator == user: # No portfolio action_label
if domain_request.creator == user:
action_label = "Edit" if domain_request.status in editable_statuses else "Manage"
else:
action_label = "View"
# No portfolio deletable
is_deletable = domain_request.status in deletable_statuses
# If we're working with a portfolio
if user.is_org_user(request):
portfolio = request.session.get("portfolio")
is_deletable = (
domain_request.status in deletable_statuses and user.has_edit_request_portfolio_permission(portfolio)
) and domain_request.creator == user
if user.has_edit_request_portfolio_permission(portfolio) and domain_request.creator == user:
action_label = "Edit" if domain_request.status in editable_statuses else "Manage" action_label = "Edit" if domain_request.status in editable_statuses else "Manage"
else: else:
action_label = "View" action_label = "View"