Review feedback: FSM unit tests, indexes, comments, etc.

This commit is contained in:
Neil Martinsen-Burrell 2022-11-08 14:53:50 -06:00
parent 64f6d03023
commit bce29c2402
No known key found for this signature in database
GPG key ID: 6A3C818CC10D0184
6 changed files with 151 additions and 45 deletions

View file

@ -86,11 +86,12 @@ INSTALLED_APPS = [
"djangooidc",
# library to simplify form templating
"widget_tweaks",
# library for Finite State Machine statuses
"django_fsm",
# let's be sure to install our own application!
"registrar",
# Our internal API application
"api",
"django_fsm",
]
# Middleware are routines for processing web requests.

View file

@ -1,4 +1,4 @@
# Generated by Django 4.1.3 on 2022-11-07 19:39
# Generated by Django 4.1.3 on 2022-11-08 20:17
from django.conf import settings
from django.db import migrations, models
@ -25,12 +25,24 @@ class Migration(migrations.Migration):
verbose_name="ID",
),
),
("first_name", models.TextField(help_text="First name", null=True)),
(
"first_name",
models.TextField(db_index=True, help_text="First name", null=True),
),
("middle_name", models.TextField(help_text="Middle name", null=True)),
("last_name", models.TextField(help_text="Last name", null=True)),
(
"last_name",
models.TextField(db_index=True, help_text="Last name", null=True),
),
("title", models.TextField(help_text="Title", null=True)),
("email", models.TextField(help_text="Email", null=True)),
("phone", models.TextField(help_text="Phone", null=True)),
(
"email",
models.TextField(db_index=True, help_text="Email", null=True),
),
(
"phone",
models.TextField(db_index=True, help_text="Phone", null=True),
),
],
),
migrations.CreateModel(
@ -73,7 +85,6 @@ class Migration(migrations.Migration):
],
default="started",
max_length=50,
protected=True,
),
),
(
@ -122,7 +133,9 @@ class Migration(migrations.Migration):
),
(
"organization_name",
models.TextField(help_text="Organization name", null=True),
models.TextField(
db_index=True, help_text="Organization name", null=True
),
),
(
"street_address",
@ -146,7 +159,9 @@ class Migration(migrations.Migration):
),
(
"zip_code",
models.CharField(help_text="ZIP code", max_length=10, null=True),
models.CharField(
db_index=True, help_text="ZIP code", max_length=10, null=True
),
),
(
"purpose",

View file

@ -103,7 +103,7 @@ class Website(models.Model):
DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,6}")
@classmethod
def string_could_be_domain(cls, domain):
def string_could_be_domain(cls, domain: str) -> bool:
"""Return True if the string could be a domain name, otherwise False.
TODO: when we have a Domain class, this could be a classmethod there.
@ -112,21 +112,34 @@ class Website(models.Model):
return True
return False
def could_be_domain(self) -> bool:
"""Could this instance be a domain?"""
# short-circuit if self.website is null/None
if not self.website:
return False
return self.string_could_be_domain(str(self.website))
def __str__(self) -> str:
return str(self.website)
class Contact(models.Model):
"""Contact information follows a similar pattern for each contact."""
first_name = models.TextField(null=True, help_text="First name")
first_name = models.TextField(null=True, help_text="First name", db_index=True)
middle_name = models.TextField(null=True, help_text="Middle name")
last_name = models.TextField(null=True, help_text="Last name")
last_name = models.TextField(null=True, help_text="Last name", db_index=True)
title = models.TextField(null=True, help_text="Title")
email = models.TextField(null=True, help_text="Email")
phone = models.TextField(null=True, help_text="Phone")
email = models.TextField(null=True, help_text="Email", db_index=True)
phone = models.TextField(null=True, help_text="Phone", db_index=True)
class DomainApplication(TimeStampedModel):
"""A registrant's application for a new domain."""
# #### Contants for choice fields ####
STARTED = "started"
SUBMITTED = "submitted"
INVESTIGATING = "investigating"
@ -137,22 +150,6 @@ class DomainApplication(TimeStampedModel):
(INVESTIGATING, INVESTIGATING),
(APPROVED, APPROVED),
]
status = FSMField(
choices=STATUS_CHOICES, # possible states as an array of constants
default=STARTED, # sensible default
protected=True, # cannot change state directly, must use methods!
)
creator = models.ForeignKey(
User, on_delete=models.PROTECT, related_name="applications_created"
)
investigator = models.ForeignKey(
User,
null=True,
on_delete=models.SET_NULL,
related_name="applications_investigating",
)
# data fields from the initial form
FEDERAL = "federal"
INTERSTATE = "interstate"
@ -178,14 +175,35 @@ class DomainApplication(TimeStampedModel):
(CITY, "a city, town, township, village, etc."),
(SPECIAL_DISTRICT, "an independent organization within a single state"),
]
organization_type = models.CharField(
max_length=255, choices=ORGANIZATION_CHOICES, help_text="Type of Organization"
)
EXECUTIVE = "Executive"
JUDICIAL = "Judicial"
LEGISLATIVE = "Legislative"
BRANCH_CHOICES = [(x, x) for x in (EXECUTIVE, JUDICIAL, LEGISLATIVE)]
# #### Internal fields about the application #####
status = FSMField(
choices=STATUS_CHOICES, # possible states as an array of constants
default=STARTED, # sensible default
protected=False, # can change state directly, particularly in Django admin
)
# This is the application user who created this application. The contact
# information that they gave is in the `submitter` field
creator = models.ForeignKey(
User, on_delete=models.PROTECT, related_name="applications_created"
)
investigator = models.ForeignKey(
User,
null=True,
on_delete=models.SET_NULL,
related_name="applications_investigating",
)
# ##### data fields from the initial form #####
organization_type = models.CharField(
max_length=255, choices=ORGANIZATION_CHOICES, help_text="Type of Organization"
)
federal_branch = models.CharField(
max_length=50,
choices=BRANCH_CHOICES,
@ -197,14 +215,18 @@ class DomainApplication(TimeStampedModel):
null=True, help_text="Is your ogranization an election office?"
)
organization_name = models.TextField(null=True, help_text="Organization name")
organization_name = models.TextField(
null=True, help_text="Organization name", db_index=True
)
street_address = models.TextField(null=True, help_text="Street Address")
unit_type = models.CharField(max_length=15, null=True, help_text="Unit type")
unit_number = models.CharField(max_length=255, null=True, help_text="Unit number")
state_territory = models.CharField(
max_length=2, null=True, help_text="State/Territory"
)
zip_code = models.CharField(max_length=10, null=True, help_text="ZIP code")
zip_code = models.CharField(
max_length=10, null=True, help_text="ZIP code", db_index=True
)
authorizing_official = models.ForeignKey(
Contact,
@ -225,6 +247,8 @@ class DomainApplication(TimeStampedModel):
)
alternative_domains = models.ManyToManyField(Website, related_name="alternatives+")
# This is the contact information provided by the applicant. The
# application user who created it is in the `creator` field.
submitter = models.ForeignKey(
Contact,
null=True,
@ -250,16 +274,17 @@ class DomainApplication(TimeStampedModel):
null=True, help_text="Acknowledged .gov acceptable use policy"
)
def can_submit(self):
"""Return True if this instance can be marked as submitted."""
if not Website.string_could_be_domain(self.requested_domain):
return False
return True
@transition(
field="status", source=STARTED, target=SUBMITTED, conditions=[can_submit]
)
@transition(field="status", source=STARTED, target=SUBMITTED)
def submit(self):
"""Submit an application that is started."""
# don't need to do anything inside this method although we could
# check our conditions here inside the `submit` method so that we
# can raise more informative exceptions
# requested_domain could be None here
if (not self.requested_domain) or (not self.requested_domain.could_be_domain()):
raise ValueError("Requested domain is not a legal domain name.")
# if no exception was raised, then we don't need to do anything
# inside this method, keep the `pass` here to remind us of that
pass

View file

@ -46,3 +46,19 @@ class TestDomainApplication(TestCase):
application.alternative_domains.add(gov_website)
application.other_contacts.add(contact)
application.save()
def test_status_fsm_submit_fail(self):
user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(creator=user)
with self.assertRaises(ValueError):
# can't submit an application with a null domain name
application.submit()
def test_status_fsm_submit_succeed(self):
user, _ = User.objects.get_or_create()
site = Website.objects.create(website="igorville.gov")
application = DomainApplication.objects.create(
creator=user, requested_domain=site
)
application.submit()
self.assertEqual(application.status, application.SUBMITTED)