mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-16 06:24:12 +02:00
Merge branch 'main' into za/850-epp-contact-get
This commit is contained in:
commit
7d91764048
8 changed files with 491 additions and 35 deletions
|
@ -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."""
|
||||||
|
@ -472,7 +521,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
# Detail view
|
# Detail view
|
||||||
form = DomainApplicationAdminForm
|
form = DomainApplicationAdminForm
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(None, {"fields": ["status", "investigator", "creator"]}),
|
(None, {"fields": ["status", "investigator", "creator", "approved_domain"]}),
|
||||||
(
|
(
|
||||||
"Type of organization",
|
"Type of organization",
|
||||||
{
|
{
|
||||||
|
@ -542,29 +591,57 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
# Get the original application from the database
|
# Get the original application from the database
|
||||||
original_obj = models.DomainApplication.objects.get(pk=obj.pk)
|
original_obj = models.DomainApplication.objects.get(pk=obj.pk)
|
||||||
|
|
||||||
if obj.status != original_obj.status:
|
if (
|
||||||
status_method_mapping = {
|
obj
|
||||||
models.DomainApplication.STARTED: None,
|
and original_obj.status == models.DomainApplication.APPROVED
|
||||||
models.DomainApplication.SUBMITTED: obj.submit,
|
and (
|
||||||
models.DomainApplication.IN_REVIEW: obj.in_review,
|
obj.status == models.DomainApplication.REJECTED
|
||||||
models.DomainApplication.ACTION_NEEDED: obj.action_needed,
|
or obj.status == models.DomainApplication.INELIGIBLE
|
||||||
models.DomainApplication.APPROVED: obj.approve,
|
)
|
||||||
models.DomainApplication.WITHDRAWN: obj.withdraw,
|
and not obj.domain_is_not_active()
|
||||||
models.DomainApplication.REJECTED: obj.reject,
|
):
|
||||||
models.DomainApplication.INELIGIBLE: obj.reject_with_prejudice,
|
# If an admin tried to set an approved application to
|
||||||
}
|
# rejected or ineligible and the related domain is already
|
||||||
selected_method = status_method_mapping.get(obj.status)
|
# active, shortcut the action and throw a friendly
|
||||||
if selected_method is None:
|
# error message. This action would still not go through
|
||||||
logger.warning("Unknown status selected in django admin")
|
# shortcut or not as the rules are duplicated on the model,
|
||||||
else:
|
# but the error would be an ugly Django error screen.
|
||||||
# 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)
|
# 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:
|
else:
|
||||||
# Clear the success message
|
# Clear the success message
|
||||||
messages.set_level(request, messages.ERROR)
|
messages.set_level(request, messages.ERROR)
|
||||||
|
|
|
@ -149,7 +149,7 @@ class UserFixture:
|
||||||
"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": "user", "permissions": ["change_user"]},
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
147
src/registrar/migrations/0031_transitiondomain_and_more.py
Normal file
147
src/registrar/migrations/0031_transitiondomain_and_more.py
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
# Generated by Django 4.2.1 on 2023-09-15 21:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django_fsm
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0030_alter_user_status"),
|
||||||
|
]
|
||||||
|
|
||||||
|
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(
|
||||||
|
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="domainapplication",
|
||||||
|
name="approved_domain",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
blank=True,
|
||||||
|
help_text="The approved domain",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="domain_application",
|
||||||
|
to="registrar.domain",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
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(
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -593,6 +593,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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue