mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-06-02 02:28:32 +02:00
Merge branch 'main' into za/1128-email-bouncebacks
This commit is contained in:
commit
631fa3b9c6
14 changed files with 661 additions and 74 deletions
|
@ -12,6 +12,7 @@ from django.http.response import HttpResponseRedirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from epplibwrapper.errors import ErrorCode, RegistryError
|
from epplibwrapper.errors import ErrorCode, RegistryError
|
||||||
from registrar.models.domain import Domain
|
from registrar.models.domain import Domain
|
||||||
|
from registrar.models.user import User
|
||||||
from registrar.utility import csv_export
|
from registrar.utility import csv_export
|
||||||
from . import models
|
from . import models
|
||||||
from auditlog.models import LogEntry # type: ignore
|
from auditlog.models import LogEntry # type: ignore
|
||||||
|
@ -538,6 +539,9 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
||||||
# to activate the edit/delete/view buttons
|
# to activate the edit/delete/view buttons
|
||||||
filter_horizontal = ("other_contacts",)
|
filter_horizontal = ("other_contacts",)
|
||||||
|
|
||||||
|
# Table ordering
|
||||||
|
ordering = ["domain__name"]
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
"""Set the read-only state on form elements.
|
"""Set the read-only state on form elements.
|
||||||
We have 1 conditions that determine which fields are read-only:
|
We have 1 conditions that determine which fields are read-only:
|
||||||
|
@ -589,6 +593,27 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
"""Custom domain applications admin class."""
|
"""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
|
# Columns
|
||||||
list_display = [
|
list_display = [
|
||||||
"requested_domain",
|
"requested_domain",
|
||||||
|
@ -600,7 +625,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
]
|
]
|
||||||
|
|
||||||
# Filters
|
# Filters
|
||||||
list_filter = ("status", "organization_type", "investigator")
|
list_filter = ("status", "organization_type", InvestigatorFilter)
|
||||||
|
|
||||||
# Search
|
# Search
|
||||||
search_fields = [
|
search_fields = [
|
||||||
|
@ -676,6 +701,23 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
|
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
|
# Trigger action when a fieldset is changed
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
if obj and obj.creator.status != models.User.RESTRICTED:
|
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"
|
change_list_template = "django/admin/domain_change_list.html"
|
||||||
readonly_fields = ["state", "expiration_date"]
|
readonly_fields = ["state", "expiration_date"]
|
||||||
|
|
||||||
|
# Table ordering
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
def export_data_type(self, request):
|
def export_data_type(self, request):
|
||||||
# match the CSV example with all the fields
|
# match the CSV example with all the fields
|
||||||
response = HttpResponse(content_type="text/csv")
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -8,6 +8,7 @@ from typing import Optional
|
||||||
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
|
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
@ -200,6 +201,14 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
"""Get the `cr_date` element from the registry."""
|
"""Get the `cr_date` element from the registry."""
|
||||||
return self._get_property("cr_date")
|
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
|
@Cache
|
||||||
def last_transferred_date(self) -> date:
|
def last_transferred_date(self) -> date:
|
||||||
"""Get the `tr_date` element from the registry."""
|
"""Get the `tr_date` element from the registry."""
|
||||||
|
@ -963,6 +972,16 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
def isActive(self):
|
def isActive(self):
|
||||||
return self.state == Domain.State.CREATED
|
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):
|
def map_epp_contact_to_public_contact(self, contact: eppInfo.InfoContactResultData, contact_id, contact_type):
|
||||||
"""Maps the Epp contact representation to a PublicContact object.
|
"""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):
|
def _fetch_cache(self, fetch_hosts=False, fetch_contacts=False):
|
||||||
"""Contact registry for info about a domain."""
|
"""Contact registry for info about a domain."""
|
||||||
try:
|
try:
|
||||||
# get info from registry
|
|
||||||
data_response = self._get_or_create_domain()
|
data_response = self._get_or_create_domain()
|
||||||
cache = self._extract_data_from_response(data_response)
|
cache = self._extract_data_from_response(data_response)
|
||||||
|
cleaned = self._clean_cache(cache, data_response)
|
||||||
# remove null properties (to distinguish between "a value of None" and null)
|
self._update_hosts_and_contacts(cleaned, fetch_hosts, fetch_contacts)
|
||||||
cleaned = self._remove_null_properties(cache)
|
self._update_dates(cleaned)
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
self._cache = cleaned
|
self._cache = cleaned
|
||||||
|
|
||||||
|
@ -1621,6 +1613,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
|
|
||||||
def _extract_data_from_response(self, data_response):
|
def _extract_data_from_response(self, data_response):
|
||||||
|
"""extract data from response from registry"""
|
||||||
data = data_response.res_data[0]
|
data = data_response.res_data[0]
|
||||||
return {
|
return {
|
||||||
"auth_info": getattr(data, "auth_info", ...),
|
"auth_info": getattr(data, "auth_info", ...),
|
||||||
|
@ -1635,6 +1628,15 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
"up_date": getattr(data, "up_date", ...),
|
"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):
|
def _remove_null_properties(self, cache):
|
||||||
return {k: v for k, v in cache.items() if v is not ...}
|
return {k: v for k, v in cache.items() if v is not ...}
|
||||||
|
|
||||||
|
@ -1648,6 +1650,42 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
dnssec_data = extension
|
dnssec_data = extension
|
||||||
return dnssec_data
|
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):
|
def _get_contacts(self, contacts):
|
||||||
choices = PublicContact.ContactTypeChoices
|
choices = PublicContact.ContactTypeChoices
|
||||||
# We expect that all these fields get populated,
|
# We expect that all these fields get populated,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import logging
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django_fsm import FSMField, transition # type: ignore
|
from django_fsm import FSMField, transition # type: ignore
|
||||||
|
from django.utils import timezone
|
||||||
from registrar.models.domain import Domain
|
from registrar.models.domain import Domain
|
||||||
|
|
||||||
from .utility.time_stamped_model import TimeStampedModel
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
@ -547,6 +548,14 @@ class DomainApplication(TimeStampedModel):
|
||||||
help_text="Acknowledged .gov acceptable use policy",
|
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):
|
def __str__(self):
|
||||||
try:
|
try:
|
||||||
if self.requested_domain and self.requested_domain.name:
|
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):
|
if not DraftDomain.string_could_be_domain(self.requested_domain.name):
|
||||||
raise ValueError("Requested domain is not a valid 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(
|
self._send_status_update_email(
|
||||||
"submission confirmation",
|
"submission confirmation",
|
||||||
"emails/submission_confirmation.txt",
|
"emails/submission_confirmation.txt",
|
||||||
|
|
|
@ -229,6 +229,7 @@ class DomainInformation(TimeStampedModel):
|
||||||
da_dict.pop("alternative_domains", None)
|
da_dict.pop("alternative_domains", None)
|
||||||
da_dict.pop("requested_domain", None)
|
da_dict.pop("requested_domain", None)
|
||||||
da_dict.pop("approved_domain", None)
|
da_dict.pop("approved_domain", None)
|
||||||
|
da_dict.pop("submission_date", None)
|
||||||
other_contacts = da_dict.pop("other_contacts", [])
|
other_contacts = da_dict.pop("other_contacts", [])
|
||||||
domain_info = cls(**da_dict)
|
domain_info = cls(**da_dict)
|
||||||
domain_info.domain_application = domain_application
|
domain_info.domain_application = domain_application
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<div class="margin-top-4 tablet:grid-col-10">
|
<div class="margin-top-4 tablet:grid-col-10">
|
||||||
|
|
||||||
<div
|
<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"
|
role="region"
|
||||||
aria-labelledby="summary-box-key-information"
|
aria-labelledby="summary-box-key-information"
|
||||||
>
|
>
|
||||||
|
@ -17,7 +17,9 @@
|
||||||
<span class="text-bold text-primary-darker">
|
<span class="text-bold text-primary-darker">
|
||||||
Status:
|
Status:
|
||||||
</span>
|
</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
|
DNS needed
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ domain.state|title }}
|
{{ domain.state|title }}
|
||||||
|
@ -26,13 +28,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
|
||||||
|
{% include "includes/domain_dates.html" %}
|
||||||
|
|
||||||
{% url 'domain-dns-nameservers' pk=domain.id as url %}
|
{% url 'domain-dns-nameservers' pk=domain.id as url %}
|
||||||
{% if domain.nameservers|length > 0 %}
|
{% 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 %}
|
{% 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 %}
|
{% else %}
|
||||||
{% if domain.is_editable %}
|
{% 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 we’ll need information about your domain name servers.</p>
|
<p> No DNS name servers have been added yet. Before your domain can be used we’ll need information about your domain name servers.</p>
|
||||||
<a class="usa-button margin-bottom-1" href="{{url}}"> Add DNS name servers </a>
|
<a class="usa-button margin-bottom-1" href="{{url}}"> Add DNS name servers </a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||||
Hi.
|
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
|
YOU NEED A LOGIN.GOV ACCOUNT
|
||||||
You’ll need a Login.gov account to manage your .gov domain. Login.gov provides
|
You’ll need a Login.gov account to manage your .gov domain. Login.gov provides
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th data-sortable scope="col" role="columnheader">Domain name</th>
|
<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 data-sortable scope="col" role="columnheader">Status</th>
|
||||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -50,9 +50,11 @@
|
||||||
<th th scope="row" role="rowheader" data-label="Domain name">
|
<th th scope="row" role="rowheader" data-label="Domain name">
|
||||||
{{ domain.name }}
|
{{ domain.name }}
|
||||||
</th>
|
</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">
|
<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
|
DNS needed
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ domain.state|title }}
|
{{ domain.state|title }}
|
||||||
|
@ -99,7 +101,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th data-sortable scope="col" role="columnheader">Domain name</th>
|
<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 data-sortable scope="col" role="columnheader">Status</th>
|
||||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -110,7 +112,13 @@
|
||||||
<th th scope="row" role="rowheader" data-label="Domain name">
|
<th th scope="row" role="rowheader" data-label="Domain name">
|
||||||
{{ application.requested_domain.name|default:"New domain request" }}
|
{{ application.requested_domain.name|default:"New domain request" }}
|
||||||
</th>
|
</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 data-label="Status">{{ application.get_status_display }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if application.status == "started" or application.status == "action needed" or application.status == "withdrawn" %}
|
{% if application.status == "started" or application.status == "action needed" or application.status == "withdrawn" %}
|
||||||
|
|
12
src/registrar/templates/includes/domain_dates.html
Normal file
12
src/registrar/templates/includes/domain_dates.html
Normal 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 %}
|
|
@ -15,13 +15,7 @@ from registrar.admin import (
|
||||||
ContactAdmin,
|
ContactAdmin,
|
||||||
UserDomainRoleAdmin,
|
UserDomainRoleAdmin,
|
||||||
)
|
)
|
||||||
from registrar.models import (
|
from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website
|
||||||
Domain,
|
|
||||||
DomainApplication,
|
|
||||||
DomainInformation,
|
|
||||||
User,
|
|
||||||
DomainInvitation,
|
|
||||||
)
|
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
from .common import (
|
from .common import (
|
||||||
completed_application,
|
completed_application,
|
||||||
|
@ -323,6 +317,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
self.admin = DomainApplicationAdmin(model=DomainApplication, admin_site=self.site)
|
self.admin = DomainApplicationAdmin(model=DomainApplication, admin_site=self.site)
|
||||||
self.superuser = create_superuser()
|
self.superuser = create_superuser()
|
||||||
self.staffuser = create_user()
|
self.staffuser = create_user()
|
||||||
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
|
||||||
def test_short_org_name_in_applications_list(self):
|
def test_short_org_name_in_applications_list(self):
|
||||||
"""
|
"""
|
||||||
|
@ -637,6 +632,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
"no_other_contacts_rationale",
|
"no_other_contacts_rationale",
|
||||||
"anything_else",
|
"anything_else",
|
||||||
"is_policy_acknowledged",
|
"is_policy_acknowledged",
|
||||||
|
"submission_date",
|
||||||
"current_websites",
|
"current_websites",
|
||||||
"other_contacts",
|
"other_contacts",
|
||||||
"alternative_domains",
|
"alternative_domains",
|
||||||
|
@ -860,12 +856,224 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
with self.assertRaises(DomainInformation.DoesNotExist):
|
with self.assertRaises(DomainInformation.DoesNotExist):
|
||||||
domain_information.refresh_from_db()
|
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):
|
def tearDown(self):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
Domain.objects.all().delete()
|
Domain.objects.all().delete()
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
DomainApplication.objects.all().delete()
|
DomainApplication.objects.all().delete()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
Contact.objects.all().delete()
|
||||||
|
Website.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
class DomainInvitationAdminTest(TestCase):
|
class DomainInvitationAdminTest(TestCase):
|
||||||
|
|
|
@ -1966,6 +1966,9 @@ class TestExpirationDate(MockEppLib):
|
||||||
"""
|
"""
|
||||||
super().setUp()
|
super().setUp()
|
||||||
# for the tests, need a domain in the ready state
|
# 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)
|
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
|
# 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)
|
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):
|
with self.assertRaises(RegistryError):
|
||||||
self.domain_w_error.renew_domain()
|
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):
|
def test_expiration_date_updated_on_info_domain_call(self):
|
||||||
"""assert that expiration date in db is updated on info domain call"""
|
"""assert that expiration date in db is updated on info domain call"""
|
||||||
# force fetch_cache to be called
|
# force fetch_cache to be called
|
||||||
|
@ -1999,6 +2019,36 @@ class TestExpirationDate(MockEppLib):
|
||||||
self.assertEquals(self.domain.expiration_date, test_date)
|
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):
|
class TestAnalystClientHold(MockEppLib):
|
||||||
"""Rule: Analysts may suspend or restore a domain by using client hold"""
|
"""Rule: Analysts may suspend or restore a domain by using client hold"""
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ from django.test import Client, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from .common import MockEppLib, completed_application, create_user # type: ignore
|
from .common import MockEppLib, completed_application, create_user # type: ignore
|
||||||
|
|
||||||
from django_webtest import WebTest # type: ignore
|
from django_webtest import WebTest # type: ignore
|
||||||
import boto3_mocking # type: ignore
|
import boto3_mocking # type: ignore
|
||||||
|
|
||||||
|
@ -100,7 +99,7 @@ class LoggedInTests(TestWithUser):
|
||||||
response = self.client.get("/")
|
response = self.client.get("/")
|
||||||
# count = 2 because it is also in screenreader content
|
# count = 2 because it is also in screenreader content
|
||||||
self.assertContains(response, "igorville.gov", count=2)
|
self.assertContains(response, "igorville.gov", count=2)
|
||||||
self.assertContains(response, "DNS needed")
|
self.assertContains(response, "Expired")
|
||||||
# clean up
|
# clean up
|
||||||
role.delete()
|
role.delete()
|
||||||
|
|
||||||
|
@ -1331,6 +1330,12 @@ class TestDomainDetail(TestDomainOverview):
|
||||||
|
|
||||||
|
|
||||||
class TestDomainManagers(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):
|
def test_domain_managers(self):
|
||||||
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
|
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
|
||||||
self.assertContains(response, "Domain managers")
|
self.assertContains(response, "Domain managers")
|
||||||
|
@ -1457,6 +1462,189 @@ class TestDomainManagers(TestDomainOverview):
|
||||||
Content=ANY,
|
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):
|
def test_domain_invitation_cancel(self):
|
||||||
"""Posting to the delete view deletes an invitation."""
|
"""Posting to the delete view deletes an invitation."""
|
||||||
email_address = "mayor@igorville.gov"
|
email_address = "mayor@igorville.gov"
|
||||||
|
|
|
@ -17,7 +17,6 @@ from django.views.generic.edit import FormMixin
|
||||||
|
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
Domain,
|
Domain,
|
||||||
DomainInformation,
|
|
||||||
DomainInvitation,
|
DomainInvitation,
|
||||||
User,
|
User,
|
||||||
UserDomainRole,
|
UserDomainRole,
|
||||||
|
@ -644,21 +643,27 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
"""Get an absolute URL for this domain."""
|
"""Get an absolute URL for this domain."""
|
||||||
return self.request.build_absolute_uri(reverse("domain", kwargs={"pk": self.object.id}))
|
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,
|
"""Performs the sending of the domain invitation email,
|
||||||
does not make a domain information object
|
does not make a domain information object
|
||||||
email: string- email to send to
|
email: string- email to send to
|
||||||
add_success: bool- default True indicates:
|
add_success: bool- default True indicates:
|
||||||
adding a success message to the view if the email sending succeeds"""
|
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)
|
# Set a default email address to send to for staff
|
||||||
domainInfo = domainInfoResults.first()
|
requester_email = "help@get.gov"
|
||||||
first = ""
|
|
||||||
last = ""
|
# Check if the email requester has a valid email address
|
||||||
if domainInfo is not None:
|
if not requester.is_staff and requester.email is not None and requester.email.strip() != "":
|
||||||
first = domainInfo.creator.first_name
|
requester_email = requester.email
|
||||||
last = domainInfo.creator.last_name
|
elif not requester.is_staff:
|
||||||
full_name = f"{first} {last}"
|
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:
|
try:
|
||||||
send_templated_email(
|
send_templated_email(
|
||||||
|
@ -668,7 +673,7 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
context={
|
context={
|
||||||
"domain_url": self._domain_abs_url(),
|
"domain_url": self._domain_abs_url(),
|
||||||
"domain": self.object,
|
"domain": self.object,
|
||||||
"full_name": full_name,
|
"requester_email": requester_email,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except EmailSendingError:
|
except EmailSendingError:
|
||||||
|
@ -683,7 +688,7 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
if add_success:
|
if add_success:
|
||||||
messages.success(self.request, f"Invited {email} to this domain.")
|
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."""
|
"""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)
|
invitation, created = DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
|
||||||
if not created:
|
if not created:
|
||||||
|
@ -693,21 +698,22 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
f"{email_address} has already been invited to this domain.",
|
f"{email_address} has already been invited to this domain.",
|
||||||
)
|
)
|
||||||
else:
|
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())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
"""Add the specified user on this domain."""
|
"""Add the specified user on this domain."""
|
||||||
requested_email = form.cleaned_data["email"]
|
requested_email = form.cleaned_data["email"]
|
||||||
|
requester = self.request.user
|
||||||
# look up a user with that email
|
# look up a user with that email
|
||||||
try:
|
try:
|
||||||
requested_user = User.objects.get(email=requested_email)
|
requested_user = User.objects.get(email=requested_email)
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
# no matching user, go make an invitation
|
# no matching user, go make an invitation
|
||||||
return self._make_invitation(requested_email)
|
return self._make_invitation(requested_email, requester)
|
||||||
else:
|
else:
|
||||||
# if user already exists then just send an email
|
# 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:
|
try:
|
||||||
UserDomainRole.objects.create(
|
UserDomainRole.objects.create(
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from django.db.models import F
|
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
from registrar.models import DomainApplication
|
from registrar.models import DomainApplication, Domain, UserDomainRole
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
|
@ -14,12 +13,9 @@ def index(request):
|
||||||
# the active applications table
|
# the active applications table
|
||||||
context["domain_applications"] = applications.exclude(status="approved")
|
context["domain_applications"] = applications.exclude(status="approved")
|
||||||
|
|
||||||
domains = request.user.permissions.values(
|
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
|
||||||
"role",
|
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
||||||
pk=F("domain__id"),
|
domains = Domain.objects.filter(id__in=domain_ids)
|
||||||
name=F("domain__name"),
|
|
||||||
created_time=F("domain__created_at"),
|
|
||||||
state=F("domain__state"),
|
|
||||||
)
|
|
||||||
context["domains"] = domains
|
context["domains"] = domains
|
||||||
return render(request, "home.html", context)
|
return render(request, "home.html", context)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue