mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-29 17:00:02 +02:00
Merge branch 'main' into bl/test-commit-signing
This commit is contained in:
commit
99eb0cd484
54 changed files with 1771 additions and 442 deletions
22
.github/workflows/deploy-sandbox.yaml
vendored
22
.github/workflows/deploy-sandbox.yaml
vendored
|
@ -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:
|
||||
|
@ -69,4 +69,4 @@ jobs:
|
|||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '🥳 Successfully deployed to developer sandbox **[${{ env.ENVIRONMENT }}](https://getgov-${{ env.ENVIRONMENT }}.app.cloud.gov/)**.'
|
||||
})
|
||||
})
|
||||
|
|
12
.github/workflows/test.yaml
vendored
12
.github/workflows/test.yaml
vendored
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -6,7 +6,7 @@ applications:
|
|||
path: ../../src
|
||||
instances: 1
|
||||
memory: 512M
|
||||
stack: cflinuxfs3
|
||||
stack: cflinuxfs4
|
||||
timeout: 180
|
||||
command: ./run.sh
|
||||
health-check-type: http
|
||||
|
|
|
@ -6,7 +6,7 @@ applications:
|
|||
path: ../../src
|
||||
instances: 1
|
||||
memory: 512M
|
||||
stack: cflinuxfs3
|
||||
stack: cflinuxfs4
|
||||
timeout: 180
|
||||
command: ./run.sh
|
||||
health-check-type: http
|
||||
|
|
|
@ -6,7 +6,7 @@ applications:
|
|||
path: ../../src
|
||||
instances: 1
|
||||
memory: 512M
|
||||
stack: cflinuxfs3
|
||||
stack: cflinuxfs4
|
||||
timeout: 180
|
||||
command: ./run.sh
|
||||
health-check-type: http
|
||||
|
|
|
@ -6,7 +6,7 @@ applications:
|
|||
path: ../../src
|
||||
instances: 1
|
||||
memory: 512M
|
||||
stack: cflinuxfs3
|
||||
stack: cflinuxfs4
|
||||
timeout: 180
|
||||
command: ./run.sh
|
||||
health-check-type: http
|
||||
|
|
|
@ -6,7 +6,7 @@ applications:
|
|||
path: ../../src
|
||||
instances: 1
|
||||
memory: 512M
|
||||
stack: cflinuxfs3
|
||||
stack: cflinuxfs4
|
||||
timeout: 180
|
||||
command: ./run.sh
|
||||
health-check-type: http
|
||||
|
|
|
@ -6,7 +6,7 @@ applications:
|
|||
path: ../../src
|
||||
instances: 1
|
||||
memory: 512M
|
||||
stack: cflinuxfs3
|
||||
stack: cflinuxfs4
|
||||
timeout: 180
|
||||
command: ./run.sh
|
||||
health-check-type: http
|
||||
|
|
|
@ -6,7 +6,7 @@ applications:
|
|||
path: ../../src
|
||||
instances: 1
|
||||
memory: 512M
|
||||
stack: cflinuxfs3
|
||||
stack: cflinuxfs4
|
||||
timeout: 180
|
||||
command: ./run.sh
|
||||
health-check-type: http
|
||||
|
|
|
@ -6,7 +6,7 @@ applications:
|
|||
path: ../../src
|
||||
instances: 1
|
||||
memory: 512M
|
||||
stack: cflinuxfs3
|
||||
stack: cflinuxfs4
|
||||
timeout: 180
|
||||
command: ./run.sh
|
||||
health-check-type: http
|
||||
|
|
|
@ -6,7 +6,7 @@ applications:
|
|||
path: ../../src
|
||||
instances: 1
|
||||
memory: 512M
|
||||
stack: cflinuxfs3
|
||||
stack: cflinuxfs4
|
||||
timeout: 180
|
||||
command: ./run.sh
|
||||
health-check-type: http
|
||||
|
|
|
@ -6,7 +6,7 @@ applications:
|
|||
path: ../../src
|
||||
instances: 1
|
||||
memory: 512M
|
||||
stack: cflinuxfs3
|
||||
stack: cflinuxfs4
|
||||
timeout: 180
|
||||
command: ./run.sh
|
||||
health-check-type: http
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"defaults": {
|
||||
"concurrency": 1,
|
||||
"timeout": 10000,
|
||||
"timeout": 30000,
|
||||
"hideElements": "a[href='/whoami/']"
|
||||
|
||||
},
|
||||
|
|
742
src/Pipfile.lock
generated
742
src/Pipfile.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
|
|
|
@ -62,6 +62,9 @@ services:
|
|||
- POSTGRES_PASSWORD=feedabee
|
||||
|
||||
node:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: node.Dockerfile
|
||||
image: node
|
||||
volumes:
|
||||
- .:/app
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}`)
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
from .application_wizard import *
|
||||
from .domain import DomainAddUserForm
|
||||
from .domain import DomainAddUserForm, NameserverFormset
|
||||
|
|
|
@ -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 you’re 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.",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
273
src/registrar/migrations/0018_domaininformation.py
Normal file
273
src/registrar/migrations/0018_domaininformation.py
Normal 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",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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",
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
250
src/registrar/models/domain_information.py
Normal file
250
src/registrar/models/domain_information.py
Normal 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"
|
20
src/registrar/public/img/registrar/dotgov_401_illo.svg
Normal file
20
src/registrar/public/img/registrar/dotgov_401_illo.svg
Normal 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 |
18
src/registrar/public/img/registrar/dotgov_404_illo.svg
Normal file
18
src/registrar/public/img/registrar/dotgov_404_illo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 53 KiB |
59
src/registrar/public/img/registrar/dotgov_500_illo.svg
Normal file
59
src/registrar/public/img/registrar/dotgov_500_illo.svg
Normal 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 |
|
@ -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>
|
||||
{% if friendly_message %}
|
||||
<p>{{ friendly_message }}</p>
|
||||
{% else %}
|
||||
<p>{% translate "Authorization failed." %}</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>
|
||||
<blockquote>{{ log_identifier }}</blockquote>
|
||||
<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
|
||||
{% 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 %}
|
||||
|
|
45
src/registrar/templates/403.html
Normal file
45
src/registrar/templates/403.html
Normal 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 %}
|
|
@ -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 couldn’t 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 can’t find what you’re 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 %}
|
||||
|
|
|
@ -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>
|
||||
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 friendly_message %}
|
||||
<p>{{ friendly_message }}</p>
|
||||
{% else %}
|
||||
<p>{% translate "An internal server error occurred." %}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if log_identifier %}
|
||||
<p>Here's a unique identifier for this error.</p>
|
||||
<blockquote>{{ log_identifier }}</blockquote>
|
||||
<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
|
||||
{% 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 flex-align-self-end">
|
||||
<img
|
||||
src="{%static 'img/registrar/dotgov_500_illo.svg' %}"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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" %}
|
||||
/>
|
||||
/>
|
||||
|
|
52
src/registrar/templates/domain_nameservers.html
Normal file
52
src/registrar/templates/domain_nameservers.html
Normal 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 #}
|
|
@ -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 %}
|
||||
>
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
src="{% static 'img/CISA_logo.png' %}"
|
||||
alt="CISA logo"
|
||||
role="img"
|
||||
width="48px"
|
||||
width="56px"
|
||||
/></a>
|
||||
</div>
|
||||
<section
|
||||
|
|
|
@ -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 %}
|
||||
|
@ -71,4 +75,4 @@ error messages, if necessary.
|
|||
</span>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"})
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from .application import *
|
||||
from .domain import (
|
||||
DomainView,
|
||||
DomainNameserversView,
|
||||
DomainUsersView,
|
||||
DomainAddUserView,
|
||||
DomainInvitationDeleteView,
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue