mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-18 02:19:23 +02:00
Respond to PR feedback
This commit is contained in:
parent
f6c70f88a9
commit
0f87d9ea9a
31 changed files with 392 additions and 355 deletions
|
@ -1,6 +1,6 @@
|
||||||
"""Internal API views"""
|
"""Internal API views"""
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
from django.core.exceptions import BadRequest
|
from django.core.exceptions import BadRequest
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
@ -11,7 +11,6 @@ import requests
|
||||||
|
|
||||||
from cachetools.func import ttl_cache
|
from cachetools.func import ttl_cache
|
||||||
|
|
||||||
from registrar.models import Domain
|
|
||||||
|
|
||||||
DOMAIN_FILE_URL = (
|
DOMAIN_FILE_URL = (
|
||||||
"https://raw.githubusercontent.com/cisagov/dotgov-data/main/current-full.csv"
|
"https://raw.githubusercontent.com/cisagov/dotgov-data/main/current-full.csv"
|
||||||
|
@ -27,6 +26,7 @@ def _domains():
|
||||||
Fetch a file from DOMAIN_FILE_URL, parse the CSV for the domain,
|
Fetch a file from DOMAIN_FILE_URL, parse the CSV for the domain,
|
||||||
lowercase everything and return the list.
|
lowercase everything and return the list.
|
||||||
"""
|
"""
|
||||||
|
Domain = apps.get_model("registrar.Domain")
|
||||||
# 5 second timeout
|
# 5 second timeout
|
||||||
file_contents = requests.get(DOMAIN_FILE_URL, timeout=5).text
|
file_contents = requests.get(DOMAIN_FILE_URL, timeout=5).text
|
||||||
domains = set()
|
domains = set()
|
||||||
|
@ -65,6 +65,7 @@ def available(request, domain=""):
|
||||||
Response is a JSON dictionary with the key "available" and value true or
|
Response is a JSON dictionary with the key "available" and value true or
|
||||||
false.
|
false.
|
||||||
"""
|
"""
|
||||||
|
Domain = apps.get_model("registrar.Domain")
|
||||||
# validate that the given domain could be a domain name and fail early if
|
# validate that the given domain could be a domain name and fail early if
|
||||||
# not.
|
# not.
|
||||||
if not (
|
if not (
|
||||||
|
|
|
@ -75,12 +75,7 @@ class DomainApplicationFixture:
|
||||||
|
|
||||||
# any fields not specified here will be filled in with fake data or defaults
|
# any fields not specified here will be filled in with fake data or defaults
|
||||||
# NOTE BENE: each fixture must have `organization_name` for uniqueness!
|
# NOTE BENE: each fixture must have `organization_name` for uniqueness!
|
||||||
DA = [
|
# Here is a more complete example as a template:
|
||||||
{
|
|
||||||
"status": "started",
|
|
||||||
"organization_name": "Example - Finished but not Submitted",
|
|
||||||
},
|
|
||||||
# an example of a more manual application
|
|
||||||
# {
|
# {
|
||||||
# "status": "started",
|
# "status": "started",
|
||||||
# "organization_name": "Example - Just started",
|
# "organization_name": "Example - Just started",
|
||||||
|
@ -103,6 +98,11 @@ class DomainApplicationFixture:
|
||||||
# "current_websites": [],
|
# "current_websites": [],
|
||||||
# "alternative_domains": [],
|
# "alternative_domains": [],
|
||||||
# },
|
# },
|
||||||
|
DA = [
|
||||||
|
{
|
||||||
|
"status": "started",
|
||||||
|
"organization_name": "Example - Finished but not Submitted",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"status": "submitted",
|
"status": "submitted",
|
||||||
"organization_name": "Example - Submitted but pending Investigation",
|
"organization_name": "Example - Submitted but pending Investigation",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations # allows forward references in annotations
|
from __future__ import annotations # allows forward references in annotations
|
||||||
from itertools import zip_longest
|
from itertools import zip_longest
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Callable
|
||||||
from phonenumber_field.formfields import PhoneNumberField # type: ignore
|
from phonenumber_field.formfields import PhoneNumberField # type: ignore
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
@ -8,6 +9,7 @@ from django.core.validators import RegexValidator
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from registrar.models import Contact, DomainApplication, Domain
|
from registrar.models import Contact, DomainApplication, Domain
|
||||||
|
from registrar.utility import errors
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -69,6 +71,83 @@ class RegistrarFormSet(forms.BaseFormSet):
|
||||||
self.application = kwargs.pop("application", None)
|
self.application = kwargs.pop("application", None)
|
||||||
super(RegistrarFormSet, self).__init__(*args, **kwargs)
|
super(RegistrarFormSet, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def should_delete(self, cleaned):
|
||||||
|
"""Should this entry be deleted from the database?"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def pre_update(self, db_obj, cleaned):
|
||||||
|
"""Code to run before an item in the formset is saved."""
|
||||||
|
for key, value in cleaned.items():
|
||||||
|
setattr(db_obj, key, value)
|
||||||
|
|
||||||
|
def pre_create(self, db_obj, cleaned):
|
||||||
|
"""Code to run before an item in the formset is created in the database."""
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
def to_database(self, obj: DomainApplication):
|
||||||
|
"""
|
||||||
|
Adds this form's cleaned data to `obj` and saves `obj`.
|
||||||
|
|
||||||
|
Does nothing if form is not valid.
|
||||||
|
|
||||||
|
Hint: Subclass should call `self._to_database(...)`.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _to_database(
|
||||||
|
self,
|
||||||
|
obj: DomainApplication,
|
||||||
|
join: str,
|
||||||
|
should_delete: Callable,
|
||||||
|
pre_update: Callable,
|
||||||
|
pre_create: Callable,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Performs the actual work of saving.
|
||||||
|
|
||||||
|
Has hooks such as `should_delete` and `pre_update` by which the
|
||||||
|
subclass can control behavior. Add more hooks whenever needed.
|
||||||
|
"""
|
||||||
|
if not self.is_valid():
|
||||||
|
return
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
query = getattr(obj, join).order_by("created_at").all() # order matters
|
||||||
|
|
||||||
|
# the use of `zip` pairs the forms in the formset with the
|
||||||
|
# related objects gotten from the database -- there should always be
|
||||||
|
# at least as many forms as database entries: extra forms means new
|
||||||
|
# entries, but fewer forms is _not_ the correct way to delete items
|
||||||
|
# (likely a client-side error or an attempt at data tampering)
|
||||||
|
|
||||||
|
for db_obj, post_data in zip_longest(query, self.forms, fillvalue=None):
|
||||||
|
|
||||||
|
cleaned = post_data.cleaned_data if post_data is not None else {}
|
||||||
|
|
||||||
|
# matching database object exists, update it
|
||||||
|
if db_obj is not None and cleaned:
|
||||||
|
if should_delete(cleaned):
|
||||||
|
db_obj.delete()
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
pre_update(db_obj, cleaned)
|
||||||
|
db_obj.save()
|
||||||
|
|
||||||
|
# no matching database object, create it
|
||||||
|
elif db_obj is None and cleaned:
|
||||||
|
kwargs = pre_create(db_obj, cleaned)
|
||||||
|
getattr(obj, join).create(**kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def on_fetch(cls, query):
|
||||||
|
"""Code to run when fetching formset's objects from the database."""
|
||||||
|
return query.values()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_database(cls, obj: DomainApplication, join: str, on_fetch: Callable):
|
||||||
|
"""Returns a dict of form field values gotten from `obj`."""
|
||||||
|
return on_fetch(getattr(obj, join).order_by("created_at")) # order matters
|
||||||
|
|
||||||
|
|
||||||
class OrganizationTypeForm(RegistrarForm):
|
class OrganizationTypeForm(RegistrarForm):
|
||||||
organization_type = forms.ChoiceField(
|
organization_type = forms.ChoiceField(
|
||||||
|
@ -299,53 +378,35 @@ class AuthorizingOfficialForm(RegistrarForm):
|
||||||
|
|
||||||
|
|
||||||
class CurrentSitesForm(RegistrarForm):
|
class CurrentSitesForm(RegistrarForm):
|
||||||
def to_database(self, obj):
|
website = forms.URLField(
|
||||||
if not self.is_valid():
|
required=False,
|
||||||
return
|
label="Public website",
|
||||||
obj.save()
|
)
|
||||||
normalized = Domain.normalize(self.cleaned_data["current_site"], blank=True)
|
|
||||||
if normalized:
|
|
||||||
# TODO: ability to update existing records
|
class BaseCurrentSitesFormSet(RegistrarFormSet):
|
||||||
obj.current_websites.create(website=normalized)
|
JOIN = "current_websites"
|
||||||
|
|
||||||
|
def should_delete(self, cleaned):
|
||||||
|
website = cleaned.get("website", "")
|
||||||
|
return website.strip() == ""
|
||||||
|
|
||||||
|
def to_database(self, obj: DomainApplication):
|
||||||
|
self._to_database(
|
||||||
|
obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_database(cls, obj):
|
def from_database(cls, obj):
|
||||||
current_website = obj.current_websites.first()
|
return super().from_database(obj, cls.JOIN, cls.on_fetch)
|
||||||
if current_website is not None:
|
|
||||||
return {"current_site": current_website.website}
|
|
||||||
else:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
current_site = forms.CharField(
|
|
||||||
required=False,
|
|
||||||
label=(
|
|
||||||
"Enter your organization’s website in the required format, like"
|
|
||||||
" www.city.com."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean_current_site(self):
|
CurrentSitesFormSet = forms.formset_factory(
|
||||||
"""This field should be a legal domain name."""
|
CurrentSitesForm,
|
||||||
inputted_site = self.cleaned_data["current_site"]
|
extra=1,
|
||||||
if not inputted_site:
|
absolute_max=1500, # django default; use `max_num` to limit entries
|
||||||
# empty string is fine
|
formset=BaseCurrentSitesFormSet,
|
||||||
return inputted_site
|
)
|
||||||
|
|
||||||
# something has been inputted
|
|
||||||
|
|
||||||
if inputted_site.startswith("http://") or inputted_site.startswith("https://"):
|
|
||||||
# strip of the protocol that the pasted from their web browser
|
|
||||||
inputted_site = inputted_site.split("//", 1)[1]
|
|
||||||
|
|
||||||
if Domain.string_could_be_domain(inputted_site):
|
|
||||||
return inputted_site
|
|
||||||
else:
|
|
||||||
# string could not be a domain
|
|
||||||
raise forms.ValidationError(
|
|
||||||
"Enter your organization’s website in the required format, like"
|
|
||||||
" www.city.com.",
|
|
||||||
code="invalid",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AlternativeDomainForm(RegistrarForm):
|
class AlternativeDomainForm(RegistrarForm):
|
||||||
|
@ -354,59 +415,67 @@ class AlternativeDomainForm(RegistrarForm):
|
||||||
label="Alternative domain",
|
label="Alternative domain",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def clean_alternative_domain(self):
|
||||||
|
"""Validation code for domain names."""
|
||||||
|
try:
|
||||||
|
requested = self.cleaned_data.get("alternative_domain", None)
|
||||||
|
validated = Domain.validate(requested, blank_ok=True)
|
||||||
|
except errors.ExtraDotsError:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
"Please enter a domain without any periods.",
|
||||||
|
code="invalid",
|
||||||
|
)
|
||||||
|
except errors.DomainUnavailableError:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
"ERROR MESSAGE GOES HERE",
|
||||||
|
code="invalid",
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
"Please enter a valid domain name using only letters, "
|
||||||
|
"numbers, and hyphens",
|
||||||
|
code="invalid",
|
||||||
|
)
|
||||||
|
return validated
|
||||||
|
|
||||||
|
|
||||||
class BaseAlternativeDomainFormSet(RegistrarFormSet):
|
class BaseAlternativeDomainFormSet(RegistrarFormSet):
|
||||||
def to_database(self, obj: DomainApplication):
|
JOIN = "alternative_domains"
|
||||||
if not self.is_valid():
|
|
||||||
return
|
|
||||||
|
|
||||||
obj.save()
|
def should_delete(self, cleaned):
|
||||||
query = obj.alternative_domains.order_by("created_at").all() # order matters
|
domain = cleaned.get("alternative_domain", "")
|
||||||
|
return domain.strip() == ""
|
||||||
|
|
||||||
# the use of `zip` pairs the forms in the formset with the
|
def pre_update(self, db_obj, cleaned):
|
||||||
# related objects gotten from the database -- there should always be
|
|
||||||
# at least as many forms as database entries: extra forms means new
|
|
||||||
# entries, but fewer forms is _not_ the correct way to delete items
|
|
||||||
# (likely a client-side error or an attempt at data tampering)
|
|
||||||
|
|
||||||
for db_obj, post_data in zip_longest(query, self.forms, fillvalue=None):
|
|
||||||
|
|
||||||
cleaned = post_data.cleaned_data if post_data is not None else {}
|
|
||||||
domain = cleaned.get("alternative_domain", None)
|
domain = cleaned.get("alternative_domain", None)
|
||||||
|
if domain is not None:
|
||||||
|
db_obj.website = f"{domain}.gov"
|
||||||
|
|
||||||
# matching database object exists, update or delete it
|
def pre_create(self, db_obj, cleaned):
|
||||||
if db_obj is not None and isinstance(domain, str):
|
domain = cleaned.get("alternative_domain", None)
|
||||||
entry_was_erased = domain.strip() == ""
|
if domain is not None:
|
||||||
if entry_was_erased:
|
return {"website": f"{domain}.gov"}
|
||||||
db_obj.delete()
|
else:
|
||||||
continue
|
return {}
|
||||||
try:
|
|
||||||
normalized = Domain.normalize(domain, "gov", blank=True)
|
|
||||||
except ValueError as e:
|
|
||||||
logger.debug(e)
|
|
||||||
continue
|
|
||||||
db_obj.website = normalized
|
|
||||||
db_obj.save()
|
|
||||||
|
|
||||||
# no matching database object, create it
|
def to_database(self, obj: DomainApplication):
|
||||||
elif db_obj is None and domain is not None:
|
self._to_database(
|
||||||
try:
|
obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create
|
||||||
normalized = Domain.normalize(domain, "gov", blank=True)
|
)
|
||||||
except ValueError as e:
|
|
||||||
logger.debug(e)
|
@classmethod
|
||||||
continue
|
def on_fetch(cls, query):
|
||||||
obj.alternative_domains.create(website=normalized)
|
return [{"alternative_domain": domain.sld} for domain in query]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_database(cls, obj):
|
def from_database(cls, obj):
|
||||||
query = obj.alternative_domains.order_by("created_at").all() # order matters
|
return super().from_database(obj, cls.JOIN, cls.on_fetch)
|
||||||
return [{"alternative_domain": domain.sld} for domain in query]
|
|
||||||
|
|
||||||
|
|
||||||
AlternativeDomainFormSet = forms.formset_factory(
|
AlternativeDomainFormSet = forms.formset_factory(
|
||||||
AlternativeDomainForm,
|
AlternativeDomainForm,
|
||||||
extra=1,
|
extra=1,
|
||||||
absolute_max=1500,
|
absolute_max=1500, # django default; use `max_num` to limit entries
|
||||||
formset=BaseAlternativeDomainFormSet,
|
formset=BaseAlternativeDomainFormSet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -415,16 +484,14 @@ class DotGovDomainForm(RegistrarForm):
|
||||||
def to_database(self, obj):
|
def to_database(self, obj):
|
||||||
if not self.is_valid():
|
if not self.is_valid():
|
||||||
return
|
return
|
||||||
normalized = Domain.normalize(
|
domain = self.cleaned_data.get("requested_domain", None)
|
||||||
self.cleaned_data["requested_domain"], "gov", blank=True
|
if domain:
|
||||||
)
|
|
||||||
if normalized:
|
|
||||||
requested_domain = getattr(obj, "requested_domain", None)
|
requested_domain = getattr(obj, "requested_domain", None)
|
||||||
if requested_domain is not None:
|
if requested_domain is not None:
|
||||||
requested_domain.name = normalized
|
requested_domain.name = f"{domain}.gov"
|
||||||
requested_domain.save()
|
requested_domain.save()
|
||||||
else:
|
else:
|
||||||
requested_domain = Domain.objects.create(name=normalized)
|
requested_domain = Domain.objects.create(name=f"{domain}.gov")
|
||||||
obj.requested_domain = requested_domain
|
obj.requested_domain = requested_domain
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
|
@ -438,16 +505,12 @@ class DotGovDomainForm(RegistrarForm):
|
||||||
values["requested_domain"] = requested_domain.sld
|
values["requested_domain"] = requested_domain.sld
|
||||||
return values
|
return values
|
||||||
|
|
||||||
requested_domain = forms.CharField(label="What .gov domain do you want?")
|
|
||||||
|
|
||||||
def clean_requested_domain(self):
|
def clean_requested_domain(self):
|
||||||
"""Requested domains need to be legal top-level domains, not subdomains.
|
"""Validation code for domain names."""
|
||||||
|
try:
|
||||||
If they end with `.gov`, then we can reasonably take that off. If they have
|
requested = self.cleaned_data.get("requested_domain", None)
|
||||||
any other dots in them, raise an error.
|
validated = Domain.validate(requested)
|
||||||
"""
|
except errors.BlankValueError:
|
||||||
requested = self.cleaned_data["requested_domain"]
|
|
||||||
if not requested:
|
|
||||||
# none or empty string
|
# none or empty string
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
"Enter the .gov domain you want. Don’t include “www” or “.gov.” For"
|
"Enter the .gov domain you want. Don’t include “www” or “.gov.” For"
|
||||||
|
@ -455,20 +518,25 @@ class DotGovDomainForm(RegistrarForm):
|
||||||
" the quotes).",
|
" the quotes).",
|
||||||
code="invalid",
|
code="invalid",
|
||||||
)
|
)
|
||||||
if requested.endswith(".gov"):
|
except errors.ExtraDotsError:
|
||||||
requested = requested[:-4]
|
|
||||||
if "." in requested:
|
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
"Enter the .gov domain you want without any periods.",
|
"Enter the .gov domain you want without any periods.",
|
||||||
code="invalid",
|
code="invalid",
|
||||||
)
|
)
|
||||||
if not Domain.string_could_be_domain(requested + ".gov"):
|
except errors.DomainUnavailableError:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
"ERROR MESSAGE GOES HERE",
|
||||||
|
code="invalid",
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
"Enter a domain using only letters, "
|
"Enter a domain using only letters, "
|
||||||
"numbers, or hyphens (though we don't recommend using hyphens).",
|
"numbers, or hyphens (though we don't recommend using hyphens).",
|
||||||
code="invalid",
|
code="invalid",
|
||||||
)
|
)
|
||||||
return requested
|
return validated
|
||||||
|
|
||||||
|
requested_domain = forms.CharField(label="What .gov domain do you want?")
|
||||||
|
|
||||||
|
|
||||||
class PurposeForm(RegistrarForm):
|
class PurposeForm(RegistrarForm):
|
||||||
|
@ -595,47 +663,26 @@ class OtherContactsForm(RegistrarForm):
|
||||||
|
|
||||||
|
|
||||||
class BaseOtherContactsFormSet(RegistrarFormSet):
|
class BaseOtherContactsFormSet(RegistrarFormSet):
|
||||||
def to_database(self, obj):
|
JOIN = "other_contacts"
|
||||||
if not self.is_valid():
|
|
||||||
return
|
|
||||||
obj.save()
|
|
||||||
|
|
||||||
query = obj.other_contacts.order_by("created_at").all()
|
def should_delete(self, cleaned):
|
||||||
|
|
||||||
# the use of `zip` pairs the forms in the formset with the
|
|
||||||
# related objects gotten from the database -- there should always be
|
|
||||||
# at least as many forms as database entries: extra forms means new
|
|
||||||
# entries, but fewer forms is _not_ the correct way to delete items
|
|
||||||
# (likely a client-side error or an attempt at data tampering)
|
|
||||||
|
|
||||||
for db_obj, post_data in zip_longest(query, self.forms, fillvalue=None):
|
|
||||||
|
|
||||||
cleaned = post_data.cleaned_data if post_data is not None else {}
|
|
||||||
|
|
||||||
# matching database object exists, update it
|
|
||||||
if db_obj is not None and cleaned:
|
|
||||||
empty = (isinstance(v, str) and not v.strip() for v in cleaned.values())
|
empty = (isinstance(v, str) and not v.strip() for v in cleaned.values())
|
||||||
erased = all(empty)
|
return all(empty)
|
||||||
if erased:
|
|
||||||
db_obj.delete()
|
|
||||||
continue
|
|
||||||
for key, value in cleaned.items():
|
|
||||||
setattr(db_obj, key, value)
|
|
||||||
db_obj.save()
|
|
||||||
|
|
||||||
# no matching database object, create it
|
def to_database(self, obj: DomainApplication):
|
||||||
elif db_obj is None and cleaned:
|
self._to_database(
|
||||||
obj.other_contacts.create(**cleaned)
|
obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_database(cls, obj):
|
def from_database(cls, obj):
|
||||||
return obj.other_contacts.order_by("created_at").values() # order matters
|
return super().from_database(obj, cls.JOIN, cls.on_fetch)
|
||||||
|
|
||||||
|
|
||||||
OtherContactsFormSet = forms.formset_factory(
|
OtherContactsFormSet = forms.formset_factory(
|
||||||
OtherContactsForm,
|
OtherContactsForm,
|
||||||
extra=1,
|
extra=1,
|
||||||
absolute_max=1500,
|
absolute_max=1500, # django default; use `max_num` to limit entries
|
||||||
formset=BaseOtherContactsFormSet,
|
formset=BaseOtherContactsFormSet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,9 @@ from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django_fsm import FSMField, transition # type: ignore
|
from django_fsm import FSMField, transition # type: ignore
|
||||||
|
|
||||||
|
from api.views import in_domains
|
||||||
from epp.mock_epp import domain_info, domain_check
|
from epp.mock_epp import domain_info, domain_check
|
||||||
|
from registrar.utility import errors
|
||||||
|
|
||||||
from .utility.time_stamped_model import TimeStampedModel
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
|
||||||
|
@ -92,60 +94,6 @@ class Domain(TimeStampedModel):
|
||||||
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
|
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
|
||||||
DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,6}")
|
DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,6}")
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def normalize(cls, domain: str, tld=None, blank=False) -> str: # noqa: C901
|
|
||||||
"""Return `domain` in form `<second level>.<tld>`.
|
|
||||||
|
|
||||||
Raises ValueError if string cannot be normalized.
|
|
||||||
|
|
||||||
This does not guarantee the returned string is a valid domain name.
|
|
||||||
|
|
||||||
Set `blank` to True to allow empty strings.
|
|
||||||
"""
|
|
||||||
if blank and len(domain.strip()) == 0:
|
|
||||||
return ""
|
|
||||||
cleaned = domain.lower()
|
|
||||||
# starts with https or http
|
|
||||||
if cleaned.startswith("https://"):
|
|
||||||
cleaned = cleaned[8:]
|
|
||||||
if cleaned.startswith("http://"):
|
|
||||||
cleaned = cleaned[7:]
|
|
||||||
# has url parts
|
|
||||||
if "/" in cleaned:
|
|
||||||
cleaned = cleaned.split("/")[0]
|
|
||||||
# has query parts
|
|
||||||
if "?" in cleaned:
|
|
||||||
cleaned = cleaned.split("?")[0]
|
|
||||||
# has fragments
|
|
||||||
if "#" in cleaned:
|
|
||||||
cleaned = cleaned.split("#")[0]
|
|
||||||
# replace disallowed chars
|
|
||||||
re.sub(r"^[^A-Za-z0-9.-]+", "", cleaned)
|
|
||||||
|
|
||||||
parts = cleaned.split(".")
|
|
||||||
# has subdomains or invalid repetitions
|
|
||||||
if cleaned.count(".") > 0:
|
|
||||||
# remove invalid repetitions
|
|
||||||
while parts[-1] == parts[-2]:
|
|
||||||
parts.pop()
|
|
||||||
# remove subdomains
|
|
||||||
parts = parts[-2:]
|
|
||||||
hasTLD = len(parts) == 2
|
|
||||||
if hasTLD:
|
|
||||||
# set correct tld
|
|
||||||
if tld is not None:
|
|
||||||
parts[-1] = tld
|
|
||||||
else:
|
|
||||||
# add tld
|
|
||||||
if tld is not None:
|
|
||||||
parts.append(tld)
|
|
||||||
else:
|
|
||||||
raise ValueError("You must specify a tld for %s" % domain)
|
|
||||||
|
|
||||||
cleaned = ".".join(parts)
|
|
||||||
|
|
||||||
return cleaned
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def string_could_be_domain(cls, domain: str | None) -> bool:
|
def string_could_be_domain(cls, domain: str | None) -> bool:
|
||||||
"""Return True if the string could be a domain name, otherwise False."""
|
"""Return True if the string could be a domain name, otherwise False."""
|
||||||
|
@ -153,6 +101,29 @@ class Domain(TimeStampedModel):
|
||||||
return False
|
return False
|
||||||
return bool(cls.DOMAIN_REGEX.match(domain))
|
return bool(cls.DOMAIN_REGEX.match(domain))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls, domain: str | None, blank_ok=False) -> str:
|
||||||
|
"""Attempt to determine if a domain name could be requested."""
|
||||||
|
if domain is None:
|
||||||
|
raise errors.BlankValueError()
|
||||||
|
if not isinstance(domain, str):
|
||||||
|
raise ValueError("Domain name must be a string")
|
||||||
|
domain = domain.lower().strip()
|
||||||
|
if domain == "":
|
||||||
|
if blank_ok:
|
||||||
|
return domain
|
||||||
|
else:
|
||||||
|
raise errors.BlankValueError()
|
||||||
|
if domain.endswith(".gov"):
|
||||||
|
domain = domain[:-4]
|
||||||
|
if "." in domain:
|
||||||
|
raise errors.ExtraDotsError()
|
||||||
|
if not Domain.string_could_be_domain(domain + ".gov"):
|
||||||
|
raise ValueError()
|
||||||
|
if in_domains(domain):
|
||||||
|
raise errors.DomainUnavailableError()
|
||||||
|
return domain
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def available(cls, domain: str) -> bool:
|
def available(cls, domain: str) -> bool:
|
||||||
"""Check if a domain is available.
|
"""Check if a domain is available.
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- Test page -->
|
|
||||||
{% extends 'application_form.html' %}
|
{% extends 'application_form.html' %}
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
|
@ -6,7 +5,7 @@
|
||||||
|
|
||||||
<p id="instructions">Is there anything else we should know about your domain request?</p>
|
<p id="instructions">Is there anything else we should know about your domain request?</p>
|
||||||
|
|
||||||
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post">
|
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||||
<div class="usa-form-group">
|
<div class="usa-form-group">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- Test page -->
|
|
||||||
{% extends 'application_form.html' %}
|
{% extends 'application_form.html' %}
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
@ -43,7 +42,7 @@
|
||||||
|
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
|
|
||||||
<form class="usa-form usa-form--large" id="step__{{steps.current}}" method="post" novalidate>
|
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<fieldset class="usa-fieldset">
|
<fieldset class="usa-fieldset">
|
||||||
|
|
|
@ -1,14 +1,28 @@
|
||||||
<!-- Test page -->
|
|
||||||
{% extends 'application_form.html' %}
|
{% extends 'application_form.html' %}
|
||||||
{% load widget_tweaks field_helpers %}
|
{% load widget_tweaks field_helpers %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block form_content %}
|
{% block form_content %}
|
||||||
|
|
||||||
<form class="usa-form usa-form--large" id="step__{{steps.current}}" method="post">
|
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<div>
|
||||||
|
{{ forms.0.management_form }}
|
||||||
|
{# TODO: aria-describedby to associate these instructions with the input! #}
|
||||||
|
<p id="website_instructions">
|
||||||
|
Enter your organization’s public website, if you have one. For example, www.city.com.
|
||||||
|
</p>
|
||||||
|
{% for form in forms.0 %}
|
||||||
|
{% input_with_errors form.website %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{% input_with_errors forms.0.current_site %}
|
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--unstyled">
|
||||||
|
<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 site</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
{% extends 'application_form.html' %}
|
{% extends 'application_form.html' %}
|
||||||
{% load widget_tweaks static%}
|
{% load widget_tweaks field_helpers static %}
|
||||||
|
|
||||||
{% block form_content %}
|
{% block form_content %}
|
||||||
<p> Before requesting a .gov domain, <a href="{% url 'todo' %}">please make sure it meets our naming requirements.</a> Your domain name must:
|
<div id="preamble">
|
||||||
|
<p>Before requesting a .gov domain, <a href="{% url 'todo' %}">please make sure it meets our naming requirements.</a> Your domain name must:
|
||||||
<ul class="usa-list">
|
<ul class="usa-list">
|
||||||
<li>Be available </li>
|
<li>Be available </li>
|
||||||
<li>Be unique </li>
|
<li>Be unique </li>
|
||||||
|
@ -11,83 +12,38 @@
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>Note that <strong> only federal agencies can request generic terms </strong>like vote.gov.</p>
|
<p>Note that <strong>only federal agencies can request generic terms</strong> like vote.gov.</p>
|
||||||
|
|
||||||
<p>We’ll try to give you the domain you want. We first need to make sure your request meets our requirements. We’ll work with you to find the best domain for your organization.</p>
|
<p>We’ll try to give you the domain you want. We first need to make sure your request meets our requirements. We’ll work with you to find the best domain for your organization.</p>
|
||||||
|
|
||||||
<p>Here are a few domain examples for your type of organization.</p>
|
<p>Here are a few domain examples for your type of organization.</p>
|
||||||
<div class="domain-example">
|
<div id="domain-example">
|
||||||
{% include "includes/domain_example__city.html" %}
|
{% include "includes/domain_example__city.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||||
<h2> What .gov domain do you want? </h2>
|
<h2>What .gov domain do you want?</h2>
|
||||||
<p class="domain_instructions"> After you enter your domain, we’ll make sure it’s available and that it meets some of our naming requirements. If your domain passes these initial checks, we’ll verify that it meets all of our requirements once you complete and submit the rest of this form. </p>
|
{# TODO: aria-describedby to associate these instructions with the input! #}
|
||||||
|
<p id="domain_instructions">After you enter your domain, we’ll make sure it’s available and that it meets some of our naming requirements. If your domain passes these initial checks, we’ll verify that it meets all of our requirements once you complete and submit the rest of this form.</p>
|
||||||
|
|
||||||
<p> This question is required. </p>
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{% input_with_errors forms.0.requested_domain www_gov=True %}
|
||||||
{% if forms.0.requested_domain.errors %}
|
<button type="button" class="usa-button">Check availability</button>
|
||||||
<div class="usa-form-group usa-form-group--error">
|
|
||||||
{% for error in forms.0.requested_domain.errors %}
|
|
||||||
<span class="usa-error-message" id="input-error-message" role="alert">
|
|
||||||
{{ error }}
|
|
||||||
</span>
|
|
||||||
{% endfor %}
|
|
||||||
<div class="display-flex flex-align-center">
|
|
||||||
<span class="padding-top-05 padding-right-2px">www.</span>
|
|
||||||
{{ forms.0.requested_domain|add_class:"usa-input usa-input--error"|attr:"aria-describedby:domain_instructions"|attr:"aria-invalid:true" }}
|
|
||||||
<span class="padding-top-05 padding-left-2px">.gov </span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="display-flex flex-align-center">
|
|
||||||
<span class="padding-top-05 padding-right-2px">www.</span>
|
|
||||||
{{ forms.0.requested_domain|add_class:"usa-input"|attr:"aria-describedby:domain_instructions" }}
|
|
||||||
<span class="padding-top-05 padding-left-2px">.gov </span>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<button type="button" class="usa-button">Check availability </button>
|
|
||||||
|
|
||||||
<h2>Alternative domains</h2>
|
<h2>Alternative domains</h2>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{% if forms.0.alternative_domain.errors %}
|
|
||||||
<div class="usa-form-group usa-form-group--error">
|
|
||||||
{{ forms.0.alternative_domain|add_label_class:"usa-label usa-label--error" }}
|
|
||||||
{% for error in forms.0.alternative_domain.errors %}
|
|
||||||
<span class="usa-error-message" id="input-error-message" role="alert">
|
|
||||||
{{ error }}
|
|
||||||
</span>
|
|
||||||
{% endfor %}
|
|
||||||
<div class="display-flex flex-align-center">
|
|
||||||
<span class="padding-top-05 padding-right-2px">www.</span>
|
|
||||||
{ forms.0.alternative_domain|add_class:"usa-input usa-input--error"|attr:"aria-describedby:domain_instructions"|attr:"aria-invalid:true" }}
|
|
||||||
<span class="padding-top-05 padding-left-2px">.gov </span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{{ forms.0.alternative_domain|add_label_class:"usa-label" }}
|
|
||||||
<div class="display-flex flex-align-center">
|
|
||||||
<span class="padding-top-05 padding-right-2px">www.</span>
|
|
||||||
{{ forms.0.alternative_domain|add_class:"usa-input"|attr:"aria-describedby:domain_instructions" }}
|
|
||||||
<span class="padding-top-05 padding-left-2px">.gov </span>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{{ forms.1.management_form }}
|
{{ forms.1.management_form }}
|
||||||
<p class="alt_domain_instructions">Are there other domains you’d like if we can’t give you your first choice? Entering alternative domains is optional.</p>
|
{# TODO: aria-describedby to associate these instructions with the input! #}
|
||||||
|
<p id="alt_domain_instructions">Are there other domains you’d like if we can’t give you your first choice? Entering alternative domains is optional.</p>
|
||||||
|
|
||||||
{% for form in forms.1 %}
|
{% for form in forms.1 %}
|
||||||
{{ form.alternative_domain|add_label_class:"usa-label" }}
|
{% input_with_errors form.alternative_domain www_gov=True %}
|
||||||
<div class="display-flex flex-align-center">
|
|
||||||
<span class="padding-top-05 padding-right-2px">www.</span>
|
|
||||||
{{ form.alternative_domain|add_class:"usa-input"|attr:"aria-describedby:alt_domain_instructions" }}
|
|
||||||
<span class="padding-top-05 padding-left-2px">.gov </span>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--unstyled">
|
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--unstyled">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
<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>
|
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||||
</svg><span class="margin-left-05">Add another alternative</span>
|
</svg><span class="margin-left-05">Add another alternative</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load static widget_tweaks namespaced_urls %}
|
{% load static widget_tweaks dynamic_question_tags namespaced_urls %}
|
||||||
|
|
||||||
{% block title %}Apply for a .gov domain – {{form_titles|get_item:steps.current}}{% endblock %}
|
{% block title %}Apply for a .gov domain – {{form_titles|get_item:steps.current}}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -12,30 +12,26 @@
|
||||||
<main id="main-content" class="grid-container register-form-step">
|
<main id="main-content" class="grid-container register-form-step">
|
||||||
{% if steps.prev %}
|
{% if steps.prev %}
|
||||||
<a href="{% namespaced_url 'application' steps.prev %}" class="breadcrumb__back">
|
<a href="{% namespaced_url 'application' steps.prev %}" class="breadcrumb__back">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#arrow_back"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#arrow_back"></use>
|
||||||
</svg><span class="margin-left-05">Previous step </span>
|
</svg><span class="margin-left-05">Previous step</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% for form in forms %}
|
{% comment %}
|
||||||
{% if form.errors %}
|
to make sense of this loop, consider that
|
||||||
{% for error in form.non_field_errors %}
|
a context variable of `forms` contains all
|
||||||
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2">
|
the forms for this page; each of these
|
||||||
<div class="usa-alert__body">
|
may be itself a formset and contain additional
|
||||||
{{ error|escape }}
|
forms, hence `forms.forms`
|
||||||
</div>
|
{% endcomment %}
|
||||||
</div>
|
{% for outer in forms %}
|
||||||
{% endfor %}
|
{% if outer|isformset %}
|
||||||
{% for field in form %}
|
{% for inner in outer.forms %}
|
||||||
{% for error in field.errors %}
|
{% include "includes/form_errors.html" with form=inner %}
|
||||||
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2">
|
|
||||||
<div class="usa-alert__body">
|
|
||||||
{{ error|escape }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{% include "includes/form_errors.html" with form=outer %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- Test page -->
|
|
||||||
{% extends 'application_form.html' %}
|
{% extends 'application_form.html' %}
|
||||||
{% load widget_tweaks field_helpers %}
|
{% load widget_tweaks field_helpers %}
|
||||||
|
|
||||||
|
@ -16,6 +15,7 @@
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- Test page -->
|
|
||||||
{% extends 'application_form.html' %}
|
{% extends 'application_form.html' %}
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
{% load dynamic_question_tags %}
|
{% load dynamic_question_tags %}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- Test page -->
|
|
||||||
{% extends 'application_form.html' %}
|
{% extends 'application_form.html' %}
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
{% load dynamic_question_tags %}
|
{% load dynamic_question_tags %}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- Test page -->
|
|
||||||
{% extends 'application_form.html' %}
|
{% extends 'application_form.html' %}
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
{% load dynamic_question_tags %}
|
{% load dynamic_question_tags %}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<p id="instructions">We’d like to contact other employees with administrative or technical responsibilities in your organization. For example, they could be involved in managing your organization or its technical infrastructure. This information will help us assess your eligibility and understand the purpose of the .gov domain. These contacts should be in addition to you and your authorizing official. </p>
|
<p id="instructions">We’d like to contact other employees with administrative or technical responsibilities in your organization. For example, they could be involved in managing your organization or its technical infrastructure. This information will help us assess your eligibility and understand the purpose of the .gov domain. These contacts should be in addition to you and your authorizing official. </p>
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
|
|
||||||
<form class="usa-form usa-form--large" id="step__{{steps.current}}" method="post" novalidate>
|
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ forms.0.management_form }}
|
{{ forms.0.management_form }}
|
||||||
{# forms.0 is a formset and this iterates over its forms #}
|
{# forms.0 is a formset and this iterates over its forms #}
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--unstyled">
|
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--unstyled">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
<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>
|
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||||
</svg><span class="margin-left-05">Add another contact</span>
|
</svg><span class="margin-left-05">Add another contact</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
<!-- Test page -->
|
|
||||||
{% extends 'application_form.html' %}
|
{% extends 'application_form.html' %}
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
{% block form_content %}
|
{% block form_content %}
|
||||||
|
|
||||||
<p id="instructions">.Gov domain names are intended for use on the internet. They should be registered with an intent to deploy services, not simply to reserve a name. .Gov domains should not be registered for primarily internal use.</p>
|
<div id="instructions">
|
||||||
|
<p>.Gov domain names are intended for use on the internet. They should be registered with an intent to deploy services, not simply to reserve a name. .Gov domains should not be registered for primarily internal use.</p>
|
||||||
<p id="instructions">Describe the reason for your domain request. Explain how you plan to use this domain. Will you use it for a website and/or email? Are you moving your website from another top-level domain (like .com or .org)? Read about <a href="#">activities that are prohibited on .gov domains.</a></p>
|
<p>Describe the reason for your domain request. Explain how you plan to use this domain. Will you use it for a website and/or email? Are you moving your website from another top-level domain (like .com or .org)? Read about <a href="#">activities that are prohibited on .gov domains.</a></p>
|
||||||
|
<p> This question is required.</p>
|
||||||
<p> This question is required. </p>
|
</div>
|
||||||
|
|
||||||
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||||
<div class="usa-form-group">
|
<div class="usa-form-group">
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- Test page -->
|
|
||||||
{% extends 'application_form.html' %}
|
{% extends 'application_form.html' %}
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
<!-- Test page -->
|
|
||||||
{% extends 'application_form.html' %}
|
{% extends 'application_form.html' %}
|
||||||
{% load static widget_tweaks namespaced_urls %}
|
{% load static widget_tweaks namespaced_urls %}
|
||||||
|
|
||||||
{% block form_content %}
|
{% block form_content %}
|
||||||
|
|
||||||
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post">
|
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{% for step in steps.all|slice:":-1" %}
|
{% for step in steps.all|slice:":-1" %}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- Test page -->
|
|
||||||
{% extends 'application_form.html' %}
|
{% extends 'application_form.html' %}
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
@ -7,7 +6,7 @@
|
||||||
|
|
||||||
<p id="instructions"> We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain. <strong> Security emails are made public.</strong> We recommend using an alias, like security@<domain.gov>.</p>
|
<p id="instructions"> We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain. <strong> Security emails are made public.</strong> We recommend using an alias, like security@<domain.gov>.</p>
|
||||||
|
|
||||||
<form class="usa-form usa-form--large" id="step__{{steps.current}}" method="post" novalidate>
|
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{% if forms.0.security_email.errors %}
|
{% if forms.0.security_email.errors %}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="usa-sidenav__item sidenav__step--locked">
|
<li class="usa-sidenav__item sidenav__step--locked">
|
||||||
<span>
|
<span>
|
||||||
<svg class="usa-icon" aria-hidden="true" focsuable="false" role="img" width="24"height="24" >
|
<svg class="usa-icon" aria-hidden="true" focsuable="false" role="img" width="24" height="24">
|
||||||
<title id="locked-step__{{forloop.counter}}">lock icon</title>
|
<title id="locked-step__{{forloop.counter}}">lock icon</title>
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#lock"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#lock"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- Test page -->
|
|
||||||
{% extends 'application_form.html' %}
|
{% extends 'application_form.html' %}
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
|
@ -20,11 +19,11 @@
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{{ field|add_class:"usa-input--error usa-textarea usa-character-count__field"|attr:"aria-describedby:instructions"|attr:"maxlength=500"|attr:"aria-invalid:true" }}
|
{{ field|add_class:"usa-input--error usa-textarea usa-character-count__field"|attr:"maxlength=500"|attr:"aria-invalid:true" }}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ field|add_label_class:"usa-label" }}
|
{{ field|add_label_class:"usa-label" }}
|
||||||
{{ field|add_class:"usa-textarea usa-character-count__field"|attr:"aria-describedby:instructions"|attr:"maxlength=500" }}
|
{{ field|add_class:"usa-textarea usa-character-count__field"|attr:"maxlength=500" }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<span class="usa-character-count__message" id="with-hint-textarea-info with-hint-textarea-hint"> You can enter up to 500 characters </span>
|
<span class="usa-character-count__message" id="with-hint-textarea-info with-hint-textarea-hint"> You can enter up to 500 characters </span>
|
||||||
|
@ -42,11 +41,11 @@
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{{ field|add_class:"usa-input--error usa-textarea usa-character-count__field"|attr:"aria-describedby:instructions"|attr:"maxlength=500"|attr:"aria-invalid:true" }}
|
{{ field|add_class:"usa-input--error usa-textarea usa-character-count__field"|attr:"maxlength=500"|attr:"aria-invalid:true" }}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ field|add_label_class:"usa-label" }}
|
{{ field|add_label_class:"usa-label" }}
|
||||||
{{ field|add_class:"usa-textarea usa-character-count__field"|attr:"aria-describedby:instructions"|attr:"maxlength=500" }}
|
{{ field|add_class:"usa-textarea usa-character-count__field"|attr:"maxlength=500" }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<span class="usa-character-count__message" id="with-hint-textarea-info with-hint-textarea-hint"> You can enter up to 500 characters </span>
|
<span class="usa-character-count__message" id="with-hint-textarea-info with-hint-textarea-hint"> You can enter up to 500 characters </span>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- Test page -->
|
|
||||||
{% extends 'application_form.html' %}
|
{% extends 'application_form.html' %}
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
@ -16,7 +15,7 @@
|
||||||
|
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
|
|
||||||
<form class="usa-form usa-form--large" id="step__{{steps.current}}" method="post" novalidate>
|
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<fieldset class="usa-fieldset">
|
<fieldset class="usa-fieldset">
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- Test page -->
|
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %} Hello {% endblock %}
|
{% block title %} Hello {% endblock %}
|
||||||
|
|
18
src/registrar/templates/includes/form_errors.html
Normal file
18
src/registrar/templates/includes/form_errors.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{% if form.errors %}
|
||||||
|
{% for error in form.non_field_errors %}
|
||||||
|
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2">
|
||||||
|
<div class="usa-alert__body">
|
||||||
|
{{ error|escape }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% for field in form %}
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2">
|
||||||
|
<div class="usa-alert__body">
|
||||||
|
{{ error|escape }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
|
@ -21,9 +21,17 @@ error messages, if necessary.
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ field|add_label_class:"usa-label" }}
|
{{ field|add_label_class:"usa-label" }}
|
||||||
|
{% if www_gov %}
|
||||||
|
<div class="display-flex flex-align-center">
|
||||||
|
<span class="padding-top-05 padding-right-2px">www.</span>
|
||||||
|
{% endif %}
|
||||||
{% if required %}
|
{% if required %}
|
||||||
{{ field|add_class:input_class|attr:"required" }}
|
{{ field|add_class:input_class|attr:"required" }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ field|add_class:input_class }}
|
{{ field|add_class:input_class }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if www_gov %}
|
||||||
|
<span class="padding-top-05 padding-left-2px">.gov </span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- Test page -->
|
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %} Hello {% endblock %}
|
{% block title %} Hello {% endblock %}
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
from django import template
|
from django import template
|
||||||
|
from django.forms import BaseFormSet
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def isformset(value):
|
||||||
|
return isinstance(value, BaseFormSet)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def radio_buttons_by_value(boundfield):
|
def radio_buttons_by_value(boundfield):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django import template
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
def _field_context(field, input_class, add_class, required=False):
|
def _field_context(field, input_class, add_class, *, required=False, www_gov=False):
|
||||||
"""Helper to construct template context.
|
"""Helper to construct template context.
|
||||||
|
|
||||||
input_class is the CSS class to use on the input element, add_class
|
input_class is the CSS class to use on the input element, add_class
|
||||||
|
@ -17,17 +17,19 @@ def _field_context(field, input_class, add_class, required=False):
|
||||||
context = {"field": field, "input_class": input_class}
|
context = {"field": field, "input_class": input_class}
|
||||||
if required:
|
if required:
|
||||||
context["required"] = True
|
context["required"] = True
|
||||||
|
if www_gov:
|
||||||
|
context["www_gov"] = True
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag("includes/input_with_errors.html")
|
@register.inclusion_tag("includes/input_with_errors.html")
|
||||||
def input_with_errors(field, add_class=None):
|
def input_with_errors(field, add_class=None, www_gov=False):
|
||||||
"""Make an input field along with error handling.
|
"""Make an input field along with error handling.
|
||||||
|
|
||||||
field is a form field instance. add_class is a string of additional
|
field is a form field instance. add_class is a string of additional
|
||||||
classes (space separated) to add to "usa-input" on the <input> field.
|
classes (space separated) to add to "usa-input" on the <input> field.
|
||||||
"""
|
"""
|
||||||
return _field_context(field, "usa-input", add_class)
|
return _field_context(field, "usa-input", add_class, www_gov=www_gov)
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag("includes/input_with_errors.html")
|
@register.inclusion_tag("includes/input_with_errors.html")
|
||||||
|
@ -37,4 +39,4 @@ def select_with_errors(field, add_class=None, required=False):
|
||||||
field is a form field instance. add_class is a string of additional
|
field is a form field instance. add_class is a string of additional
|
||||||
classes (space separated) to add to "usa-select" on the field.
|
classes (space separated) to add to "usa-select" on the field.
|
||||||
"""
|
"""
|
||||||
return _field_context(field, "usa-select", add_class, required)
|
return _field_context(field, "usa-select", add_class, required=required)
|
||||||
|
|
|
@ -27,24 +27,18 @@ class TestFormValidation(TestCase):
|
||||||
form = OrganizationContactForm(data={"zipcode": zipcode})
|
form = OrganizationContactForm(data={"zipcode": zipcode})
|
||||||
self.assertNotIn("zipcode", form.errors)
|
self.assertNotIn("zipcode", form.errors)
|
||||||
|
|
||||||
def test_current_site_invalid(self):
|
def test_website_invalid(self):
|
||||||
form = CurrentSitesForm(data={"current_site": "nah"})
|
form = CurrentSitesForm(data={"website": "nah"})
|
||||||
self.assertEqual(
|
self.assertEqual(form.errors["website"], ["Enter a valid URL."])
|
||||||
form.errors["current_site"],
|
|
||||||
[
|
|
||||||
"Enter your organization’s website in the required format, like"
|
|
||||||
" www.city.com."
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_current_site_valid(self):
|
def test_website_valid(self):
|
||||||
form = CurrentSitesForm(data={"current_site": "hyphens-rule.gov.uk"})
|
form = CurrentSitesForm(data={"website": "hyphens-rule.gov.uk"})
|
||||||
self.assertEqual(len(form.errors), 0)
|
self.assertEqual(len(form.errors), 0)
|
||||||
|
|
||||||
def test_current_site_scheme_valid(self):
|
def test_website_scheme_valid(self):
|
||||||
form = CurrentSitesForm(data={"current_site": "http://hyphens-rule.gov.uk"})
|
form = CurrentSitesForm(data={"website": "http://hyphens-rule.gov.uk"})
|
||||||
self.assertEqual(len(form.errors), 0)
|
self.assertEqual(len(form.errors), 0)
|
||||||
form = CurrentSitesForm(data={"current_site": "https://hyphens-rule.gov.uk"})
|
form = CurrentSitesForm(data={"website": "https://hyphens-rule.gov.uk"})
|
||||||
self.assertEqual(len(form.errors), 0)
|
self.assertEqual(len(form.errors), 0)
|
||||||
|
|
||||||
def test_requested_domain_valid(self):
|
def test_requested_domain_valid(self):
|
||||||
|
|
|
@ -256,7 +256,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
current_sites_page = ao_result.follow()
|
current_sites_page = ao_result.follow()
|
||||||
current_sites_form = current_sites_page.form
|
current_sites_form = current_sites_page.form
|
||||||
current_sites_form["current_sites-current_site"] = "www.city.com"
|
current_sites_form["current_sites-0-website"] = "www.city.com"
|
||||||
|
|
||||||
# test saving the page
|
# test saving the page
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
@ -266,7 +266,8 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
# should see results in db
|
# should see results in db
|
||||||
application = DomainApplication.objects.get() # there's only one
|
application = DomainApplication.objects.get() # there's only one
|
||||||
self.assertEquals(
|
self.assertEquals(
|
||||||
application.current_websites.filter(website="city.com").count(), 1
|
application.current_websites.filter(website="http://www.city.com").count(),
|
||||||
|
1,
|
||||||
)
|
)
|
||||||
|
|
||||||
# test next button
|
# test next button
|
||||||
|
@ -742,12 +743,6 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
def test_application_ao_dynamic_text(self):
|
def test_application_ao_dynamic_text(self):
|
||||||
type_page = self.app.get(reverse("application:")).follow()
|
type_page = self.app.get(reverse("application:")).follow()
|
||||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
|
||||||
# resetting the session key on each new request, thus destroying the concept
|
|
||||||
# of a "session". We are going to do it manually, saving the session ID here
|
|
||||||
# and then setting the cookie on each request.
|
|
||||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
|
||||||
|
|
||||||
# ---- TYPE PAGE ----
|
# ---- TYPE PAGE ----
|
||||||
type_form = type_page.form
|
type_form = type_page.form
|
||||||
type_form["organization_type-organization_type"] = "federal"
|
type_form["organization_type-organization_type"] = "federal"
|
||||||
|
@ -804,6 +799,38 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
ao_page = election_page.click(str(self.TITLES["authorizing_official"]), index=0)
|
ao_page = election_page.click(str(self.TITLES["authorizing_official"]), index=0)
|
||||||
self.assertContains(ao_page, "Domain requests from cities")
|
self.assertContains(ao_page, "Domain requests from cities")
|
||||||
|
|
||||||
|
def test_application_formsets(self):
|
||||||
|
"""Users are able to add more than one of some fields."""
|
||||||
|
current_sites_page = self.app.get(reverse("application:current_sites"))
|
||||||
|
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||||
|
# resetting the session key on each new request, thus destroying the concept
|
||||||
|
# of a "session". We are going to do it manually, saving the session ID here
|
||||||
|
# and then setting the cookie on each request.
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
|
||||||
|
# fill in the form field
|
||||||
|
current_sites_form = current_sites_page.form
|
||||||
|
self.assertIn("current_sites-0-website", current_sites_form.fields)
|
||||||
|
self.assertNotIn("current_sites-1-website", current_sites_form.fields)
|
||||||
|
current_sites_form["current_sites-0-website"] = "https://example.com"
|
||||||
|
|
||||||
|
# click "Add another"
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
current_sites_result = current_sites_form.submit("submit_button", value="save")
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
current_sites_form = current_sites_result.follow().form
|
||||||
|
|
||||||
|
# verify that there are two form fields
|
||||||
|
value = current_sites_form["current_sites-0-website"].value
|
||||||
|
self.assertEqual(value, "https://example.com")
|
||||||
|
self.assertIn("current_sites-1-website", current_sites_form.fields)
|
||||||
|
# and it is correctly referenced in the ManyToOne relationship
|
||||||
|
application = DomainApplication.objects.get() # there's only one
|
||||||
|
self.assertEquals(
|
||||||
|
application.current_websites.filter(website="https://example.com").count(),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
@skip("WIP")
|
@skip("WIP")
|
||||||
def test_application_edit_restore(self):
|
def test_application_edit_restore(self):
|
||||||
"""
|
"""
|
||||||
|
|
10
src/registrar/utility/errors.py
Normal file
10
src/registrar/utility/errors.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
class BlankValueError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ExtraDotsError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DomainUnavailableError(ValueError):
|
||||||
|
pass
|
|
@ -378,7 +378,7 @@ class AuthorizingOfficial(ApplicationWizard):
|
||||||
|
|
||||||
class CurrentSites(ApplicationWizard):
|
class CurrentSites(ApplicationWizard):
|
||||||
template_name = "application_current_sites.html"
|
template_name = "application_current_sites.html"
|
||||||
forms = [forms.CurrentSitesForm]
|
forms = [forms.CurrentSitesFormSet]
|
||||||
|
|
||||||
|
|
||||||
class DotgovDomain(ApplicationWizard):
|
class DotgovDomain(ApplicationWizard):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue