Merge branch 'main' into za/1128-email-bouncebacks

This commit is contained in:
zandercymatics 2023-12-27 12:23:05 -07:00
commit 631fa3b9c6
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
14 changed files with 661 additions and 74 deletions

View file

@ -12,6 +12,7 @@ from django.http.response import HttpResponseRedirect
from django.urls import reverse
from epplibwrapper.errors import ErrorCode, RegistryError
from registrar.models.domain import Domain
from registrar.models.user import User
from registrar.utility import csv_export
from . import models
from auditlog.models import LogEntry # type: ignore
@ -538,6 +539,9 @@ class DomainInformationAdmin(ListHeaderAdmin):
# to activate the edit/delete/view buttons
filter_horizontal = ("other_contacts",)
# Table ordering
ordering = ["domain__name"]
def get_readonly_fields(self, request, obj=None):
"""Set the read-only state on form elements.
We have 1 conditions that determine which fields are read-only:
@ -589,6 +593,27 @@ class DomainApplicationAdmin(ListHeaderAdmin):
"""Custom domain applications admin class."""
class InvestigatorFilter(admin.SimpleListFilter):
"""Custom investigator filter that only displays users with the manager role"""
title = "investigator"
# Match the old param name to avoid unnecessary refactoring
parameter_name = "investigator__id__exact"
def lookups(self, request, model_admin):
"""Lookup reimplementation, gets users of is_staff.
Returns a list of tuples consisting of (user.id, user)
"""
privileged_users = User.objects.filter(is_staff=True).order_by("first_name", "last_name", "email")
return [(user.id, user) for user in privileged_users]
def queryset(self, request, queryset):
"""Custom queryset implementation, filters by investigator"""
if self.value() is None:
return queryset
else:
return queryset.filter(investigator__id__exact=self.value())
# Columns
list_display = [
"requested_domain",
@ -600,7 +625,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
]
# Filters
list_filter = ("status", "organization_type", "investigator")
list_filter = ("status", "organization_type", InvestigatorFilter)
# Search
search_fields = [
@ -676,6 +701,23 @@ class DomainApplicationAdmin(ListHeaderAdmin):
filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
# Table ordering
ordering = ["requested_domain__name"]
# lists in filter_horizontal are not sorted properly, sort them
# by website
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name in ("current_websites", "alternative_domains"):
kwargs["queryset"] = models.Website.objects.all().order_by("website") # Sort websites
return super().formfield_for_manytomany(db_field, request, **kwargs)
def formfield_for_foreignkey(self, db_field, request, **kwargs):
# Removes invalid investigator options from the investigator dropdown
if db_field.name == "investigator":
kwargs["queryset"] = User.objects.filter(is_staff=True)
return db_field.formfield(**kwargs)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
# Trigger action when a fieldset is changed
def save_model(self, request, obj, form, change):
if obj and obj.creator.status != models.User.RESTRICTED:
@ -865,6 +907,9 @@ class DomainAdmin(ListHeaderAdmin):
change_list_template = "django/admin/domain_change_list.html"
readonly_fields = ["state", "expiration_date"]
# Table ordering
ordering = ["name"]
def export_data_type(self, request):
# match the CSV example with all the fields
response = HttpResponse(content_type="text/csv")

View file

@ -0,0 +1,17 @@
# Generated by Django 4.2.7 on 2023-12-13 15:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0056_alter_domain_state_alter_domainapplication_status_and_more"),
]
operations = [
migrations.AddField(
model_name="domainapplication",
name="submission_date",
field=models.DateField(blank=True, default=None, help_text="Date submitted", null=True),
),
]

View file

@ -8,6 +8,7 @@ from typing import Optional
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
from django.db import models
from django.utils import timezone
from typing import Any
@ -200,6 +201,14 @@ class Domain(TimeStampedModel, DomainHelper):
"""Get the `cr_date` element from the registry."""
return self._get_property("cr_date")
@creation_date.setter # type: ignore
def creation_date(self, cr_date: date):
"""
Direct setting of the creation date in the registry is not implemented.
Creation date can only be set by registry."""
raise NotImplementedError()
@Cache
def last_transferred_date(self) -> date:
"""Get the `tr_date` element from the registry."""
@ -963,6 +972,16 @@ class Domain(TimeStampedModel, DomainHelper):
def isActive(self):
return self.state == Domain.State.CREATED
def is_expired(self):
"""
Check if the domain's expiration date is in the past.
Returns True if expired, False otherwise.
"""
if self.expiration_date is None:
return True
now = timezone.now().date()
return self.expiration_date < now
def map_epp_contact_to_public_contact(self, contact: eppInfo.InfoContactResultData, contact_id, contact_type):
"""Maps the Epp contact representation to a PublicContact object.
@ -1582,38 +1601,11 @@ class Domain(TimeStampedModel, DomainHelper):
def _fetch_cache(self, fetch_hosts=False, fetch_contacts=False):
"""Contact registry for info about a domain."""
try:
# get info from registry
data_response = self._get_or_create_domain()
cache = self._extract_data_from_response(data_response)
# remove null properties (to distinguish between "a value of None" and null)
cleaned = self._remove_null_properties(cache)
if "statuses" in cleaned:
cleaned["statuses"] = [status.state for status in cleaned["statuses"]]
cleaned["dnssecdata"] = self._get_dnssec_data(data_response.extensions)
# Capture and store old hosts and contacts from cache if they exist
old_cache_hosts = self._cache.get("hosts")
old_cache_contacts = self._cache.get("contacts")
if fetch_contacts:
cleaned["contacts"] = self._get_contacts(cleaned.get("_contacts", []))
if old_cache_hosts is not None:
logger.debug("resetting cleaned['hosts'] to old_cache_hosts")
cleaned["hosts"] = old_cache_hosts
if fetch_hosts:
cleaned["hosts"] = self._get_hosts(cleaned.get("_hosts", []))
if old_cache_contacts is not None:
cleaned["contacts"] = old_cache_contacts
# if expiration date from registry does not match what is in db,
# update the db
if "ex_date" in cleaned and cleaned["ex_date"] != self.expiration_date:
self.expiration_date = cleaned["ex_date"]
self.save()
cleaned = self._clean_cache(cache, data_response)
self._update_hosts_and_contacts(cleaned, fetch_hosts, fetch_contacts)
self._update_dates(cleaned)
self._cache = cleaned
@ -1621,6 +1613,7 @@ class Domain(TimeStampedModel, DomainHelper):
logger.error(e)
def _extract_data_from_response(self, data_response):
"""extract data from response from registry"""
data = data_response.res_data[0]
return {
"auth_info": getattr(data, "auth_info", ...),
@ -1635,6 +1628,15 @@ class Domain(TimeStampedModel, DomainHelper):
"up_date": getattr(data, "up_date", ...),
}
def _clean_cache(self, cache, data_response):
"""clean up the cache"""
# remove null properties (to distinguish between "a value of None" and null)
cleaned = self._remove_null_properties(cache)
if "statuses" in cleaned:
cleaned["statuses"] = [status.state for status in cleaned["statuses"]]
cleaned["dnssecdata"] = self._get_dnssec_data(data_response.extensions)
return cleaned
def _remove_null_properties(self, cache):
return {k: v for k, v in cache.items() if v is not ...}
@ -1648,6 +1650,42 @@ class Domain(TimeStampedModel, DomainHelper):
dnssec_data = extension
return dnssec_data
def _update_hosts_and_contacts(self, cleaned, fetch_hosts, fetch_contacts):
"""Capture and store old hosts and contacts from cache if they don't exist"""
old_cache_hosts = self._cache.get("hosts")
old_cache_contacts = self._cache.get("contacts")
if fetch_contacts:
cleaned["contacts"] = self._get_contacts(cleaned.get("_contacts", []))
if old_cache_hosts is not None:
logger.debug("resetting cleaned['hosts'] to old_cache_hosts")
cleaned["hosts"] = old_cache_hosts
if fetch_hosts:
cleaned["hosts"] = self._get_hosts(cleaned.get("_hosts", []))
if old_cache_contacts is not None:
cleaned["contacts"] = old_cache_contacts
def _update_dates(self, cleaned):
"""Update dates (expiration and creation) from cleaned"""
requires_save = False
# if expiration date from registry does not match what is in db,
# update the db
if "ex_date" in cleaned and cleaned["ex_date"] != self.expiration_date:
self.expiration_date = cleaned["ex_date"]
requires_save = True
# if creation_date from registry does not match what is in db,
# update the db
if "cr_date" in cleaned and cleaned["cr_date"] != self.created_at:
self.created_at = cleaned["cr_date"]
requires_save = True
# if either registration date or creation date need updating
if requires_save:
self.save()
def _get_contacts(self, contacts):
choices = PublicContact.ContactTypeChoices
# We expect that all these fields get populated,

View file

@ -6,6 +6,7 @@ import logging
from django.apps import apps
from django.db import models
from django_fsm import FSMField, transition # type: ignore
from django.utils import timezone
from registrar.models.domain import Domain
from .utility.time_stamped_model import TimeStampedModel
@ -547,6 +548,14 @@ class DomainApplication(TimeStampedModel):
help_text="Acknowledged .gov acceptable use policy",
)
# submission date records when application is submitted
submission_date = models.DateField(
null=True,
blank=True,
default=None,
help_text="Date submitted",
)
def __str__(self):
try:
if self.requested_domain and self.requested_domain.name:
@ -617,6 +626,10 @@ class DomainApplication(TimeStampedModel):
if not DraftDomain.string_could_be_domain(self.requested_domain.name):
raise ValueError("Requested domain is not a valid domain name.")
# Update submission_date to today
self.submission_date = timezone.now().date()
self.save()
self._send_status_update_email(
"submission confirmation",
"emails/submission_confirmation.txt",

View file

@ -229,6 +229,7 @@ class DomainInformation(TimeStampedModel):
da_dict.pop("alternative_domains", None)
da_dict.pop("requested_domain", None)
da_dict.pop("approved_domain", None)
da_dict.pop("submission_date", None)
other_contacts = da_dict.pop("other_contacts", [])
domain_info = cls(**da_dict)
domain_info.domain_application = domain_application

View file

@ -6,7 +6,7 @@
<div class="margin-top-4 tablet:grid-col-10">
<div
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2{% if domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED%} dotgov-status-box--action-need{% endif %}"
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2{% if not domain.is_expired %}{% if domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %} dotgov-status-box--action-need{% endif %}{% endif %}"
role="region"
aria-labelledby="summary-box-key-information"
>
@ -17,7 +17,9 @@
<span class="text-bold text-primary-darker">
Status:
</span>
{% if domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED%}
{% if domain.is_expired %}
Expired
{% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED%}
DNS needed
{% else %}
{{ domain.state|title }}
@ -27,12 +29,15 @@
</div>
<br>
{% include "includes/domain_dates.html" %}
{% url 'domain-dns-nameservers' pk=domain.id as url %}
{% if domain.nameservers|length > 0 %}
{% include "includes/summary_item.html" with title='DNS name servers' domains='true' value=domain.nameservers list='true' edit_link=url editable=domain.is_editable %}
{% else %}
{% if domain.is_editable %}
<h2 class="margin-top-neg-1"> DNS name servers </h2>
<h2 class="margin-top-3"> DNS name servers </h2>
<p> No DNS name servers have been added yet. Before your domain can be used well need information about your domain name servers.</p>
<a class="usa-button margin-bottom-1" href="{{url}}"> Add DNS name servers </a>
{% else %}

View file

@ -1,7 +1,7 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi.
{{ full_name }} has added you as a manager on {{ domain.name }}.
{{ requester_email }} has added you as a manager on {{ domain.name }}.
YOU NEED A LOGIN.GOV ACCOUNT
Youll need a Login.gov account to manage your .gov domain. Login.gov provides

View file

@ -39,7 +39,7 @@
<thead>
<tr>
<th data-sortable scope="col" role="columnheader">Domain name</th>
<th data-sortable scope="col" role="columnheader">Date created</th>
<th data-sortable scope="col" role="columnheader">Expires</th>
<th data-sortable scope="col" role="columnheader">Status</th>
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
</tr>
@ -50,9 +50,11 @@
<th th scope="row" role="rowheader" data-label="Domain name">
{{ domain.name }}
</th>
<td data-sort-value="{{ domain.created_time|date:"U" }}" data-label="Date created">{{ domain.created_time|date }}</td>
<td data-sort-value="{{ domain.expiration_date|date:"U" }}" data-label="Expires">{{ domain.expiration_date|date }}</td>
<td data-label="Status">
{% if domain.state == "unknown" or domain.state == "dns needed"%}
{% if domain.is_expired %}
Expired
{% elif domain.state == "unknown" or domain.state == "dns needed"%}
DNS needed
{% else %}
{{ domain.state|title }}
@ -99,7 +101,7 @@
<thead>
<tr>
<th data-sortable scope="col" role="columnheader">Domain name</th>
<th data-sortable scope="col" role="columnheader">Date created</th>
<th data-sortable scope="col" role="columnheader">Date submitted</th>
<th data-sortable scope="col" role="columnheader">Status</th>
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
</tr>
@ -110,7 +112,13 @@
<th th scope="row" role="rowheader" data-label="Domain name">
{{ application.requested_domain.name|default:"New domain request" }}
</th>
<td data-sort-value="{{ application.created_at|date:"U" }}" data-label="Date created">{{ application.created_at|date }}</td>
<td data-sort-value="{{ application.submission_date|date:"U" }}" data-label="Date submitted">
{% if application.submission_date %}
{{ application.submission_date|date }}
{% else %}
<span class="text-base">Not submitted</span>
{% endif %}
</td>
<td data-label="Status">{{ application.get_status_display }}</td>
<td>
{% if application.status == "started" or application.status == "action needed" or application.status == "withdrawn" %}

View file

@ -0,0 +1,12 @@
{% if domain.expiration_date or domain.created_at %}
<p class="margin-y-0">
{% if domain.expiration_date %}
<strong class="text-primary-dark">Expires:</strong>
{{ domain.expiration_date|date }}
{% if domain.is_expired %} <span class="text-error"><strong>(expired)</strong></span>{% endif %}
<br/>
{% endif %}
{% if domain.created_at %}
<strong class="text-primary-dark">Date created:</strong> {{ domain.created_at|date }}{% endif %}
</p>
{% endif %}

View file

@ -15,13 +15,7 @@ from registrar.admin import (
ContactAdmin,
UserDomainRoleAdmin,
)
from registrar.models import (
Domain,
DomainApplication,
DomainInformation,
User,
DomainInvitation,
)
from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website
from registrar.models.user_domain_role import UserDomainRole
from .common import (
completed_application,
@ -323,6 +317,7 @@ class TestDomainApplicationAdmin(MockEppLib):
self.admin = DomainApplicationAdmin(model=DomainApplication, admin_site=self.site)
self.superuser = create_superuser()
self.staffuser = create_user()
self.client = Client(HTTP_HOST="localhost:8080")
def test_short_org_name_in_applications_list(self):
"""
@ -637,6 +632,7 @@ class TestDomainApplicationAdmin(MockEppLib):
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",
"submission_date",
"current_websites",
"other_contacts",
"alternative_domains",
@ -860,12 +856,224 @@ class TestDomainApplicationAdmin(MockEppLib):
with self.assertRaises(DomainInformation.DoesNotExist):
domain_information.refresh_from_db()
def test_has_correct_filters(self):
"""
This test verifies that DomainApplicationAdmin has the correct filters set up.
It retrieves the current list of filters from DomainApplicationAdmin
and checks that it matches the expected list of filters.
"""
request = self.factory.get("/")
request.user = self.superuser
# Grab the current list of table filters
readonly_fields = self.admin.get_list_filter(request)
expected_fields = ("status", "organization_type", DomainApplicationAdmin.InvestigatorFilter)
self.assertEqual(readonly_fields, expected_fields)
def test_table_sorted_alphabetically(self):
"""
This test verifies that the DomainApplicationAdmin table is sorted alphabetically
by the 'requested_domain__name' field.
It creates a list of DomainApplication instances in a non-alphabetical order,
then retrieves the queryset from the DomainApplicationAdmin and checks
that it matches the expected queryset,
which is sorted alphabetically by the 'requested_domain__name' field.
"""
# Creates a list of DomainApplications in scrambled order
multiple_unalphabetical_domain_objects("application")
request = self.factory.get("/")
request.user = self.superuser
# Get the expected list of alphabetically sorted DomainApplications
expected_order = DomainApplication.objects.order_by("requested_domain__name")
# Get the returned queryset
queryset = self.admin.get_queryset(request)
# Check the order
self.assertEqual(
list(queryset),
list(expected_order),
)
def test_displays_investigator_filter(self):
"""
This test verifies that the investigator filter in the admin interface for
the DomainApplication model displays correctly.
It creates two DomainApplication instances, each with a different investigator.
It then simulates a staff user logging in and applying the investigator filter
on the DomainApplication admin page.
We then test if the page displays the filter we expect, but we do not test
if we get back the correct response in the table. This is to isolate if
the filter displays correctly, when the filter isn't filtering correctly.
"""
# Create a mock DomainApplication object, with a fake investigator
application: DomainApplication = generic_domain_object("application", "SomeGuy")
investigator_user = User.objects.filter(username=application.investigator.username).get()
investigator_user.is_staff = True
investigator_user.save()
p = "userpass"
self.client.login(username="staffuser", password=p)
response = self.client.get(
"/admin/registrar/domainapplication/",
{
"investigator__id__exact": investigator_user.id,
},
follow=True,
)
# Then, test if the filter actually exists
self.assertIn("filters", response.context)
# Assert the content of filters and search_query
filters = response.context["filters"]
self.assertEqual(
filters,
[
{
"parameter_name": "investigator",
"parameter_value": "SomeGuy first_name:investigator SomeGuy last_name:investigator",
},
],
)
def test_investigator_filter_filters_correctly(self):
"""
This test verifies that the investigator filter in the admin interface for
the DomainApplication model works correctly.
It creates two DomainApplication instances, each with a different investigator.
It then simulates a staff user logging in and applying the investigator filter
on the DomainApplication admin page.
It then verifies that it was applied correctly.
The test checks that the response contains the expected DomainApplication pbjects
in the table.
"""
# Create a mock DomainApplication object, with a fake investigator
application: DomainApplication = generic_domain_object("application", "SomeGuy")
investigator_user = User.objects.filter(username=application.investigator.username).get()
investigator_user.is_staff = True
investigator_user.save()
# Create a second mock DomainApplication object, to test filtering
application: DomainApplication = generic_domain_object("application", "BadGuy")
another_user = User.objects.filter(username=application.investigator.username).get()
another_user.is_staff = True
another_user.save()
p = "userpass"
self.client.login(username="staffuser", password=p)
response = self.client.get(
"/admin/registrar/domainapplication/",
{
"investigator__id__exact": investigator_user.id,
},
follow=True,
)
expected_name = "SomeGuy first_name:investigator SomeGuy last_name:investigator"
# We expect to see this four times, two of them are from the html for the filter,
# and the other two are the html from the list entry in the table.
self.assertContains(response, expected_name, count=4)
# Check that we don't also get the thing we aren't filtering for.
# We expect to see this two times in the filter
unexpected_name = "BadGuy first_name:investigator BadGuy last_name:investigator"
self.assertContains(response, unexpected_name, count=2)
def test_investigator_dropdown_displays_only_staff(self):
"""
This test verifies that the dropdown for the 'investigator' field in the DomainApplicationAdmin
interface only displays users who are marked as staff.
It creates two DomainApplication instances, one with an investigator
who is a staff user and another with an investigator who is not a staff user.
It then retrieves the queryset for the 'investigator' dropdown from DomainApplicationAdmin
and checks that it matches the expected queryset, which only includes staff users.
"""
# Create a mock DomainApplication object, with a fake investigator
application: DomainApplication = generic_domain_object("application", "SomeGuy")
investigator_user = User.objects.filter(username=application.investigator.username).get()
investigator_user.is_staff = True
investigator_user.save()
# Create a mock DomainApplication object, with a user that is not staff
application_2: DomainApplication = generic_domain_object("application", "SomeOtherGuy")
investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get()
investigator_user_2.is_staff = False
investigator_user_2.save()
p = "userpass"
self.client.login(username="staffuser", password=p)
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
# Get the actual field from the model's meta information
investigator_field = DomainApplication._meta.get_field("investigator")
# We should only be displaying staff users, in alphabetical order
expected_dropdown = list(User.objects.filter(is_staff=True))
current_dropdown = list(self.admin.formfield_for_foreignkey(investigator_field, request).queryset)
self.assertEqual(expected_dropdown, current_dropdown)
# Non staff users should not be in the list
self.assertNotIn(application_2, current_dropdown)
def test_investigator_list_is_alphabetically_sorted(self):
"""
This test verifies that filter list for the 'investigator'
is displayed alphabetically
"""
# Create a mock DomainApplication object, with a fake investigator
application: DomainApplication = generic_domain_object("application", "SomeGuy")
investigator_user = User.objects.filter(username=application.investigator.username).get()
investigator_user.is_staff = True
investigator_user.save()
application_2: DomainApplication = generic_domain_object("application", "AGuy")
investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get()
investigator_user_2.first_name = "AGuy"
investigator_user_2.is_staff = True
investigator_user_2.save()
application_3: DomainApplication = generic_domain_object("application", "FinalGuy")
investigator_user_3 = User.objects.filter(username=application_3.investigator.username).get()
investigator_user_3.first_name = "FinalGuy"
investigator_user_3.is_staff = True
investigator_user_3.save()
p = "userpass"
self.client.login(username="staffuser", password=p)
request = RequestFactory().get("/")
expected_list = list(User.objects.filter(is_staff=True).order_by("first_name", "last_name", "email"))
# Get the actual sorted list of investigators from the lookups method
actual_list = [item for _, item in self.admin.InvestigatorFilter.lookups(self, request, self.admin)]
self.assertEqual(expected_list, actual_list)
def tearDown(self):
super().tearDown()
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
DomainApplication.objects.all().delete()
User.objects.all().delete()
Contact.objects.all().delete()
Website.objects.all().delete()
class DomainInvitationAdminTest(TestCase):

View file

@ -1966,6 +1966,9 @@ class TestExpirationDate(MockEppLib):
"""
super().setUp()
# for the tests, need a domain in the ready state
# mock data for self.domain includes the following dates:
# cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35)
# ex_date=datetime.date(2023, 5, 25)
self.domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
# for the test, need a domain that will raise an exception
self.domain_w_error, _ = Domain.objects.get_or_create(name="fake-error.gov", state=Domain.State.READY)
@ -1991,6 +1994,23 @@ class TestExpirationDate(MockEppLib):
with self.assertRaises(RegistryError):
self.domain_w_error.renew_domain()
def test_is_expired(self):
"""assert that is_expired returns true for expiration_date in past"""
# force fetch_cache to be called
self.domain.statuses
self.assertTrue(self.domain.is_expired)
def test_is_not_expired(self):
"""assert that is_expired returns false for expiration in future"""
# to do this, need to mock value returned from timezone.now
# set now to 2023-01-01
mocked_datetime = datetime.datetime(2023, 1, 1, 12, 0, 0)
# force fetch_cache which sets the expiration date to 2023-05-25
self.domain.statuses
with patch("registrar.models.domain.timezone.now", return_value=mocked_datetime):
self.assertFalse(self.domain.is_expired())
def test_expiration_date_updated_on_info_domain_call(self):
"""assert that expiration date in db is updated on info domain call"""
# force fetch_cache to be called
@ -1999,6 +2019,36 @@ class TestExpirationDate(MockEppLib):
self.assertEquals(self.domain.expiration_date, test_date)
class TestCreationDate(MockEppLib):
"""Created_at in domain model is updated from EPP"""
def setUp(self):
"""
Domain exists in registry
"""
super().setUp()
# for the tests, need a domain with a creation date
self.domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
# creation_date returned from mockDataInfoDomain with creation date:
# cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35)
self.creation_date = datetime.datetime(2023, 5, 25, 19, 45, 35)
def tearDown(self):
Domain.objects.all().delete()
super().tearDown()
def test_creation_date_setter_not_implemented(self):
"""assert that the setter for creation date is not implemented and will raise error"""
with self.assertRaises(NotImplementedError):
self.domain.creation_date = datetime.date.today()
def test_creation_date_updated_on_info_domain_call(self):
"""assert that creation date in db is updated on info domain call"""
# force fetch_cache to be called
self.domain.statuses
self.assertEquals(self.domain.created_at, self.creation_date)
class TestAnalystClientHold(MockEppLib):
"""Rule: Analysts may suspend or restore a domain by using client hold"""

View file

@ -6,7 +6,6 @@ from django.test import Client, TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model
from .common import MockEppLib, completed_application, create_user # type: ignore
from django_webtest import WebTest # type: ignore
import boto3_mocking # type: ignore
@ -100,7 +99,7 @@ class LoggedInTests(TestWithUser):
response = self.client.get("/")
# count = 2 because it is also in screenreader content
self.assertContains(response, "igorville.gov", count=2)
self.assertContains(response, "DNS needed")
self.assertContains(response, "Expired")
# clean up
role.delete()
@ -1331,6 +1330,12 @@ class TestDomainDetail(TestDomainOverview):
class TestDomainManagers(TestDomainOverview):
def tearDown(self):
"""Ensure that the user has its original permissions"""
super().tearDown()
self.user.is_staff = False
self.user.save()
def test_domain_managers(self):
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
self.assertContains(response, "Domain managers")
@ -1457,6 +1462,189 @@ class TestDomainManagers(TestDomainOverview):
Content=ANY,
)
@boto3_mocking.patching
def test_domain_invitation_email_has_email_as_requester_non_existent(self):
"""Inviting a non existent user sends them an email, with email as the name."""
# make sure there is no user with this email
email_address = "mayor@igorville.gov"
User.objects.filter(email=email_address).delete()
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
mock_client = MagicMock()
mock_client_instance = mock_client.return_value
with boto3_mocking.clients.handler_for("sesv2", mock_client):
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
add_page.form.submit()
# check the mock instance to see if `send_email` was called right
mock_client_instance.send_email.assert_called_once_with(
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
Destination={"ToAddresses": [email_address]},
Content=ANY,
)
# Check the arguments passed to send_email method
_, kwargs = mock_client_instance.send_email.call_args
# Extract the email content, and check that the message is as we expect
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
self.assertIn("info@example.com", email_content)
# Check that the requesters first/last name do not exist
self.assertNotIn("First", email_content)
self.assertNotIn("Last", email_content)
self.assertNotIn("First Last", email_content)
@boto3_mocking.patching
def test_domain_invitation_email_has_email_as_requester(self):
"""Inviting a user sends them an email, with email as the name."""
# Create a fake user object
email_address = "mayor@igorville.gov"
User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com")
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
mock_client = MagicMock()
mock_client_instance = mock_client.return_value
with boto3_mocking.clients.handler_for("sesv2", mock_client):
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
add_page.form.submit()
# check the mock instance to see if `send_email` was called right
mock_client_instance.send_email.assert_called_once_with(
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
Destination={"ToAddresses": [email_address]},
Content=ANY,
)
# Check the arguments passed to send_email method
_, kwargs = mock_client_instance.send_email.call_args
# Extract the email content, and check that the message is as we expect
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
self.assertIn("info@example.com", email_content)
# Check that the requesters first/last name do not exist
self.assertNotIn("First", email_content)
self.assertNotIn("Last", email_content)
self.assertNotIn("First Last", email_content)
@boto3_mocking.patching
def test_domain_invitation_email_has_email_as_requester_staff(self):
"""Inviting a user sends them an email, with email as the name."""
# Create a fake user object
email_address = "mayor@igorville.gov"
User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com")
# Make sure the user is staff
self.user.is_staff = True
self.user.save()
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
mock_client = MagicMock()
mock_client_instance = mock_client.return_value
with boto3_mocking.clients.handler_for("sesv2", mock_client):
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
add_page.form.submit()
# check the mock instance to see if `send_email` was called right
mock_client_instance.send_email.assert_called_once_with(
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
Destination={"ToAddresses": [email_address]},
Content=ANY,
)
# Check the arguments passed to send_email method
_, kwargs = mock_client_instance.send_email.call_args
# Extract the email content, and check that the message is as we expect
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
self.assertIn("help@get.gov", email_content)
# Check that the requesters first/last name do not exist
self.assertNotIn("First", email_content)
self.assertNotIn("Last", email_content)
self.assertNotIn("First Last", email_content)
@boto3_mocking.patching
def test_domain_invitation_email_displays_error_non_existent(self):
"""Inviting a non existent user sends them an email, with email as the name."""
# make sure there is no user with this email
email_address = "mayor@igorville.gov"
User.objects.filter(email=email_address).delete()
# Give the user who is sending the email an invalid email address
self.user.email = ""
self.user.save()
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
mock_client = MagicMock()
mock_error_message = MagicMock()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
with patch("django.contrib.messages.error") as mock_error_message:
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
add_page.form.submit().follow()
expected_message_content = "Can't send invitation email. No email is associated with your account."
# Grab the message content
returned_error_message = mock_error_message.call_args[0][1]
# Check that the message content is what we expect
self.assertEqual(expected_message_content, returned_error_message)
@boto3_mocking.patching
def test_domain_invitation_email_displays_error(self):
"""When the requesting user has no email, an error is displayed"""
# make sure there is no user with this email
# Create a fake user object
email_address = "mayor@igorville.gov"
User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com")
# Give the user who is sending the email an invalid email address
self.user.email = ""
self.user.save()
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
mock_client = MagicMock()
mock_error_message = MagicMock()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
with patch("django.contrib.messages.error") as mock_error_message:
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
add_page.form.submit().follow()
expected_message_content = "Can't send invitation email. No email is associated with your account."
# Grab the message content
returned_error_message = mock_error_message.call_args[0][1]
# Check that the message content is what we expect
self.assertEqual(expected_message_content, returned_error_message)
def test_domain_invitation_cancel(self):
"""Posting to the delete view deletes an invitation."""
email_address = "mayor@igorville.gov"

View file

@ -17,7 +17,6 @@ from django.views.generic.edit import FormMixin
from registrar.models import (
Domain,
DomainInformation,
DomainInvitation,
User,
UserDomainRole,
@ -644,21 +643,27 @@ class DomainAddUserView(DomainFormBaseView):
"""Get an absolute URL for this domain."""
return self.request.build_absolute_uri(reverse("domain", kwargs={"pk": self.object.id}))
def _send_domain_invitation_email(self, email: str, add_success=True):
def _send_domain_invitation_email(self, email: str, requester: User, add_success=True):
"""Performs the sending of the domain invitation email,
does not make a domain information object
email: string- email to send to
add_success: bool- default True indicates:
adding a success message to the view if the email sending succeeds"""
# created a new invitation in the database, so send an email
domainInfoResults = DomainInformation.objects.filter(domain=self.object)
domainInfo = domainInfoResults.first()
first = ""
last = ""
if domainInfo is not None:
first = domainInfo.creator.first_name
last = domainInfo.creator.last_name
full_name = f"{first} {last}"
# Set a default email address to send to for staff
requester_email = "help@get.gov"
# Check if the email requester has a valid email address
if not requester.is_staff and requester.email is not None and requester.email.strip() != "":
requester_email = requester.email
elif not requester.is_staff:
messages.error(self.request, "Can't send invitation email. No email is associated with your account.")
logger.error(
f"Can't send email to '{email}' on domain '{self.object}'."
f"No email exists for the requester '{requester.username}'.",
exc_info=True,
)
return None
try:
send_templated_email(
@ -668,7 +673,7 @@ class DomainAddUserView(DomainFormBaseView):
context={
"domain_url": self._domain_abs_url(),
"domain": self.object,
"full_name": full_name,
"requester_email": requester_email,
},
)
except EmailSendingError:
@ -683,7 +688,7 @@ class DomainAddUserView(DomainFormBaseView):
if add_success:
messages.success(self.request, f"Invited {email} to this domain.")
def _make_invitation(self, email_address: str):
def _make_invitation(self, email_address: str, requester: User):
"""Make a Domain invitation for this email and redirect with a message."""
invitation, created = DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
if not created:
@ -693,21 +698,22 @@ class DomainAddUserView(DomainFormBaseView):
f"{email_address} has already been invited to this domain.",
)
else:
self._send_domain_invitation_email(email=email_address)
self._send_domain_invitation_email(email=email_address, requester=requester)
return redirect(self.get_success_url())
def form_valid(self, form):
"""Add the specified user on this domain."""
requested_email = form.cleaned_data["email"]
requester = self.request.user
# look up a user with that email
try:
requested_user = User.objects.get(email=requested_email)
except User.DoesNotExist:
# no matching user, go make an invitation
return self._make_invitation(requested_email)
return self._make_invitation(requested_email, requester)
else:
# if user already exists then just send an email
self._send_domain_invitation_email(requested_email, add_success=False)
self._send_domain_invitation_email(requested_email, requester, add_success=False)
try:
UserDomainRole.objects.create(

View file

@ -1,7 +1,6 @@
from django.db.models import F
from django.shortcuts import render
from registrar.models import DomainApplication
from registrar.models import DomainApplication, Domain, UserDomainRole
def index(request):
@ -14,12 +13,9 @@ def index(request):
# the active applications table
context["domain_applications"] = applications.exclude(status="approved")
domains = request.user.permissions.values(
"role",
pk=F("domain__id"),
name=F("domain__name"),
created_time=F("domain__created_at"),
state=F("domain__state"),
)
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
domains = Domain.objects.filter(id__in=domain_ids)
context["domains"] = domains
return render(request, "home.html", context)