Merge remote-tracking branch 'origin/main' into rjm/811-approved-rejected-approved

This commit is contained in:
Rachid Mrad 2023-09-15 16:50:46 -04:00
commit f5bb77cef6
No known key found for this signature in database
GPG key ID: EF38E4CEC4A8F3CF
36 changed files with 1713 additions and 382 deletions

View file

@ -31,7 +31,7 @@ Finally, you'll need to craft a request and send it.
``` ```
request = ... request = ...
response = registry.send(request) response = registry.send(request, cleaned=True)
``` ```
Note that you'll need to attest that the data you are sending has been sanitized to remove malicious or invalid strings. Use `send(..., cleaned=True)` to do that. Note that you'll need to attest that the data you are sending has been sanitized to remove malicious or invalid strings. Use `send(..., cleaned=True)` to do that.

View file

@ -83,7 +83,7 @@ class EPPLibWrapper:
logger.warning(message, cmd_type, exc_info=True) logger.warning(message, cmd_type, exc_info=True)
raise RegistryError(message) from err raise RegistryError(message) from err
except Exception as err: except Exception as err:
message = "%s failed to execute due to an unknown error." message = "%s failed to execute due to an unknown error." % err
logger.warning(message, cmd_type, exc_info=True) logger.warning(message, cmd_type, exc_info=True)
raise RegistryError(message) from err raise RegistryError(message) from err
else: else:

View file

@ -1,4 +1,6 @@
import logging import logging
from django import forms
from django_fsm import get_available_FIELD_transitions
from django.contrib import admin, messages from django.contrib import admin, messages
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -6,10 +8,36 @@ from django.http.response import HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
from registrar.models.utility.admin_sort_fields import AdminSortFields from registrar.models.utility.admin_sort_fields import AdminSortFields
from . import models from . import models
from auditlog.models import LogEntry # type: ignore
from auditlog.admin import LogEntryAdmin # type: ignore
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CustomLogEntryAdmin(LogEntryAdmin):
"""Overwrite the generated LogEntry admin class"""
list_display = [
"created",
"resource",
"action",
"msg_short",
"user_url",
]
# We name the custom prop 'resource' because linter
# is not allowing a short_description attr on it
# This gets around the linter limitation, for now.
def resource(self, obj):
# Return the field value without a link
return f"{obj.content_type} - {obj.object_repr}"
search_help_text = "Search by resource, changes, or user."
change_form_template = "admin/change_form_no_submit.html"
add_form_template = "admin/change_form_no_submit.html"
class AuditedAdmin(admin.ModelAdmin, AdminSortFields): class AuditedAdmin(admin.ModelAdmin, AdminSortFields):
"""Custom admin to make auditing easier.""" """Custom admin to make auditing easier."""
@ -91,14 +119,12 @@ class ListHeaderAdmin(AuditedAdmin):
class UserContactInline(admin.StackedInline): class UserContactInline(admin.StackedInline):
"""Edit a user's profile on the user page.""" """Edit a user's profile on the user page."""
model = models.Contact model = models.Contact
class MyUserAdmin(BaseUserAdmin): class MyUserAdmin(BaseUserAdmin):
"""Custom user admin class to use our inlines.""" """Custom user admin class to use our inlines."""
inlines = [UserContactInline] inlines = [UserContactInline]
@ -201,34 +227,89 @@ class MyUserAdmin(BaseUserAdmin):
class HostIPInline(admin.StackedInline): class HostIPInline(admin.StackedInline):
"""Edit an ip address on the host page.""" """Edit an ip address on the host page."""
model = models.HostIP model = models.HostIP
class MyHostAdmin(AuditedAdmin): class MyHostAdmin(AuditedAdmin):
"""Custom host admin class to use our inlines.""" """Custom host admin class to use our inlines."""
inlines = [HostIPInline] inlines = [HostIPInline]
class DomainAdmin(ListHeaderAdmin): class DomainAdmin(ListHeaderAdmin):
"""Custom domain admin class to add extra buttons.""" """Custom domain admin class to add extra buttons."""
# Columns
list_display = [
"name",
"organization_type",
"state",
]
def organization_type(self, obj):
return obj.domain_info.organization_type
organization_type.admin_order_field = ( # type: ignore
"domain_info__organization_type"
)
# Filters
list_filter = ["domain_info__organization_type"]
search_fields = ["name"] search_fields = ["name"]
search_help_text = "Search by domain name." search_help_text = "Search by domain name."
change_form_template = "django/admin/domain_change_form.html" change_form_template = "django/admin/domain_change_form.html"
readonly_fields = ["state"] readonly_fields = ["state"]
def response_change(self, request, obj): def response_change(self, request, obj):
PLACE_HOLD = "_place_client_hold" # Create dictionary of action functions
EDIT_DOMAIN = "_edit_domain" ACTION_FUNCTIONS = {
if PLACE_HOLD in request.POST: "_place_client_hold": self.do_place_client_hold,
"_remove_client_hold": self.do_remove_client_hold,
"_edit_domain": self.do_edit_domain,
"_delete_domain": self.do_delete_domain,
"_get_status": self.do_get_status,
}
# Check which action button was pressed and call the corresponding function
for action, function in ACTION_FUNCTIONS.items():
if action in request.POST:
return function(request, obj)
# If no matching action button is found, return the super method
return super().response_change(request, obj)
def do_delete_domain(self, request, obj):
try:
obj.deleted()
obj.save()
except Exception as err:
self.message_user(request, err, messages.ERROR)
else:
self.message_user(
request,
("Domain %s Should now be deleted " ". Thanks!") % obj.name,
)
return HttpResponseRedirect(".")
def do_get_status(self, request, obj):
try:
statuses = obj.statuses
except Exception as err:
self.message_user(request, err, messages.ERROR)
else:
self.message_user(
request,
("Domain statuses are %s" ". Thanks!") % statuses,
)
return HttpResponseRedirect(".")
def do_place_client_hold(self, request, obj):
try: try:
obj.place_client_hold() obj.place_client_hold()
obj.save()
except Exception as err: except Exception as err:
self.message_user(request, err, messages.ERROR) self.message_user(request, err, messages.ERROR)
else: else:
@ -241,13 +322,27 @@ class DomainAdmin(ListHeaderAdmin):
% obj.name, % obj.name,
) )
return HttpResponseRedirect(".") return HttpResponseRedirect(".")
elif EDIT_DOMAIN in request.POST:
def do_remove_client_hold(self, request, obj):
try:
obj.revert_client_hold()
obj.save()
except Exception as err:
self.message_user(request, err, messages.ERROR)
else:
self.message_user(
request,
("%s is ready. This domain is accessible on the public internet.")
% obj.name,
)
return HttpResponseRedirect(".")
def do_edit_domain(self, request, obj):
# We want to know, globally, when an edit action occurs # We want to know, globally, when an edit action occurs
request.session["analyst_action"] = "edit" request.session["analyst_action"] = "edit"
# Restricts this action to this domain (pk) only # Restricts this action to this domain (pk) only
request.session["analyst_action_location"] = obj.id request.session["analyst_action_location"] = obj.id
return HttpResponseRedirect(reverse("domain", args=(obj.id,))) return HttpResponseRedirect(reverse("domain", args=(obj.id,)))
return super().response_change(request, obj)
def change_view(self, request, object_id): def change_view(self, request, object_id):
# If the analyst was recently editing a domain page, # If the analyst was recently editing a domain page,
@ -271,6 +366,126 @@ class ContactAdmin(ListHeaderAdmin):
search_fields = ["email", "first_name", "last_name"] search_fields = ["email", "first_name", "last_name"]
search_help_text = "Search by firstname, lastname or email." search_help_text = "Search by firstname, lastname or email."
list_display = [
"contact",
"email",
]
# We name the custom prop 'contact' because linter
# is not allowing a short_description attr on it
# This gets around the linter limitation, for now.
def contact(self, obj: models.Contact):
"""Duplicate the contact _str_"""
if obj.first_name or obj.last_name:
return obj.get_formatted_name()
elif obj.email:
return obj.email
elif obj.pk:
return str(obj.pk)
else:
return ""
contact.admin_order_field = "first_name" # type: ignore
class WebsiteAdmin(ListHeaderAdmin):
"""Custom website admin class."""
# Search
search_fields = [
"website",
]
search_help_text = "Search by website."
class UserDomainRoleAdmin(ListHeaderAdmin):
"""Custom domain role admin class."""
# Columns
list_display = [
"user",
"domain",
"role",
]
# Search
search_fields = [
"user__first_name",
"user__last_name",
"domain__name",
"role",
]
search_help_text = "Search by user, domain, or role."
class DomainInvitationAdmin(ListHeaderAdmin):
"""Custom domain invitation admin class."""
# Columns
list_display = [
"email",
"domain",
"status",
]
# Search
search_fields = [
"email",
"domain__name",
]
search_help_text = "Search by email or domain."
class DomainInformationAdmin(ListHeaderAdmin):
"""Customize domain information admin class."""
# Columns
list_display = [
"domain",
"organization_type",
"created_at",
"submitter",
]
# Filters
list_filter = ["organization_type"]
# Search
search_fields = [
"domain__name",
]
search_help_text = "Search by domain."
class DomainApplicationAdminForm(forms.ModelForm):
"""Custom form to limit transitions to available transitions"""
class Meta:
model = models.DomainApplication
fields = "__all__"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
application = kwargs.get("instance")
if application and application.pk:
current_state = application.status
# first option in status transitions is current state
available_transitions = [(current_state, current_state)]
transitions = get_available_FIELD_transitions(
application, models.DomainApplication._meta.get_field("status")
)
for transition in transitions:
available_transitions.append((transition.target, transition.target))
# only set the available transitions if the user is not restricted
# from editing the domain application; otherwise, the form will be
# readonly and the status field will not have a widget
if not application.creator.is_restricted():
self.fields["status"].widget.choices = available_transitions
class DomainInformationAdmin(ListHeaderAdmin): class DomainInformationAdmin(ListHeaderAdmin):
@ -282,7 +497,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
class DomainApplicationAdmin(ListHeaderAdmin): class DomainApplicationAdmin(ListHeaderAdmin):
"""Customize the applications listing view.""" """Custom domain applications admin class."""
# Set multi-selects 'read-only' (hide selects and show data) # Set multi-selects 'read-only' (hide selects and show data)
# based on user perms and application creator's status # based on user perms and application creator's status
@ -311,6 +526,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
search_help_text = "Search by domain or submitter." search_help_text = "Search by domain or submitter."
# Detail view # Detail view
form = DomainApplicationAdminForm
fieldsets = [ fieldsets = [
(None, {"fields": ["status", "investigator", "creator", "approved_domain"]}), (None, {"fields": ["status", "investigator", "creator", "approved_domain"]}),
( (
@ -324,8 +540,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
"federal_agency", "federal_agency",
"federal_type", "federal_type",
"is_election_board", "is_election_board",
"type_of_work", "about_your_organization",
"more_organization_information",
] ]
}, },
), ),
@ -363,8 +578,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
# Read only that we'll leverage for CISA Analysts # Read only that we'll leverage for CISA Analysts
analyst_readonly_fields = [ analyst_readonly_fields = [
"creator", "creator",
"type_of_work", "about_your_organization",
"more_organization_information",
"address_line1", "address_line1",
"address_line2", "address_line2",
"zipcode", "zipcode",
@ -484,13 +698,17 @@ class DomainApplicationAdmin(ListHeaderAdmin):
return super().change_view(request, object_id, form_url, extra_context) return super().change_view(request, object_id, form_url, extra_context)
admin.site.unregister(LogEntry) # Unregister the default registration
admin.site.register(LogEntry, CustomLogEntryAdmin)
admin.site.register(models.User, MyUserAdmin) admin.site.register(models.User, MyUserAdmin)
admin.site.register(models.UserDomainRole, AuditedAdmin) admin.site.register(models.UserDomainRole, UserDomainRoleAdmin)
admin.site.register(models.Contact, ContactAdmin) admin.site.register(models.Contact, ContactAdmin)
admin.site.register(models.DomainInvitation, AuditedAdmin) admin.site.register(models.DomainInvitation, DomainInvitationAdmin)
admin.site.register(models.DomainInformation, DomainInformationAdmin) admin.site.register(models.DomainInformation, DomainInformationAdmin)
admin.site.register(models.Domain, DomainAdmin) admin.site.register(models.Domain, DomainAdmin)
admin.site.register(models.Host, MyHostAdmin) admin.site.register(models.Host, MyHostAdmin)
admin.site.register(models.Nameserver, MyHostAdmin) admin.site.register(models.Nameserver, MyHostAdmin)
admin.site.register(models.Website, AuditedAdmin) admin.site.register(models.Website, WebsiteAdmin)
admin.site.register(models.PublicContact, AuditedAdmin)
admin.site.register(models.DomainApplication, DomainApplicationAdmin) admin.site.register(models.DomainApplication, DomainApplicationAdmin)
admin.site.register(models.TransitionDomain, AuditedAdmin)

View file

@ -31,7 +31,7 @@ html[data-theme="light"] {
// #{$theme-link-color} would interpolate to 'primary', so we use the source value instead // #{$theme-link-color} would interpolate to 'primary', so we use the source value instead
--link-fg: #{$theme-color-primary}; --link-fg: #{$theme-color-primary};
--link-hover-color: #{$theme-color-primary-darker}; --link-hover-color: #{$theme-color-primary};
// $theme-link-visited-color - violet-70v // $theme-link-visited-color - violet-70v
--link-selected-fg: #54278f; --link-selected-fg: #54278f;
@ -140,11 +140,6 @@ h1, h2, h3 {
font-weight: font-weight('bold'); font-weight: font-weight('bold');
} }
table > caption > a {
font-weight: font-weight('bold');
text-transform: none;
}
.change-list { .change-list {
.usa-table--striped tbody tr:nth-child(odd) td, .usa-table--striped tbody tr:nth-child(odd) td,
.usa-table--striped tbody tr:nth-child(odd) th, .usa-table--striped tbody tr:nth-child(odd) th,
@ -158,9 +153,12 @@ table > caption > a {
padding-top: 20px; padding-top: 20px;
} }
// 'Delete button' layout bug // Fix django admin button height bugs
.submit-row a.deletelink { .submit-row a.deletelink,
.delete-confirmation form .cancel-link,
.submit-row a.closelink {
height: auto!important; height: auto!important;
font-size: 14px;
} }
// Keep th from collapsing // Keep th from collapsing
@ -170,3 +168,15 @@ table > caption > a {
.min-width-81 { .min-width-81 {
min-width: 81px; min-width: 81px;
} }
.primary-th {
padding-top: 8px;
padding-bottom: 8px;
font-size: 0.75rem;
letter-spacing: 0.5px;
text-transform: none;
font-weight: font-weight('bold');
text-align: left;
background: var(--primary);
color: var(--header-link-color);
}

View file

@ -27,7 +27,7 @@ for step, view in [
(Step.ORGANIZATION_FEDERAL, views.OrganizationFederal), (Step.ORGANIZATION_FEDERAL, views.OrganizationFederal),
(Step.ORGANIZATION_ELECTION, views.OrganizationElection), (Step.ORGANIZATION_ELECTION, views.OrganizationElection),
(Step.ORGANIZATION_CONTACT, views.OrganizationContact), (Step.ORGANIZATION_CONTACT, views.OrganizationContact),
(Step.TYPE_OF_WORK, views.TypeOfWork), (Step.ABOUT_YOUR_ORGANIZATION, views.AboutYourOrganization),
(Step.AUTHORIZING_OFFICIAL, views.AuthorizingOfficial), (Step.AUTHORIZING_OFFICIAL, views.AuthorizingOfficial),
(Step.CURRENT_SITES, views.CurrentSites), (Step.CURRENT_SITES, views.CurrentSites),
(Step.DOTGOV_DOMAIN, views.DotgovDomain), (Step.DOTGOV_DOMAIN, views.DotgovDomain),

View file

@ -77,6 +77,11 @@ class UserFixture:
"first_name": "David", "first_name": "David",
"last_name": "Kennedy", "last_name": "Kennedy",
}, },
{
"username": "f14433d8-f0e9-41bf-9c72-b99b110e665d",
"first_name": "Nicolle",
"last_name": "LeClair",
},
] ]
STAFF = [ STAFF = [
@ -123,6 +128,12 @@ class UserFixture:
"last_name": "DiSarli-Analyst", "last_name": "DiSarli-Analyst",
"email": "gaby@truss.works", "email": "gaby@truss.works",
}, },
{
"username": "cfe7c2fc-e24a-480e-8b78-28645a1459b3",
"first_name": "Nicolle-Analyst",
"last_name": "LeClair-Analyst",
"email": "nicolle.leclair@ecstech.com",
},
] ]
STAFF_PERMISSIONS = [ STAFF_PERMISSIONS = [

View file

@ -310,28 +310,9 @@ class OrganizationContactForm(RegistrarForm):
return federal_agency return federal_agency
class TypeOfWorkForm(RegistrarForm): class AboutYourOrganizationForm(RegistrarForm):
type_of_work = forms.CharField( about_your_organization = forms.CharField(
# label has to end in a space to get the label_suffix to show label="About your organization",
label="What type of work does your organization do? ",
widget=forms.Textarea(),
validators=[
MaxLengthValidator(
1000,
message="Response must be less than 1000 characters.",
)
],
error_messages={"required": "Enter the type of work your organization does."},
)
more_organization_information = forms.CharField(
# label has to end in a space to get the label_suffix to show
label=(
"Describe how your organization is a government organization that is"
" independent of a state government. Include links to authorizing"
" legislation, applicable bylaws or charter, or other documentation to"
" support your claims. "
),
widget=forms.Textarea(), widget=forms.Textarea(),
validators=[ validators=[
MaxLengthValidator( MaxLengthValidator(
@ -340,9 +321,7 @@ class TypeOfWorkForm(RegistrarForm):
) )
], ],
error_messages={ error_messages={
"required": ( "required": ("Enter more information about your organization.")
"Describe how your organization is independent of a state government."
)
}, },
) )

View file

@ -2,7 +2,7 @@ import logging
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from auditlog.context import disable_auditlog # type: ignore from auditlog.context import disable_auditlog # type: ignore
from django.conf import settings
from registrar.fixtures import UserFixture, DomainApplicationFixture, DomainFixture from registrar.fixtures import UserFixture, DomainApplicationFixture, DomainFixture
@ -13,11 +13,8 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
# django-auditlog has some bugs with fixtures # django-auditlog has some bugs with fixtures
# https://github.com/jazzband/django-auditlog/issues/17 # https://github.com/jazzband/django-auditlog/issues/17
if settings.DEBUG:
with disable_auditlog(): with disable_auditlog():
UserFixture.load() UserFixture.load()
DomainApplicationFixture.load() DomainApplicationFixture.load()
DomainFixture.load() DomainFixture.load()
logger.info("All fixtures loaded.") logger.info("All fixtures loaded.")
else:
logger.warn("Refusing to load fixture data in a non DEBUG env")

View file

@ -0,0 +1,122 @@
# Generated by Django 4.2.1 on 2023-09-13 22:25
from django.db import migrations, models
import django_fsm
class Migration(migrations.Migration):
dependencies = [
("registrar", "0030_alter_user_status"),
]
operations = [
migrations.CreateModel(
name="TransitionDomain",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"username",
models.TextField(
help_text="Username - this will be an email address",
verbose_name="Username",
),
),
(
"domain_name",
models.TextField(blank=True, null=True, verbose_name="Domain name"),
),
(
"status",
models.CharField(
blank=True,
choices=[("created", "Created"), ("hold", "Hold")],
help_text="domain status during the transfer",
max_length=255,
verbose_name="Status",
),
),
(
"email_sent",
models.BooleanField(
default=False,
help_text="indicates whether email was sent",
verbose_name="email sent",
),
),
],
options={
"abstract": False,
},
),
migrations.RemoveField(
model_name="domainapplication",
name="more_organization_information",
),
migrations.RemoveField(
model_name="domainapplication",
name="type_of_work",
),
migrations.RemoveField(
model_name="domaininformation",
name="more_organization_information",
),
migrations.RemoveField(
model_name="domaininformation",
name="type_of_work",
),
migrations.AddField(
model_name="domainapplication",
name="about_your_organization",
field=models.TextField(
blank=True, help_text="Information about your organization", null=True
),
),
migrations.AddField(
model_name="domaininformation",
name="about_your_organization",
field=models.TextField(
blank=True, help_text="Information about your organization", null=True
),
),
migrations.AlterField(
model_name="domain",
name="state",
field=django_fsm.FSMField(
choices=[
("unknown", "Unknown"),
("dns needed", "Dns Needed"),
("ready", "Ready"),
("on hold", "On Hold"),
("deleted", "Deleted"),
],
default="unknown",
help_text="Very basic info about the lifecycle of this domain object",
max_length=21,
protected=True,
),
),
migrations.AlterField(
model_name="publiccontact",
name="contact_type",
field=models.CharField(
choices=[
("registrant", "Registrant"),
("admin", "Administrative"),
("tech", "Technical"),
("security", "Security"),
],
help_text="For which type of WHOIS contact",
max_length=14,
),
),
]

View file

@ -13,6 +13,7 @@ from .user_domain_role import UserDomainRole
from .public_contact import PublicContact from .public_contact import PublicContact
from .user import User from .user import User
from .website import Website from .website import Website
from .transition_domain import TransitionDomain
__all__ = [ __all__ = [
"Contact", "Contact",
@ -28,6 +29,7 @@ __all__ = [
"PublicContact", "PublicContact",
"User", "User",
"Website", "Website",
"TransitionDomain",
] ]
auditlog.register(Contact) auditlog.register(Contact)
@ -42,3 +44,4 @@ auditlog.register(UserDomainRole)
auditlog.register(PublicContact) auditlog.register(PublicContact)
auditlog.register(User) auditlog.register(User)
auditlog.register(Website) auditlog.register(Website)
auditlog.register(TransitionDomain)

View file

@ -2,7 +2,7 @@ import logging
from datetime import date from datetime import date
from string import digits from string import digits
from django_fsm import FSMField # type: ignore from django_fsm import FSMField, transition # type: ignore
from django.db import models from django.db import models
@ -105,15 +105,22 @@ class Domain(TimeStampedModel, DomainHelper):
class State(models.TextChoices): class State(models.TextChoices):
"""These capture (some of) the states a domain object can be in.""" """These capture (some of) the states a domain object can be in."""
# the normal state of a domain object -- may or may not be active! # the state is indeterminate
CREATED = "created" UNKNOWN = "unknown"
# The domain object exists in the registry
# but nameservers don't exist for it yet
DNS_NEEDED = "dns needed"
# Domain has had nameservers set, may or may not be active
READY = "ready"
# Registrar manually changed state to client hold
ON_HOLD = "on hold"
# previously existed but has been deleted from the registry # previously existed but has been deleted from the registry
DELETED = "deleted" DELETED = "deleted"
# the state is indeterminate
UNKNOWN = "unknown"
class Cache(property): class Cache(property):
""" """
Python descriptor to turn class methods into properties. Python descriptor to turn class methods into properties.
@ -193,7 +200,7 @@ class Domain(TimeStampedModel, DomainHelper):
@expiration_date.setter # type: ignore @expiration_date.setter # type: ignore
def expiration_date(self, ex_date: date): def expiration_date(self, ex_date: date):
raise NotImplementedError() pass
@Cache @Cache
def password(self) -> str: def password(self) -> str:
@ -219,17 +226,108 @@ class Domain(TimeStampedModel, DomainHelper):
Subordinate hosts (something.your-domain.gov) MUST have IP addresses, Subordinate hosts (something.your-domain.gov) MUST have IP addresses,
while non-subordinate hosts MUST NOT. while non-subordinate hosts MUST NOT.
""" """
# TODO: call EPP to get this info instead of returning fake data. try:
return [ hosts = self._get_property("hosts")
("ns1.example.com",), except Exception as err:
("ns2.example.com",), # Don't throw error as this is normal for a new domain
("ns3.example.com",), # TODO - 433 error handling ticket should address this
] logger.info("Domain is missing nameservers %s" % err)
return []
hostList = []
for host in hosts:
# TODO - this should actually have a second tuple value with the ip address
# ignored because uncertain if we will even have a way to display mult.
# and adresses can be a list of mult address
hostList.append((host["name"],))
return hostList
def _check_host(self, hostnames: list[str]):
"""check if host is available, True if available
returns boolean"""
checkCommand = commands.CheckHost(hostnames)
try:
response = registry.send(checkCommand, cleaned=True)
return response.res_data[0].avail
except RegistryError as err:
logger.warning(
"Couldn't check hosts %s. Errorcode was %s, error was %s",
hostnames,
err.code,
err,
)
return False
def _create_host(self, host, addrs):
"""Call _check_host first before using this function,
This creates the host object in the registry
doesn't add the created host to the domain
returns ErrorCode (int)"""
logger.info("Creating host")
if addrs is not None:
addresses = [epp.Ip(addr=addr) for addr in addrs]
request = commands.CreateHost(name=host, addrs=addresses)
else:
request = commands.CreateHost(name=host)
try:
logger.info("_create_host()-> sending req as %s" % request)
response = registry.send(request, cleaned=True)
return response.code
except RegistryError as e:
logger.error("Error _create_host, code was %s error was %s" % (e.code, e))
return e.code
@nameservers.setter # type: ignore @nameservers.setter # type: ignore
def nameservers(self, hosts: list[tuple[str]]): def nameservers(self, hosts: list[tuple[str]]):
# TODO: call EPP to set this info. """host should be a tuple of type str, str,... where the elements are
pass Fully qualified host name, addresses associated with the host
example: [(ns1.okay.gov, 127.0.0.1, others ips)]"""
# TODO: ticket #848 finish this implementation
# must delete nameservers as well or update
# ip version checking may need to be added in a different ticket
if len(hosts) > 13:
raise ValueError(
"Too many hosts provided, you may not have more than 13 nameservers."
)
logger.info("Setting nameservers")
logger.info(hosts)
for hostTuple in hosts:
host = hostTuple[0]
addrs = None
if len(hostTuple) > 1:
addrs = hostTuple[1:]
avail = self._check_host([host])
if avail:
createdCode = self._create_host(host=host, addrs=addrs)
# update the domain obj
if createdCode == ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY:
# add host to domain
request = commands.UpdateDomain(
name=self.name, add=[epp.HostObjSet([host])]
)
try:
registry.send(request, cleaned=True)
except RegistryError as e:
logger.error(
"Error adding nameserver, code was %s error was %s"
% (e.code, e)
)
try:
self.ready()
self.save()
except Exception as err:
logger.info(
"nameserver setter checked for create state "
"and it did not succeed. Error: %s" % err
)
# TODO - handle removed nameservers here will need to change the state
# then go back to DNS_NEEDED
@Cache @Cache
def statuses(self) -> list[str]: def statuses(self) -> list[str]:
@ -240,7 +338,12 @@ class Domain(TimeStampedModel, DomainHelper):
""" """
# implementation note: the Status object from EPP stores the string in # implementation note: the Status object from EPP stores the string in
# a dataclass property `state`, not to be confused with the `state` field here # a dataclass property `state`, not to be confused with the `state` field here
raise NotImplementedError() if "statuses" not in self._cache:
self._fetch_cache()
if "statuses" not in self._cache:
raise Exception("Can't retreive status from domain info")
else:
return self._cache["statuses"]
@statuses.setter # type: ignore @statuses.setter # type: ignore
def statuses(self, statuses: list[str]): def statuses(self, statuses: list[str]):
@ -256,9 +359,13 @@ class Domain(TimeStampedModel, DomainHelper):
@registrant_contact.setter # type: ignore @registrant_contact.setter # type: ignore
def registrant_contact(self, contact: PublicContact): def registrant_contact(self, contact: PublicContact):
# get id from PublicContact->.registry_id """Registrant is set when a domain is created,
# call UpdateDomain() command with registrant as parameter so follow on additions will update the current registrant"""
raise NotImplementedError()
logger.info("making registrant contact")
self._set_singleton_contact(
contact=contact, expectedType=contact.ContactTypeChoices.REGISTRANT
)
@Cache @Cache
def administrative_contact(self) -> PublicContact: def administrative_contact(self) -> PublicContact:
@ -267,25 +374,220 @@ class Domain(TimeStampedModel, DomainHelper):
@administrative_contact.setter # type: ignore @administrative_contact.setter # type: ignore
def administrative_contact(self, contact: PublicContact): def administrative_contact(self, contact: PublicContact):
# call CreateContact, if contact doesn't exist yet for domain logger.info("making admin contact")
# call UpdateDomain with contact, if contact.contact_type != contact.ContactTypeChoices.ADMINISTRATIVE:
# type options are[admin, billing, tech, security] raise ValueError(
# use admin as type parameter for this contact "Cannot set a registrant contact with a different contact type"
raise NotImplementedError() )
self._make_contact_in_registry(contact=contact)
self._update_domain_with_contact(contact, rem=False)
def get_default_security_contact(self):
logger.info("getting default sec contact")
contact = PublicContact.get_default_security()
contact.domain = self
return contact
def _update_epp_contact(self, contact: PublicContact):
"""Sends UpdateContact to update the actual contact object,
domain object remains unaffected
should be used when changing email address
or other contact info on an existing domain
"""
updateContact = commands.UpdateContact(
id=contact.registry_id,
# type: ignore
postal_info=self._make_epp_contact_postal_info(contact=contact),
email=contact.email,
voice=contact.voice,
fax=contact.fax,
) # type: ignore
try:
registry.send(updateContact, cleaned=True)
except RegistryError as e:
logger.error(
"Error updating contact, code was %s error was %s" % (e.code, e)
)
# TODO - ticket 433 human readable error handling here
def _update_domain_with_contact(self, contact: PublicContact, rem=False):
"""adds or removes a contact from a domain
rem being true indicates the contact will be removed from registry"""
logger.info(
"_update_domain_with_contact() received type %s " % contact.contact_type
)
domainContact = epp.DomainContact(
contact=contact.registry_id, type=contact.contact_type
)
updateDomain = commands.UpdateDomain(name=self.name, add=[domainContact])
if rem:
updateDomain = commands.UpdateDomain(name=self.name, rem=[domainContact])
try:
registry.send(updateDomain, cleaned=True)
except RegistryError as e:
logger.error(
"Error changing contact on a domain. Error code is %s error was %s"
% (e.code, e)
)
action = "add"
if rem:
action = "remove"
raise Exception(
"Can't %s the contact of type %s" % (action, contact.contact_type)
)
@Cache @Cache
def security_contact(self) -> PublicContact: def security_contact(self) -> PublicContact:
"""Get or set the security contact for this domain.""" """Get or set the security contact for this domain."""
# TODO: replace this with a real implementation try:
contact = PublicContact.get_default_security() contacts = self._get_property("contacts")
contact.domain = self for contact in contacts:
contact.email = "mayor@igorville.gov" if (
return contact "type" in contact.keys()
and contact["type"] == PublicContact.ContactTypeChoices.SECURITY
):
tempContact = self.get_default_security_contact()
tempContact.email = contact["email"]
return tempContact
except Exception as err: # use better error handling
logger.info("Couldn't get contact %s" % err)
# TODO - remove this ideally it should return None,
# but error handling needs to be
# added on the security email page so that it can handle it being none
return self.get_default_security_contact()
def _add_registrant_to_existing_domain(self, contact: PublicContact):
"""Used to change the registrant contact on an existing domain"""
updateDomain = commands.UpdateDomain(
name=self.name, registrant=contact.registry_id
)
try:
registry.send(updateDomain, cleaned=True)
except RegistryError as e:
logger.error(
"Error changing to new registrant error code is %s, error is %s"
% (e.code, e)
)
# TODO-error handling better here?
def _set_singleton_contact(self, contact: PublicContact, expectedType: str): # noqa
"""Sets the contacts by adding them to the registry as new contacts,
updates the contact if it is already in epp,
deletes any additional contacts of the matching type for this domain
does not create the PublicContact object, this should be made beforehand
(call save() on a public contact to trigger the contact setters
which inturn call this function)
Will throw error if contact type is not the same as expectType
Raises ValueError if expected type doesn't match the contact type"""
if expectedType != contact.contact_type:
raise ValueError(
"Cannot set a contact with a different contact type,"
" expected type was %s" % expectedType
)
isRegistrant = contact.contact_type == contact.ContactTypeChoices.REGISTRANT
isEmptySecurity = (
contact.contact_type == contact.ContactTypeChoices.SECURITY
and contact.email == ""
)
# get publicContact objects that have the matching
# domain and type but a different id
# like in highlander we there can only be one
hasOtherContact = (
PublicContact.objects.exclude(registry_id=contact.registry_id)
.filter(domain=self, contact_type=contact.contact_type)
.exists()
)
# if no record exists with this contact type
# make contact in registry, duplicate and errors handled there
errorCode = self._make_contact_in_registry(contact)
# contact is already added to the domain, but something may have changed on it
alreadyExistsInRegistry = errorCode == ErrorCode.OBJECT_EXISTS
# if an error occured besides duplication, stop
if (
not alreadyExistsInRegistry
and errorCode != ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY
):
# TODO- ticket #433 look here for error handling
raise Exception("Unable to add contact to registry")
# contact doesn't exist on the domain yet
logger.info("_set_singleton_contact()-> contact has been added to the registry")
# if has conflicting contacts in our db remove them
if hasOtherContact:
logger.info(
"_set_singleton_contact()-> updating domain, removing old contact"
)
existing_contact = (
PublicContact.objects.exclude(registry_id=contact.registry_id)
.filter(domain=self, contact_type=contact.contact_type)
.get()
)
if isRegistrant:
# send update domain only for registant contacts
existing_contact.delete()
self._add_registrant_to_existing_domain(contact)
else:
# remove the old contact and add a new one
try:
self._update_domain_with_contact(contact=existing_contact, rem=True)
existing_contact.delete()
except Exception as err:
logger.error(
"Raising error after removing and adding a new contact"
)
raise (err)
# update domain with contact or update the contact itself
if not isEmptySecurity:
if not alreadyExistsInRegistry and not isRegistrant:
self._update_domain_with_contact(contact=contact, rem=False)
# if already exists just update
elif alreadyExistsInRegistry:
current_contact = PublicContact.objects.filter(
registry_id=contact.registry_id
).get()
if current_contact.email != contact.email:
self._update_epp_contact(contact=contact)
else:
logger.info("removing security contact and setting default again")
# get the current contact registry id for security
current_contact = PublicContact.objects.filter(
registry_id=contact.registry_id
).get()
# don't let user delete the default without adding a new email
if current_contact.email != PublicContact.get_default_security().email:
# remove the contact
self._update_domain_with_contact(contact=current_contact, rem=True)
current_contact.delete()
# add new contact
security_contact = self.get_default_security_contact()
security_contact.save()
@security_contact.setter # type: ignore @security_contact.setter # type: ignore
def security_contact(self, contact: PublicContact): def security_contact(self, contact: PublicContact):
# TODO: replace this with a real implementation """makes the contact in the registry,
pass for security the public contact should have the org or registrant information
from domain information (not domain application)
and should have the security email from DomainApplication"""
logger.info("making security contact in registry")
self._set_singleton_contact(
contact, expectedType=contact.ContactTypeChoices.SECURITY
)
@Cache @Cache
def technical_contact(self) -> PublicContact: def technical_contact(self) -> PublicContact:
@ -294,14 +596,19 @@ class Domain(TimeStampedModel, DomainHelper):
@technical_contact.setter # type: ignore @technical_contact.setter # type: ignore
def technical_contact(self, contact: PublicContact): def technical_contact(self, contact: PublicContact):
raise NotImplementedError() logger.info("making technical contact")
self._set_singleton_contact(
contact, expectedType=contact.ContactTypeChoices.TECHNICAL
)
def is_active(self) -> bool: def is_active(self) -> bool:
"""Is the domain live on the inter webs?""" """Currently just returns if the state is created,
# TODO: implement a check -- should be performant so it can be called for because then it should be live, theoretically.
# any number of domains on a status page Post mvp this should indicate
# this is NOT as simple as checking if Domain.Status.OK is in self.statuses Is the domain live on the inter webs?
return self.state == Domain.State.CREATED could be replaced with request to see if ok status is set
"""
return self.state == self.State.READY
def delete_request(self): def delete_request(self):
"""Delete from host. Possibly a duplicate of _delete_host?""" """Delete from host. Possibly a duplicate of _delete_host?"""
@ -316,13 +623,31 @@ class Domain(TimeStampedModel, DomainHelper):
"""Time to renew. Not implemented.""" """Time to renew. Not implemented."""
raise NotImplementedError() raise NotImplementedError()
def place_client_hold(self): def get_security_email(self):
"""This domain should not be active.""" logger.info("get_security_email-> getting the contact ")
raise NotImplementedError("This is not implemented yet.") secContact = self.security_contact
return secContact.email
def remove_client_hold(self): def clientHoldStatus(self):
"""This domain is okay to be active.""" return epp.Status(state=self.Status.CLIENT_HOLD, description="", lang="en")
raise NotImplementedError()
def _place_client_hold(self):
"""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()])
registry.send(request, cleaned=True)
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()])
registry.send(request, cleaned=True)
def _delete_domain(self):
"""This domain should be deleted from the registry
may raises RegistryError, should be caught or handled correctly by caller"""
request = commands.DeleteDomain(name=self.name)
registry.send(request)
def __str__(self) -> str: def __str__(self) -> str:
return self.name return self.name
@ -383,51 +708,147 @@ class Domain(TimeStampedModel, DomainHelper):
def _get_or_create_domain(self): def _get_or_create_domain(self):
"""Try to fetch info about this domain. Create it if it does not exist.""" """Try to fetch info about this domain. Create it if it does not exist."""
already_tried_to_create = False already_tried_to_create = False
while True: exitEarly = False
count = 0
while not exitEarly and count < 3:
try: try:
logger.info("Getting domain info from epp")
req = commands.InfoDomain(name=self.name) req = commands.InfoDomain(name=self.name)
return registry.send(req, cleaned=True).res_data[0] domainInfo = registry.send(req, cleaned=True).res_data[0]
exitEarly = True
return domainInfo
except RegistryError as e: except RegistryError as e:
count += 1
if already_tried_to_create: if already_tried_to_create:
logger.error("Already tried to create")
logger.error(e)
logger.error(e.code)
raise e raise e
if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST: if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST:
# avoid infinite loop # avoid infinite loop
already_tried_to_create = True already_tried_to_create = True
registrant = self._get_or_create_contact( self.pendingCreate()
PublicContact.get_default_registrant()
)
req = commands.CreateDomain(
name=self.name,
registrant=registrant.id,
auth_info=epp.DomainAuthInfo(
pw="2fooBAR123fooBaz"
), # not a password
)
registry.send(req, cleaned=True)
# no error, so go ahead and update state
self.state = Domain.State.CREATED
self.save() self.save()
else: else:
logger.error(e)
logger.error(e.code)
raise e raise e
def _get_or_create_contact(self, contact: PublicContact): def addRegistrant(self):
"""Try to fetch info about a contact. Create it if it does not exist.""" registrant = PublicContact.get_default_registrant()
while True: registrant.domain = self
registrant.save() # calls the registrant_contact.setter
return registrant.registry_id
@transition(field="state", source=State.UNKNOWN, target=State.DNS_NEEDED)
def pendingCreate(self):
logger.info("Changing to dns_needed")
registrantID = self.addRegistrant()
req = commands.CreateDomain(
name=self.name,
registrant=registrantID,
auth_info=epp.DomainAuthInfo(pw="2fooBAR123fooBaz"), # not a password
)
try: try:
req = commands.InfoContact(id=contact.registry_id) registry.send(req, cleaned=True)
return registry.send(req, cleaned=True).res_data[0]
except RegistryError as e: except RegistryError as err:
if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST: if err.code != ErrorCode.OBJECT_EXISTS:
create = commands.CreateContact( raise err
id=contact.registry_id,
postal_info=epp.PostalInfo( # type: ignore self.addAllDefaults()
def addAllDefaults(self):
security_contact = self.get_default_security_contact()
security_contact.save()
technical_contact = PublicContact.get_default_technical()
technical_contact.domain = self
technical_contact.save()
administrative_contact = PublicContact.get_default_administrative()
administrative_contact.domain = self
administrative_contact.save()
@transition(field="state", source=State.READY, 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
# (check prohibited statuses)
logger.info("clientHold()-> inside clientHold")
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)
def revert_client_hold(self):
"""undo a clienthold placed on a domain"""
logger.info("clientHold()-> inside clientHold")
self._remove_client_hold()
# TODO -on the client hold ticket any additional error handling here
@transition(field="state", source=State.ON_HOLD, target=State.DELETED)
def deleted(self):
"""domain is deleted in epp but is saved in our database"""
# TODO Domains may not be deleted if:
# a child host is being used by
# another .gov domains. The host must be first removed
# and/or renamed before the parent domain may be deleted.
logger.info("pendingCreate()-> inside pending create")
self._delete_domain()
# TODO - delete ticket any additional error handling here
@transition(
field="state",
source=[State.DNS_NEEDED],
target=State.READY,
)
def ready(self):
"""Transition to the ready state
domain should have nameservers and all contacts
and now should be considered live on a domain
"""
# TODO - in nameservers tickets 848 and 562
# check here if updates need to be made
# consider adding these checks as constraints
# within the transistion itself
nameserverList = self.nameservers
logger.info("Changing to ready state")
if len(nameserverList) < 2 or len(nameserverList) > 13:
raise ValueError("Not ready to become created, cannot transition yet")
logger.info("able to transition to ready state")
def _disclose_fields(self, contact: PublicContact):
"""creates a disclose object that can be added to a contact Create using
.disclose= <this function> on the command before sending.
if item is security email then make sure email is visable"""
isSecurity = contact.contact_type == contact.ContactTypeChoices.SECURITY
DF = epp.DiscloseField
fields = {DF.FAX, DF.VOICE, DF.ADDR}
if not isSecurity or (
isSecurity and contact.email == PublicContact.get_default_security().email
):
fields.add(DF.EMAIL)
return epp.Disclose(
flag=False,
fields=fields,
types={DF.ADDR: "loc"},
)
def _make_epp_contact_postal_info(self, contact: PublicContact): # type: ignore
return epp.PostalInfo( # type: ignore
name=contact.name, name=contact.name,
addr=epp.ContactAddr( addr=epp.ContactAddr(
street=[ street=[
getattr(contact, street) getattr(contact, street)
for street in ["street1", "street2", "street3"] for street in ["street1", "street2", "street3"]
if hasattr(contact, street) if hasattr(contact, street)
], ], # type: ignore
city=contact.city, city=contact.city,
pc=contact.pc, pc=contact.pc,
cc=contact.cc, cc=contact.cc,
@ -435,25 +856,77 @@ class Domain(TimeStampedModel, DomainHelper):
), ),
org=contact.org, org=contact.org,
type="loc", type="loc",
), )
def _make_contact_in_registry(self, contact: PublicContact):
"""Create the contact in the registry, ignore duplicate contact errors
returns int corresponding to ErrorCode values"""
create = commands.CreateContact(
id=contact.registry_id,
postal_info=self._make_epp_contact_postal_info(contact=contact),
email=contact.email, email=contact.email,
voice=contact.voice, voice=contact.voice,
fax=contact.fax, fax=contact.fax,
auth_info=epp.ContactAuthInfo(pw="2fooBAR123fooBaz"), auth_info=epp.ContactAuthInfo(pw="2fooBAR123fooBaz"),
) ) # type: ignore
# security contacts should only show email addresses, for now # security contacts should only show email addresses, for now
if ( create.disclose = self._disclose_fields(contact=contact)
contact.contact_type try:
== PublicContact.ContactTypeChoices.SECURITY registry.send(create, cleaned=True)
): return ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY
DF = epp.DiscloseField except RegistryError as err:
create.disclose = epp.Disclose( # don't throw an error if it is just saying this is a duplicate contact
flag=False, if err.code != ErrorCode.OBJECT_EXISTS:
fields={DF.FAX, DF.VOICE, DF.ADDR}, logger.error(
types={DF.ADDR: "loc"}, "Registry threw error for contact id %s"
" contact type is %s,"
" error code is\n %s"
" full error is %s",
contact.registry_id,
contact.contact_type,
err.code,
err,
) )
registry.send(create) # TODO - 433 Error handling here
else: else:
logger.warning(
"Registrar tried to create duplicate contact for id %s",
contact.registry_id,
)
return err.code
def _request_contact_info(self, contact: PublicContact):
req = commands.InfoContact(id=contact.registry_id)
return registry.send(req, cleaned=True).res_data[0]
def _get_or_create_contact(self, contact: PublicContact):
"""Try to fetch info about a contact. Create it if it does not exist."""
try:
return self._request_contact_info(contact)
except RegistryError as e:
if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST:
logger.info(
"_get_or_create_contact()-> contact doesn't exist so making it"
)
contact.domain = self
contact.save() # this will call the function based on type of contact
return self._request_contact_info(contact=contact)
else:
logger.error(
"Registry threw error for contact id %s"
" contact type is %s,"
" error code is\n %s"
" full error is %s",
contact.registry_id,
contact.contact_type,
e.code,
e,
)
raise e raise e
def _update_or_create_host(self, host): def _update_or_create_host(self, host):
@ -485,25 +958,33 @@ class Domain(TimeStampedModel, DomainHelper):
# remove null properties (to distinguish between "a value of None" and null) # remove null properties (to distinguish between "a value of None" and null)
cleaned = {k: v for k, v in cache.items() if v is not ...} cleaned = {k: v for k, v in cache.items() if v is not ...}
# statuses can just be a list no need to keep the epp object
if "statuses" in cleaned.keys():
cleaned["statuses"] = [status.state for status in cleaned["statuses"]]
# get contact info, if there are any # get contact info, if there are any
if ( if (
fetch_contacts # fetch_contacts and
and "_contacts" in cleaned "_contacts" in cleaned
and isinstance(cleaned["_contacts"], list) and isinstance(cleaned["_contacts"], list)
and len(cleaned["_contacts"]) and len(cleaned["_contacts"])
): ):
cleaned["contacts"] = [] cleaned["contacts"] = []
for id in cleaned["_contacts"]: for domainContact in cleaned["_contacts"]:
# we do not use _get_or_create_* because we expect the object we # we do not use _get_or_create_* because we expect the object we
# just asked the registry for still exists -- # just asked the registry for still exists --
# if not, that's a problem # if not, that's a problem
req = commands.InfoContact(id=id)
# TODO- discuss-should we check if contact is in public contacts
# and add it if not- this is really to keep in mine the transisiton
req = commands.InfoContact(id=domainContact.contact)
data = registry.send(req, cleaned=True).res_data[0] data = registry.send(req, cleaned=True).res_data[0]
# extract properties from response # extract properties from response
# (Ellipsis is used to mean "null") # (Ellipsis is used to mean "null")
# convert this to use PublicContactInstead
contact = { contact = {
"id": id, "id": domainContact.contact,
"type": domainContact.type,
"auth_info": getattr(data, "auth_info", ...), "auth_info": getattr(data, "auth_info", ...),
"cr_date": getattr(data, "cr_date", ...), "cr_date": getattr(data, "cr_date", ...),
"disclose": getattr(data, "disclose", ...), "disclose": getattr(data, "disclose", ...),
@ -522,11 +1003,13 @@ class Domain(TimeStampedModel, DomainHelper):
# get nameserver info, if there are any # get nameserver info, if there are any
if ( if (
fetch_hosts # fetch_hosts and
and "_hosts" in cleaned "_hosts" in cleaned
and isinstance(cleaned["_hosts"], list) and isinstance(cleaned["_hosts"], list)
and len(cleaned["_hosts"]) and len(cleaned["_hosts"])
): ):
# TODO- add elif in cache set it to be the old cache value
# no point in removing
cleaned["hosts"] = [] cleaned["hosts"] = []
for name in cleaned["_hosts"]: for name in cleaned["_hosts"]:
# we do not use _get_or_create_* because we expect the object we # we do not use _get_or_create_* because we expect the object we

View file

@ -378,16 +378,10 @@ class DomainApplication(TimeStampedModel):
help_text="Urbanization (Puerto Rico only)", help_text="Urbanization (Puerto Rico only)",
) )
type_of_work = models.TextField( about_your_organization = models.TextField(
null=True, null=True,
blank=True, blank=True,
help_text="Type of work of the organization", help_text="Information about your organization",
)
more_organization_information = models.TextField(
null=True,
blank=True,
help_text="More information about your organization",
) )
authorizing_official = models.ForeignKey( authorizing_official = models.ForeignKey(
@ -680,7 +674,7 @@ class DomainApplication(TimeStampedModel):
] ]
return bool(user_choice and user_choice not in excluded) return bool(user_choice and user_choice not in excluded)
def show_type_of_work(self) -> bool: def show_about_your_organization(self) -> bool:
"""Show this step if this is a special district or interstate.""" """Show this step if this is a special district or interstate."""
user_choice = self.organization_type user_choice = self.organization_type
return user_choice in [ return user_choice in [

View file

@ -134,16 +134,10 @@ class DomainInformation(TimeStampedModel):
verbose_name="Urbanization (Puerto Rico only)", verbose_name="Urbanization (Puerto Rico only)",
) )
type_of_work = models.TextField( about_your_organization = models.TextField(
null=True, null=True,
blank=True, blank=True,
help_text="Type of work of the organization", help_text="Information about your organization",
)
more_organization_information = models.TextField(
null=True,
blank=True,
help_text="Further information about the government organization",
) )
authorizing_official = models.ForeignKey( authorizing_official = models.ForeignKey(

View file

@ -23,8 +23,8 @@ class PublicContact(TimeStampedModel):
"""These are the types of contacts accepted by the registry.""" """These are the types of contacts accepted by the registry."""
REGISTRANT = "registrant", "Registrant" REGISTRANT = "registrant", "Registrant"
ADMINISTRATIVE = "administrative", "Administrative" ADMINISTRATIVE = "admin", "Administrative"
TECHNICAL = "technical", "Technical" TECHNICAL = "tech", "Technical"
SECURITY = "security", "Security" SECURITY = "security", "Security"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -149,4 +149,8 @@ class PublicContact(TimeStampedModel):
) )
def __str__(self): def __str__(self):
return f"{self.name} <{self.email}>" return (
f"{self.name} <{self.email}>"
f"id: {self.registry_id} "
f"type: {self.contact_type}"
)

View file

@ -0,0 +1,42 @@
from django.db import models
from .utility.time_stamped_model import TimeStampedModel
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,
verbose_name="Username",
help_text="Username - this will be an email address",
)
domain_name = models.TextField(
null=True,
blank=True,
verbose_name="Domain name",
)
status = models.CharField(
max_length=255,
null=False,
blank=True,
choices=StatusChoices.choices,
verbose_name="Status",
help_text="domain status during the transfer",
)
email_sent = models.BooleanField(
null=False,
default=False,
verbose_name="email sent",
help_text="indicates whether email was sent",
)
def __str__(self):
return self.username

View file

@ -4,17 +4,30 @@
{% for app in app_list %} {% for app in app_list %}
<div class="app-{{ app.app_label }} module{% if app.app_url in request.path|urlencode %} current-app{% endif %}"> <div class="app-{{ app.app_label }} module{% if app.app_url in request.path|urlencode %} current-app{% endif %}">
<table> <table>
<caption>
<a href="{{ app.app_url }}" class="section" title="{% blocktranslate with name=app.name %}Models in the {{ name }} application{% endblocktranslate %}">{{ app.name }}</a>
</caption>
{# .gov override #} {# .gov override: add headers #}
{% if show_changelinks %}
<colgroup span="3"></colgroup>
{% else %}
<colgroup span="2"></colgroup>
{% endif %}
<thead> <thead>
<tr> <tr>
<th scope="col">Model</th>
<th><span class="display-inline-block min-width-25">Add</span></th>
{% if show_changelinks %} {% if show_changelinks %}
<th> <th colspan="3" class="primary-th" scope="colgroup">
{{ app.name }}
</th>
{% else %}
<th colspan="2" class="primary-th" scope="colgroup">
{{ app.name }}
</th>
{% endif %}
</tr>
<tr>
<th scope="col">Model</th>
<th scope="col"><span class="display-inline-block min-width-25">Add</span></th>
{% if show_changelinks %}
<th scope="col">
<span class="display-inline-block min-width-81"> <span class="display-inline-block min-width-81">
{% translate 'View/Change' %}</th> {% translate 'View/Change' %}</th>
</span> </span>

View file

@ -2,6 +2,24 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% block extrahead %}
<link rel="icon" type="image/png" sizes="32x32"
href="{% static 'img/registrar/favicons/favicon-32.png' %}"
>
<link rel="icon" type="image/png" sizes="192x192"
href="{% static 'img/registrar/favicons/favicon-192.png' %}"
>
<link rel="icon" type="image/svg+xml"
href="{% static 'img/registrar/favicons/favicon.svg' %}"
>
<link rel="shortcut icon" type="image/x-icon"
href="{% static 'img/registrar/favicons/favicon.ico' %}"
>
<link rel="apple-touch-icon" size="180x180"
href="{% static 'img/registrar/favicons/favicon-180.png' %}"
>
{% endblock %}
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
{% block extrastyle %}{{ block.super }} {% block extrastyle %}{{ block.super }}

View file

@ -0,0 +1,20 @@
{% extends "admin/change_form.html" %}
{% comment %} Replace the Django ul markup with a div. We'll edit the child markup accordingly in change_form_object_tools {% endcomment %}
{% block object-tools %}
{% if change and not is_popup %}
<div class="object-tools">
{% block object-tools-items %}
{{ block.super }}
{% endblock %}
</div>
{% endif %}
{% endblock %}
{% block submit_buttons_top %}
{# Do not render the submit buttons #}
{% endblock %}
{% block submit_buttons_bottom %}
{# Do not render the submit buttons #}
{% endblock %}

View file

@ -0,0 +1,23 @@
{% extends 'application_form.html' %}
{% load field_helpers %}
{% block form_instructions %}
<p>Wed like to know more about your organization. Include the following in your response: </p>
<ul class="usa-list">
<li>The type of work your organization does </li>
<li>How your organization is a government organization that is independent of a state government </li>
<li>Include links to authorizing legislation, applicable bylaws or charter, or other documentation to support your claims.</li>
</ul>
</p>
{% endblock %}
{% block form_required_fields_help_text %}
<p class="text-semibold"><abbr class="usa-hint usa-hint--required" title="required">*</abbr>This question is required.</p>
{% endblock %}
{% block form_fields %}
{% with attr_maxlength=1000 add_label_class="usa-sr-only" %}
{% input_with_errors forms.0.about_your_organization %}
{% endwith %}
{% endblock %}

View file

@ -46,9 +46,8 @@
Incomplete Incomplete
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if step == Step.TYPE_OF_WORK %} {% if step == Step.ABOUT_YOUR_ORGANIZATION %}
<p>{{ application.type_of_work|default:"Incomplete" }}</p> <p>{{ application.about_your_organization|default:"Incomplete" }}</p>
<p>{{ application.more_organization_information|default:"Incomplete" }}</p>
{% endif %} {% endif %}
{% if step == Step.AUTHORIZING_OFFICIAL %} {% if step == Step.AUTHORIZING_OFFICIAL %}
{% if application.authorizing_official %} {% if application.authorizing_official %}

View file

@ -6,6 +6,15 @@
{% block content %} {% block content %}
<main id="main-content" class="grid-container"> <main id="main-content" class="grid-container">
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8"> <div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
<a href="{% url 'home' %}" class="breadcrumb__back">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
</svg>
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
Back to manage your domains
</p>
</a>
<h1>Domain request for {{ domainapplication.requested_domain.name }}</h1> <h1>Domain request for {{ domainapplication.requested_domain.name }}</h1>
<div <div
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2" class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
@ -68,12 +77,8 @@
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=domainapplication address='true' heading_level=heading_level %} {% include "includes/summary_item.html" with title='Organization name and mailing address' value=domainapplication address='true' heading_level=heading_level %}
{% endif %} {% endif %}
{% if domainapplication.type_of_work %} {% if domainapplication.about_your_organization %}
{% include "includes/summary_item.html" with title='Type of work' value=domainapplication.type_of_work heading_level=heading_level %} {% include "includes/summary_item.html" with title='About your organization' value=domainapplication.about_your_organization heading_level=heading_level %}
{% endif %}
{% if domainapplication.more_organization_information %}
{% include "includes/summary_item.html" with title='More information about your organization' value=domainapplication.more_organization_information heading_level=heading_level %}
{% endif %} {% endif %}
{% if domainapplication.authorizing_official %} {% if domainapplication.authorizing_official %}

View file

@ -1,10 +0,0 @@
{% extends 'application_form.html' %}
{% load field_helpers %}
{% block form_fields %}
{% with attr_maxlength=1000 %}
{% input_with_errors forms.0.type_of_work %}
{% input_with_errors forms.0.more_organization_information %}
{% endwith %}
{% endblock %}

View file

@ -8,8 +8,14 @@
{% block field_sets %} {% block field_sets %}
<div class="submit-row"> <div class="submit-row">
<input id="manageDomainSubmitButton" type="submit" value="Manage Domain" name="_edit_domain"> {% if original.state == original.State.READY %}
<input type="submit" value="Place hold" name="_place_client_hold"> <input type="submit" value="Place hold" name="_place_client_hold">
{% elif original.state == original.State.ON_HOLD %}
<input type="submit" value="Remove hold" name="_remove_client_hold">
{% endif %}
<input id="manageDomainSubmitButton" type="submit" value="Manage Domain" name="_edit_domain">
<input type="submit" value="get status" name="_get_status">
<input type="submit" value="EPP Delete Domain" name="_delete_domain">
</div> </div>
{{ block.super }} {{ block.super }}
{% endblock %} {% endblock %}

View file

@ -10,8 +10,7 @@
<h1>Authorizing official</h1> <h1>Authorizing official</h1>
<p>Your authorizing official is the person within your organization who can <p>Your authorizing official is the person within your organization who can
authorize domain requests. This is generally the highest-ranking or authorize domain requests. This person must be in a role of significant, executive responsibility within the organization. Read more about <a class="usa-link" href="{% public_site_url 'domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization' %}">who can serve as an authorizing official</a>.</p>
highest-elected official in your organization. Read more about <a class="usa-link" href="{% public_site_url 'domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization' %}">who can serve as an authorizing official</a>.</p>
{% include "includes/required_fields.html" %} {% include "includes/required_fields.html" %}

View file

@ -6,7 +6,7 @@
<div class="margin-top-4 tablet:grid-col-10"> <div class="margin-top-4 tablet:grid-col-10">
{% url 'domain-nameservers' pk=domain.id as url %} {% url 'domain-nameservers' pk=domain.id as url %}
{% if domain.nameservers %} {% if domain.nameservers|length > 0 %}
{% include "includes/summary_item.html" with title='DNS name servers' value=domain.nameservers list='true' edit_link=url %} {% include "includes/summary_item.html" with title='DNS name servers' value=domain.nameservers list='true' edit_link=url %}
{% else %} {% else %}
<h2 class="margin-top-neg-1"> DNS name servers </h2> <h2 class="margin-top-neg-1"> DNS name servers </h2>

View file

@ -20,7 +20,7 @@
<tbody> <tbody>
{% for permission in domain.permissions.all %} {% for permission in domain.permissions.all %}
<tr> <tr>
<th scope="row" role="rowheader" data-sort-value="{{ user.email }}" data-label="Email"> <th scope="row" role="rowheader" data-sort-value="{{ permission.user.email }}" data-label="Email">
{{ permission.user.email }} {{ permission.user.email }}
</th> </th>
<td data-label="Role">{{ permission.role|title }}</td> <td data-label="Role">{{ permission.role|title }}</td>
@ -57,7 +57,7 @@
<tbody> <tbody>
{% for invitation in domain.invitations.all %} {% for invitation in domain.invitations.all %}
<tr> <tr>
<th scope="row" role="rowheader" data-sort-value="{{ user.email }}" data-label="Email"> <th scope="row" role="rowheader" data-sort-value="{{ invitation.user.email }}" data-label="Email">
{{ invitation.email }} {{ invitation.email }}
</th> </th>
<td data-sort-value="{{ invitation.created_at|date:"U" }}" data-label="Date created">{{ invitation.created_at|date }} </td> <td data-sort-value="{{ invitation.created_at|date:"U" }}" data-label="Date created">{{ invitation.created_at|date }} </td>

View file

@ -10,9 +10,9 @@ Organization name and mailing address:
{{ application.city }}, {{ application.state_territory }} {{ application.city }}, {{ application.state_territory }}
{{ application.zipcode }}{% if application.urbanization %} {{ application.zipcode }}{% if application.urbanization %}
{{ application.urbanization }}{% endif %}{% endspaceless %} {{ application.urbanization }}{% endif %}{% endspaceless %}
{% if application.type_of_work %}{# if block makes one newline if it's false #} {% if application.about_your_organization %}{# if block makes one newline if it's false #}
Type of work: About your organization:
{% spaceless %}{{ application.type_of_work }}{% endspaceless %} {% spaceless %}{{ application.about_your_organization }}{% endspaceless %}
{% endif %} {% endif %}
Authorizing official: Authorizing official:
{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.authorizing_official %}{% endspaceless %} {% spaceless %}{% include "emails/includes/contact.txt" with contact=application.authorizing_official %}{% endspaceless %}

View file

@ -1,10 +1,12 @@
import datetime
import os import os
import logging import logging
from contextlib import contextmanager from contextlib import contextmanager
import random import random
from string import ascii_uppercase from string import ascii_uppercase
from unittest.mock import Mock from django.test import TestCase
from unittest.mock import MagicMock, Mock, patch
from typing import List, Dict from typing import List, Dict
from django.conf import settings from django.conf import settings
@ -18,8 +20,15 @@ from registrar.models import (
DomainInvitation, DomainInvitation,
User, User,
DomainInformation, DomainInformation,
PublicContact,
Domain, Domain,
) )
from epplibwrapper import (
commands,
common,
RegistryError,
ErrorCode,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -241,7 +250,7 @@ class AuditedAdminMockData:
is_policy_acknowledged: boolean = True, is_policy_acknowledged: boolean = True,
state_territory: str = "NY", state_territory: str = "NY",
zipcode: str = "10002", zipcode: str = "10002",
type_of_work: str = "e-Government", about_your_organization: str = "e-Government",
anything_else: str = "There is more", anything_else: str = "There is more",
authorizing_official: Contact = self.dummy_contact(item_name, "authorizing_official"), authorizing_official: Contact = self.dummy_contact(item_name, "authorizing_official"),
submitter: Contact = self.dummy_contact(item_name, "submitter"), submitter: Contact = self.dummy_contact(item_name, "submitter"),
@ -258,7 +267,7 @@ class AuditedAdminMockData:
is_policy_acknowledged=True, is_policy_acknowledged=True,
state_territory="NY", state_territory="NY",
zipcode="10002", zipcode="10002",
type_of_work="e-Government", about_your_organization="e-Government",
anything_else="There is more", anything_else="There is more",
authorizing_official=self.dummy_contact(item_name, "authorizing_official"), authorizing_official=self.dummy_contact(item_name, "authorizing_official"),
submitter=self.dummy_contact(item_name, "submitter"), submitter=self.dummy_contact(item_name, "submitter"),
@ -430,15 +439,21 @@ def create_user():
return User.objects.create_user( return User.objects.create_user(
username="staffuser", username="staffuser",
email="user@example.com", email="user@example.com",
is_staff=True,
password=p, password=p,
) )
def create_ready_domain():
domain, _ = Domain.objects.get_or_create(name="city.gov", state=Domain.State.READY)
return domain
def completed_application( def completed_application(
has_other_contacts=True, has_other_contacts=True,
has_current_website=True, has_current_website=True,
has_alternative_gov_domain=True, has_alternative_gov_domain=True,
has_type_of_work=True, has_about_your_organization=True,
has_anything_else=True, has_anything_else=True,
status=DomainApplication.STARTED, status=DomainApplication.STARTED,
user=False, user=False,
@ -486,8 +501,8 @@ def completed_application(
creator=user, creator=user,
status=status, status=status,
) )
if has_type_of_work: if has_about_your_organization:
domain_application_kwargs["type_of_work"] = "e-Government" domain_application_kwargs["about_your_organization"] = "e-Government"
if has_anything_else: if has_anything_else:
domain_application_kwargs["anything_else"] = "There is more" domain_application_kwargs["anything_else"] = "There is more"
@ -526,3 +541,121 @@ def generic_domain_object(domain_type, object_name):
mock = AuditedAdminMockData() mock = AuditedAdminMockData()
application = mock.create_full_dummy_domain_object(domain_type, object_name) application = mock.create_full_dummy_domain_object(domain_type, object_name)
return application return application
class MockEppLib(TestCase):
class fakedEppObject(object):
""""""
def __init__(self, auth_info=..., cr_date=..., contacts=..., hosts=...):
self.auth_info = auth_info
self.cr_date = cr_date
self.contacts = contacts
self.hosts = hosts
mockDataInfoDomain = fakedEppObject(
"fakepw",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
contacts=[common.DomainContact(contact="123", type="security")],
hosts=["fake.host.com"],
)
infoDomainNoContact = fakedEppObject(
"security",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
contacts=[],
hosts=["fake.host.com"],
)
mockDataInfoContact = fakedEppObject(
"anotherPw", cr_date=datetime.datetime(2023, 7, 25, 19, 45, 35)
)
mockDataInfoHosts = fakedEppObject(
"lastPw", cr_date=datetime.datetime(2023, 8, 25, 19, 45, 35)
)
def mockSend(self, _request, cleaned):
"""Mocks the registry.send function used inside of domain.py
registry is imported from epplibwrapper
returns objects that simulate what would be in a epp response
but only relevant pieces for tests"""
if isinstance(_request, commands.InfoDomain):
if getattr(_request, "name", None) == "security.gov":
return MagicMock(res_data=[self.infoDomainNoContact])
return MagicMock(res_data=[self.mockDataInfoDomain])
elif isinstance(_request, commands.InfoContact):
return MagicMock(res_data=[self.mockDataInfoContact])
elif (
isinstance(_request, commands.CreateContact)
and getattr(_request, "id", None) == "fail"
and self.mockedSendFunction.call_count == 3
):
# use this for when a contact is being updated
# sets the second send() to fail
raise RegistryError(code=ErrorCode.OBJECT_EXISTS)
return MagicMock(res_data=[self.mockDataInfoHosts])
def setUp(self):
"""mock epp send function as this will fail locally"""
self.mockSendPatch = patch("registrar.models.domain.registry.send")
self.mockedSendFunction = self.mockSendPatch.start()
self.mockedSendFunction.side_effect = self.mockSend
def _convertPublicContactToEpp(
self, contact: PublicContact, disclose_email=False, createContact=True
):
DF = common.DiscloseField
fields = {DF.FAX, DF.VOICE, DF.ADDR}
if not disclose_email:
fields.add(DF.EMAIL)
di = common.Disclose(
flag=False,
fields=fields,
types={DF.ADDR: "loc"},
)
# check docs here looks like we may have more than one address field but
addr = common.ContactAddr(
[
getattr(contact, street)
for street in ["street1", "street2", "street3"]
if hasattr(contact, street)
], # type: ignore
city=contact.city,
pc=contact.pc,
cc=contact.cc,
sp=contact.sp,
) # type: ignore
pi = common.PostalInfo(
name=contact.name,
addr=addr,
org=contact.org,
type="loc",
)
ai = common.ContactAuthInfo(pw="2fooBAR123fooBaz")
if createContact:
return commands.CreateContact(
id=contact.registry_id,
postal_info=pi, # type: ignore
email=contact.email,
voice=contact.voice,
fax=contact.fax,
auth_info=ai,
disclose=di,
vat=None,
ident=None,
notify_email=None,
) # type: ignore
else:
return commands.UpdateContact(
id=contact.registry_id,
postal_info=pi,
email=contact.email,
voice=contact.voice,
fax=contact.fax,
)
def tearDown(self):
self.mockSendPatch.stop()

View file

@ -7,11 +7,13 @@ from django.urls import reverse
from registrar.admin import ( from registrar.admin import (
DomainAdmin, DomainAdmin,
DomainApplicationAdmin, DomainApplicationAdmin,
DomainApplicationAdminForm,
ListHeaderAdmin, ListHeaderAdmin,
MyUserAdmin, MyUserAdmin,
AuditedAdmin, AuditedAdmin,
) )
from registrar.models import ( from registrar.models import (
Domain,
DomainApplication, DomainApplication,
DomainInformation, DomainInformation,
Domain, Domain,
@ -24,11 +26,14 @@ from .common import (
mock_user, mock_user,
create_superuser, create_superuser,
create_user, create_user,
create_ready_domain,
multiple_unalphabetical_domain_objects, multiple_unalphabetical_domain_objects,
MockEppLib,
) )
from django.contrib.sessions.backends.db import SessionStore from django.contrib.sessions.backends.db import SessionStore
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from unittest.mock import patch from unittest.mock import patch
from unittest import skip
from django.conf import settings from django.conf import settings
from unittest.mock import MagicMock from unittest.mock import MagicMock
@ -38,6 +43,102 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class TestDomainAdmin(MockEppLib):
def setUp(self):
self.site = AdminSite()
self.admin = DomainAdmin(model=Domain, admin_site=self.site)
self.client = Client(HTTP_HOST="localhost:8080")
self.superuser = create_superuser()
self.staffuser = create_user()
super().setUp()
def test_place_and_remove_hold(self):
domain = create_ready_domain()
# get admin page and assert Place Hold button
p = "userpass"
self.client.login(username="staffuser", password=p)
response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.pk),
follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Place hold")
self.assertNotContains(response, "Remove hold")
# submit place_client_hold and assert Remove Hold button
response = self.client.post(
"/admin/registrar/domain/{}/change/".format(domain.pk),
{"_place_client_hold": "Place hold", "name": domain.name},
follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Remove hold")
self.assertNotContains(response, "Place hold")
# submit remove client hold and assert Place hold button
response = self.client.post(
"/admin/registrar/domain/{}/change/".format(domain.pk),
{"_remove_client_hold": "Remove hold", "name": domain.name},
follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Place hold")
self.assertNotContains(response, "Remove hold")
@skip("Waiting on epp lib to implement")
def test_place_and_remove_hold_epp(self):
raise
def tearDown(self):
User.objects.all().delete()
super().tearDown()
class TestDomainApplicationAdminForm(TestCase):
def setUp(self):
# Create a test application with an initial state of started
self.application = completed_application()
def test_form_choices(self):
# Create a form instance with the test application
form = DomainApplicationAdminForm(instance=self.application)
# Verify that the form choices match the available transitions for started
expected_choices = [("started", "started"), ("submitted", "submitted")]
self.assertEqual(form.fields["status"].widget.choices, expected_choices)
def test_form_choices_when_no_instance(self):
# Create a form instance without an instance
form = DomainApplicationAdminForm()
# Verify that the form choices show all choices when no instance is provided;
# this is necessary to show all choices when creating a new domain
# application in django admin;
# note that FSM ensures that no domain application exists with invalid status,
# so don't need to test for invalid status
self.assertEqual(
form.fields["status"].widget.choices,
DomainApplication._meta.get_field("status").choices,
)
def test_form_choices_when_ineligible(self):
# Create a form instance with a domain application with ineligible status
ineligible_application = DomainApplication(status="ineligible")
# Attempt to create a form with the ineligible application
# The form should not raise an error, but choices should be the
# full list of possible choices
form = DomainApplicationAdminForm(instance=ineligible_application)
self.assertEqual(
form.fields["status"].widget.choices,
DomainApplication._meta.get_field("status").choices,
)
class TestDomainApplicationAdmin(TestCase): class TestDomainApplicationAdmin(TestCase):
def setUp(self): def setUp(self):
self.site = AdminSite() self.site = AdminSite()
@ -343,8 +444,7 @@ class TestDomainApplicationAdmin(TestCase):
"state_territory", "state_territory",
"zipcode", "zipcode",
"urbanization", "urbanization",
"type_of_work", "about_your_organization",
"more_organization_information",
"authorizing_official", "authorizing_official",
"approved_domain", "approved_domain",
"requested_domain", "requested_domain",
@ -368,8 +468,7 @@ class TestDomainApplicationAdmin(TestCase):
expected_fields = [ expected_fields = [
"creator", "creator",
"type_of_work", "about_your_organization",
"more_organization_information",
"address_line1", "address_line1",
"address_line2", "address_line2",
"zipcode", "zipcode",

View file

@ -48,7 +48,7 @@ class TestEmails(TestCase):
self.assertIn("Testy2 Tester2", body) self.assertIn("Testy2 Tester2", body)
self.assertIn("Current website for your organization:", body) self.assertIn("Current website for your organization:", body)
self.assertIn("city.com", body) self.assertIn("city.com", body)
self.assertIn("Type of work:", body) self.assertIn("About your organization:", body)
self.assertIn("Anything else", body) self.assertIn("Anything else", body)
@boto3_mocking.patching @boto3_mocking.patching
@ -126,26 +126,26 @@ class TestEmails(TestCase):
self.assertRegex(body, r"city.gov\n\nPurpose of your domain:") self.assertRegex(body, r"city.gov\n\nPurpose of your domain:")
@boto3_mocking.patching @boto3_mocking.patching
def test_submission_confirmation_type_of_work_spacing(self): def test_submission_confirmation_about_your_organization_spacing(self):
"""Test line spacing with type of work.""" """Test line spacing with about your organization."""
application = completed_application(has_type_of_work=True) application = completed_application(has_about_your_organization=True)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
application.submit() application.submit()
_, kwargs = self.mock_client.send_email.call_args _, kwargs = self.mock_client.send_email.call_args
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
self.assertIn("Type of work:", body) self.assertIn("About your organization:", body)
# spacing should be right between adjacent elements # spacing should be right between adjacent elements
self.assertRegex(body, r"10002\n\nType of work:") self.assertRegex(body, r"10002\n\nAbout your organization:")
@boto3_mocking.patching @boto3_mocking.patching
def test_submission_confirmation_no_type_of_work_spacing(self): def test_submission_confirmation_no_about_your_organization_spacing(self):
"""Test line spacing without type of work.""" """Test line spacing without about your organization."""
application = completed_application(has_type_of_work=False) application = completed_application(has_about_your_organization=False)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
application.submit() application.submit()
_, kwargs = self.mock_client.send_email.call_args _, kwargs = self.mock_client.send_email.call_args
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
self.assertNotIn("Type of work:", body) self.assertNotIn("About your organization:", body)
# spacing should be right between adjacent elements # spacing should be right between adjacent elements
self.assertRegex(body, r"10002\n\nAuthorizing official:") self.assertRegex(body, r"10002\n\nAuthorizing official:")

View file

@ -13,7 +13,7 @@ from registrar.forms.application_wizard import (
TribalGovernmentForm, TribalGovernmentForm,
PurposeForm, PurposeForm,
AnythingElseForm, AnythingElseForm,
TypeOfWorkForm, AboutYourOrganizationForm,
) )
from registrar.forms.domain import ContactForm from registrar.forms.domain import ContactForm
@ -118,7 +118,7 @@ class TestFormValidation(TestCase):
["Response must be less than 1000 characters."], ["Response must be less than 1000 characters."],
) )
def test_anything_else_form_type_of_work_character_count_invalid(self): def test_anything_else_form_about_your_organization_character_count_invalid(self):
"""Response must be less than 1000 characters.""" """Response must be less than 1000 characters."""
form = AnythingElseForm( form = AnythingElseForm(
data={ data={
@ -147,43 +147,12 @@ class TestFormValidation(TestCase):
["Response must be less than 1000 characters."], ["Response must be less than 1000 characters."],
) )
def test_anything_else_form_more_organization_information_character_count_invalid(
self,
):
"""Response must be less than 1000 characters."""
form = TypeOfWorkForm(
data={
"more_organization_information": "Bacon ipsum dolor amet fatback"
"shankle, drumstick doner chicken landjaeger turkey andouille."
"Buffalo biltong chuck pork chop tongue bresaola turkey. Doner"
"ground round strip steak, jowl tail chuck ribeye bacon"
"beef ribs swine filet ball tip pancetta strip steak sirloin"
"mignon ham spare ribs rump. Tail shank biltong beef ribs doner"
"buffalo swine bacon. Tongue cow picanha brisket bacon chuck"
"leberkas pork loin pork, drumstick capicola. Doner short loin"
"ground round fatback turducken chislic shoulder turducken"
"spare ribs, burgdoggen kielbasa kevin frankfurter ball tip"
"pancetta cupim. Turkey meatball andouille porchetta hamburger"
"pork chop corned beef. Brisket short ribs turducken, pork chop"
"chislic turkey ball pork chop leberkas rump, rump bacon, jowl"
"tip ham. Shankle salami tongue venison short ribs kielbasa"
"tri-tip ham hock swine hamburger. Flank meatball corned beef"
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
"beef ribs rump jowl tenderloin swine sausage biltong"
"bacon rump tail boudin meatball boudin meatball boudin"
"strip steak pastrami."
}
)
self.assertEqual(
form.errors["more_organization_information"],
["Response must be less than 1000 characters."],
)
def test_anything_else_form_character_count_invalid(self): def test_anything_else_form_character_count_invalid(self):
"""Response must be less than 1000 characters.""" """Response must be less than 1000 characters."""
form = TypeOfWorkForm( form = AboutYourOrganizationForm(
data={ data={
"type_of_work": "Bacon ipsum dolor amet fatback strip steak pastrami" "about_your_organization": "Bacon ipsum dolor amet fatback"
"strip steak pastrami"
"shankle, drumstick doner chicken landjaeger turkey andouille." "shankle, drumstick doner chicken landjaeger turkey andouille."
"Buffalo biltong chuck pork chop tongue bresaola turkey. Doner" "Buffalo biltong chuck pork chop tongue bresaola turkey. Doner"
"ground round strip steak, jowl tail chuck ribeye bacon" "ground round strip steak, jowl tail chuck ribeye bacon"
@ -204,7 +173,7 @@ class TestFormValidation(TestCase):
} }
) )
self.assertEqual( self.assertEqual(
form.errors["type_of_work"], form.errors["about_your_organization"],
["Response must be less than 1000 characters."], ["Response must be less than 1000 characters."],
) )

View file

@ -5,54 +5,25 @@ This file tests the various ways in which the registrar interacts with the regis
""" """
from django.test import TestCase from django.test import TestCase
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from unittest.mock import patch, MagicMock from unittest.mock import patch, call
import datetime import datetime
from registrar.models import Domain # add in DomainApplication, User, from registrar.models import Domain
from unittest import skip from unittest import skip
from epplibwrapper import commands from registrar.models.domain_application import DomainApplication
from registrar.models.domain_information import DomainInformation
from registrar.models.draft_domain import DraftDomain
from registrar.models.public_contact import PublicContact
from registrar.models.user import User
from .common import MockEppLib
from epplibwrapper import (
class TestDomainCache(TestCase): commands,
class fakedEppObject(object): common,
""""""
def __init__(self, auth_info=..., cr_date=..., contacts=..., hosts=...):
self.auth_info = auth_info
self.cr_date = cr_date
self.contacts = contacts
self.hosts = hosts
mockDataInfoDomain = fakedEppObject(
"fakepw",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
contacts=["123"],
hosts=["fake.host.com"],
)
mockDataInfoContact = fakedEppObject(
"anotherPw", cr_date=datetime.datetime(2023, 7, 25, 19, 45, 35)
)
mockDataInfoHosts = fakedEppObject(
"lastPw", cr_date=datetime.datetime(2023, 8, 25, 19, 45, 35)
) )
def mockSend(self, _request, cleaned):
""""""
if isinstance(_request, commands.InfoDomain):
return MagicMock(res_data=[self.mockDataInfoDomain])
elif isinstance(_request, commands.InfoContact):
return MagicMock(res_data=[self.mockDataInfoContact])
return MagicMock(res_data=[self.mockDataInfoHosts])
def setUp(self):
"""mock epp send function as this will fail locally"""
self.patcher = patch("registrar.models.domain.registry.send")
self.mock_foo = self.patcher.start()
self.mock_foo.side_effect = self.mockSend
def tearDown(self):
self.patcher.stop()
class TestDomainCache(MockEppLib):
def test_cache_sets_resets(self): def test_cache_sets_resets(self):
"""Cache should be set on getter and reset on setter calls""" """Cache should be set on getter and reset on setter calls"""
domain, _ = Domain.objects.get_or_create(name="igorville.gov") domain, _ = Domain.objects.get_or_create(name="igorville.gov")
@ -66,11 +37,20 @@ class TestDomainCache(TestCase):
self.assertFalse("avail" in domain._cache.keys()) self.assertFalse("avail" in domain._cache.keys())
# using a setter should clear the cache # using a setter should clear the cache
domain.nameservers = [("", "")] domain.expiration_date = datetime.date.today()
self.assertEquals(domain._cache, {}) self.assertEquals(domain._cache, {})
# send should have been called only once # send should have been called only once
self.mock_foo.assert_called_once() self.mockedSendFunction.assert_has_calls(
[
call(
commands.InfoDomain(name="igorville.gov", auth_info=None),
cleaned=True,
),
call(commands.InfoContact(id="123", auth_info=None), cleaned=True),
call(commands.InfoHost(name="fake.host.com"), cleaned=True),
]
)
def test_cache_used_when_avail(self): def test_cache_used_when_avail(self):
"""Cache is pulled from if the object has already been accessed""" """Cache is pulled from if the object has already been accessed"""
@ -85,7 +65,15 @@ class TestDomainCache(TestCase):
self.assertEqual(domain._cache["cr_date"], self.mockDataInfoDomain.cr_date) self.assertEqual(domain._cache["cr_date"], self.mockDataInfoDomain.cr_date)
# send was only called once & not on the second getter call # send was only called once & not on the second getter call
self.mock_foo.assert_called_once() expectedCalls = [
call(
commands.InfoDomain(name="igorville.gov", auth_info=None), cleaned=True
),
call(commands.InfoContact(id="123", auth_info=None), cleaned=True),
call(commands.InfoHost(name="fake.host.com"), cleaned=True),
]
self.mockedSendFunction.assert_has_calls(expectedCalls)
def test_cache_nested_elements(self): def test_cache_nested_elements(self):
"""Cache works correctly with the nested objects cache and hosts""" """Cache works correctly with the nested objects cache and hosts"""
@ -93,7 +81,8 @@ class TestDomainCache(TestCase):
# the cached contacts and hosts should be dictionaries of what is passed to them # the cached contacts and hosts should be dictionaries of what is passed to them
expectedContactsDict = { expectedContactsDict = {
"id": self.mockDataInfoDomain.contacts[0], "id": self.mockDataInfoDomain.contacts[0].contact,
"type": self.mockDataInfoDomain.contacts[0].type,
"auth_info": self.mockDataInfoContact.auth_info, "auth_info": self.mockDataInfoContact.auth_info,
"cr_date": self.mockDataInfoContact.cr_date, "cr_date": self.mockDataInfoContact.cr_date,
} }
@ -127,7 +116,6 @@ class TestDomainCreation(TestCase):
Given that a valid domain application exists Given that a valid domain application exists
""" """
@skip("not implemented yet")
def test_approved_application_creates_domain_locally(self): def test_approved_application_creates_domain_locally(self):
""" """
Scenario: Analyst approves a domain application Scenario: Analyst approves a domain application
@ -135,7 +123,21 @@ class TestDomainCreation(TestCase):
Then a Domain exists in the database with the same `name` Then a Domain exists in the database with the same `name`
But a domain object does not exist in the registry But a domain object does not exist in the registry
""" """
raise patcher = patch("registrar.models.domain.Domain._get_or_create_domain")
mocked_domain_creation = patcher.start()
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(
creator=user, requested_domain=draft_domain
)
# skip using the submit method
application.status = DomainApplication.SUBMITTED
# transition to approve state
application.approve()
# should hav information present for this domain
domain = Domain.objects.get(name="igorville.gov")
self.assertTrue(domain)
mocked_domain_creation.assert_not_called()
@skip("not implemented yet") @skip("not implemented yet")
def test_accessing_domain_properties_creates_domain_in_registry(self): def test_accessing_domain_properties_creates_domain_in_registry(self):
@ -149,6 +151,7 @@ class TestDomainCreation(TestCase):
""" """
raise raise
@skip("assertion broken with mock addition")
def test_empty_domain_creation(self): def test_empty_domain_creation(self):
"""Can't create a completely empty domain.""" """Can't create a completely empty domain."""
with self.assertRaisesRegex(IntegrityError, "name"): with self.assertRaisesRegex(IntegrityError, "name"):
@ -158,6 +161,7 @@ class TestDomainCreation(TestCase):
"""Can create with just a name.""" """Can create with just a name."""
Domain.objects.create(name="igorville.gov") Domain.objects.create(name="igorville.gov")
@skip("assertion broken with mock addition")
def test_duplicate_creation(self): def test_duplicate_creation(self):
"""Can't create domain if name is not unique.""" """Can't create domain if name is not unique."""
Domain.objects.create(name="igorville.gov") Domain.objects.create(name="igorville.gov")
@ -174,8 +178,13 @@ class TestDomainCreation(TestCase):
domain.save() domain.save()
self.assertIn("ok", domain.status) self.assertIn("ok", domain.status)
def tearDown(self) -> None:
DomainInformation.objects.all().delete()
DomainApplication.objects.all().delete()
Domain.objects.all().delete()
class TestRegistrantContacts(TestCase):
class TestRegistrantContacts(MockEppLib):
"""Rule: Registrants may modify their WHOIS data""" """Rule: Registrants may modify their WHOIS data"""
def setUp(self): def setUp(self):
@ -184,9 +193,14 @@ class TestRegistrantContacts(TestCase):
Given the registrant is logged in Given the registrant is logged in
And the registrant is the admin on a domain And the registrant is the admin on a domain
""" """
pass super().setUp()
self.domain, _ = Domain.objects.get_or_create(name="security.gov")
def tearDown(self):
super().tearDown()
# self.contactMailingAddressPatch.stop()
# self.createContactPatch.stop()
@skip("not implemented yet")
def test_no_security_email(self): def test_no_security_email(self):
""" """
Scenario: Registrant has not added a security contact email Scenario: Registrant has not added a security contact email
@ -195,9 +209,44 @@ class TestRegistrantContacts(TestCase):
Then the domain has a valid security contact with CISA defaults Then the domain has a valid security contact with CISA defaults
And disclose flags are set to keep the email address hidden And disclose flags are set to keep the email address hidden
""" """
raise
@skip("not implemented yet") # making a domain should make it domain
expectedSecContact = PublicContact.get_default_security()
expectedSecContact.domain = self.domain
self.domain.pendingCreate()
self.assertEqual(self.mockedSendFunction.call_count, 8)
self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 4)
self.assertEqual(
PublicContact.objects.get(
domain=self.domain,
contact_type=PublicContact.ContactTypeChoices.SECURITY,
).email,
expectedSecContact.email,
)
id = PublicContact.objects.get(
domain=self.domain,
contact_type=PublicContact.ContactTypeChoices.SECURITY,
).registry_id
expectedSecContact.registry_id = id
expectedCreateCommand = self._convertPublicContactToEpp(
expectedSecContact, disclose_email=False
)
expectedUpdateDomain = commands.UpdateDomain(
name=self.domain.name,
add=[
common.DomainContact(
contact=expectedSecContact.registry_id, type="security"
)
],
)
self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True)
self.mockedSendFunction.assert_any_call(expectedUpdateDomain, cleaned=True)
def test_user_adds_security_email(self): def test_user_adds_security_email(self):
""" """
Scenario: Registrant adds a security contact email Scenario: Registrant adds a security contact email
@ -207,9 +256,41 @@ class TestRegistrantContacts(TestCase):
And Domain sends `commands.UpdateDomain` to the registry with the newly And Domain sends `commands.UpdateDomain` to the registry with the newly
created contact of type 'security' created contact of type 'security'
""" """
raise # make a security contact that is a PublicContact
self.domain.pendingCreate() # make sure a security email already exists
expectedSecContact = PublicContact.get_default_security()
expectedSecContact.domain = self.domain
expectedSecContact.email = "newEmail@fake.com"
expectedSecContact.registry_id = "456"
expectedSecContact.name = "Fakey McFakerson"
# calls the security contact setter as if you did
# self.domain.security_contact=expectedSecContact
expectedSecContact.save()
# no longer the default email it should be disclosed
expectedCreateCommand = self._convertPublicContactToEpp(
expectedSecContact, disclose_email=True
)
expectedUpdateDomain = commands.UpdateDomain(
name=self.domain.name,
add=[
common.DomainContact(
contact=expectedSecContact.registry_id, type="security"
)
],
)
# check that send has triggered the create command for the contact
receivedSecurityContact = PublicContact.objects.get(
domain=self.domain, contact_type=PublicContact.ContactTypeChoices.SECURITY
)
self.assertEqual(receivedSecurityContact, expectedSecContact)
self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True)
self.mockedSendFunction.assert_any_call(expectedUpdateDomain, cleaned=True)
@skip("not implemented yet")
def test_security_email_is_idempotent(self): def test_security_email_is_idempotent(self):
""" """
Scenario: Registrant adds a security contact email twice, due to a UI glitch Scenario: Registrant adds a security contact email twice, due to a UI glitch
@ -217,12 +298,33 @@ class TestRegistrantContacts(TestCase):
to the registry twice with identical data to the registry twice with identical data
Then no errors are raised in Domain Then no errors are raised in Domain
""" """
# implementation note: this requires seeing what happens when these are actually
# sent like this, and then implementing appropriate mocks for any errors the
# registry normally sends in this case
raise
@skip("not implemented yet") security_contact = self.domain.get_default_security_contact()
security_contact.registry_id = "fail"
security_contact.save()
self.domain.security_contact = security_contact
expectedCreateCommand = self._convertPublicContactToEpp(
security_contact, disclose_email=False
)
expectedUpdateDomain = commands.UpdateDomain(
name=self.domain.name,
add=[
common.DomainContact(
contact=security_contact.registry_id, type="security"
)
],
)
expected_calls = [
call(expectedCreateCommand, cleaned=True),
call(expectedCreateCommand, cleaned=True),
call(expectedUpdateDomain, cleaned=True),
]
self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True)
self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 1)
def test_user_deletes_security_email(self): def test_user_deletes_security_email(self):
""" """
Scenario: Registrant clears out an existing security contact email Scenario: Registrant clears out an existing security contact email
@ -234,9 +336,64 @@ class TestRegistrantContacts(TestCase):
And the domain has a valid security contact with CISA defaults And the domain has a valid security contact with CISA defaults
And disclose flags are set to keep the email address hidden And disclose flags are set to keep the email address hidden
""" """
raise old_contact = self.domain.get_default_security_contact()
old_contact.registry_id = "fail"
old_contact.email = "user.entered@email.com"
old_contact.save()
new_contact = self.domain.get_default_security_contact()
new_contact.registry_id = "fail"
new_contact.email = ""
self.domain.security_contact = new_contact
firstCreateContactCall = self._convertPublicContactToEpp(
old_contact, disclose_email=True
)
updateDomainAddCall = commands.UpdateDomain(
name=self.domain.name,
add=[
common.DomainContact(contact=old_contact.registry_id, type="security")
],
)
self.assertEqual(
PublicContact.objects.filter(domain=self.domain).get().email,
PublicContact.get_default_security().email,
)
# this one triggers the fail
secondCreateContact = self._convertPublicContactToEpp(
new_contact, disclose_email=True
)
updateDomainRemCall = commands.UpdateDomain(
name=self.domain.name,
rem=[
common.DomainContact(contact=old_contact.registry_id, type="security")
],
)
defaultSecID = (
PublicContact.objects.filter(domain=self.domain).get().registry_id
)
default_security = PublicContact.get_default_security()
default_security.registry_id = defaultSecID
createDefaultContact = self._convertPublicContactToEpp(
default_security, disclose_email=False
)
updateDomainWDefault = commands.UpdateDomain(
name=self.domain.name,
add=[common.DomainContact(contact=defaultSecID, type="security")],
)
expected_calls = [
call(firstCreateContactCall, cleaned=True),
call(updateDomainAddCall, cleaned=True),
call(secondCreateContact, cleaned=True),
call(updateDomainRemCall, cleaned=True),
call(createDefaultContact, cleaned=True),
call(updateDomainWDefault, cleaned=True),
]
self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True)
@skip("not implemented yet")
def test_updates_security_email(self): def test_updates_security_email(self):
""" """
Scenario: Registrant replaces one valid security contact email with another Scenario: Registrant replaces one valid security contact email with another
@ -245,7 +402,39 @@ class TestRegistrantContacts(TestCase):
security contact email security contact email
Then Domain sends `commands.UpdateContact` to the registry Then Domain sends `commands.UpdateContact` to the registry
""" """
raise security_contact = self.domain.get_default_security_contact()
security_contact.email = "originalUserEmail@gmail.com"
security_contact.registry_id = "fail"
security_contact.save()
expectedCreateCommand = self._convertPublicContactToEpp(
security_contact, disclose_email=True
)
expectedUpdateDomain = commands.UpdateDomain(
name=self.domain.name,
add=[
common.DomainContact(
contact=security_contact.registry_id, type="security"
)
],
)
security_contact.email = "changedEmail@email.com"
security_contact.save()
expectedSecondCreateCommand = self._convertPublicContactToEpp(
security_contact, disclose_email=True
)
updateContact = self._convertPublicContactToEpp(
security_contact, disclose_email=True, createContact=False
)
expected_calls = [
call(expectedCreateCommand, cleaned=True),
call(expectedUpdateDomain, cleaned=True),
call(expectedSecondCreateCommand, cleaned=True),
call(updateContact, cleaned=True),
]
self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True)
self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 1)
@skip("not implemented yet") @skip("not implemented yet")
def test_update_is_unsuccessful(self): def test_update_is_unsuccessful(self):
@ -411,7 +600,7 @@ class TestRegistrantDNSSEC(TestCase):
def test_user_adds_dns_data(self): def test_user_adds_dns_data(self):
""" """
Scenario: Registrant adds DNS data Scenario: Registrant adds DNS data
...
""" """
raise raise
@ -419,7 +608,7 @@ class TestRegistrantDNSSEC(TestCase):
def test_dnssec_is_idempotent(self): def test_dnssec_is_idempotent(self):
""" """
Scenario: Registrant adds DNS data twice, due to a UI glitch Scenario: Registrant adds DNS data twice, due to a UI glitch
...
""" """
# implementation note: this requires seeing what happens when these are actually # implementation note: this requires seeing what happens when these are actually
# sent like this, and then implementing appropriate mocks for any errors the # sent like this, and then implementing appropriate mocks for any errors the

View file

@ -660,12 +660,14 @@ class DomainApplicationTests(TestWithUser, WebTest):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
contact_result = org_contact_form.submit() contact_result = org_contact_form.submit()
# the post request should return a redirect to the type of work page # the post request should return a redirect to the
# if it was successful. # about your organization page if it was successful.
self.assertEqual(contact_result.status_code, 302) self.assertEqual(contact_result.status_code, 302)
self.assertEqual(contact_result["Location"], "/register/type_of_work/") self.assertEqual(
contact_result["Location"], "/register/about_your_organization/"
)
def test_application_type_of_work_special(self): def test_application_about_your_organization_special(self):
"""Special districts have to answer an additional question.""" """Special districts have to answer an additional question."""
type_page = self.app.get(reverse("application:")).follow() type_page = self.app.get(reverse("application:")).follow()
# django-webtest does not handle cookie-based sessions well because it keeps # django-webtest does not handle cookie-based sessions well because it keeps
@ -684,7 +686,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
contact_page = type_result.follow() contact_page = type_result.follow()
self.assertContains(contact_page, self.TITLES[Step.TYPE_OF_WORK]) self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION])
def test_application_no_other_contacts(self): def test_application_no_other_contacts(self):
"""Applicants with no other contacts have to give a reason.""" """Applicants with no other contacts have to give a reason."""
@ -704,7 +706,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
actual_url_slug = no_contacts_page.request.path.split("/")[-2] actual_url_slug = no_contacts_page.request.path.split("/")[-2]
self.assertEqual(expected_url_slug, actual_url_slug) self.assertEqual(expected_url_slug, actual_url_slug)
def test_application_type_of_work_interstate(self): def test_application_about_your_organiztion_interstate(self):
"""Special districts have to answer an additional question.""" """Special districts have to answer an additional question."""
type_page = self.app.get(reverse("application:")).follow() type_page = self.app.get(reverse("application:")).follow()
# django-webtest does not handle cookie-based sessions well because it keeps # django-webtest does not handle cookie-based sessions well because it keeps
@ -723,7 +725,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
contact_page = type_result.follow() contact_page = type_result.follow()
self.assertContains(contact_page, self.TITLES[Step.TYPE_OF_WORK]) self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION])
def test_application_tribal_government(self): def test_application_tribal_government(self):
"""Tribal organizations have to answer an additional question.""" """Tribal organizations have to answer an additional question."""
@ -1313,6 +1315,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
page = result.follow() page = result.follow()
self.assertContains(page, "The name servers for this domain have been updated") self.assertContains(page, "The name servers for this domain have been updated")
@skip("Broken by adding registry connection fix in ticket 848")
def test_domain_nameservers_form_invalid(self): def test_domain_nameservers_form_invalid(self):
"""Can change domain's nameservers. """Can change domain's nameservers.
@ -1410,6 +1413,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
) )
self.assertContains(page, "Domain security email") self.assertContains(page, "Domain security email")
@skip("Ticket 912 needs to fix this one")
def test_domain_security_email_form(self): def test_domain_security_email_form(self):
"""Adding a security email works. """Adding a security email works.

View file

@ -30,7 +30,7 @@ class Step(StrEnum):
ORGANIZATION_FEDERAL = "organization_federal" ORGANIZATION_FEDERAL = "organization_federal"
ORGANIZATION_ELECTION = "organization_election" ORGANIZATION_ELECTION = "organization_election"
ORGANIZATION_CONTACT = "organization_contact" ORGANIZATION_CONTACT = "organization_contact"
TYPE_OF_WORK = "type_of_work" ABOUT_YOUR_ORGANIZATION = "about_your_organization"
AUTHORIZING_OFFICIAL = "authorizing_official" AUTHORIZING_OFFICIAL = "authorizing_official"
CURRENT_SITES = "current_sites" CURRENT_SITES = "current_sites"
DOTGOV_DOMAIN = "dotgov_domain" DOTGOV_DOMAIN = "dotgov_domain"
@ -77,7 +77,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
Step.ORGANIZATION_FEDERAL: _("Federal government branch"), Step.ORGANIZATION_FEDERAL: _("Federal government branch"),
Step.ORGANIZATION_ELECTION: _("Election office"), Step.ORGANIZATION_ELECTION: _("Election office"),
Step.ORGANIZATION_CONTACT: _("Organization name and mailing address"), Step.ORGANIZATION_CONTACT: _("Organization name and mailing address"),
Step.TYPE_OF_WORK: _("Type of work"), Step.ABOUT_YOUR_ORGANIZATION: _("About your organization"),
Step.AUTHORIZING_OFFICIAL: _("Authorizing official"), Step.AUTHORIZING_OFFICIAL: _("Authorizing official"),
Step.CURRENT_SITES: _("Current website for your organization"), Step.CURRENT_SITES: _("Current website for your organization"),
Step.DOTGOV_DOMAIN: _(".gov domain"), Step.DOTGOV_DOMAIN: _(".gov domain"),
@ -100,7 +100,9 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
Step.ORGANIZATION_ELECTION: lambda w: w.from_model( Step.ORGANIZATION_ELECTION: lambda w: w.from_model(
"show_organization_election", False "show_organization_election", False
), ),
Step.TYPE_OF_WORK: lambda w: w.from_model("show_type_of_work", False), Step.ABOUT_YOUR_ORGANIZATION: lambda w: w.from_model(
"show_about_your_organization", False
),
Step.NO_OTHER_CONTACTS: lambda w: w.from_model( Step.NO_OTHER_CONTACTS: lambda w: w.from_model(
"show_no_other_contacts_rationale", False "show_no_other_contacts_rationale", False
), ),
@ -373,9 +375,9 @@ class OrganizationContact(ApplicationWizard):
forms = [forms.OrganizationContactForm] forms = [forms.OrganizationContactForm]
class TypeOfWork(ApplicationWizard): class AboutYourOrganization(ApplicationWizard):
template_name = "application_type_of_work.html" template_name = "application_about_your_organization.html"
forms = [forms.TypeOfWorkForm] forms = [forms.AboutYourOrganizationForm]
class AuthorizingOfficial(ApplicationWizard): class AuthorizingOfficial(ApplicationWizard):

View file

@ -137,6 +137,10 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
def get_initial(self): def get_initial(self):
"""The initial value for the form (which is a formset here).""" """The initial value for the form (which is a formset here)."""
domain = self.get_object() domain = self.get_object()
nameservers = domain.nameservers
if nameservers is None:
return []
return [{"server": name} for name, *ip in domain.nameservers] return [{"server": name} for name, *ip in domain.nameservers]
def get_success_url(self): def get_success_url(self):
@ -268,6 +272,7 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin):
# Set the security email from the form # Set the security email from the form
new_email = form.cleaned_data.get("security_email", "") new_email = form.cleaned_data.get("security_email", "")
domain = self.get_object() domain = self.get_object()
contact = domain.security_contact contact = domain.security_contact
contact.email = new_email contact.email = new_email