Merge remote-tracking branch 'origin/main' into rjm/415-character-limit-validation

This commit is contained in:
rachidatecs 2023-05-12 13:00:40 -04:00
commit 01770d4216
No known key found for this signature in database
GPG key ID: 3CEBBFA7325E5525
24 changed files with 715 additions and 14 deletions

View file

@ -32,6 +32,17 @@ jobs:
working-directory: ./src
run: docker compose run app python manage.py test
django-migrations-complete:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check for complete migrations
working-directory: ./src
run: |
docker compose run app ./manage.py makemigrations --dry-run --verbosity 3 && \
docker compose run app ./manage.py makemigrations --check
pa11y-scan:
runs-on: ubuntu-20.04
steps:
@ -53,5 +64,6 @@ jobs:
- name: run pa11y
working-directory: ./src
run: |
sleep 10;
npm i -g pa11y-ci
pa11y-ci

View file

@ -22,6 +22,18 @@ Visit the running application at [http://localhost:8080](http://localhost:8080).
We use the branch convention of `initials/branch-topic` (ex: `lmm/fix-footer`). This allows for automated deployment to a developer sandbox namespaced to the initials.
## Merging and PRs
To bring your feature branch up-to-date wih main:
```
git checkout main
git pull
git checkout <feature-branch>
git merge orgin/main
git push
```
## Setting Vars
Non-secret environment variables for local development are set in [src/docker-compose.yml](../../src/docker-compose.yml).
@ -34,6 +46,28 @@ DJANGO_SECRET_LOGIN_KEY="<...>"
You'll need to create the `.env` file yourself. Get the secrets from Cloud.gov by running `cf env getgov-YOURSANDBOX`. More information is available in [rotate_application_secrets.md](../operations/runbooks/rotate_application_secrets.md).
## Adding user to /admin
The endpoint /admin can be used to view and manage site content, including but not limited to user information and the list of current applications in the database. To be able to view and use /admin locally:
1. Login via login.gov
2. Go to the home page and make sure you can see the part where you can submit an application
3. Go to /admin and it will tell you that UUID is not authorized, copy that UUID for use in 4
4. in src/registrar/fixtures.py add to the ADMINS list in that file by adding your UUID as your username along with your first and last name. See below:
```
ADMINS = [
{
"username": "<UUID here>",
"first_name": "",
"last_name": "",
},
...
]
```
5. In the browser, navigate to /admins. To verify that all is working correctly, under "domain applications" you should see fake domains with various fake statuses.
## Viewing Logs
If you run via `docker-compose up`, you'll see the logs in your terminal.

View file

@ -21,4 +21,4 @@ CISA lacks a scalable, efficient, and secure method of managing the .gov TLD pro
| **Growth and use:** Regular growth in the overall number of .gov domains registered, with clear increases in election orgs, major metro areas, and state legislatures/courts | - Raw count of registered .gov domains increases <br /> - Number of YoY applications per month increases <br /> - Percent of 100 most populous cities, counties, etc. (per Census data) using .gov domains increases |
| **Data:** The program maintains authoritative contacts at, metadata about, and hostname information for all registered .gov domains, and is able to track that .gov domains are actually used | - Time-to-generate internal reports decreases <br /> - Results of periodic data quality audit show improvements month-over-month |
| **User satisfaction:** Getting a .gov domain is as easy and intuitive as possible | - Completion rate of form improves <br /> - Time from domain request to approval decreases <br /> - Number of domains requiring analyst data changes decreases |
| **Program reputation and experience:** The .gov program is viewed as trustworthy and responsive | - Response time for inquiries decreases <br /> - Resolution time decreases <br /> - Rate of repeat issues for tickets decreases <br /> - Number of SLTT organizations in CoP increases |
| **Program reputation and experience:** The .gov program is viewed as trustworthy and responsive | - Response time for inquiries decreases <br /> - Resolution time decreases <br /> - Rate of repeat issues for tickets decreases <br /> - Number of SLTT organizations in Community of Practice increases |

View file

@ -6,7 +6,7 @@ applications:
path: ../../src
instances: 1
memory: 512M
stack: cflinuxfs3
stack: cflinuxfs4
timeout: 180
command: ./run.sh
health-check-type: http

View file

@ -6,7 +6,7 @@ applications:
path: ../../src
instances: 1
memory: 512M
stack: cflinuxfs3
stack: cflinuxfs4
timeout: 180
command: ./run.sh
health-check-type: http

View file

@ -6,7 +6,7 @@ applications:
path: ../../src
instances: 1
memory: 512M
stack: cflinuxfs3
stack: cflinuxfs4
timeout: 180
command: ./run.sh
health-check-type: http

View file

@ -6,7 +6,7 @@ applications:
path: ../../src
instances: 1
memory: 512M
stack: cflinuxfs3
stack: cflinuxfs4
timeout: 180
command: ./run.sh
health-check-type: http

View file

@ -6,7 +6,7 @@ applications:
path: ../../src
instances: 1
memory: 512M
stack: cflinuxfs3
stack: cflinuxfs4
timeout: 180
command: ./run.sh
health-check-type: http

View file

@ -6,7 +6,7 @@ applications:
path: ../../src
instances: 1
memory: 512M
stack: cflinuxfs3
stack: cflinuxfs4
timeout: 180
command: ./run.sh
health-check-type: http

View file

@ -6,7 +6,7 @@ applications:
path: ../../src
instances: 1
memory: 512M
stack: cflinuxfs3
stack: cflinuxfs4
timeout: 180
command: ./run.sh
health-check-type: http

View file

@ -6,7 +6,7 @@ applications:
path: ../../src
instances: 1
memory: 512M
stack: cflinuxfs3
stack: cflinuxfs4
timeout: 180
command: ./run.sh
health-check-type: http

View file

@ -6,7 +6,7 @@ applications:
path: ../../src
instances: 1
memory: 512M
stack: cflinuxfs3
stack: cflinuxfs4
timeout: 180
command: ./run.sh
health-check-type: http

View file

@ -6,7 +6,7 @@ applications:
path: ../../src
instances: 1
memory: 512M
stack: cflinuxfs3
stack: cflinuxfs4
timeout: 180
command: ./run.sh
health-check-type: http

View file

@ -1,7 +1,7 @@
{
"defaults": {
"concurrency": 1,
"timeout": 10000,
"timeout": 30000,
"hideElements": "a[href='/whoami/']"
},

View file

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

View file

@ -417,6 +417,10 @@ footer {
color: color('primary');
}
.usa-identifier__logo {
height: units(7);
}
abbr[title] {
// workaround for underlining abbr element
border-bottom: none;

View file

@ -44,6 +44,11 @@ class UserFixture:
"first_name": "Neil",
"last_name": "Martinsen-Burrell",
},
{
"username": "7185e6cd-d3c8-4adc-90a3-ceddba71d24f",
"first_name": "Jon",
"last_name": "Roberts",
},
{
"username": "5f283494-31bd-49b5-b024-a7e7cae00848",
"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 .domain_application import DomainApplication
from .domain_information import DomainInformation
from .domain import Domain
from .host_ip import HostIP
from .host import Host
@ -15,6 +16,7 @@ from .website import Website
__all__ = [
"Contact",
"DomainApplication",
"DomainInformation",
"Domain",
"DomainInvitation",
"HostIP",

View file

@ -9,7 +9,7 @@ from django_fsm import FSMField, transition # type: ignore
from .utility.time_stamped_model import TimeStampedModel
from ..utility.email import send_templated_email, EmailSendingError
from itertools import chain
logger = logging.getLogger(__name__)
@ -520,6 +520,10 @@ class DomainApplication(TimeStampedModel):
Domain = apps.get_model("registrar.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
UserDomainRole = apps.get_model("registrar.UserDomainRole")
UserDomainRole.objects.get_or_create(
@ -577,3 +581,26 @@ class DomainApplication(TimeStampedModel):
if self.organization_type == DomainApplication.OrganizationChoices.FEDERAL:
return True
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

@ -52,7 +52,7 @@
src="{% static 'img/CISA_logo.png' %}"
alt="CISA logo"
role="img"
width="48px"
width="56px"
/></a>
</div>
<section

View file

@ -4,6 +4,7 @@ from django.db.utils import IntegrityError
from registrar.models import (
Contact,
DomainApplication,
DomainInformation,
User,
Website,
Domain,
@ -63,6 +64,33 @@ class TestDomainApplication(TestCase):
application.other_contacts.add(contact)
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):
user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(creator=user)
@ -166,6 +194,24 @@ class TestPermissions(TestCase):
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):
"""Test the retrieval of invitations."""