Merge pull request #582 from cisagov/jon/541

Domain Application Information 🎉
This commit is contained in:
Jon 2023-05-10 14:49:34 -04:00 committed by GitHub
commit 1ec355290e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 652 additions and 1 deletions

View file

@ -55,6 +55,7 @@ admin.site.register(models.UserDomainRole, AuditedAdmin)
admin.site.register(models.Contact, AuditedAdmin) admin.site.register(models.Contact, AuditedAdmin)
admin.site.register(models.DomainInvitation, AuditedAdmin) admin.site.register(models.DomainInvitation, AuditedAdmin)
admin.site.register(models.DomainApplication, AuditedAdmin) admin.site.register(models.DomainApplication, AuditedAdmin)
admin.site.register(models.DomainInformation, AuditedAdmin)
admin.site.register(models.Domain, AuditedAdmin) admin.site.register(models.Domain, AuditedAdmin)
admin.site.register(models.Host, MyHostAdmin) admin.site.register(models.Host, MyHostAdmin)
admin.site.register(models.Nameserver, MyHostAdmin) admin.site.register(models.Nameserver, MyHostAdmin)

View file

@ -44,6 +44,11 @@ class UserFixture:
"first_name": "Neil", "first_name": "Neil",
"last_name": "Martinsen-Burrell", "last_name": "Martinsen-Burrell",
}, },
{
"username": "7185e6cd-d3c8-4adc-90a3-ceddba71d24f",
"first_name": "Jon",
"last_name": "Roberts",
},
{ {
"username": "5f283494-31bd-49b5-b024-a7e7cae00848", "username": "5f283494-31bd-49b5-b024-a7e7cae00848",
"first_name": "Rachid", "first_name": "Rachid",

View file

@ -0,0 +1,273 @@
# Generated by Django 4.1.6 on 2023-05-08 15:30
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("registrar", "0017_alter_domainapplication_status_and_more"),
]
operations = [
migrations.CreateModel(
name="DomainInformation",
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)),
(
"organization_type",
models.CharField(
blank=True,
choices=[
(
"federal",
"Federal: an agency of the U.S. government's executive, legislative, or judicial branches",
),
(
"interstate",
"Interstate: an organization of two or more states",
),
(
"state_or_territory",
"State or territory: one of the 50 U.S. states, the District of Columbia, American Samoa, Guam, Northern Mariana Islands, Puerto Rico, or the U.S. Virgin Islands",
),
(
"tribal",
"Tribal: a tribal government recognized by the federal or a state government",
),
("county", "County: a county, parish, or borough"),
("city", "City: a city, town, township, village, etc."),
(
"special_district",
"Special district: an independent organization within a single state",
),
(
"school_district",
"School district: a school district that is not part of a local government",
),
],
help_text="Type of Organization",
max_length=255,
null=True,
),
),
(
"federally_recognized_tribe",
models.BooleanField(
help_text="Is the tribe federally recognized", null=True
),
),
(
"state_recognized_tribe",
models.BooleanField(
help_text="Is the tribe recognized by a state", null=True
),
),
(
"tribe_name",
models.TextField(blank=True, help_text="Name of tribe", null=True),
),
(
"federal_agency",
models.TextField(blank=True, help_text="Federal agency", null=True),
),
(
"federal_type",
models.CharField(
blank=True,
choices=[
("executive", "Executive"),
("judicial", "Judicial"),
("legislative", "Legislative"),
],
help_text="Federal government branch",
max_length=50,
null=True,
),
),
(
"is_election_board",
models.BooleanField(
blank=True,
help_text="Is your organization an election office?",
null=True,
),
),
(
"organization_name",
models.TextField(
blank=True,
db_index=True,
help_text="Organization name",
null=True,
),
),
(
"address_line1",
models.TextField(blank=True, help_text="Street address", null=True),
),
(
"address_line2",
models.CharField(
blank=True,
help_text="Street address line 2",
max_length=15,
null=True,
),
),
("city", models.TextField(blank=True, help_text="City", null=True)),
(
"state_territory",
models.CharField(
blank=True,
help_text="State, territory, or military post",
max_length=2,
null=True,
),
),
(
"zipcode",
models.CharField(
blank=True,
db_index=True,
help_text="Zip code",
max_length=10,
null=True,
),
),
(
"urbanization",
models.TextField(
blank=True,
help_text="Urbanization (Puerto Rico only)",
null=True,
),
),
(
"type_of_work",
models.TextField(
blank=True,
help_text="Type of work of the organization",
null=True,
),
),
(
"more_organization_information",
models.TextField(
blank=True,
help_text="Further information about the government organization",
null=True,
),
),
(
"purpose",
models.TextField(
blank=True, help_text="Purpose of your domain", null=True
),
),
(
"no_other_contacts_rationale",
models.TextField(
blank=True,
help_text="Reason for listing no additional contacts",
null=True,
),
),
(
"anything_else",
models.TextField(
blank=True, help_text="Anything else we should know?", null=True
),
),
(
"is_policy_acknowledged",
models.BooleanField(
blank=True,
help_text="Acknowledged .gov acceptable use policy",
null=True,
),
),
(
"security_email",
models.EmailField(
blank=True,
help_text="Security email for public use",
max_length=320,
null=True,
),
),
(
"authorizing_official",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="information_authorizing_official",
to="registrar.contact",
),
),
(
"creator",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="information_created",
to=settings.AUTH_USER_MODEL,
),
),
(
"domain",
models.OneToOneField(
blank=True,
help_text="Domain to which this information belongs",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="domain_info",
to="registrar.domain",
),
),
(
"domain_application",
models.OneToOneField(
blank=True,
help_text="Associated domain application",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="domainapplication_info",
to="registrar.domainapplication",
),
),
(
"other_contacts",
models.ManyToManyField(
blank=True,
related_name="contact_applications_information",
to="registrar.contact",
),
),
(
"submitter",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="submitted_applications_information",
to="registrar.contact",
),
),
],
options={
"verbose_name_plural": "Domain Information",
},
),
]

View file

@ -0,0 +1,47 @@
# Generated by Django 4.1.6 on 2023-05-09 19:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0018_domaininformation"),
]
operations = [
migrations.AlterField(
model_name="domainapplication",
name="organization_type",
field=models.CharField(
blank=True,
choices=[
(
"federal",
"Federal: an agency of the U.S. government's executive, legislative, or judicial branches",
),
("interstate", "Interstate: an organization of two or more states"),
(
"state_or_territory",
"State or territory: one of the 50 U.S. states, the District of Columbia, American Samoa, Guam, Northern Mariana Islands, Puerto Rico, or the U.S. Virgin Islands",
),
(
"tribal",
"Tribal: a tribal government recognized by the federal or a state government",
),
("county", "County: a county, parish, or borough"),
("city", "City: a city, town, township, village, etc."),
(
"special_district",
"Special district: an independent organization within a single state",
),
(
"school_district",
"School district: a school district that is not part of a local government",
),
],
help_text="Type of organization",
max_length=255,
null=True,
),
),
]

View file

@ -2,6 +2,7 @@ from auditlog.registry import auditlog # type: ignore
from .contact import Contact from .contact import Contact
from .domain_application import DomainApplication from .domain_application import DomainApplication
from .domain_information import DomainInformation
from .domain import Domain from .domain import Domain
from .host_ip import HostIP from .host_ip import HostIP
from .host import Host from .host import Host
@ -15,6 +16,7 @@ from .website import Website
__all__ = [ __all__ = [
"Contact", "Contact",
"DomainApplication", "DomainApplication",
"DomainInformation",
"Domain", "Domain",
"DomainInvitation", "DomainInvitation",
"HostIP", "HostIP",

View file

@ -9,7 +9,7 @@ from django_fsm import FSMField, transition # type: ignore
from .utility.time_stamped_model import TimeStampedModel from .utility.time_stamped_model import TimeStampedModel
from ..utility.email import send_templated_email, EmailSendingError from ..utility.email import send_templated_email, EmailSendingError
from itertools import chain
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -520,6 +520,10 @@ class DomainApplication(TimeStampedModel):
Domain = apps.get_model("registrar.Domain") Domain = apps.get_model("registrar.Domain")
created_domain, _ = Domain.objects.get_or_create(name=self.requested_domain) created_domain, _ = Domain.objects.get_or_create(name=self.requested_domain)
# copy the information from domainapplication into domaininformation
DomainInformation = apps.get_model("registrar.DomainInformation")
DomainInformation.create_from_da(self)
# create the permission for the user # create the permission for the user
UserDomainRole = apps.get_model("registrar.UserDomainRole") UserDomainRole = apps.get_model("registrar.UserDomainRole")
UserDomainRole.objects.get_or_create( UserDomainRole.objects.get_or_create(
@ -577,3 +581,26 @@ class DomainApplication(TimeStampedModel):
if self.organization_type == DomainApplication.OrganizationChoices.FEDERAL: if self.organization_type == DomainApplication.OrganizationChoices.FEDERAL:
return True return True
return False return False
def to_dict(self):
"""This is to process to_dict for Domain Information, making it friendly
to "copy" it
More information can be found at this- (This used #5)
https://stackoverflow.com/questions/21925671/convert-django-model-object-to-dict-with-all-of-the-fields-intact/29088221#29088221
""" # noqa 590
opts = self._meta
data = {}
for field in chain(opts.concrete_fields, opts.private_fields):
if field.get_internal_type() in ("ForeignKey", "OneToOneField"):
# get the related instance of the FK value
fk_id = field.value_from_object(self)
if fk_id:
data[field.name] = field.related_model.objects.get(id=fk_id)
else:
data[field.name] = None
else:
data[field.name] = field.value_from_object(self)
for field in opts.many_to_many:
data[field.name] = field.value_from_object(self)
return data

View file

@ -0,0 +1,250 @@
from __future__ import annotations
from .domain_application import DomainApplication
from .utility.time_stamped_model import TimeStampedModel
import logging
from django.db import models
logger = logging.getLogger(__name__)
class DomainInformation(TimeStampedModel):
"""A registrant's domain information for that domain, exported from
DomainApplication. We use these field from DomainApplication with few exceptation
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
the application once approved, so copying them that way we can make changes
after its approved. Most fields here are copied from Application."""
StateTerritoryChoices = DomainApplication.StateTerritoryChoices
OrganizationChoices = DomainApplication.OrganizationChoices
BranchChoices = DomainApplication.BranchChoices
AGENCY_CHOICES = DomainApplication.AGENCY_CHOICES
# This is the application user who created this application. The contact
# information that they gave is in the `submitter` field
creator = models.ForeignKey(
"registrar.User",
on_delete=models.PROTECT,
related_name="information_created",
)
domain_application = models.OneToOneField(
"registrar.DomainApplication",
on_delete=models.PROTECT,
blank=True,
null=True,
related_name="domainapplication_info",
help_text="Associated domain application",
unique=True,
)
# ##### data fields from the initial form #####
organization_type = models.CharField(
max_length=255,
choices=OrganizationChoices.choices,
null=True,
blank=True,
help_text="Type of Organization",
)
federally_recognized_tribe = models.BooleanField(
null=True,
help_text="Is the tribe federally recognized",
)
state_recognized_tribe = models.BooleanField(
null=True,
help_text="Is the tribe recognized by a state",
)
tribe_name = models.TextField(
null=True,
blank=True,
help_text="Name of tribe",
)
federal_agency = models.TextField(
null=True,
blank=True,
help_text="Federal agency",
)
federal_type = models.CharField(
max_length=50,
choices=BranchChoices.choices,
null=True,
blank=True,
help_text="Federal government branch",
)
is_election_board = models.BooleanField(
null=True,
blank=True,
help_text="Is your organization an election office?",
)
organization_name = models.TextField(
null=True,
blank=True,
help_text="Organization name",
db_index=True,
)
address_line1 = models.TextField(
null=True,
blank=True,
help_text="Street address",
)
address_line2 = models.CharField(
max_length=15,
null=True,
blank=True,
help_text="Street address line 2",
)
city = models.TextField(
null=True,
blank=True,
help_text="City",
)
state_territory = models.CharField(
max_length=2,
null=True,
blank=True,
help_text="State, territory, or military post",
)
zipcode = models.CharField(
max_length=10,
null=True,
blank=True,
help_text="Zip code",
db_index=True,
)
urbanization = models.TextField(
null=True,
blank=True,
help_text="Urbanization (Puerto Rico only)",
)
type_of_work = models.TextField(
null=True,
blank=True,
help_text="Type of work of the organization",
)
more_organization_information = models.TextField(
null=True,
blank=True,
help_text="Further information about the government organization",
)
authorizing_official = models.ForeignKey(
"registrar.Contact",
null=True,
blank=True,
related_name="information_authorizing_official",
on_delete=models.PROTECT,
)
domain = models.OneToOneField(
"registrar.Domain",
on_delete=models.PROTECT,
blank=True,
null=True,
# Access this information via Domain as "domain.domain_info"
related_name="domain_info",
help_text="Domain to which this information belongs",
)
# This is the contact information provided by the applicant. The
# application user who created it is in the `creator` field.
submitter = models.ForeignKey(
"registrar.Contact",
null=True,
blank=True,
related_name="submitted_applications_information",
on_delete=models.PROTECT,
)
purpose = models.TextField(
null=True,
blank=True,
help_text="Purpose of your domain",
)
other_contacts = models.ManyToManyField(
"registrar.Contact",
blank=True,
related_name="contact_applications_information",
)
no_other_contacts_rationale = models.TextField(
null=True,
blank=True,
help_text="Reason for listing no additional contacts",
)
anything_else = models.TextField(
null=True,
blank=True,
help_text="Anything else we should know?",
)
is_policy_acknowledged = models.BooleanField(
null=True,
blank=True,
help_text="Acknowledged .gov acceptable use policy",
)
security_email = models.EmailField(
max_length=320,
null=True,
blank=True,
help_text="Security email for public use",
)
def __str__(self):
try:
if self.domain and self.domain.name:
return self.domain.name
else:
return f"domain info set up and created by {self.creator}"
except Exception:
return ""
@classmethod
def create_from_da(cls, domain_application):
"""Takes in a DomainApplication dict and converts it into DomainInformation"""
da_dict = domain_application.to_dict()
# remove the id so one can be assinged on creation
da_id = da_dict.pop("id")
# check if we have a record that corresponds with the domain
# application, if so short circuit the create
domain_info = cls.objects.filter(domain_application__id=da_id).first()
if domain_info:
return domain_info
# the following information below is not needed in the domain information:
da_dict.pop("status")
da_dict.pop("current_websites")
da_dict.pop("investigator")
da_dict.pop("alternative_domains")
# use the requested_domain to create information for this domain
da_dict["domain"] = da_dict.pop("requested_domain")
other_contacts = da_dict.pop("other_contacts")
domain_info = cls(**da_dict)
domain_info.domain_application = domain_application
# Save so the object now have PK
# (needed to process the manytomany below before, first)
domain_info.save()
# Process the remaining "many to many" stuff
domain_info.other_contacts.add(*other_contacts)
domain_info.save()
return domain_info
class Meta:
verbose_name_plural = "Domain Information"

View file

@ -4,6 +4,7 @@ from django.db.utils import IntegrityError
from registrar.models import ( from registrar.models import (
Contact, Contact,
DomainApplication, DomainApplication,
DomainInformation,
User, User,
Website, Website,
Domain, Domain,
@ -63,6 +64,33 @@ class TestDomainApplication(TestCase):
application.other_contacts.add(contact) application.other_contacts.add(contact)
application.save() application.save()
def test_domain_info(self):
"""Can create domain info with all fields."""
user, _ = User.objects.get_or_create()
contact = Contact.objects.create()
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
information = DomainInformation.objects.create(
creator=user,
organization_type=DomainInformation.OrganizationChoices.FEDERAL,
federal_type=DomainInformation.BranchChoices.EXECUTIVE,
is_election_board=False,
organization_name="Test",
address_line1="100 Main St.",
address_line2="APT 1A",
state_territory="CA",
zipcode="12345-6789",
authorizing_official=contact,
submitter=contact,
purpose="Igorville rules!",
anything_else="All of Igorville loves the dotgov program.",
is_policy_acknowledged=True,
domain=domain,
)
information.other_contacts.add(contact)
information.save()
self.assertEqual(information.domain.id, domain.id)
self.assertEqual(information.id, domain.domain_info.id)
def test_status_fsm_submit_fail(self): def test_status_fsm_submit_fail(self):
user, _ = User.objects.get_or_create() user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(creator=user) application = DomainApplication.objects.create(creator=user)
@ -166,6 +194,24 @@ class TestPermissions(TestCase):
self.assertTrue(UserDomainRole.objects.get(user=user, domain=domain)) self.assertTrue(UserDomainRole.objects.get(user=user, domain=domain))
class TestDomainInfo(TestCase):
"""Test creation of Domain Information when approved."""
def test_approval_creates_info(self):
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(
creator=user, requested_domain=domain
)
# skip using the submit method
application.status = DomainApplication.SUBMITTED
application.approve()
# should be an information present for this domain
self.assertTrue(DomainInformation.objects.get(domain=domain))
class TestInvitations(TestCase): class TestInvitations(TestCase):
"""Test the retrieval of invitations.""" """Test the retrieval of invitations."""