Merge remote-tracking branch 'origin' into rh/482-youve-been-added-to-domain

This commit is contained in:
Rebecca Hsieh 2023-09-20 15:33:41 -07:00
commit acf5a8715d
No known key found for this signature in database
GPG key ID: 644527A2F375A379
13 changed files with 899 additions and 374 deletions

View file

@ -130,6 +130,7 @@ class MyUserAdmin(BaseUserAdmin):
inlines = [UserContactInline] inlines = [UserContactInline]
list_display = ( list_display = (
"username",
"email", "email",
"first_name", "first_name",
"last_name", "last_name",
@ -159,10 +160,51 @@ class MyUserAdmin(BaseUserAdmin):
("Important dates", {"fields": ("last_login", "date_joined")}), ("Important dates", {"fields": ("last_login", "date_joined")}),
) )
analyst_fieldsets = (
(
None,
{"fields": ("password", "status")},
),
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
(
"Permissions",
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
)
},
),
("Important dates", {"fields": ("last_login", "date_joined")}),
)
analyst_readonly_fields = [
"password",
"Personal Info",
"first_name",
"last_name",
"email",
"Permissions",
"is_active",
"is_staff",
"is_superuser",
"Important dates",
"last_login",
"date_joined",
]
def get_list_display(self, request): def get_list_display(self, request):
if not request.user.is_superuser: if not request.user.is_superuser:
# Customize the list display for staff users # Customize the list display for staff users
return ("email", "first_name", "last_name", "is_staff", "is_superuser") return (
"email",
"first_name",
"last_name",
"is_staff",
"is_superuser",
"status",
)
# Use the default list display for non-staff users # Use the default list display for non-staff users
return super().get_list_display(request) return super().get_list_display(request)
@ -171,11 +213,18 @@ class MyUserAdmin(BaseUserAdmin):
if not request.user.is_superuser: if not request.user.is_superuser:
# If the user doesn't have permission to change the model, # If the user doesn't have permission to change the model,
# show a read-only fieldset # show a read-only fieldset
return ((None, {"fields": []}),) return self.analyst_fieldsets
# If the user has permission to change the model, show all fields # If the user has permission to change the model, show all fields
return super().get_fieldsets(request, obj) return super().get_fieldsets(request, obj)
def get_readonly_fields(self, request, obj=None):
if request.user.is_superuser:
return () # No read-only fields for superusers
elif request.user.is_staff:
return self.analyst_readonly_fields # Read-only fields for staff
return () # No read-only fields for other users
class HostIPInline(admin.StackedInline): class HostIPInline(admin.StackedInline):
"""Edit an ip address on the host page.""" """Edit an ip address on the host page."""
@ -189,9 +238,428 @@ class MyHostAdmin(AuditedAdmin):
inlines = [HostIPInline] inlines = [HostIPInline]
class ContactAdmin(ListHeaderAdmin):
"""Custom contact admin class to add search."""
search_fields = ["email", "first_name", "last_name"]
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."
fieldsets = [
(None, {"fields": ["creator", "domain_application"]}),
(
"Type of organization",
{
"fields": [
"organization_type",
"federally_recognized_tribe",
"state_recognized_tribe",
"tribe_name",
"federal_agency",
"federal_type",
"is_election_board",
"about_your_organization",
]
},
),
(
"Organization name and mailing address",
{
"fields": [
"organization_name",
"address_line1",
"address_line2",
"city",
"state_territory",
"zipcode",
"urbanization",
]
},
),
("Authorizing official", {"fields": ["authorizing_official"]}),
(".gov domain", {"fields": ["domain"]}),
("Your contact information", {"fields": ["submitter"]}),
("Other employees from your organization?", {"fields": ["other_contacts"]}),
(
"No other employees from your organization?",
{"fields": ["no_other_contacts_rationale"]},
),
("Anything else we should know?", {"fields": ["anything_else"]}),
(
"Requirements for operating .gov domains",
{"fields": ["is_policy_acknowledged"]},
),
]
# Read only that we'll leverage for CISA Analysts
analyst_readonly_fields = [
"creator",
"type_of_work",
"more_organization_information",
"address_line1",
"address_line2",
"zipcode",
"domain",
"submitter",
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",
]
def get_readonly_fields(self, request, obj=None):
"""Set the read-only state on form elements.
We have 1 conditions that determine which fields are read-only:
admin user permissions.
"""
readonly_fields = list(self.readonly_fields)
if request.user.is_superuser:
return readonly_fields
else:
readonly_fields.extend([field for field in self.analyst_readonly_fields])
return readonly_fields
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 DomainApplicationAdmin(ListHeaderAdmin):
"""Custom domain applications admin class."""
# Columns
list_display = [
"requested_domain",
"status",
"organization_type",
"created_at",
"submitter",
"investigator",
]
# Filters
list_filter = ("status", "organization_type", "investigator")
# Search
search_fields = [
"requested_domain__name",
"submitter__email",
"submitter__first_name",
"submitter__last_name",
]
search_help_text = "Search by domain or submitter."
# Detail view
form = DomainApplicationAdminForm
fieldsets = [
(None, {"fields": ["status", "investigator", "creator", "approved_domain"]}),
(
"Type of organization",
{
"fields": [
"organization_type",
"federally_recognized_tribe",
"state_recognized_tribe",
"tribe_name",
"federal_agency",
"federal_type",
"is_election_board",
"about_your_organization",
]
},
),
(
"Organization name and mailing address",
{
"fields": [
"organization_name",
"address_line1",
"address_line2",
"city",
"state_territory",
"zipcode",
"urbanization",
]
},
),
("Authorizing official", {"fields": ["authorizing_official"]}),
("Current websites", {"fields": ["current_websites"]}),
(".gov domain", {"fields": ["requested_domain", "alternative_domains"]}),
("Purpose of your domain", {"fields": ["purpose"]}),
("Your contact information", {"fields": ["submitter"]}),
("Other employees from your organization?", {"fields": ["other_contacts"]}),
(
"No other employees from your organization?",
{"fields": ["no_other_contacts_rationale"]},
),
("Anything else we should know?", {"fields": ["anything_else"]}),
(
"Requirements for operating .gov domains",
{"fields": ["is_policy_acknowledged"]},
),
]
# Read only that we'll leverage for CISA Analysts
analyst_readonly_fields = [
"creator",
"about_your_organization",
"address_line1",
"address_line2",
"zipcode",
"requested_domain",
"alternative_domains",
"purpose",
"submitter",
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",
]
# Trigger action when a fieldset is changed
def save_model(self, request, obj, form, change):
if obj and obj.creator.status != models.User.RESTRICTED:
if change: # Check if the application is being edited
# Get the original application from the database
original_obj = models.DomainApplication.objects.get(pk=obj.pk)
if (
obj
and original_obj.status == models.DomainApplication.APPROVED
and (
obj.status == models.DomainApplication.REJECTED
or obj.status == models.DomainApplication.INELIGIBLE
)
and not obj.domain_is_not_active()
):
# If an admin tried to set an approved application to
# rejected or ineligible and the related domain is already
# active, shortcut the action and throw a friendly
# error message. This action would still not go through
# shortcut or not as the rules are duplicated on the model,
# but the error would be an ugly Django error screen.
# Clear the success message
messages.set_level(request, messages.ERROR)
messages.error(
request,
"This action is not permitted. The domain "
+ "is already active.",
)
else:
if obj.status != original_obj.status:
status_method_mapping = {
models.DomainApplication.STARTED: None,
models.DomainApplication.SUBMITTED: obj.submit,
models.DomainApplication.IN_REVIEW: obj.in_review,
models.DomainApplication.ACTION_NEEDED: obj.action_needed,
models.DomainApplication.APPROVED: obj.approve,
models.DomainApplication.WITHDRAWN: obj.withdraw,
models.DomainApplication.REJECTED: obj.reject,
models.DomainApplication.INELIGIBLE: (
obj.reject_with_prejudice
),
}
selected_method = status_method_mapping.get(obj.status)
if selected_method is None:
logger.warning("Unknown status selected in django admin")
else:
# This is an fsm in model which will throw an error if the
# transition condition is violated, so we roll back the
# status to what it was before the admin user changed it and
# let the fsm method set it.
obj.status = original_obj.status
selected_method()
super().save_model(request, obj, form, change)
else:
# Clear the success message
messages.set_level(request, messages.ERROR)
messages.error(
request,
"This action is not permitted for applications "
+ "with a restricted creator.",
)
def get_readonly_fields(self, request, obj=None):
"""Set the read-only state on form elements.
We have 2 conditions that determine which fields are read-only:
admin user permissions and the application creator's status, so
we'll use the baseline readonly_fields and extend it as needed.
"""
readonly_fields = list(self.readonly_fields)
# Check if the creator is restricted
if obj and obj.creator.status == models.User.RESTRICTED:
# For fields like CharField, IntegerField, etc., the widget used is
# straightforward and the readonly_fields list can control their behavior
readonly_fields.extend([field.name for field in self.model._meta.fields])
# Add the multi-select fields to readonly_fields:
# Complex fields like ManyToManyField require special handling
readonly_fields.extend(
["current_websites", "other_contacts", "alternative_domains"]
)
if request.user.is_superuser:
return readonly_fields
else:
readonly_fields.extend([field for field in self.analyst_readonly_fields])
return readonly_fields
def display_restricted_warning(self, request, obj):
if obj and obj.creator.status == models.User.RESTRICTED:
messages.warning(
request,
"Cannot edit an application with a restricted creator.",
)
def change_view(self, request, object_id, form_url="", extra_context=None):
obj = self.get_object(request, object_id)
self.display_restricted_warning(request, obj)
return super().change_view(request, object_id, form_url, extra_context)
class DomainInformationInline(admin.StackedInline):
"""Edit a domain information on the domain page.
We had issues inheriting from both StackedInline
and the source DomainInformationAdmin since these
classes conflict, so we'll just pull what we need
from DomainInformationAdmin"""
model = models.DomainInformation
fieldsets = DomainInformationAdmin.fieldsets
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields
def get_readonly_fields(self, request, obj=None):
return DomainInformationAdmin.get_readonly_fields(self, request, obj=None)
class DomainAdmin(ListHeaderAdmin): class DomainAdmin(ListHeaderAdmin):
"""Custom domain admin class to add extra buttons.""" """Custom domain admin class to add extra buttons."""
inlines = [DomainInformationInline]
# Columns # Columns
list_display = [ list_display = [
"name", "name",
@ -207,7 +675,7 @@ class DomainAdmin(ListHeaderAdmin):
) )
# Filters # Filters
list_filter = ["domain_info__organization_type"] list_filter = ["domain_info__organization_type", "state"]
search_fields = ["name"] search_fields = ["name"]
search_help_text = "Search by domain name." search_help_text = "Search by domain name."
@ -312,306 +780,11 @@ class DomainAdmin(ListHeaderAdmin):
return super().has_change_permission(request, obj) return super().has_change_permission(request, obj)
class ContactAdmin(ListHeaderAdmin): class DraftDomainAdmin(ListHeaderAdmin):
"""Custom contact admin class to add search.""" """Custom draft domain admin class."""
search_fields = ["email", "first_name", "last_name"] search_fields = ["name"]
search_help_text = "Search by firstname, lastname or email." search_help_text = "Search by draft domain name."
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 DomainApplicationAdmin(ListHeaderAdmin):
"""Custom domain applications admin class."""
# Set multi-selects 'read-only' (hide selects and show data)
# based on user perms and application creator's status
# form = DomainApplicationForm
# Columns
list_display = [
"requested_domain",
"status",
"organization_type",
"created_at",
"submitter",
"investigator",
]
# Filters
list_filter = ("status", "organization_type", "investigator")
# Search
search_fields = [
"requested_domain__name",
"submitter__email",
"submitter__first_name",
"submitter__last_name",
]
search_help_text = "Search by domain or submitter."
# Detail view
form = DomainApplicationAdminForm
fieldsets = [
(None, {"fields": ["status", "investigator", "creator"]}),
(
"Type of organization",
{
"fields": [
"organization_type",
"federally_recognized_tribe",
"state_recognized_tribe",
"tribe_name",
"federal_agency",
"federal_type",
"is_election_board",
"about_your_organization",
]
},
),
(
"Organization name and mailing address",
{
"fields": [
"organization_name",
"address_line1",
"address_line2",
"city",
"state_territory",
"zipcode",
"urbanization",
]
},
),
("Authorizing official", {"fields": ["authorizing_official"]}),
("Current websites", {"fields": ["current_websites"]}),
(".gov domain", {"fields": ["requested_domain", "alternative_domains"]}),
("Purpose of your domain", {"fields": ["purpose"]}),
("Your contact information", {"fields": ["submitter"]}),
("Other employees from your organization?", {"fields": ["other_contacts"]}),
(
"No other employees from your organization?",
{"fields": ["no_other_contacts_rationale"]},
),
("Anything else we should know?", {"fields": ["anything_else"]}),
(
"Requirements for operating .gov domains",
{"fields": ["is_policy_acknowledged"]},
),
]
# Read only that we'll leverage for CISA Analysts
analyst_readonly_fields = [
"creator",
"about_your_organization",
"address_line1",
"address_line2",
"zipcode",
"requested_domain",
"alternative_domains",
"purpose",
"submitter",
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",
]
# Trigger action when a fieldset is changed
def save_model(self, request, obj, form, change):
if obj and obj.creator.status != models.User.RESTRICTED:
if change: # Check if the application is being edited
# Get the original application from the database
original_obj = models.DomainApplication.objects.get(pk=obj.pk)
if obj.status != original_obj.status:
status_method_mapping = {
models.DomainApplication.STARTED: None,
models.DomainApplication.SUBMITTED: obj.submit,
models.DomainApplication.IN_REVIEW: obj.in_review,
models.DomainApplication.ACTION_NEEDED: obj.action_needed,
models.DomainApplication.APPROVED: obj.approve,
models.DomainApplication.WITHDRAWN: obj.withdraw,
models.DomainApplication.REJECTED: obj.reject,
models.DomainApplication.INELIGIBLE: obj.reject_with_prejudice,
}
selected_method = status_method_mapping.get(obj.status)
if selected_method is None:
logger.warning("Unknown status selected in django admin")
else:
# This is an fsm in model which will throw an error if the
# transition condition is violated, so we roll back the
# status to what it was before the admin user changed it and
# let the fsm method set it.
obj.status = original_obj.status
selected_method()
super().save_model(request, obj, form, change)
else:
# Clear the success message
messages.set_level(request, messages.ERROR)
messages.error(
request,
"This action is not permitted for applications "
+ "with a restricted creator.",
)
def get_readonly_fields(self, request, obj=None):
"""Set the read-only state on form elements.
We have 2 conditions that determine which fields are read-only:
admin user permissions and the application creator's status, so
we'll use the baseline readonly_fields and extend it as needed.
"""
readonly_fields = list(self.readonly_fields)
# Check if the creator is restricted
if obj and obj.creator.status == models.User.RESTRICTED:
# For fields like CharField, IntegerField, etc., the widget used is
# straightforward and the readonly_fields list can control their behavior
readonly_fields.extend([field.name for field in self.model._meta.fields])
# Add the multi-select fields to readonly_fields:
# Complex fields like ManyToManyField require special handling
readonly_fields.extend(
["current_websites", "other_contacts", "alternative_domains"]
)
if request.user.is_superuser:
return readonly_fields
else:
readonly_fields.extend([field for field in self.analyst_readonly_fields])
return readonly_fields
def display_restricted_warning(self, request, obj):
if obj and obj.creator.status == models.User.RESTRICTED:
messages.warning(
request,
"Cannot edit an application with a restricted creator.",
)
def change_view(self, request, object_id, form_url="", extra_context=None):
obj = self.get_object(request, object_id)
self.display_restricted_warning(request, obj)
return super().change_view(request, object_id, form_url, extra_context)
admin.site.unregister(LogEntry) # Unregister the default registration admin.site.unregister(LogEntry) # Unregister the default registration
@ -622,6 +795,7 @@ admin.site.register(models.Contact, ContactAdmin)
admin.site.register(models.DomainInvitation, DomainInvitationAdmin) 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.DraftDomain, DraftDomainAdmin)
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, WebsiteAdmin) admin.site.register(models.Website, WebsiteAdmin)

View file

@ -143,13 +143,23 @@ class UserFixture:
"permissions": ["view_logentry"], "permissions": ["view_logentry"],
}, },
{"app_label": "registrar", "model": "contact", "permissions": ["view_contact"]}, {"app_label": "registrar", "model": "contact", "permissions": ["view_contact"]},
{
"app_label": "registrar",
"model": "domaininformation",
"permissions": ["change_domaininformation"],
},
{ {
"app_label": "registrar", "app_label": "registrar",
"model": "domainapplication", "model": "domainapplication",
"permissions": ["change_domainapplication"], "permissions": ["change_domainapplication"],
}, },
{"app_label": "registrar", "model": "domain", "permissions": ["view_domain"]}, {"app_label": "registrar", "model": "domain", "permissions": ["view_domain"]},
{"app_label": "registrar", "model": "user", "permissions": ["view_user"]}, {
"app_label": "registrar",
"model": "draftdomain",
"permissions": ["change_draftdomain"],
},
{"app_label": "registrar", "model": "user", "permissions": ["change_user"]},
] ]
@classmethod @classmethod

View file

@ -6,12 +6,12 @@ from phonenumber_field.formfields import PhoneNumberField # type: ignore
from django import forms from django import forms
from django.core.validators import RegexValidator, MaxLengthValidator from django.core.validators import RegexValidator, MaxLengthValidator
from django.urls import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from api.views import DOMAIN_API_MESSAGES from api.views import DOMAIN_API_MESSAGES
from registrar.models import Contact, DomainApplication, DraftDomain, Domain from registrar.models import Contact, DomainApplication, DraftDomain, Domain
from registrar.templatetags.url_helpers import public_site_url
from registrar.utility import errors from registrar.utility import errors
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -181,7 +181,6 @@ class TribalGovernmentForm(RegistrarForm):
self.cleaned_data["federally_recognized_tribe"] self.cleaned_data["federally_recognized_tribe"]
or self.cleaned_data["state_recognized_tribe"] or self.cleaned_data["state_recognized_tribe"]
): ):
todo_url = reverse("todo")
raise forms.ValidationError( raise forms.ValidationError(
# no sec because we are using it to include an internal URL # no sec because we are using it to include an internal URL
# into a link. There should be no user-facing input in the # into a link. There should be no user-facing input in the
@ -190,10 +189,10 @@ class TribalGovernmentForm(RegistrarForm):
"You cant complete this application yet. " "You cant complete this application yet. "
"Only tribes recognized by the U.S. federal government " "Only tribes recognized by the U.S. federal government "
"or by a U.S. state government are eligible for .gov " "or by a U.S. state government are eligible for .gov "
'domains. Please use our <a href="{}">contact form</a> to ' 'domains. Use our <a href="{}">contact form</a> to '
"tell us more about your tribe and why you want a .gov " "tell us more about your tribe and why you want a .gov "
"domain. Well review your information and get back " "domain. Well review your information and get back "
"to you.".format(todo_url) "to you.".format(public_site_url("contact"))
), ),
code="invalid", code="invalid",
) )

View file

@ -1,6 +1,7 @@
# Generated by Django 4.2.1 on 2023-09-13 22:25 # Generated by Django 4.2.1 on 2023-09-15 21:05
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
import django_fsm import django_fsm
@ -10,6 +11,23 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
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.CreateModel( migrations.CreateModel(
name="TransitionDomain", name="TransitionDomain",
fields=[ fields=[
@ -89,20 +107,27 @@ class Migration(migrations.Migration):
), ),
), ),
migrations.AlterField( migrations.AlterField(
model_name="domain", model_name="domainapplication",
name="state", name="approved_domain",
field=django_fsm.FSMField( field=models.OneToOneField(
choices=[ blank=True,
("unknown", "Unknown"), help_text="The approved domain",
("dns needed", "Dns Needed"), null=True,
("ready", "Ready"), on_delete=django.db.models.deletion.SET_NULL,
("on hold", "On Hold"), related_name="domain_application",
("deleted", "Deleted"), to="registrar.domain",
], ),
default="unknown", ),
help_text="Very basic info about the lifecycle of this domain object", migrations.AlterField(
max_length=21, model_name="domaininformation",
protected=True, name="domain",
field=models.OneToOneField(
blank=True,
help_text="Domain to which this information belongs",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="domain_info",
to="registrar.domain",
), ),
), ),
migrations.AlterField( migrations.AlterField(

View file

@ -332,24 +332,23 @@ class Domain(TimeStampedModel, DomainHelper):
@Cache @Cache
def statuses(self) -> list[str]: def statuses(self) -> list[str]:
""" """
Get or set the domain `status` elements from the registry. Get the domain `status` elements from the registry.
A domain's status indicates various properties. See Domain.Status. A domain's status indicates various properties. See Domain.Status.
""" """
# implementation note: the Status object from EPP stores the string in try:
# a dataclass property `state`, not to be confused with the `state` field here return self._get_property("statuses")
if "statuses" not in self._cache: except KeyError:
self._fetch_cache() logger.error("Can't retrieve status from domain info")
if "statuses" not in self._cache: return []
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]):
# TODO: there are a long list of rules in the RFC about which statuses """
# can be combined; check that here and raise errors for invalid combinations - We will not implement this. Statuses are set by the registry
# some statuses cannot be set by the client at all when we run delete and client hold, and these are the only statuses
we will be triggering.
"""
raise NotImplementedError() raise NotImplementedError()
@Cache @Cache
@ -610,6 +609,11 @@ class Domain(TimeStampedModel, DomainHelper):
""" """
return self.state == self.State.READY return self.state == self.State.READY
def delete_request(self):
"""Delete from host. Possibly a duplicate of _delete_host?"""
# TODO fix in ticket #901
pass
def transfer(self): def transfer(self):
"""Going somewhere. Not implemented.""" """Going somewhere. Not implemented."""
raise NotImplementedError() raise NotImplementedError()
@ -663,9 +667,6 @@ class Domain(TimeStampedModel, DomainHelper):
help_text="Very basic info about the lifecycle of this domain object", help_text="Very basic info about the lifecycle of this domain object",
) )
def isActive(self):
return self.state == Domain.State.CREATED
# ForeignKey on UserDomainRole creates a "permissions" member for # ForeignKey on UserDomainRole creates a "permissions" member for
# all of the user-roles that are in place for this domain # all of the user-roles that are in place for this domain

View file

@ -405,7 +405,7 @@ class DomainApplication(TimeStampedModel):
blank=True, blank=True,
help_text="The approved domain", help_text="The approved domain",
related_name="domain_application", related_name="domain_application",
on_delete=models.PROTECT, on_delete=models.SET_NULL,
) )
requested_domain = models.OneToOneField( requested_domain = models.OneToOneField(
@ -471,6 +471,11 @@ class DomainApplication(TimeStampedModel):
except Exception: except Exception:
return "" return ""
def domain_is_not_active(self):
if self.approved_domain:
return not self.approved_domain.is_active()
return True
def _send_status_update_email( def _send_status_update_email(
self, new_status, email_template, email_template_subject self, new_status, email_template, email_template_subject
): ):
@ -594,11 +599,22 @@ class DomainApplication(TimeStampedModel):
"emails/domain_request_withdrawn_subject.txt", "emails/domain_request_withdrawn_subject.txt",
) )
@transition(field="status", source=[IN_REVIEW, APPROVED], target=REJECTED) @transition(
field="status",
source=[IN_REVIEW, APPROVED],
target=REJECTED,
conditions=[domain_is_not_active],
)
def reject(self): def reject(self):
"""Reject an application that has been submitted. """Reject an application that has been submitted.
As a side effect, an email notification is sent, similar to in_review""" As side effects this will delete the domain and domain_information
(will cascade), and send an email notification."""
if self.status == self.APPROVED:
self.approved_domain.delete_request()
self.approved_domain.delete()
self.approved_domain = None
self._send_status_update_email( self._send_status_update_email(
"action needed", "action needed",
@ -606,14 +622,25 @@ class DomainApplication(TimeStampedModel):
"emails/status_change_rejected_subject.txt", "emails/status_change_rejected_subject.txt",
) )
@transition(field="status", source=[IN_REVIEW, APPROVED], target=INELIGIBLE) @transition(
field="status",
source=[IN_REVIEW, APPROVED],
target=INELIGIBLE,
conditions=[domain_is_not_active],
)
def reject_with_prejudice(self): def reject_with_prejudice(self):
"""The applicant is a bad actor, reject with prejudice. """The applicant is a bad actor, reject with prejudice.
No email As a side effect, but we block the applicant from editing No email As a side effect, but we block the applicant from editing
any existing domains/applications and from submitting new aplications. any existing domains/applications and from submitting new aplications.
We do this by setting an ineligible status on the user, which the We do this by setting an ineligible status on the user, which the
permissions classes test against""" permissions classes test against. This will also delete the domain
and domain_information (will cascade) when they exist."""
if self.status == self.APPROVED:
self.approved_domain.delete_request()
self.approved_domain.delete()
self.approved_domain = None
self.creator.restrict_user() self.creator.restrict_user()

View file

@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
class DomainInformation(TimeStampedModel): class DomainInformation(TimeStampedModel):
"""A registrant's domain information for that domain, exported from """A registrant's domain information for that domain, exported from
DomainApplication. We use these field from DomainApplication with few exceptation DomainApplication. We use these field from DomainApplication with few exceptions
which are 'removed' via pop at the bottom of this file. Most of design for domain which are 'removed' via pop at the bottom of this file. Most of design for domain
management's user information are based on application, but we cannot change management's user information are based on application, but we cannot change
the application once approved, so copying them that way we can make changes the application once approved, so copying them that way we can make changes
@ -150,7 +150,7 @@ class DomainInformation(TimeStampedModel):
domain = models.OneToOneField( domain = models.OneToOneField(
"registrar.Domain", "registrar.Domain",
on_delete=models.PROTECT, on_delete=models.CASCADE,
blank=True, blank=True,
null=True, null=True,
# Access this information via Domain as "domain.domain_info" # Access this information via Domain as "domain.domain_info"

View file

@ -45,7 +45,7 @@ class User(AbstractUser):
def __str__(self): def __str__(self):
# this info is pulled from Login.gov # this info is pulled from Login.gov
if self.first_name or self.last_name: if self.first_name or self.last_name:
return f"{self.first_name or ''} {self.last_name or ''}" return f"{self.first_name or ''} {self.last_name or ''} {self.email or ''}"
elif self.email: elif self.email:
return self.email return self.email
else: else:

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

@ -548,17 +548,29 @@ class MockEppLib(TestCase):
class fakedEppObject(object): class fakedEppObject(object):
"""""" """"""
def __init__(self, auth_info=..., cr_date=..., contacts=..., hosts=...): def __init__(
self,
auth_info=...,
cr_date=...,
contacts=...,
hosts=...,
statuses=...,
):
self.auth_info = auth_info self.auth_info = auth_info
self.cr_date = cr_date self.cr_date = cr_date
self.contacts = contacts self.contacts = contacts
self.hosts = hosts self.hosts = hosts
self.statuses = statuses
mockDataInfoDomain = fakedEppObject( mockDataInfoDomain = fakedEppObject(
"fakepw", "fakepw",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
contacts=[common.DomainContact(contact="123", type="security")], contacts=[common.DomainContact(contact="123", type="security")],
hosts=["fake.host.com"], hosts=["fake.host.com"],
statuses=[
common.Status(state="serverTransferProhibited", description="", lang="en"),
common.Status(state="inactive", description="", lang="en"),
],
) )
infoDomainNoContact = fakedEppObject( infoDomainNoContact = fakedEppObject(
"security", "security",

View file

@ -1,5 +1,7 @@
from django.test import TestCase, RequestFactory, Client from django.test import TestCase, RequestFactory, Client
from django.contrib.admin.sites import AdminSite from django.contrib.admin.sites import AdminSite
from contextlib import ExitStack
from django.contrib import messages
from django.urls import reverse from django.urls import reverse
from registrar.admin import ( from registrar.admin import (
@ -535,7 +537,160 @@ class TestDomainApplicationAdmin(TestCase):
"Cannot edit an application with a restricted creator.", "Cannot edit an application with a restricted creator.",
) )
def test_error_when_saving_approved_to_rejected_and_domain_is_active(self):
# Create an instance of the model
application = completed_application(status=DomainApplication.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
application.approved_domain = domain
application.save()
# Create a request object with a superuser
request = self.factory.post(
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
request.user = self.superuser
# Define a custom implementation for is_active
def custom_is_active(self):
return True # Override to return True
# Use ExitStack to combine patch contexts
with ExitStack() as stack:
# Patch Domain.is_active and django.contrib.messages.error simultaneously
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
stack.enter_context(patch.object(messages, "error"))
# Simulate saving the model
application.status = DomainApplication.REJECTED
self.admin.save_model(request, application, None, True)
# Assert that the error message was called with the correct argument
messages.error.assert_called_once_with(
request,
"This action is not permitted. The domain " + "is already active.",
)
def test_side_effects_when_saving_approved_to_rejected(self):
# Create an instance of the model
application = completed_application(status=DomainApplication.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
domain_information = DomainInformation.objects.create(
creator=self.superuser, domain=domain
)
application.approved_domain = domain
application.save()
# Create a request object with a superuser
request = self.factory.post(
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
request.user = self.superuser
# Define a custom implementation for is_active
def custom_is_active(self):
return False # Override to return False
# Use ExitStack to combine patch contexts
with ExitStack() as stack:
# Patch Domain.is_active and django.contrib.messages.error simultaneously
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
stack.enter_context(patch.object(messages, "error"))
# Simulate saving the model
application.status = DomainApplication.REJECTED
self.admin.save_model(request, application, None, True)
# Assert that the error message was never called
messages.error.assert_not_called()
self.assertEqual(application.approved_domain, None)
# Assert that Domain got Deleted
with self.assertRaises(Domain.DoesNotExist):
domain.refresh_from_db()
# Assert that DomainInformation got Deleted
with self.assertRaises(DomainInformation.DoesNotExist):
domain_information.refresh_from_db()
def test_error_when_saving_approved_to_ineligible_and_domain_is_active(self):
# Create an instance of the model
application = completed_application(status=DomainApplication.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
application.approved_domain = domain
application.save()
# Create a request object with a superuser
request = self.factory.post(
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
request.user = self.superuser
# Define a custom implementation for is_active
def custom_is_active(self):
return True # Override to return True
# Use ExitStack to combine patch contexts
with ExitStack() as stack:
# Patch Domain.is_active and django.contrib.messages.error simultaneously
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
stack.enter_context(patch.object(messages, "error"))
# Simulate saving the model
application.status = DomainApplication.INELIGIBLE
self.admin.save_model(request, application, None, True)
# Assert that the error message was called with the correct argument
messages.error.assert_called_once_with(
request,
"This action is not permitted. The domain " + "is already active.",
)
def test_side_effects_when_saving_approved_to_ineligible(self):
# Create an instance of the model
application = completed_application(status=DomainApplication.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
domain_information = DomainInformation.objects.create(
creator=self.superuser, domain=domain
)
application.approved_domain = domain
application.save()
# Create a request object with a superuser
request = self.factory.post(
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
request.user = self.superuser
# Define a custom implementation for is_active
def custom_is_active(self):
return False # Override to return False
# Use ExitStack to combine patch contexts
with ExitStack() as stack:
# Patch Domain.is_active and django.contrib.messages.error simultaneously
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
stack.enter_context(patch.object(messages, "error"))
# Simulate saving the model
application.status = DomainApplication.INELIGIBLE
self.admin.save_model(request, application, None, True)
# Assert that the error message was never called
messages.error.assert_not_called()
self.assertEqual(application.approved_domain, None)
# Assert that Domain got Deleted
with self.assertRaises(Domain.DoesNotExist):
domain.refresh_from_db()
# Assert that DomainInformation got Deleted
with self.assertRaises(DomainInformation.DoesNotExist):
domain_information.refresh_from_db()
def tearDown(self): def tearDown(self):
Domain.objects.all().delete()
DomainInformation.objects.all().delete() DomainInformation.objects.all().delete()
DomainApplication.objects.all().delete() DomainApplication.objects.all().delete()
User.objects.all().delete() User.objects.all().delete()
@ -632,6 +787,7 @@ class MyUserAdminTest(TestCase):
"last_name", "last_name",
"is_staff", "is_staff",
"is_superuser", "is_superuser",
"status",
) )
self.assertEqual(list_display, expected_list_display) self.assertEqual(list_display, expected_list_display)
@ -648,7 +804,12 @@ class MyUserAdminTest(TestCase):
request = self.client.request().wsgi_request request = self.client.request().wsgi_request
request.user = create_user() request.user = create_user()
fieldsets = self.admin.get_fieldsets(request) fieldsets = self.admin.get_fieldsets(request)
expected_fieldsets = ((None, {"fields": []}),) expected_fieldsets = (
(None, {"fields": ("password", "status")}),
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
("Permissions", {"fields": ("is_active", "is_staff", "is_superuser")}),
("Important dates", {"fields": ("last_login", "date_joined")}),
)
self.assertEqual(fieldsets, expected_fieldsets) self.assertEqual(fieldsets, expected_fieldsets)
def tearDown(self): def tearDown(self):

View file

@ -1,5 +1,6 @@
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
from registrar.models import ( from registrar.models import (
Contact, Contact,
@ -439,7 +440,26 @@ class TestDomainApplication(TestCase):
application = completed_application(status=DomainApplication.INELIGIBLE) application = completed_application(status=DomainApplication.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed): with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice() application.reject()
def test_transition_not_allowed_approved_rejected_when_domain_is_active(self):
"""Create an application with status approved, create a matching domain that
is active, and call reject against transition rules"""
application = completed_application(status=DomainApplication.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
application.approved_domain = domain
application.save()
# Define a custom implementation for is_active
def custom_is_active(self):
return True # Override to return True
# Use patch to temporarily replace is_active with the custom implementation
with patch.object(Domain, "is_active", custom_is_active):
# Now, when you call is_active on Domain, it will return True
with self.assertRaises(TransitionNotAllowed):
application.reject()
def test_transition_not_allowed_started_ineligible(self): def test_transition_not_allowed_started_ineligible(self):
"""Create an application with status started and call reject """Create an application with status started and call reject
@ -495,6 +515,25 @@ class TestDomainApplication(TestCase):
with self.assertRaises(TransitionNotAllowed): with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice() application.reject_with_prejudice()
def test_transition_not_allowed_approved_ineligible_when_domain_is_active(self):
"""Create an application with status approved, create a matching domain that
is active, and call reject_with_prejudice against transition rules"""
application = completed_application(status=DomainApplication.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
application.approved_domain = domain
application.save()
# Define a custom implementation for is_active
def custom_is_active(self):
return True # Override to return True
# Use patch to temporarily replace is_active with the custom implementation
with patch.object(Domain, "is_active", custom_is_active):
# Now, when you call is_active on Domain, it will return True
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
class TestPermissions(TestCase): class TestPermissions(TestCase):

View file

@ -34,6 +34,8 @@ class TestDomainCache(MockEppLib):
# (see InfoDomainResult) # (see InfoDomainResult)
self.assertEquals(domain._cache["auth_info"], self.mockDataInfoDomain.auth_info) self.assertEquals(domain._cache["auth_info"], self.mockDataInfoDomain.auth_info)
self.assertEquals(domain._cache["cr_date"], self.mockDataInfoDomain.cr_date) self.assertEquals(domain._cache["cr_date"], self.mockDataInfoDomain.cr_date)
status_list = [status.state for status in self.mockDataInfoDomain.statuses]
self.assertEquals(domain._cache["statuses"], status_list)
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
@ -49,7 +51,8 @@ class TestDomainCache(MockEppLib):
), ),
call(commands.InfoContact(id="123", auth_info=None), cleaned=True), call(commands.InfoContact(id="123", auth_info=None), cleaned=True),
call(commands.InfoHost(name="fake.host.com"), cleaned=True), call(commands.InfoHost(name="fake.host.com"), cleaned=True),
] ],
any_order=False, # Ensure calls are in the specified order
) )
def test_cache_used_when_avail(self): def test_cache_used_when_avail(self):
@ -106,16 +109,14 @@ class TestDomainCache(MockEppLib):
domain._get_property("hosts") domain._get_property("hosts")
self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
def tearDown(self) -> None:
Domain.objects.all().delete()
super().tearDown()
class TestDomainCreation(TestCase):
class TestDomainCreation(MockEppLib):
"""Rule: An approved domain application must result in a domain""" """Rule: An approved domain application must result in a domain"""
def setUp(self):
"""
Background:
Given that a valid domain application exists
"""
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
@ -123,8 +124,6 @@ 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
""" """
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") draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
user, _ = User.objects.get_or_create() user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create( application = DomainApplication.objects.create(
@ -137,19 +136,46 @@ class TestDomainCreation(TestCase):
# should hav information present for this domain # should hav information present for this domain
domain = Domain.objects.get(name="igorville.gov") domain = Domain.objects.get(name="igorville.gov")
self.assertTrue(domain) self.assertTrue(domain)
mocked_domain_creation.assert_not_called() self.mockedSendFunction.assert_not_called()
@skip("not implemented yet")
def test_accessing_domain_properties_creates_domain_in_registry(self): def test_accessing_domain_properties_creates_domain_in_registry(self):
""" """
Scenario: A registrant checks the status of a newly approved domain Scenario: A registrant checks the status of a newly approved domain
Given that no domain object exists in the registry Given that no domain object exists in the registry
When a property is accessed When a property is accessed
Then Domain sends `commands.CreateDomain` to the registry Then Domain sends `commands.CreateDomain` to the registry
And `domain.state` is set to `CREATED` And `domain.state` is set to `UNKNOWN`
And `domain.is_active()` returns False And `domain.is_active()` returns False
""" """
raise domain = Domain.objects.create(name="beef-tongue.gov")
# trigger getter
_ = domain.statuses
# contacts = PublicContact.objects.filter(domain=domain,
# type=PublicContact.ContactTypeChoices.REGISTRANT).get()
# Called in _fetch_cache
self.mockedSendFunction.assert_has_calls(
[
# TODO: due to complexity of the test, will return to it in
# a future ticket
# call(
# commands.CreateDomain(name="beef-tongue.gov",
# id=contact.registry_id, auth_info=None),
# cleaned=True,
# ),
call(
commands.InfoDomain(name="beef-tongue.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),
],
any_order=False, # Ensure calls are in the specified order
)
self.assertEqual(domain.state, Domain.State.UNKNOWN)
self.assertEqual(domain.is_active(), False)
@skip("assertion broken with mock addition") @skip("assertion broken with mock addition")
def test_empty_domain_creation(self): def test_empty_domain_creation(self):
@ -168,20 +194,71 @@ class TestDomainCreation(TestCase):
with self.assertRaisesRegex(IntegrityError, "name"): with self.assertRaisesRegex(IntegrityError, "name"):
Domain.objects.create(name="igorville.gov") Domain.objects.create(name="igorville.gov")
@skip("cannot activate a domain without mock registry")
def test_get_status(self):
"""Returns proper status based on `state`."""
domain = Domain.objects.create(name="igorville.gov")
domain.save()
self.assertEqual(None, domain.status)
domain.activate()
domain.save()
self.assertIn("ok", domain.status)
def tearDown(self) -> None: def tearDown(self) -> None:
DomainInformation.objects.all().delete() DomainInformation.objects.all().delete()
DomainApplication.objects.all().delete() DomainApplication.objects.all().delete()
Domain.objects.all().delete() Domain.objects.all().delete()
super().tearDown()
class TestDomainStatuses(MockEppLib):
"""Domain statuses are set by the registry"""
def test_get_status(self):
"""Domain 'statuses' getter returns statuses by calling epp"""
domain, _ = Domain.objects.get_or_create(name="chicken-liver.gov")
# trigger getter
_ = domain.statuses
status_list = [status.state for status in self.mockDataInfoDomain.statuses]
self.assertEquals(domain._cache["statuses"], status_list)
# Called in _fetch_cache
self.mockedSendFunction.assert_has_calls(
[
call(
commands.InfoDomain(name="chicken-liver.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),
],
any_order=False, # Ensure calls are in the specified order
)
def test_get_status_returns_empty_list_when_value_error(self):
"""Domain 'statuses' getter returns an empty list
when value error"""
domain, _ = Domain.objects.get_or_create(name="pig-knuckles.gov")
def side_effect(self):
raise KeyError
patcher = patch("registrar.models.domain.Domain._get_property")
mocked_get = patcher.start()
mocked_get.side_effect = side_effect
# trigger getter
_ = domain.statuses
with self.assertRaises(KeyError):
_ = domain._cache["statuses"]
self.assertEquals(_, [])
patcher.stop()
@skip("not implemented yet")
def test_place_client_hold_sets_status(self):
"""Domain 'place_client_hold' method causes the registry to change statuses"""
raise
@skip("not implemented yet")
def test_revert_client_hold_sets_status(self):
"""Domain 'revert_client_hold' method causes the registry to change statuses"""
raise
def tearDown(self) -> None:
Domain.objects.all().delete()
super().tearDown()
class TestRegistrantContacts(MockEppLib): class TestRegistrantContacts(MockEppLib):