mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-13 13:09:41 +02:00
Merge pull request #1664 from cisagov/dk/1623-notify-analysts-with-contact-related-objects
Issue #1623: notify analysts with contact related objects
This commit is contained in:
commit
3b303ffe83
4 changed files with 157 additions and 8 deletions
|
@ -20,6 +20,8 @@ from . import models
|
||||||
from auditlog.models import LogEntry # type: ignore
|
from auditlog.models import LogEntry # type: ignore
|
||||||
from auditlog.admin import LogEntryAdmin # type: ignore
|
from auditlog.admin import LogEntryAdmin # type: ignore
|
||||||
from django_fsm import TransitionNotAllowed # type: ignore
|
from django_fsm import TransitionNotAllowed # type: ignore
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.html import escape
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -452,6 +454,60 @@ class ContactAdmin(ListHeaderAdmin):
|
||||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||||
return readonly_fields # Read-only fields for analysts
|
return readonly_fields # Read-only fields for analysts
|
||||||
|
|
||||||
|
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||||
|
"""Extend the change_view for Contact objects in django admin.
|
||||||
|
Customize to display related objects to the Contact. These will be passed
|
||||||
|
through the messages construct to the template for display to the user."""
|
||||||
|
|
||||||
|
# Fetch the Contact instance
|
||||||
|
contact = models.Contact.objects.get(pk=object_id)
|
||||||
|
|
||||||
|
# initialize related_objects array
|
||||||
|
related_objects = []
|
||||||
|
# for all defined fields in the model
|
||||||
|
for related_field in contact._meta.get_fields():
|
||||||
|
# if the field is a relation to another object
|
||||||
|
if related_field.is_relation:
|
||||||
|
# Check if the related field is not None
|
||||||
|
related_manager = getattr(contact, related_field.name)
|
||||||
|
if related_manager is not None:
|
||||||
|
# Check if it's a ManyToManyField/reverse ForeignKey or a OneToOneField
|
||||||
|
# Do this by checking for get_queryset method on the related_manager
|
||||||
|
if hasattr(related_manager, "get_queryset"):
|
||||||
|
# Handles ManyToManyRel and ManyToOneRel
|
||||||
|
queryset = related_manager.get_queryset()
|
||||||
|
else:
|
||||||
|
# Handles OneToOne rels, ie. User
|
||||||
|
queryset = [related_manager]
|
||||||
|
|
||||||
|
for obj in queryset:
|
||||||
|
# for each object, build the edit url in this view and add as tuple
|
||||||
|
# to the related_objects array
|
||||||
|
app_label = obj._meta.app_label
|
||||||
|
model_name = obj._meta.model_name
|
||||||
|
obj_id = obj.id
|
||||||
|
change_url = reverse("admin:%s_%s_change" % (app_label, model_name), args=[obj_id])
|
||||||
|
related_objects.append((change_url, obj))
|
||||||
|
|
||||||
|
if related_objects:
|
||||||
|
message = "<ul class='messagelist_content-list--unstyled'>"
|
||||||
|
for i, (url, obj) in enumerate(related_objects):
|
||||||
|
if i < 5:
|
||||||
|
escaped_obj = escape(obj)
|
||||||
|
message += f"<li>Joined to {obj.__class__.__name__}: <a href='{url}'>{escaped_obj}</a></li>"
|
||||||
|
message += "</ul>"
|
||||||
|
if len(related_objects) > 5:
|
||||||
|
related_objects_over_five = len(related_objects) - 5
|
||||||
|
message += f"<p class='font-sans-3xs'>And {related_objects_over_five} more...</p>"
|
||||||
|
|
||||||
|
message_html = mark_safe(message) # nosec
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
message_html,
|
||||||
|
)
|
||||||
|
|
||||||
|
return super().change_view(request, object_id, form_url, extra_context=extra_context)
|
||||||
|
|
||||||
|
|
||||||
class WebsiteAdmin(ListHeaderAdmin):
|
class WebsiteAdmin(ListHeaderAdmin):
|
||||||
"""Custom website admin class."""
|
"""Custom website admin class."""
|
||||||
|
|
|
@ -258,3 +258,15 @@ h1, h2, h3,
|
||||||
#select2-id_user-results {
|
#select2-id_user-results {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Content list inside of a DjA alert, unstyled
|
||||||
|
.messagelist_content-list--unstyled {
|
||||||
|
padding-left: 0;
|
||||||
|
li {
|
||||||
|
font-family: "Source Sans Pro Web", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif;
|
||||||
|
font-size: 13.92px!important;
|
||||||
|
background: none!important;
|
||||||
|
padding: 0!important;
|
||||||
|
margin: 0!important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -526,6 +526,7 @@ def completed_application(
|
||||||
has_anything_else=True,
|
has_anything_else=True,
|
||||||
status=DomainApplication.ApplicationStatus.STARTED,
|
status=DomainApplication.ApplicationStatus.STARTED,
|
||||||
user=False,
|
user=False,
|
||||||
|
submitter=False,
|
||||||
name="city.gov",
|
name="city.gov",
|
||||||
):
|
):
|
||||||
"""A completed domain application."""
|
"""A completed domain application."""
|
||||||
|
@ -541,7 +542,8 @@ def completed_application(
|
||||||
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")
|
alt, _ = Website.objects.get_or_create(website="city1.gov")
|
||||||
current, _ = Website.objects.get_or_create(website="city.com")
|
current, _ = Website.objects.get_or_create(website="city.com")
|
||||||
you, _ = Contact.objects.get_or_create(
|
if not submitter:
|
||||||
|
submitter, _ = Contact.objects.get_or_create(
|
||||||
first_name="Testy2",
|
first_name="Testy2",
|
||||||
last_name="Tester2",
|
last_name="Tester2",
|
||||||
title="Admin Tester",
|
title="Admin Tester",
|
||||||
|
@ -567,7 +569,7 @@ def completed_application(
|
||||||
zipcode="10002",
|
zipcode="10002",
|
||||||
authorizing_official=ao,
|
authorizing_official=ao,
|
||||||
requested_domain=domain,
|
requested_domain=domain,
|
||||||
submitter=you,
|
submitter=submitter,
|
||||||
creator=user,
|
creator=user,
|
||||||
status=status,
|
status=status,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1737,7 +1737,86 @@ class ContactAdminTest(TestCase):
|
||||||
|
|
||||||
self.assertEqual(readonly_fields, expected_fields)
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
|
||||||
|
def test_change_view_for_joined_contact_five_or_less(self):
|
||||||
|
"""Create a contact, join it to 4 domain requests. The 5th join will be a user.
|
||||||
|
Assert that the warning on the contact form lists 5 joins."""
|
||||||
|
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
|
||||||
|
# Create an instance of the model
|
||||||
|
contact, _ = Contact.objects.get_or_create(user=self.staffuser)
|
||||||
|
|
||||||
|
# join it to 4 domain requests. The 5th join will be a user.
|
||||||
|
application1 = completed_application(submitter=contact, name="city1.gov")
|
||||||
|
application2 = completed_application(submitter=contact, name="city2.gov")
|
||||||
|
application3 = completed_application(submitter=contact, name="city3.gov")
|
||||||
|
application4 = completed_application(submitter=contact, name="city4.gov")
|
||||||
|
|
||||||
|
with patch("django.contrib.messages.warning") as mock_warning:
|
||||||
|
# Use the test client to simulate the request
|
||||||
|
response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk]))
|
||||||
|
|
||||||
|
# Assert that the error message was called with the correct argument
|
||||||
|
# Note: The 5th join will be a user.
|
||||||
|
mock_warning.assert_called_once_with(
|
||||||
|
response.wsgi_request,
|
||||||
|
"<ul class='messagelist_content-list--unstyled'>"
|
||||||
|
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||||
|
f"domainapplication/{application1.pk}/change/'>city1.gov</a></li>"
|
||||||
|
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||||
|
f"domainapplication/{application2.pk}/change/'>city2.gov</a></li>"
|
||||||
|
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||||
|
f"domainapplication/{application3.pk}/change/'>city3.gov</a></li>"
|
||||||
|
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||||
|
f"domainapplication/{application4.pk}/change/'>city4.gov</a></li>"
|
||||||
|
"<li>Joined to User: <a href='/admin/registrar/"
|
||||||
|
f"user/{self.staffuser.pk}/change/'>staff@example.com</a></li>"
|
||||||
|
"</ul>",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_change_view_for_joined_contact_five_or_more(self):
|
||||||
|
"""Create a contact, join it to 5 domain requests. The 6th join will be a user.
|
||||||
|
Assert that the warning on the contact form lists 5 joins and a '1 more' ellispsis."""
|
||||||
|
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
|
||||||
|
# Create an instance of the model
|
||||||
|
# join it to 5 domain requests. The 6th join will be a user.
|
||||||
|
contact, _ = Contact.objects.get_or_create(user=self.staffuser)
|
||||||
|
application1 = completed_application(submitter=contact, name="city1.gov")
|
||||||
|
application2 = completed_application(submitter=contact, name="city2.gov")
|
||||||
|
application3 = completed_application(submitter=contact, name="city3.gov")
|
||||||
|
application4 = completed_application(submitter=contact, name="city4.gov")
|
||||||
|
application5 = completed_application(submitter=contact, name="city5.gov")
|
||||||
|
|
||||||
|
with patch("django.contrib.messages.warning") as mock_warning:
|
||||||
|
# Use the test client to simulate the request
|
||||||
|
response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk]))
|
||||||
|
|
||||||
|
logger.info(mock_warning)
|
||||||
|
|
||||||
|
# Assert that the error message was called with the correct argument
|
||||||
|
# Note: The 6th join will be a user.
|
||||||
|
mock_warning.assert_called_once_with(
|
||||||
|
response.wsgi_request,
|
||||||
|
"<ul class='messagelist_content-list--unstyled'>"
|
||||||
|
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||||
|
f"domainapplication/{application1.pk}/change/'>city1.gov</a></li>"
|
||||||
|
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||||
|
f"domainapplication/{application2.pk}/change/'>city2.gov</a></li>"
|
||||||
|
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||||
|
f"domainapplication/{application3.pk}/change/'>city3.gov</a></li>"
|
||||||
|
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||||
|
f"domainapplication/{application4.pk}/change/'>city4.gov</a></li>"
|
||||||
|
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||||
|
f"domainapplication/{application5.pk}/change/'>city5.gov</a></li>"
|
||||||
|
"</ul>"
|
||||||
|
"<p class='font-sans-3xs'>And 1 more...</p>",
|
||||||
|
)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
DomainApplication.objects.all().delete()
|
||||||
|
Contact.objects.all().delete()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue