Merge branch 'main' into bl/test-commit-signing

This commit is contained in:
brandonlenz 2023-05-15 12:20:57 -04:00
commit 99eb0cd484
No known key found for this signature in database
GPG key ID: FC8818009789E370
54 changed files with 1771 additions and 442 deletions

View file

@ -5,18 +5,18 @@ run-name: Build and deploy developer sandbox for branch ${{ github.head_ref }}
on:
pull_request:
branches:
- 'ik/**'
- 'rjm/**'
- 'jon/**'
- 'sspj/**'
- 'mr/**'
- 'nmb/**'
- 'ab/**'
- 'bl/**'
jobs:
variables:
if: |
startsWith(github.head_ref, 'ik/')
|| startsWith(github.head_ref, 'jon')
|| startsWith(github.head_ref, 'sspj/')
|| startsWith(github.head_ref, 'mr/')
|| startsWith(github.head_ref, 'nmb/')
|| startsWith(github.head_ref, 'ab/')
|| startsWith(github.head_ref, 'bl/')
|| startsWith(github.head_ref, 'rjm/')
outputs:
environment: ${{ steps.var.outputs.environment}}
runs-on: "ubuntu-latest"
@ -40,7 +40,7 @@ jobs:
docker compose run node npx gulp compile
- name: Collect static assets
working-directory: ./src
run: docker compose run app python manage.py collectstatic
run: docker compose run app python manage.py collectstatic --no-input
- name: Deploy to cloud.gov sandbox
uses: 18f/cg-deploy-action@main
env:

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,24 @@ 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
History preservation and merge contexts are more important to us than a clean and linear history, so we will merge instead of rebasing.
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
```
Resources:
- [https://frontend.turing.edu/lessons/module-3/merge-vs-rebase.html](https://frontend.turing.edu/lessons/module-3/merge-vs-rebase.html)
- [https://www.atlassian.com/git/tutorials/merging-vs-rebasing](https://www.atlassian.com/git/tutorials/merging-vs-rebasing)
- [https://www.simplilearn.com/git-rebase-vs-merge-article](https://www.simplilearn.com/git-rebase-vs-merge-article)
## Setting Vars
Non-secret environment variables for local development are set in [src/docker-compose.yml](../../src/docker-compose.yml).
@ -41,6 +59,28 @@ cp ./.env-example .env
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/']"
},

742
src/Pipfile.lock generated

File diff suppressed because it is too large Load diff

View file

@ -49,7 +49,7 @@ class ViewsTest(TestCase):
# assert
self.assertEqual(response.status_code, 500)
self.assertTemplateUsed(response, "500.html")
self.assertIn("Server Error", response.content.decode("utf-8"))
self.assertIn("server error", response.content.decode("utf-8"))
def test_login_callback_reads_next(self, mock_client):
# setup

View file

@ -62,6 +62,9 @@ services:
- POSTGRES_PASSWORD=feedabee
node:
build:
context: .
dockerfile: node.Dockerfile
image: node
volumes:
- .:/app

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

@ -230,4 +230,34 @@ function handleValidationClick(e) {
})();
/**
* An IIFE that attaches a click handler for our dynamic nameservers form
*
* Only does something on a single page, but it should be fast enough to run
* it everywhere.
*/
(function prepareForms() {
let serverForm = document.querySelectorAll(".server-form")
let container = document.querySelector("#form-container")
let addButton = document.querySelector("#add-form")
let totalForms = document.querySelector("#id_form-TOTAL_FORMS")
let formNum = serverForm.length-1
addButton.addEventListener('click', addForm)
function addForm(e){
let newForm = serverForm[2].cloneNode(true)
let formNumberRegex = RegExp(`form-(\\d){1}-`,'g')
let formLabelRegex = RegExp(`Name server (\\d){1}`, 'g')
let formExampleRegex = RegExp(`ns(\\d){1}`, 'g')
formNum++
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum}-`)
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `Name server ${formNum+1}`)
newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum+1}`)
container.insertBefore(newForm, addButton)
newForm.querySelector("input").value = ""
totalForms.setAttribute('value', `${formNum+1}`)
}
})();

View file

@ -417,8 +417,18 @@ footer {
color: color('primary');
}
.usa-identifier__logo {
height: units(7);
}
abbr[title] {
// workaround for underlining abbr element
border-bottom: none;
text-decoration: none;
}
.usa-textarea {
@include at-media('tablet') {
height: units('mobile');
}
}

View file

@ -78,6 +78,11 @@ urlpatterns = [
),
path("domain/<int:pk>", views.DomainView.as_view(), name="domain"),
path("domain/<int:pk>/users", views.DomainUsersView.as_view(), name="domain-users"),
path(
"domain/<int:pk>/nameservers",
views.DomainNameserversView.as_view(),
name="domain-nameservers",
),
path(
"domain/<int:pk>/users/add",
views.DomainAddUserView.as_view(),

View file

@ -44,6 +44,21 @@ 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",
"last_name": "Mrad",
},
{
"username": "eb2214cd-fc0c-48c0-9dbd-bc4cd6820c74",
"first_name": "Alysia",
"last_name": "Broddrick",
},
]
@classmethod

View file

@ -1,2 +1,2 @@
from .application_wizard import *
from .domain import DomainAddUserForm
from .domain import DomainAddUserForm, NameserverFormset

View file

@ -5,7 +5,7 @@ from typing import Callable
from phonenumber_field.formfields import PhoneNumberField # type: ignore
from django import forms
from django.core.validators import RegexValidator
from django.core.validators import RegexValidator, MaxLengthValidator
from django.urls import reverse
from django.utils.safestring import mark_safe
@ -315,6 +315,12 @@ class TypeOfWorkForm(RegistrarForm):
# label has to end in a space to get the label_suffix to show
label="What type of work does your organization do? ",
widget=forms.Textarea(),
validators=[
MaxLengthValidator(
1000,
message="Response must be less than 1000 characters.",
)
],
error_messages={"required": "Enter the type of work your organization does."},
)
@ -327,6 +333,12 @@ class TypeOfWorkForm(RegistrarForm):
" support your claims. "
),
widget=forms.Textarea(),
validators=[
MaxLengthValidator(
1000,
message="Response must be less than 1000 characters.",
)
],
error_messages={
"required": (
"Describe how your organization is independent of a state government."
@ -554,6 +566,12 @@ class PurposeForm(RegistrarForm):
purpose = forms.CharField(
label="Purpose",
widget=forms.Textarea(),
validators=[
MaxLengthValidator(
1000,
message="Response must be less than 1000 characters.",
)
],
error_messages={
"required": "Describe how you'll use the .gov domain youre requesting."
},
@ -696,6 +714,12 @@ class AnythingElseForm(RegistrarForm):
required=False,
label="Anything else we should know?",
widget=forms.Textarea(),
validators=[
MaxLengthValidator(
1000,
message="Response must be less than 1000 characters.",
)
],
)

View file

@ -1,6 +1,7 @@
"""Forms for domain management."""
from django import forms
from django.forms import formset_factory
class DomainAddUserForm(forms.Form):
@ -8,3 +9,16 @@ class DomainAddUserForm(forms.Form):
"""Form for adding a user to a domain."""
email = forms.EmailField(label="Email")
class DomainNameserverForm(forms.Form):
"""Form for changing nameservers."""
server = forms.CharField(label="Name server")
NameserverFormset = formset_factory(
DomainNameserverForm,
extra=1,
)

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

@ -1,6 +1,8 @@
import logging
import re
from typing import List
from django.apps import apps
from django.core.exceptions import ValidationError
from django.db import models
@ -215,6 +217,24 @@ class Domain(TimeStampedModel):
def __str__(self) -> str:
return self.name
def nameservers(self) -> List[str]:
"""A list of the nameservers for this domain.
TODO: call EPP to get this info instead of returning fake data.
"""
return [
# reserved example domain
"ns1.example.com",
"ns2.example.com",
"ns3.example.com",
]
def set_nameservers(self, new_nameservers: List[str]):
"""Set the nameservers for this domain."""
# TODO: call EPP to set these values in the registry instead of doing
# nothing.
logger.warn("TODO: Fake setting nameservers to %s", new_nameservers)
@property
def roid(self):
return self._get_property("roid")

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

@ -0,0 +1,20 @@
<svg width="404" height="409" viewBox="0 0 404 409" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M291.707 328.743C240.024 358.583 133.444 374.87 78.8899 280.379C14.3648 168.618 78.2559 99.3488 140.956 63.1491C203.655 26.9495 296.801 80.4848 337.226 150.503C377.652 220.522 343.391 298.903 291.707 328.743Z" fill="#F5F8FA"/>
<circle cx="276.88" cy="130.594" r="8" transform="rotate(135 276.88 130.594)" fill="#7AA5C1"/>
<circle cx="288.196" cy="119.279" r="8" transform="rotate(135 288.196 119.279)" fill="#7AA5C1"/>
<circle cx="231.626" cy="175.849" r="8" transform="rotate(135 231.626 175.849)" fill="#7AA5C1"/>
<circle cx="186.371" cy="221.104" r="8" transform="rotate(135 186.371 221.104)" fill="#7AA5C1"/>
<circle cx="242.939" cy="164.535" r="8" transform="rotate(135 242.939 164.535)" fill="#7AA5C1"/>
<circle cx="197.686" cy="209.788" r="8" transform="rotate(135 197.686 209.788)" fill="#7AA5C1"/>
<circle cx="220.312" cy="187.163" r="8" transform="rotate(135 220.312 187.163)" fill="#7AA5C1"/>
<circle cx="175.057" cy="232.417" r="8" transform="rotate(135 175.057 232.417)" fill="#7AA5C1"/>
<circle cx="163.743" cy="243.731" r="8" transform="rotate(135 163.743 243.731)" fill="#7AA5C1"/>
<circle cx="152.43" cy="255.045" r="8" transform="rotate(135 152.43 255.045)" fill="#7AA5C1"/>
<circle cx="141.116" cy="266.358" r="8" transform="rotate(135 141.116 266.358)" fill="#7AA5C1"/>
<circle cx="129.802" cy="277.672" r="8" transform="rotate(135 129.802 277.672)" fill="#7AA5C1"/>
<circle cx="118.489" cy="288.986" r="8" transform="rotate(135 118.489 288.986)" fill="#7AA5C1"/>
<circle cx="254.253" cy="153.221" r="8" transform="rotate(135 254.253 153.221)" fill="#7AA5C1"/>
<circle cx="265.566" cy="141.908" r="8" transform="rotate(135 265.566 141.908)" fill="#7AA5C1"/>
<circle cx="208.998" cy="198.476" r="8" transform="rotate(135 208.998 198.476)" fill="#7AA5C1"/>
<circle cx="203.342" cy="203.999" r="120.001" stroke="#7AA5C1" stroke-width="16"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 53 KiB

View file

@ -0,0 +1,59 @@
<svg width="409" height="214" viewBox="0 0 409 214" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M366.004 90.4612C372.017 135.603 322.608 199.102 196.168 205.902C-32.9139 218.22 19.0655 18.8457 205.511 8.81994C299.204 3.78172 359.99 45.3195 366.004 90.4612Z" fill="#F5F8FA"/>
<circle cx="213.873" cy="37.4943" r="6.56803" fill="#7AA5C1"/>
<circle cx="212.214" cy="58.4272" r="6.56803" fill="#7AA5C1"/>
<circle cx="231.089" cy="66.2297" r="6.56803" fill="#7AA5C1"/>
<circle cx="235.451" cy="85.568" r="6.56803" fill="#7AA5C1"/>
<circle cx="252.535" cy="99.7787" r="6.56803" fill="#7AA5C1"/>
<circle cx="273.981" cy="115.121" r="6.56803" fill="#7AA5C1"/>
<circle cx="372.072" cy="182.568" r="6.56803" fill="#7AA5C1"/>
<circle cx="402.423" cy="189.642" r="6.56803" fill="#7AA5C1"/>
<circle cx="220.441" cy="99.6379" r="6.56803" fill="#7AA5C1"/>
<circle cx="231.089" cy="118.336" r="6.56803" fill="#7AA5C1"/>
<circle cx="262.722" cy="143.568" r="6.56803" fill="#7AA5C1"/>
<circle cx="248.722" cy="125.568" r="6.56803" fill="#7AA5C1"/>
<circle cx="288.537" cy="138.04" r="6.56803" fill="#7AA5C1"/>
<circle cx="300.051" cy="160.304" r="6.56803" fill="#7AA5C1"/>
<circle cx="338.722" cy="170.568" r="6.56803" fill="#7AA5C1"/>
<circle cx="349.914" cy="189.575" r="6.56803" fill="#7AA5C1"/>
<circle cx="330.21" cy="189.575" r="6.56803" fill="#7AA5C1"/>
<circle cx="316.722" cy="173.568" r="6.56803" fill="#7AA5C1"/>
<circle cx="305.722" cy="190.568" r="6.56803" fill="#7AA5C1"/>
<circle cx="287.722" cy="184.568" r="6.56803" fill="#7AA5C1"/>
<circle cx="269.894" cy="183.007" r="6.56803" fill="#7AA5C1"/>
<circle cx="250.19" cy="189.575" r="6.56803" fill="#7AA5C1"/>
<circle cx="220.722" cy="192.568" r="6.56803" fill="#7AA5C1"/>
<circle cx="233.722" cy="173.568" r="6.56803" fill="#7AA5C1"/>
<circle cx="252.722" cy="165.568" r="6.56803" fill="#7AA5C1"/>
<circle cx="180.203" cy="189.575" r="6.56803" fill="#7AA5C1"/>
<circle cx="161.329" cy="196.143" r="6.56803" fill="#7AA5C1"/>
<circle cx="140.795" cy="189.575" r="6.56803" fill="#7AA5C1"/>
<path d="M122.733 189.575C122.733 193.203 119.793 196.143 116.165 196.143C112.538 196.143 109.597 193.203 109.597 189.575C109.597 185.948 112.538 183.007 116.165 183.007C119.793 183.007 122.733 185.948 122.733 189.575Z" fill="#7AA5C1"/>
<circle cx="91.5343" cy="189.642" r="6.56803" fill="#7AA5C1"/>
<circle cx="57.8852" cy="185.432" r="6.56803" fill="#7AA5C1"/>
<circle cx="198.722" cy="195.568" r="6.56803" fill="#7AA5C1"/>
<circle cx="193.34" cy="70.2917" r="6.56803" fill="#7AA5C1"/>
<circle cx="98.9323" cy="156.735" r="6.56803" fill="#7AA5C1"/>
<circle cx="25.247" cy="189.642" r="6.56803" fill="#7AA5C1"/>
<circle cx="7.26164" cy="190.584" r="6.56803" fill="#7AA5C1"/>
<circle cx="76.7592" cy="173.44" r="6.56803" fill="#7AA5C1"/>
<circle cx="129.722" cy="172.568" r="6.56803" fill="#7AA5C1"/>
<circle cx="147.722" cy="164.568" r="6.56803" fill="#7AA5C1"/>
<circle cx="164.722" cy="173.568" r="6.56803" fill="#7AA5C1"/>
<circle cx="174.465" cy="150.167" r="6.56803" fill="#7AA5C1"/>
<circle cx="186.771" cy="169.871" r="6.56803" fill="#7AA5C1"/>
<circle cx="198.266" cy="150.167" r="6.56803" fill="#7AA5C1"/>
<circle cx="232.747" cy="151.176" r="6.56803" fill="#7AA5C1"/>
<circle cx="273.722" cy="162.568" r="6.56803" fill="#7AA5C1"/>
<circle cx="210.589" cy="163.303" r="6.56803" fill="#7AA5C1"/>
<circle cx="204.834" cy="124.904" r="6.56803" fill="#7AA5C1"/>
<circle cx="193.34" cy="108.553" r="6.56803" fill="#7AA5C1"/>
<circle cx="171.181" cy="119.345" r="6.56803" fill="#7AA5C1"/>
<circle cx="160.499" cy="138.04" r="6.56803" fill="#7AA5C1"/>
<circle cx="139.153" cy="144.608" r="6.56803" fill="#7AA5C1"/>
<circle cx="170.351" cy="95.4167" r="6.56803" fill="#7AA5C1"/>
<circle cx="151.477" cy="115.121" r="6.56803" fill="#7AA5C1"/>
<circle cx="125.204" cy="137.031" r="6.56803" fill="#7AA5C1"/>
<circle cx="112.881" cy="163.303" r="6.56803" fill="#7AA5C1"/>
<circle cx="204.834" cy="86.6427" r="6.56803" fill="#7AA5C1"/>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -1,28 +1,45 @@
{% extends "base.html" %}
{% load i18n %}
{% load i18n static %}
{% block title %}{% translate "Unauthorized" %}{% endblock %}
{% block title %}{% translate "Unauthorized | " %}{% endblock %}
{% block content %}
<main id="main-content" class="grid-container">
<h1>{% translate "Unauthorized" %}</h1>
<div class="grid-row grow-gap">
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
<h1>
{% translate "You are not authorized to view this page" %}
</h1>
<h2>
{% translate "Status 401" %}
</h2>
{% if friendly_message %}
<p>{{ friendly_message }}</p>
{% else %}
<p>{% translate "Authorization failed." %}</p>
{% endif %}
<p><a href="{% url 'login' %}">
{% translate "Would you like to try logging in again?" %}
</a></p>
<p>
You must be an authorized user and need to be signed in to view this page.
Would you like to <a href="{% url 'login' %}"> try logging in again?</a>
</p>
<p>
If you would like help with this error <a href="https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/contact/"> contact us </a>
</p>
{% if log_identifier %}
<p>Here's a unique identifier for this error.</p>
<blockquote>{{ log_identifier }}</blockquote>
<p class="text-semibold">{{ log_identifier }}</p>
<p>{% translate "Please include it if you contact us." %}</p>
{% endif %}
TODO: Content team to create a "how to contact us" footer for the error pages
</div>
<div class="tablet:grid-col-4">
<img
src="{% static 'img/registrar/dotgov_401_illo.svg' %}"
alt=""
/>
</div>
</div>
</main>
{% endblock %}

View file

@ -0,0 +1,45 @@
{% extends "base.html" %}
{% load i18n static %}
{% block title %}{% translate "Forbidden | " %}{% endblock %}
{% block content %}
<main id="main-content" class="grid-container">
<div class="grid-row grow-gap">
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
<h1>
{% translate "You do not have the right permissions to view this page." %}
</h1>
<h2>
{% translate "Status 403" %}
</h2>
{% if friendly_message %}
<p>{{ friendly_message }}</p>
{% else %}
<p>{% translate "Forbidden." %}</p>
{% endif %}
<p>
You must be an authorized user and need to be signed in to view this page.
Would you like to <a href="{% url 'login' %}"> try logging in again?</a>
</p>
<p>
If you would like help with this error <a href="https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/contact/"> contact us </a>
</p>
{% if log_identifier %}
<p>Here's a unique identifier for this error.</p>
<p class="text-semibold">{{ log_identifier }}</p>
<p>{% translate "Please include it if you contact us." %}</p>
{% endif %}
</div>
<div class="tablet:grid-col-4">
<img
src="{% static 'img/registrar/dotgov_401_illo.svg' %}"
alt=""
/>
</div>
</div>
</main>
{% endblock %}

View file

@ -1,15 +1,31 @@
{% extends "base.html" %}
{% load i18n %}
{% load i18n static %}
{% block title %}{% translate "Page not found" %}{% endblock %}
{% block title %}{% translate "Page not found | " %}{% endblock %}
{% block content %}
<main id="main-content" class="grid-container">
<div class="grid-row grid-gap">
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
<h1>
{% translate "We couldnt find that page" %}
</h1>
<h2>
{% translate "Status 404" %}
</h2>
<h1>{% translate "Page not found" %}</h1>
<p> Try going to the <a href="/">homepage</a>. If you cant find what youre looking for, <a href= "https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/contact/">contact us.</a>
</p>
</div>
<div class="tablet:grid-col-4">
<img
src="{% static 'img/registrar/dotgov_404_illo.svg' %}"
alt=""
/>
</div>
</div>
<p>{% translate "The requested page could not be found." %}</p>
</main>
{% endblock %}

View file

@ -1,24 +1,39 @@
{% extends "base.html" %}
{% load i18n %}
{% load i18n static %}
{% block title %}{% translate "Server error" %}{% endblock %}
{% block title %}{% translate "Server error | " %}{% endblock %}
{% block content %}
<main id="main-content" class="grid-container">
<h1>{% translate "Server Error" %}</h1>
<div class="grid-row grid-gap">
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
<h1>
{% translate "We're having some trouble" %}
</h1>
<h2>
{% translate "Status 500 server error" %}
</h2>
{% if friendly_message %}
<p>{{ friendly_message }}</p>
{% else %}
<p>{% translate "An internal server error occurred." %}</p>
<p>
Sorry! Try waiting a few minutes and then reloading the page.
<a href="https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/contact/"> Contact us </a> if you need help.
</p>
{% endif %}
{% if log_identifier %}
<p>Here's a unique identifier for this error.</p>
<blockquote>{{ log_identifier }}</blockquote>
<p class="text-semibold">{{ log_identifier }}</p>
<p>{% translate "Please include it if you contact us." %}</p>
{% endif %}
TODO: Content team to create a "how to contact us" footer for the error pages
</div>
<div class="tablet:grid-col-4 flex-align-self-end">
<img
src="{%static 'img/registrar/dotgov_500_illo.svg' %}"
alt=""
/>
</div>
</div>
</main>
{% endblock %}

View file

@ -11,7 +11,7 @@
{% block form_fields %}
{% with add_label_class="usa-sr-only" attr_maxlength=500 %}
{% with add_label_class="usa-sr-only" attr_maxlength=1000 %}
{% input_with_errors forms.0.anything_else %}
{% endwith %}
{% endblock %}

View file

@ -19,7 +19,7 @@ Read about <a href="{% url 'todo' %}">activities that are prohibited on .gov dom
{% block form_fields %}
{% with attr_maxlength=500 add_label_class="usa-sr-only" %}
{% with attr_maxlength=1000 add_label_class="usa-sr-only" %}
{% input_with_errors forms.0.purpose %}
{% endwith %}
{% endblock %}

View file

@ -3,7 +3,7 @@
{% block form_fields %}
{% with attr_maxlength=500 %}
{% with attr_maxlength=1000 %}
{% input_with_errors forms.0.type_of_work %}
{% input_with_errors forms.0.more_organization_information %}
{% endwith %}

View file

@ -4,5 +4,6 @@
{# hint: spacing in the class string matters #}
class="{{ uswds_input_class }}{% if classes %} {{ classes }}{% endif %}"
{% if widget.value != None %}value="{{ widget.value|stringformat:'s' }}"{% endif %}
{% if sublabel_text %}aria-describedby="{{ widget.attrs.id }}__sublabel"{% endif %}
{% include "django/forms/widgets/attrs.html" %}
/>

View file

@ -0,0 +1,52 @@
{% extends "domain_base.html" %}
{% load static field_helpers%}
{% block title %}Domain name servers | {{ domain.name }} | {% endblock %}
{% block domain_content %}
{# this is right after the messages block in the parent template #}
{% for form in formset %}
{% include "includes/form_errors.html" with form=form %}
{% endfor %}
<h1>Domain name servers</h1>
<p>Before your domain can be used we'll need information about your domain
name servers.</p>
<p><a class="usa-link" href="{% url "todo" %}">Get help with domain servers.</a></p>
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
<div class="server-form">
{% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" %}
{% if forloop.counter <= 2 %}
{% with attr_required=True %}
{% input_with_errors form.server %}
{% endwith %}
{% else %}
{% input_with_errors form.server %}
{% endif %}
{% endwith %}
</div>
{% endfor %}
<button type="button" class="usa-button usa-button--unstyled display-block" id="add-form">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
</svg><span class="margin-left-05">Add another name server</span>
</button>
<button
type="submit"
class="usa-button"
>Save</button>
</form>
{% endblock %} {# domain_content #}

View file

@ -13,7 +13,7 @@
</li>
<li class="usa-sidenav__item">
{% url 'todo' as url %}
{% url 'domain-nameservers' pk=domain.id as url %}
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>

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

@ -28,6 +28,10 @@ error messages, if necessary.
{% include "django/forms/label.html" %}
{% endif %}
{% if sublabel_text %}
<p id="{{ widget.attrs.id }}__sublabel" class="text-base margin-top-2px margin-bottom-1">{{ sublabel_text }}</p>
{% endif %}
{% if field.errors %}
<div id="{{ widget.attrs.id }}__error-message">
{% for error in field.errors %}

View file

@ -6,3 +6,9 @@ from django.template.defaulttags import register
@register.filter
def get_item(dictionary, key):
return dictionary.get(key)
@register.filter
def concat(arg1, arg2):
"""concatenate arg1 & arg2"""
return str(arg1) + str(arg2)

View file

@ -11,6 +11,9 @@ from registrar.forms.application_wizard import (
OtherContactsForm,
RequirementsForm,
TribalGovernmentForm,
PurposeForm,
AnythingElseForm,
TypeOfWorkForm,
)
@ -85,6 +88,125 @@ class TestFormValidation(TestCase):
["Enter an email address in the required format, like name@example.com."],
)
def test_purpose_form_character_count_invalid(self):
"""Response must be less than 1000 characters."""
form = PurposeForm(
data={
"purpose": "Bacon ipsum dolor amet fatback strip steak pastrami"
"shankle, drumstick doner chicken landjaeger turkey andouille."
"Buffalo biltong chuck pork chop tongue bresaola turkey. Doner"
"ground round strip steak, jowl tail chuck ribeye bacon"
"beef ribs swine filet ball tip pancetta strip steak sirloin"
"mignon ham spare ribs rump. Tail shank biltong beef ribs doner"
"buffalo swine bacon. Tongue cow picanha brisket bacon chuck"
"leberkas pork loin pork, drumstick capicola. Doner short loin"
"ground round fatback turducken chislic shoulder turducken"
"spare ribs, burgdoggen kielbasa kevin frankfurter ball tip"
"pancetta cupim. Turkey meatball andouille porchetta hamburger"
"pork chop corned beef. Brisket short ribs turducken, pork chop"
"chislic turkey ball pork chop leberkas rump, rump bacon, jowl"
"tip ham. Shankle salami tongue venison short ribs kielbasa"
"tri-tip ham hock swine hamburger. Flank meatball corned beef"
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
"beef ribs rump jowl tenderloin swine sausage biltong"
"bacon rump tail boudin meatball boudin meatball boudin."
}
)
self.assertEqual(
form.errors["purpose"],
["Response must be less than 1000 characters."],
)
def test_anything_else_form_type_of_work_character_count_invalid(self):
"""Response must be less than 1000 characters."""
form = AnythingElseForm(
data={
"anything_else": "Bacon ipsum dolor amet fatback strip steak pastrami"
"shankle, drumstick doner chicken landjaeger turkey andouille."
"Buffalo biltong chuck pork chop tongue bresaola turkey. Doner"
"ground round strip steak, jowl tail chuck ribeye bacon"
"beef ribs swine filet ball tip pancetta strip steak sirloin"
"mignon ham spare ribs rump. Tail shank biltong beef ribs doner"
"buffalo swine bacon. Tongue cow picanha brisket bacon chuck"
"leberkas pork loin pork, drumstick capicola. Doner short loin"
"ground round fatback turducken chislic shoulder turducken"
"spare ribs, burgdoggen kielbasa kevin frankfurter ball tip"
"pancetta cupim. Turkey meatball andouille porchetta hamburger"
"pork chop corned beef. Brisket short ribs turducken, pork chop"
"chislic turkey ball pork chop leberkas rump, rump bacon, jowl"
"tip ham. Shankle salami tongue venison short ribs kielbasa"
"tri-tip ham hock swine hamburger. Flank meatball corned beef"
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
"beef ribs rump jowl tenderloin swine sausage biltong"
"bacon rump tail boudin meatball boudin meatball boudin."
}
)
self.assertEqual(
form.errors["anything_else"],
["Response must be less than 1000 characters."],
)
def test_anything_else_form_more_organization_information_character_count_invalid(
self,
):
"""Response must be less than 1000 characters."""
form = TypeOfWorkForm(
data={
"more_organization_information": "Bacon ipsum dolor amet fatback"
"shankle, drumstick doner chicken landjaeger turkey andouille."
"Buffalo biltong chuck pork chop tongue bresaola turkey. Doner"
"ground round strip steak, jowl tail chuck ribeye bacon"
"beef ribs swine filet ball tip pancetta strip steak sirloin"
"mignon ham spare ribs rump. Tail shank biltong beef ribs doner"
"buffalo swine bacon. Tongue cow picanha brisket bacon chuck"
"leberkas pork loin pork, drumstick capicola. Doner short loin"
"ground round fatback turducken chislic shoulder turducken"
"spare ribs, burgdoggen kielbasa kevin frankfurter ball tip"
"pancetta cupim. Turkey meatball andouille porchetta hamburger"
"pork chop corned beef. Brisket short ribs turducken, pork chop"
"chislic turkey ball pork chop leberkas rump, rump bacon, jowl"
"tip ham. Shankle salami tongue venison short ribs kielbasa"
"tri-tip ham hock swine hamburger. Flank meatball corned beef"
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
"beef ribs rump jowl tenderloin swine sausage biltong"
"bacon rump tail boudin meatball boudin meatball boudin"
"strip steak pastrami."
}
)
self.assertEqual(
form.errors["more_organization_information"],
["Response must be less than 1000 characters."],
)
def test_anything_else_form_character_count_invalid(self):
"""Response must be less than 1000 characters."""
form = TypeOfWorkForm(
data={
"type_of_work": "Bacon ipsum dolor amet fatback strip steak pastrami"
"shankle, drumstick doner chicken landjaeger turkey andouille."
"Buffalo biltong chuck pork chop tongue bresaola turkey. Doner"
"ground round strip steak, jowl tail chuck ribeye bacon"
"beef ribs swine filet ball tip pancetta strip steak sirloin"
"mignon ham spare ribs rump. Tail shank biltong beef ribs doner"
"buffalo swine bacon. Tongue cow picanha brisket bacon chuck"
"leberkas pork loin pork, drumstick capicola. Doner short loin"
"ground round fatback turducken chislic shoulder turducken"
"spare ribs, burgdoggen kielbasa kevin frankfurter ball tip"
"pancetta cupim. Turkey meatball andouille porchetta hamburger"
"pork chop corned beef. Brisket short ribs turducken, pork chop"
"chislic turkey ball pork chop leberkas rump, rump bacon, jowl"
"tip ham. Shankle salami tongue venison short ribs kielbasa"
"tri-tip ham hock swine hamburger. Flank meatball corned beef"
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
"beef ribs rump jowl tenderloin swine sausage biltong"
"bacon rump tail boudin meatball boudin meatball boudin."
}
)
self.assertEqual(
form.errors["type_of_work"],
["Response must be less than 1000 characters."],
)
def test_authorizing_official_phone_invalid(self):
"""Must be a valid phone number."""
form = AuthorizingOfficialForm(data={"phone": "boss@boss"})

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."""

View file

@ -1058,6 +1058,11 @@ class TestDomainPermissions(TestWithDomainPermissions):
)
self.assertEqual(response.status_code, 302)
response = self.client.get(
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 302)
def test_no_domain_role(self):
"""Logged in but no role gets 403 Forbidden."""
self.client.force_login(self.user)
@ -1079,6 +1084,12 @@ class TestDomainPermissions(TestWithDomainPermissions):
)
self.assertEqual(response.status_code, 403)
with less_console_noise():
response = self.client.get(
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 403)
class TestDomainDetail(TestWithDomainPermissions, WebTest):
def setUp(self):
@ -1222,6 +1233,55 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
home_page = self.app.get(reverse("home"))
self.assertContains(home_page, self.domain.name)
def test_domain_nameservers(self):
"""Can load domain's nameservers page."""
page = self.client.get(
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
)
self.assertContains(page, "Domain name servers")
def test_domain_nameservers_form(self):
"""Can change domain's nameservers.
Uses self.app WebTest because we need to interact with forms.
"""
nameservers_page = self.app.get(
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
)
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a post, response should be a redirect
self.assertEqual(result.status_code, 302)
self.assertEqual(
result["Location"],
reverse("domain-nameservers", kwargs={"pk": self.domain.id}),
)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
page = result.follow()
self.assertContains(page, "The name servers for this domain have been updated")
def test_domain_nameservers_form_invalid(self):
"""Can change domain's nameservers.
Uses self.app WebTest because we need to interact with forms.
"""
nameservers_page = self.app.get(
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
)
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# first two nameservers are required, so if we empty one out we should
# get a form error
nameservers_page.form["form-0-server"] = ""
with less_console_noise(): # swallow logged warning message
result = nameservers_page.form.submit()
# form submission was a post with an error, response should be a 200
# error text appears twice, once at the top of the page, once around
# the field.
self.assertContains(result, "This field is required", count=2, status_code=200)
class TestApplicationStatus(TestWithUser, WebTest):
def setUp(self):

View file

@ -1,6 +1,7 @@
from .application import *
from .domain import (
DomainView,
DomainNameserversView,
DomainUsersView,
DomainAddUserView,
DomainInvitationDeleteView,

View file

@ -12,7 +12,7 @@ from django.views.generic.edit import DeleteView, FormMixin
from registrar.models import Domain, DomainInvitation, User, UserDomainRole
from ..forms import DomainAddUserForm
from ..forms import DomainAddUserForm, NameserverFormset
from ..utility.email import send_templated_email, EmailSendingError
from .utility import DomainPermission
@ -29,6 +29,73 @@ class DomainView(DomainPermission, DetailView):
context_object_name = "domain"
class DomainNameserversView(DomainPermission, FormMixin, DetailView):
"""Domain nameserver editing view."""
model = Domain
template_name = "domain_nameservers.html"
context_object_name = "domain"
form_class = NameserverFormset
def get_initial(self):
"""The initial value for the form (which is a formset here)."""
domain = self.get_object()
return [{"server": server} for server in domain.nameservers()]
def get_success_url(self):
"""Redirect to the overview page for the domain."""
return reverse("domain-nameservers", kwargs={"pk": self.object.pk})
def get_context_data(self, **kwargs):
"""Adjust context from FormMixin for formsets."""
context = super().get_context_data(**kwargs)
# use "formset" instead of "form" for the key
context["formset"] = context.pop("form")
return context
def get_form(self, **kwargs):
"""Override the labels and required fields every time we get a formset."""
formset = super().get_form(**kwargs)
for i, form in enumerate(formset):
form.fields["server"].label += f" {i+1}"
if i < 2:
form.fields["server"].required = True
else:
form.fields["server"].required = False
return formset
def post(self, request, *args, **kwargs):
"""Formset submission posts to this view."""
self.object = self.get_object()
formset = self.get_form()
if formset.is_valid():
return self.form_valid(formset)
else:
return self.form_invalid(formset)
def form_valid(self, formset):
"""The formset is valid, perform something with it."""
# Set the nameservers from the formset
nameservers = []
for form in formset:
try:
nameservers.append(form.cleaned_data["server"])
except KeyError:
# no server information in this field, skip it
pass
domain = self.get_object()
domain.set_nameservers(nameservers)
messages.success(
self.request, "The name servers for this domain have been updated"
)
# superclass has the redirect
return super().form_valid(formset)
class DomainUsersView(DomainPermission, DetailView):
"""User management page in the domain details."""

View file

@ -51,6 +51,7 @@
10038 OUTOFSCOPE http://app:8080/(robots.txt|sitemap.xml|TODO|edit/)
10038 OUTOFSCOPE http://app:8080/users
10038 OUTOFSCOPE http://app:8080/users/add
10038 OUTOFSCOPE http://app:8080/nameservers
10038 OUTOFSCOPE http://app:8080/delete
10038 OUTOFSCOPE http://app:8080/withdraw
10038 OUTOFSCOPE http://app:8080/withdrawconfirmed