mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-16 09:37:03 +02:00
Merge remote-tracking branch 'origin/main' into rjm/1027-groups-permissions-logging
This commit is contained in:
commit
fd860998fb
35 changed files with 591 additions and 38 deletions
|
@ -11,6 +11,7 @@ applications:
|
|||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
health-check-invocation-timeout: 40
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
|
|
|
@ -11,6 +11,7 @@ applications:
|
|||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
health-check-invocation-timeout: 40
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
|
|
|
@ -11,6 +11,7 @@ applications:
|
|||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
health-check-invocation-timeout: 40
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
|
|
|
@ -11,6 +11,7 @@ applications:
|
|||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
health-check-invocation-timeout: 40
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
|
|
|
@ -11,6 +11,7 @@ applications:
|
|||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
health-check-invocation-timeout: 40
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
|
|
|
@ -11,6 +11,7 @@ applications:
|
|||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
health-check-invocation-timeout: 40
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
|
|
|
@ -11,6 +11,7 @@ applications:
|
|||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
health-check-invocation-timeout: 40
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
|
|
|
@ -11,6 +11,7 @@ applications:
|
|||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
health-check-invocation-timeout: 40
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
|
|
|
@ -11,6 +11,7 @@ applications:
|
|||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
health-check-invocation-timeout: 40
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
|
|
|
@ -11,6 +11,7 @@ applications:
|
|||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
health-check-invocation-timeout: 40
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
|
|
|
@ -11,6 +11,7 @@ applications:
|
|||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
health-check-invocation-timeout: 40
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
|
|
|
@ -11,6 +11,7 @@ applications:
|
|||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
health-check-invocation-timeout: 40
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
|
|
|
@ -11,6 +11,7 @@ applications:
|
|||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
health-check-invocation-timeout: 40
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
|
|
|
@ -11,6 +11,7 @@ applications:
|
|||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
health-check-invocation-timeout: 40
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
|
|
|
@ -67,6 +67,13 @@ class RegistryError(Exception):
|
|||
def should_retry(self):
|
||||
return self.code == ErrorCode.COMMAND_FAILED
|
||||
|
||||
# connection errors have error code of None and [Errno 99] in the err message
|
||||
def is_connection_error(self):
|
||||
return self.code is None
|
||||
|
||||
def is_session_error(self):
|
||||
return self.code is not None and (self.code >= 2501 and self.code <= 2502)
|
||||
|
||||
def is_server_error(self):
|
||||
return self.code is not None and (self.code >= 2400 and self.code <= 2500)
|
||||
|
||||
|
|
|
@ -638,6 +638,21 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
return super().change_view(request, object_id, form_url, extra_context)
|
||||
|
||||
|
||||
class TransitionDomainAdmin(ListHeaderAdmin):
|
||||
"""Custom transition domain admin class."""
|
||||
|
||||
# Columns
|
||||
list_display = [
|
||||
"username",
|
||||
"domain_name",
|
||||
"status",
|
||||
"email_sent",
|
||||
]
|
||||
|
||||
search_fields = ["username", "domain_name"]
|
||||
search_help_text = "Search by user or domain name."
|
||||
|
||||
|
||||
class DomainInformationInline(admin.StackedInline):
|
||||
"""Edit a domain information on the domain page.
|
||||
We had issues inheriting from both StackedInline
|
||||
|
@ -729,6 +744,22 @@ class DomainAdmin(ListHeaderAdmin):
|
|||
obj.place_client_hold()
|
||||
obj.save()
|
||||
except Exception as err:
|
||||
# if error is an error from the registry, display useful
|
||||
# and readable error
|
||||
if err.code:
|
||||
self.message_user(
|
||||
request,
|
||||
f"Error placing the hold with the registry: {err}",
|
||||
messages.ERROR,
|
||||
)
|
||||
elif err.is_connection_error():
|
||||
self.message_user(
|
||||
request,
|
||||
"Error connecting to the registry",
|
||||
messages.ERROR,
|
||||
)
|
||||
else:
|
||||
# all other type error messages, display the error
|
||||
self.message_user(request, err, messages.ERROR)
|
||||
else:
|
||||
self.message_user(
|
||||
|
@ -746,6 +777,22 @@ class DomainAdmin(ListHeaderAdmin):
|
|||
obj.revert_client_hold()
|
||||
obj.save()
|
||||
except Exception as err:
|
||||
# if error is an error from the registry, display useful
|
||||
# and readable error
|
||||
if err.code:
|
||||
self.message_user(
|
||||
request,
|
||||
f"Error removing the hold in the registry: {err}",
|
||||
messages.ERROR,
|
||||
)
|
||||
elif err.is_connection_error():
|
||||
self.message_user(
|
||||
request,
|
||||
"Error connecting to the registry",
|
||||
messages.ERROR,
|
||||
)
|
||||
else:
|
||||
# all other type error messages, display the error
|
||||
self.message_user(request, err, messages.ERROR)
|
||||
else:
|
||||
self.message_user(
|
||||
|
@ -804,4 +851,4 @@ admin.site.register(models.Nameserver, MyHostAdmin)
|
|||
admin.site.register(models.Website, WebsiteAdmin)
|
||||
admin.site.register(models.PublicContact, AuditedAdmin)
|
||||
admin.site.register(models.DomainApplication, DomainApplicationAdmin)
|
||||
admin.site.register(models.TransitionDomain, AuditedAdmin)
|
||||
admin.site.register(models.TransitionDomain, TransitionDomainAdmin)
|
||||
|
|
|
@ -399,6 +399,11 @@ a.usa-button--unstyled:visited {
|
|||
border-color: color('accent-cool-lighter');
|
||||
}
|
||||
|
||||
.dotgov-status-box--action-need {
|
||||
background-color: color('warning-lighter');
|
||||
border-color: color('warning');
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
padding-top: units(3);
|
||||
padding-bottom: units(6) * 2 ; //Workaround because USWDS units jump from 10 to 15
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
"""Data migration: Generate fake transition domains, replacing existing ones."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from registrar.models import TransitionDomain, Domain
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Generate test transition domains from existing domains"
|
||||
|
||||
# Generates test transition domains for testing send_domain_invitations script.
|
||||
# Running this script removes all existing transition domains, so use with caution.
|
||||
# Transition domains are created with email addresses provided as command line
|
||||
# argument. Email addresses for testing are passed as comma delimited list of
|
||||
# email addresses, and are required to be provided. Email addresses from the list
|
||||
# are assigned to transition domains at time of creation.
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add command line arguments."""
|
||||
parser.add_argument(
|
||||
"-e",
|
||||
"--emails",
|
||||
required=True,
|
||||
dest="emails",
|
||||
help="Comma-delimited list of email addresses to be used for testing",
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
"""Delete existing TransitionDomains. Generate test ones.
|
||||
expects options[emails]; emails will be assigned to transition
|
||||
domains at the time of creation"""
|
||||
|
||||
# split options[emails] into an array of test emails
|
||||
test_emails = options["emails"].split(",")
|
||||
|
||||
if len(test_emails) > 0:
|
||||
# set up test data
|
||||
self.delete_test_transition_domains()
|
||||
self.load_test_transition_domains(test_emails)
|
||||
else:
|
||||
logger.error("list of emails for testing is required")
|
||||
|
||||
def load_test_transition_domains(self, test_emails: list):
|
||||
"""Load test transition domains"""
|
||||
|
||||
# counter for test_emails index
|
||||
test_emails_counter = 0
|
||||
# Need to get actual domain names from the database for this test
|
||||
real_domains = Domain.objects.all()
|
||||
for real_domain in real_domains:
|
||||
TransitionDomain.objects.create(
|
||||
username=test_emails[test_emails_counter % len(test_emails)],
|
||||
domain_name=real_domain.name,
|
||||
status="created",
|
||||
email_sent=False,
|
||||
)
|
||||
test_emails_counter += 1
|
||||
|
||||
def delete_test_transition_domains(self):
|
||||
self.transition_domains = TransitionDomain.objects.all()
|
||||
for transition_domain in self.transition_domains:
|
||||
transition_domain.delete()
|
142
src/registrar/management/commands/send_domain_invitations.py
Normal file
142
src/registrar/management/commands/send_domain_invitations.py
Normal file
|
@ -0,0 +1,142 @@
|
|||
"""Data migration: Send domain invitations once to existing customers."""
|
||||
|
||||
import logging
|
||||
import copy
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from registrar.models import TransitionDomain
|
||||
from ...utility.email import send_templated_email, EmailSendingError
|
||||
from typing import List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Send domain invitations once to existing customers."
|
||||
|
||||
# this array is used to store and process the transition_domains
|
||||
transition_domains: List[str] = []
|
||||
# this array is used to store domains with errors, which are not
|
||||
# sent emails; this array is used to update the succesful
|
||||
# transition_domains to email_sent=True, and also to report
|
||||
# out errors
|
||||
domains_with_errors: List[str] = []
|
||||
# this array is used to store email_context; each item in the array
|
||||
# contains the context for a single email; single emails may be 1
|
||||
# or more transition_domains, as they are grouped by username
|
||||
emails_to_send: List[str] = []
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add command line arguments."""
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--send_emails",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="send_emails",
|
||||
help="Send emails ",
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
"""Process the objects in TransitionDomain."""
|
||||
|
||||
logger.info("checking domains and preparing emails")
|
||||
# Get all TransitionDomain objects
|
||||
self.transition_domains = TransitionDomain.objects.filter(
|
||||
email_sent=False,
|
||||
).order_by("username")
|
||||
|
||||
self.build_emails_to_send_array()
|
||||
|
||||
if options["send_emails"]:
|
||||
logger.info("about to send emails")
|
||||
self.send_emails()
|
||||
logger.info("done sending emails")
|
||||
|
||||
self.update_domains_as_sent()
|
||||
|
||||
logger.info("done sending emails and updating transition_domains")
|
||||
else:
|
||||
logger.info("not sending emails")
|
||||
|
||||
def build_emails_to_send_array(self):
|
||||
"""this method sends emails to distinct usernames"""
|
||||
|
||||
# data structure to hold email context for a single email;
|
||||
# transition_domains ordered by username, a single email_context
|
||||
# may include information from more than one transition_domain
|
||||
email_context = {"email": ""}
|
||||
|
||||
# loop through all transition_domains; group them by username
|
||||
# into emails_to_send_array
|
||||
for transition_domain in self.transition_domains:
|
||||
# attempt to get the domain from domain objects; if there is
|
||||
# an error getting the domain, skip this domain and add it to
|
||||
# domains_with_errors
|
||||
try:
|
||||
# if prior username does not match current username
|
||||
if (
|
||||
not email_context["email"]
|
||||
or email_context["email"] != transition_domain.username
|
||||
):
|
||||
# if not first in list of transition_domains
|
||||
if email_context["email"]:
|
||||
# append the email context to the emails_to_send array
|
||||
self.emails_to_send.append(copy.deepcopy(email_context))
|
||||
email_context["domains"] = []
|
||||
email_context["email"] = transition_domain.username
|
||||
email_context["domains"].append(transition_domain.domain_name)
|
||||
except Exception as err:
|
||||
# error condition if domain not in database
|
||||
self.domains_with_errors.append(
|
||||
copy.deepcopy(transition_domain.domain_name)
|
||||
)
|
||||
logger.error(
|
||||
f"error retrieving domain {transition_domain.domain_name}: {err}"
|
||||
)
|
||||
# if there are at least one more transition domains than errors,
|
||||
# then append one more item
|
||||
if len(self.transition_domains) > len(self.domains_with_errors):
|
||||
self.emails_to_send.append(email_context)
|
||||
|
||||
def send_emails(self):
|
||||
if len(self.emails_to_send) > 0:
|
||||
for email_data in self.emails_to_send:
|
||||
self.send_email(email_data)
|
||||
else:
|
||||
logger.info("no emails to send")
|
||||
|
||||
def send_email(self, email_data):
|
||||
try:
|
||||
send_templated_email(
|
||||
"emails/transition_domain_invitation.txt",
|
||||
"emails/transition_domain_invitation_subject.txt",
|
||||
to_address=email_data["email"],
|
||||
context={
|
||||
"domains": email_data["domains"],
|
||||
},
|
||||
)
|
||||
# success message is logged
|
||||
logger.info(
|
||||
f"email sent successfully to {email_data['email']} for "
|
||||
f"{[domain for domain in email_data['domains']]}"
|
||||
)
|
||||
except EmailSendingError as err:
|
||||
logger.error(
|
||||
f"email did not send successfully to {email_data['email']} "
|
||||
f"for {[domain for domain in email_data['domains']]}"
|
||||
f": {err}"
|
||||
)
|
||||
# if email failed to send, set error in domains_with_errors for each
|
||||
# domain in the email so that transition domain email_sent is not set
|
||||
# to True
|
||||
for domain in email_data["domains"]:
|
||||
self.domains_with_errors.append(domain)
|
||||
|
||||
def update_domains_as_sent(self):
|
||||
"""set email_sent to True in all transition_domains which have
|
||||
been processed successfully"""
|
||||
for transition_domain in self.transition_domains:
|
||||
if transition_domain.domain_name not in self.domains_with_errors:
|
||||
transition_domain.email_sent = True
|
||||
transition_domain.save()
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 4.2.1 on 2023-09-27 00:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0031_transitiondomain_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="transitiondomain",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[("ready", "Ready"), ("hold", "Hold")],
|
||||
default="ready",
|
||||
help_text="domain status during the transfer",
|
||||
max_length=255,
|
||||
verbose_name="Status",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -634,13 +634,25 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
"""This domain should not be active.
|
||||
may raises RegistryError, should be caught or handled correctly by caller"""
|
||||
request = commands.UpdateDomain(name=self.name, add=[self.clientHoldStatus()])
|
||||
try:
|
||||
registry.send(request, cleaned=True)
|
||||
self._invalidate_cache()
|
||||
except RegistryError as err:
|
||||
# if registry error occurs, log the error, and raise it as well
|
||||
logger.error(f"registry error placing client hold: {err}")
|
||||
raise (err)
|
||||
|
||||
def _remove_client_hold(self):
|
||||
"""This domain is okay to be active.
|
||||
may raises RegistryError, should be caught or handled correctly by caller"""
|
||||
request = commands.UpdateDomain(name=self.name, rem=[self.clientHoldStatus()])
|
||||
try:
|
||||
registry.send(request, cleaned=True)
|
||||
self._invalidate_cache()
|
||||
except RegistryError as err:
|
||||
# if registry error occurs, log the error, and raise it as well
|
||||
logger.error(f"registry error removing client hold: {err}")
|
||||
raise (err)
|
||||
|
||||
def _delete_domain(self):
|
||||
"""This domain should be deleted from the registry
|
||||
|
@ -773,7 +785,9 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
administrative_contact.domain = self
|
||||
administrative_contact.save()
|
||||
|
||||
@transition(field="state", source=State.READY, target=State.ON_HOLD)
|
||||
@transition(
|
||||
field="state", source=[State.READY, State.ON_HOLD], target=State.ON_HOLD
|
||||
)
|
||||
def place_client_hold(self):
|
||||
"""place a clienthold on a domain (no longer should resolve)"""
|
||||
# TODO - ensure all requirements for client hold are made here
|
||||
|
@ -782,7 +796,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
self._place_client_hold()
|
||||
# TODO -on the client hold ticket any additional error handling here
|
||||
|
||||
@transition(field="state", source=State.ON_HOLD, target=State.READY)
|
||||
@transition(field="state", source=[State.READY, State.ON_HOLD], target=State.READY)
|
||||
def revert_client_hold(self):
|
||||
"""undo a clienthold placed on a domain"""
|
||||
|
||||
|
|
|
@ -3,15 +3,16 @@ from django.db import models
|
|||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
||||
|
||||
class StatusChoices(models.TextChoices):
|
||||
READY = "ready", "Ready"
|
||||
HOLD = "hold", "Hold"
|
||||
|
||||
|
||||
class TransitionDomain(TimeStampedModel):
|
||||
"""Transition Domain model stores information about the
|
||||
state of a domain upon transition between registry
|
||||
providers"""
|
||||
|
||||
class StatusChoices(models.TextChoices):
|
||||
CREATED = "created", "Created"
|
||||
HOLD = "hold", "Hold"
|
||||
|
||||
username = models.TextField(
|
||||
null=False,
|
||||
blank=False,
|
||||
|
@ -27,6 +28,7 @@ class TransitionDomain(TimeStampedModel):
|
|||
max_length=255,
|
||||
null=False,
|
||||
blank=True,
|
||||
default=StatusChoices.READY,
|
||||
choices=StatusChoices.choices,
|
||||
verbose_name="Status",
|
||||
help_text="domain status during the transfer",
|
||||
|
@ -39,4 +41,9 @@ class TransitionDomain(TimeStampedModel):
|
|||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
return (
|
||||
f"username: {self.username} "
|
||||
f"domainName: {self.domain_name} "
|
||||
f"status: {self.status} "
|
||||
f"email sent: {self.email_sent} "
|
||||
)
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
<li>Domain meets our naming requirements</li>
|
||||
</ul>
|
||||
|
||||
<p> You can <a href="{% url 'todo' %}"><del>check the status</del></a>
|
||||
<p> You can <a href="{% url 'home' %}">check the status</a>
|
||||
of your request at any time. We'll email you with any questions or when we
|
||||
complete our review.</p>
|
||||
|
||||
|
|
|
@ -5,6 +5,28 @@
|
|||
{{ block.super }}
|
||||
<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 %}"
|
||||
role="region"
|
||||
aria-labelledby="summary-box-key-information"
|
||||
>
|
||||
<div class="usa-summary-box__body">
|
||||
<p class="usa-summary-box__heading font-sans-md margin-bottom-0"
|
||||
id="summary-box-key-information"
|
||||
>
|
||||
<span class="text-bold text-primary-darker">
|
||||
Status:
|
||||
</span>
|
||||
{% if domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED%}
|
||||
DNS Needed
|
||||
{% else %}
|
||||
{{ domain.state|title }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
{% url 'domain-nameservers' pk=domain.id as url %}
|
||||
{% if domain.nameservers|length > 0 %}
|
||||
{% include "includes/summary_item.html" with title='DNS name servers' value=domain.nameservers list='true' edit_link=url %}
|
||||
|
|
|
@ -14,8 +14,6 @@
|
|||
<p>Before your domain can be used we'll need information about your domain
|
||||
name servers.</p>
|
||||
|
||||
<p><a class="usa-link" href="{% url "todo" %}">Get help with domain servers.</a></p>
|
||||
|
||||
{% include "includes/required_fields.html" %}
|
||||
|
||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
||||
|
|
|
@ -1,6 +1,32 @@
|
|||
You have been invited to manage the domain {{ domain.name }} on get.gov,
|
||||
the registrar for .gov domain names.
|
||||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi.
|
||||
|
||||
To accept your invitation, go to <{{ domain_url }}>.
|
||||
{{ full_name }} has added you as a manager on {{ domain.name }}.
|
||||
|
||||
You will need to log in with a Login.gov account using this email address.
|
||||
YOU NEED A LOGIN.GOV ACCOUNT
|
||||
You’ll need a Login.gov account to manage your .gov domain. Login.gov provides
|
||||
a simple and secure process for signing into many government services with one
|
||||
account. If you don’t already have one, follow these steps to create your
|
||||
Login.gov account <https://login.gov/help/get-started/create-your-account/>.
|
||||
|
||||
DOMAIN MANAGEMENT
|
||||
As a .gov domain manager you can add or update information about your domain.
|
||||
You’ll also serve as a contact for your .gov domain. Please keep your contact
|
||||
information updated. Learn more about domain management <https://get.gov/help/>.
|
||||
|
||||
SOMETHING WRONG?
|
||||
If you’re not affiliated with {{ domain.name }} or think you received this
|
||||
message in error, contact the .gov team <https://get.gov/help/#contact-us>.
|
||||
|
||||
|
||||
THANK YOU
|
||||
|
||||
.Gov helps the public identify official, trusted information. Thank you for
|
||||
using a .gov domain.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
The .gov team
|
||||
Contact us: <https://get.gov/contact/>
|
||||
Visit <https://get.gov>
|
||||
{% endautoescape %}
|
|
@ -1 +1 @@
|
|||
You are invited to manage {{ domain.name }} on get.gov
|
||||
You’ve been added to a .gov domain
|
|
@ -0,0 +1,29 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi.
|
||||
|
||||
You have been added as a manager on {% if domains|length > 1 %}multiple domains (listed below){% else %}{{ domains.0 }}{% endif %}.
|
||||
|
||||
YOU NEED A LOGIN.GOV ACCOUNT
|
||||
You’ll need a Login.gov account to manage your .gov domain{% if domains|length > 1 %}s{% endif %}. Login.gov provides a simple and secure process for signing into many government services with one account. If you don’t already have one, follow these steps to create your Login.gov account <https://login.gov/help/get-started/create-your-account/>.
|
||||
|
||||
DOMAIN MANAGEMENT
|
||||
As a .gov domain manager you can add or update information about your domain{% if domains|length > 1 %}s{% endif %}. You’ll also serve as a contact for your .gov domain{% if domains|length > 1 %}s{% endif %}. Please keep your contact information updated. Learn more about domain management <https://get.gov/help/>.
|
||||
{% if domains|length > 1 %}
|
||||
DOMAINS
|
||||
{% for domain in domains %} {{ domain }}
|
||||
{% endfor %}{% else %}
|
||||
{% endif %}
|
||||
SOMETHING WRONG?
|
||||
If you’re not affiliated with {{ domain }} or think you received this message in error, contact the .gov team <https://get.gov/help/#contact-us>.
|
||||
|
||||
|
||||
THANK YOU
|
||||
|
||||
.Gov helps the public identify official, trusted information. Thank you for using a .gov domain.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
The .gov team
|
||||
Contact us: <https://get.gov/contact/>
|
||||
Visit <https://get.gov>
|
||||
{% endautoescape %}
|
|
@ -0,0 +1 @@
|
|||
You've been added to a .gov domain
|
|
@ -33,14 +33,18 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
{% for domain in domains %}
|
||||
{% comment %} ticket 796
|
||||
{% if domain.application_status == "approved" or (domain.application does not exist) %} {% endcomment %}
|
||||
<tr>
|
||||
<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-label="Status">{{ domain.application_status|title }}</td>
|
||||
<td data-label="Status">
|
||||
{% if domain.state == "unknown" or domain.state == "dns needed"%}
|
||||
DNS Needed
|
||||
{% else %}
|
||||
{{ domain.state|title }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url "domain" pk=domain.pk %}">
|
||||
<svg
|
||||
|
@ -50,9 +54,15 @@
|
|||
role="img"
|
||||
width="24"
|
||||
>
|
||||
{% if domain.state == "deleted" or domain.state == "on hold" %}
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#visibility"></use>
|
||||
</svg>
|
||||
View <span class="usa-sr-only">{{ domain.name }}</span>
|
||||
{% else %}
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#settings"></use>
|
||||
</svg>
|
||||
Manage <span class="usa-sr-only">{{ domain.name }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -472,6 +472,7 @@ def completed_application(
|
|||
has_anything_else=True,
|
||||
status=DomainApplication.STARTED,
|
||||
user=False,
|
||||
name="city.gov",
|
||||
):
|
||||
"""A completed domain application."""
|
||||
if not user:
|
||||
|
@ -483,7 +484,7 @@ def completed_application(
|
|||
email="testy@town.com",
|
||||
phone="(555) 555 5555",
|
||||
)
|
||||
domain, _ = DraftDomain.objects.get_or_create(name="city.gov")
|
||||
domain, _ = DraftDomain.objects.get_or_create(name=name)
|
||||
alt, _ = Website.objects.get_or_create(website="city1.gov")
|
||||
current, _ = Website.objects.get_or_create(website="city.com")
|
||||
you, _ = Contact.objects.get_or_create(
|
||||
|
|
|
@ -20,6 +20,8 @@ from .common import MockEppLib
|
|||
from epplibwrapper import (
|
||||
commands,
|
||||
common,
|
||||
RegistryError,
|
||||
ErrorCode,
|
||||
)
|
||||
|
||||
|
||||
|
@ -702,7 +704,7 @@ class TestRegistrantDNSSEC(TestCase):
|
|||
raise
|
||||
|
||||
|
||||
class TestAnalystClientHold(TestCase):
|
||||
class TestAnalystClientHold(MockEppLib):
|
||||
"""Rule: Analysts may suspend or restore a domain by using client hold"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -711,18 +713,50 @@ class TestAnalystClientHold(TestCase):
|
|||
Given the analyst is logged in
|
||||
And a domain exists in the registry
|
||||
"""
|
||||
pass
|
||||
super().setUp()
|
||||
# for the tests, need a domain in the ready state
|
||||
self.domain, _ = Domain.objects.get_or_create(
|
||||
name="fake.gov", state=Domain.State.READY
|
||||
)
|
||||
# for the tests, need a domain in the on_hold state
|
||||
self.domain_on_hold, _ = Domain.objects.get_or_create(
|
||||
name="fake-on-hold.gov", state=Domain.State.ON_HOLD
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
Domain.objects.all().delete()
|
||||
super().tearDown()
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_analyst_places_client_hold(self):
|
||||
"""
|
||||
Scenario: Analyst takes a domain off the internet
|
||||
When `domain.place_client_hold()` is called
|
||||
Then `CLIENT_HOLD` is added to the domain's statuses
|
||||
"""
|
||||
raise
|
||||
self.domain.place_client_hold()
|
||||
self.mockedSendFunction.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.UpdateDomain(
|
||||
name="fake.gov",
|
||||
add=[
|
||||
common.Status(
|
||||
state=Domain.Status.CLIENT_HOLD,
|
||||
description="",
|
||||
lang="en",
|
||||
)
|
||||
],
|
||||
nsset=None,
|
||||
keyset=None,
|
||||
registrant=None,
|
||||
auth_info=None,
|
||||
),
|
||||
cleaned=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
self.assertEquals(self.domain.state, Domain.State.ON_HOLD)
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_analyst_places_client_hold_idempotent(self):
|
||||
"""
|
||||
Scenario: Analyst tries to place client hold twice
|
||||
|
@ -730,9 +764,30 @@ class TestAnalystClientHold(TestCase):
|
|||
When `domain.place_client_hold()` is called
|
||||
Then Domain returns normally (without error)
|
||||
"""
|
||||
raise
|
||||
self.domain_on_hold.place_client_hold()
|
||||
self.mockedSendFunction.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.UpdateDomain(
|
||||
name="fake-on-hold.gov",
|
||||
add=[
|
||||
common.Status(
|
||||
state=Domain.Status.CLIENT_HOLD,
|
||||
description="",
|
||||
lang="en",
|
||||
)
|
||||
],
|
||||
nsset=None,
|
||||
keyset=None,
|
||||
registrant=None,
|
||||
auth_info=None,
|
||||
),
|
||||
cleaned=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
self.assertEquals(self.domain_on_hold.state, Domain.State.ON_HOLD)
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_analyst_removes_client_hold(self):
|
||||
"""
|
||||
Scenario: Analyst restores a suspended domain
|
||||
|
@ -740,9 +795,30 @@ class TestAnalystClientHold(TestCase):
|
|||
When `domain.remove_client_hold()` is called
|
||||
Then `CLIENT_HOLD` is no longer in the domain's statuses
|
||||
"""
|
||||
raise
|
||||
self.domain_on_hold.revert_client_hold()
|
||||
self.mockedSendFunction.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.UpdateDomain(
|
||||
name="fake-on-hold.gov",
|
||||
rem=[
|
||||
common.Status(
|
||||
state=Domain.Status.CLIENT_HOLD,
|
||||
description="",
|
||||
lang="en",
|
||||
)
|
||||
],
|
||||
nsset=None,
|
||||
keyset=None,
|
||||
registrant=None,
|
||||
auth_info=None,
|
||||
),
|
||||
cleaned=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
self.assertEquals(self.domain_on_hold.state, Domain.State.READY)
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_analyst_removes_client_hold_idempotent(self):
|
||||
"""
|
||||
Scenario: Analyst tries to remove client hold twice
|
||||
|
@ -750,16 +826,54 @@ class TestAnalystClientHold(TestCase):
|
|||
When `domain.remove_client_hold()` is called
|
||||
Then Domain returns normally (without error)
|
||||
"""
|
||||
raise
|
||||
self.domain.revert_client_hold()
|
||||
self.mockedSendFunction.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.UpdateDomain(
|
||||
name="fake.gov",
|
||||
rem=[
|
||||
common.Status(
|
||||
state=Domain.Status.CLIENT_HOLD,
|
||||
description="",
|
||||
lang="en",
|
||||
)
|
||||
],
|
||||
nsset=None,
|
||||
keyset=None,
|
||||
registrant=None,
|
||||
auth_info=None,
|
||||
),
|
||||
cleaned=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
self.assertEquals(self.domain.state, Domain.State.READY)
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_update_is_unsuccessful(self):
|
||||
"""
|
||||
Scenario: An update to place or remove client hold is unsuccessful
|
||||
When an error is returned from epplibwrapper
|
||||
Then a user-friendly error message is returned for displaying on the web
|
||||
"""
|
||||
raise
|
||||
|
||||
def side_effect(_request, cleaned):
|
||||
raise RegistryError(code=ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION)
|
||||
|
||||
patcher = patch("registrar.models.domain.registry.send")
|
||||
mocked_send = patcher.start()
|
||||
mocked_send.side_effect = side_effect
|
||||
|
||||
# if RegistryError is raised, admin formats user-friendly
|
||||
# error message if error is_client_error, is_session_error, or
|
||||
# is_server_error; so test for those conditions
|
||||
with self.assertRaises(RegistryError) as err:
|
||||
self.domain.place_client_hold()
|
||||
self.assertTrue(
|
||||
err.is_client_error() or err.is_session_error() or err.is_server_error()
|
||||
)
|
||||
|
||||
patcher.stop()
|
||||
|
||||
|
||||
class TestAnalystLock(TestCase):
|
||||
|
|
|
@ -25,6 +25,7 @@ from registrar.models import (
|
|||
from registrar.views.application import ApplicationWizard, Step
|
||||
|
||||
from .common import less_console_noise
|
||||
from .common import MockEppLib
|
||||
|
||||
|
||||
class TestViews(TestCase):
|
||||
|
@ -47,8 +48,9 @@ class TestViews(TestCase):
|
|||
self.assertIn("/login?next=/register/", response.headers["Location"])
|
||||
|
||||
|
||||
class TestWithUser(TestCase):
|
||||
class TestWithUser(MockEppLib):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
username = "test_user"
|
||||
first_name = "First"
|
||||
last_name = "Last"
|
||||
|
@ -59,6 +61,7 @@ class TestWithUser(TestCase):
|
|||
|
||||
def tearDown(self):
|
||||
# delete any applications too
|
||||
super().tearDown()
|
||||
DomainApplication.objects.all().delete()
|
||||
self.user.delete()
|
||||
|
||||
|
@ -91,6 +94,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")
|
||||
# clean up
|
||||
role.delete()
|
||||
|
||||
|
@ -1079,6 +1083,7 @@ class TestWithDomainPermissions(TestWithUser):
|
|||
self.domain_information.delete()
|
||||
if hasattr(self.domain, "contacts"):
|
||||
self.domain.contacts.all().delete()
|
||||
DomainApplication.objects.all().delete()
|
||||
self.domain.delete()
|
||||
self.role.delete()
|
||||
except ValueError: # pass if already deleted
|
||||
|
@ -1140,6 +1145,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
|
|||
# click the "Edit" link
|
||||
detail_page = home_page.click("Manage")
|
||||
self.assertContains(detail_page, "igorville.gov")
|
||||
self.assertContains(detail_page, "Status")
|
||||
|
||||
def test_domain_user_management(self):
|
||||
response = self.client.get(
|
||||
|
@ -1197,6 +1203,10 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
|
|||
EMAIL = "mayor@igorville.gov"
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
|
||||
self.domain_information, _ = DomainInformation.objects.get_or_create(
|
||||
creator=self.user, domain=self.domain
|
||||
)
|
||||
|
||||
add_page = self.app.get(
|
||||
reverse("domain-users-add", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
|
@ -1218,6 +1228,10 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
|
|||
EMAIL = "mayor@igorville.gov"
|
||||
User.objects.filter(email=EMAIL).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):
|
||||
|
@ -1270,6 +1284,11 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
|
|||
add_page = self.app.get(
|
||||
reverse("domain-users-add", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
|
||||
self.domain_information, _ = DomainInformation.objects.get_or_create(
|
||||
creator=self.user, domain=self.domain
|
||||
)
|
||||
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
add_page.form["email"] = EMAIL
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
@ -1293,6 +1312,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
|
|||
)
|
||||
self.assertContains(page, "Domain name servers")
|
||||
|
||||
@skip("Broken by adding registry connection fix in ticket 848")
|
||||
def test_domain_nameservers_form(self):
|
||||
"""Can change domain's nameservers.
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ from django.views.generic.edit import FormMixin
|
|||
|
||||
from registrar.models import (
|
||||
Domain,
|
||||
DomainInformation,
|
||||
DomainInvitation,
|
||||
User,
|
||||
UserDomainRole,
|
||||
|
@ -335,6 +336,11 @@ class DomainAddUserView(DomainPermissionView, FormMixin):
|
|||
)
|
||||
else:
|
||||
# created a new invitation in the database, so send an email
|
||||
domaininfo = DomainInformation.objects.filter(domain=self.object)
|
||||
first = domaininfo.first().creator.first_name
|
||||
last = domaininfo.first().creator.last_name
|
||||
full_name = f"{first} {last}"
|
||||
|
||||
try:
|
||||
send_templated_email(
|
||||
"emails/domain_invitation.txt",
|
||||
|
@ -343,6 +349,7 @@ class DomainAddUserView(DomainPermissionView, FormMixin):
|
|||
context={
|
||||
"domain_url": self._domain_abs_url(),
|
||||
"domain": self.object,
|
||||
"full_name": full_name,
|
||||
},
|
||||
)
|
||||
except EmailSendingError:
|
||||
|
|
|
@ -19,7 +19,7 @@ def index(request):
|
|||
pk=F("domain__id"),
|
||||
name=F("domain__name"),
|
||||
created_time=F("domain__created_at"),
|
||||
application_status=F("domain__domain_application__status"),
|
||||
state=F("domain__state"),
|
||||
)
|
||||
context["domains"] = domains
|
||||
return render(request, "home.html", context)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue