mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-18 18:39:21 +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"""
|
||||
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.exceptions import BadRequest
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.http import JsonResponse
|
||||
|
@ -11,7 +11,6 @@ import requests
|
|||
|
||||
from cachetools.func import ttl_cache
|
||||
|
||||
from registrar.models import Domain
|
||||
|
||||
DOMAIN_FILE_URL = (
|
||||
"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,
|
||||
lowercase everything and return the list.
|
||||
"""
|
||||
Domain = apps.get_model("registrar.Domain")
|
||||
# 5 second timeout
|
||||
file_contents = requests.get(DOMAIN_FILE_URL, timeout=5).text
|
||||
domains = set()
|
||||
|
@ -65,6 +65,7 @@ def available(request, domain=""):
|
|||
Response is a JSON dictionary with the key "available" and value true or
|
||||
false.
|
||||
"""
|
||||
Domain = apps.get_model("registrar.Domain")
|
||||
# validate that the given domain could be a domain name and fail early if
|
||||
# not.
|
||||
if not (
|
||||
|
|
|
@ -75,12 +75,7 @@ class DomainApplicationFixture:
|
|||
|
||||
# any fields not specified here will be filled in with fake data or defaults
|
||||
# NOTE BENE: each fixture must have `organization_name` for uniqueness!
|
||||
DA = [
|
||||
{
|
||||
"status": "started",
|
||||
"organization_name": "Example - Finished but not Submitted",
|
||||
},
|
||||
# an example of a more manual application
|
||||
# Here is a more complete example as a template:
|
||||
# {
|
||||
# "status": "started",
|
||||
# "organization_name": "Example - Just started",
|
||||
|
@ -103,6 +98,11 @@ class DomainApplicationFixture:
|
|||
# "current_websites": [],
|
||||
# "alternative_domains": [],
|
||||
# },
|
||||
DA = [
|
||||
{
|
||||
"status": "started",
|
||||
"organization_name": "Example - Finished but not Submitted",
|
||||
},
|
||||
{
|
||||
"status": "submitted",
|
||||
"organization_name": "Example - Submitted but pending Investigation",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations # allows forward references in annotations
|
||||
from itertools import zip_longest
|
||||
import logging
|
||||
from typing import Callable
|
||||
from phonenumber_field.formfields import PhoneNumberField # type: ignore
|
||||
|
||||
from django import forms
|
||||
|
@ -8,6 +9,7 @@ from django.core.validators import RegexValidator
|
|||
from django.utils.safestring import mark_safe
|
||||
|
||||
from registrar.models import Contact, DomainApplication, Domain
|
||||
from registrar.utility import errors
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -69,6 +71,83 @@ class RegistrarFormSet(forms.BaseFormSet):
|
|||
self.application = kwargs.pop("application", None)
|
||||
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):
|
||||
organization_type = forms.ChoiceField(
|
||||
|
@ -299,52 +378,34 @@ class AuthorizingOfficialForm(RegistrarForm):
|
|||
|
||||
|
||||
class CurrentSitesForm(RegistrarForm):
|
||||
def to_database(self, obj):
|
||||
if not self.is_valid():
|
||||
return
|
||||
obj.save()
|
||||
normalized = Domain.normalize(self.cleaned_data["current_site"], blank=True)
|
||||
if normalized:
|
||||
# TODO: ability to update existing records
|
||||
obj.current_websites.create(website=normalized)
|
||||
website = forms.URLField(
|
||||
required=False,
|
||||
label="Public website",
|
||||
)
|
||||
|
||||
|
||||
class BaseCurrentSitesFormSet(RegistrarFormSet):
|
||||
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
|
||||
def from_database(cls, obj):
|
||||
current_website = obj.current_websites.first()
|
||||
if current_website is not None:
|
||||
return {"current_site": current_website.website}
|
||||
else:
|
||||
return {}
|
||||
return super().from_database(obj, cls.JOIN, cls.on_fetch)
|
||||
|
||||
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):
|
||||
"""This field should be a legal domain name."""
|
||||
inputted_site = self.cleaned_data["current_site"]
|
||||
if not inputted_site:
|
||||
# empty string is fine
|
||||
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",
|
||||
CurrentSitesFormSet = forms.formset_factory(
|
||||
CurrentSitesForm,
|
||||
extra=1,
|
||||
absolute_max=1500, # django default; use `max_num` to limit entries
|
||||
formset=BaseCurrentSitesFormSet,
|
||||
)
|
||||
|
||||
|
||||
|
@ -354,59 +415,67 @@ class AlternativeDomainForm(RegistrarForm):
|
|||
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):
|
||||
def to_database(self, obj: DomainApplication):
|
||||
if not self.is_valid():
|
||||
return
|
||||
JOIN = "alternative_domains"
|
||||
|
||||
obj.save()
|
||||
query = obj.alternative_domains.order_by("created_at").all() # order matters
|
||||
def should_delete(self, cleaned):
|
||||
domain = cleaned.get("alternative_domain", "")
|
||||
return domain.strip() == ""
|
||||
|
||||
# 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 {}
|
||||
def pre_update(self, db_obj, cleaned):
|
||||
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
|
||||
if db_obj is not None and isinstance(domain, str):
|
||||
entry_was_erased = domain.strip() == ""
|
||||
if entry_was_erased:
|
||||
db_obj.delete()
|
||||
continue
|
||||
try:
|
||||
normalized = Domain.normalize(domain, "gov", blank=True)
|
||||
except ValueError as e:
|
||||
logger.debug(e)
|
||||
continue
|
||||
db_obj.website = normalized
|
||||
db_obj.save()
|
||||
def pre_create(self, db_obj, cleaned):
|
||||
domain = cleaned.get("alternative_domain", None)
|
||||
if domain is not None:
|
||||
return {"website": f"{domain}.gov"}
|
||||
else:
|
||||
return {}
|
||||
|
||||
# no matching database object, create it
|
||||
elif db_obj is None and domain is not None:
|
||||
try:
|
||||
normalized = Domain.normalize(domain, "gov", blank=True)
|
||||
except ValueError as e:
|
||||
logger.debug(e)
|
||||
continue
|
||||
obj.alternative_domains.create(website=normalized)
|
||||
def to_database(self, obj: DomainApplication):
|
||||
self._to_database(
|
||||
obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def on_fetch(cls, query):
|
||||
return [{"alternative_domain": domain.sld} for domain in query]
|
||||
|
||||
@classmethod
|
||||
def from_database(cls, obj):
|
||||
query = obj.alternative_domains.order_by("created_at").all() # order matters
|
||||
return [{"alternative_domain": domain.sld} for domain in query]
|
||||
return super().from_database(obj, cls.JOIN, cls.on_fetch)
|
||||
|
||||
|
||||
AlternativeDomainFormSet = forms.formset_factory(
|
||||
AlternativeDomainForm,
|
||||
extra=1,
|
||||
absolute_max=1500,
|
||||
absolute_max=1500, # django default; use `max_num` to limit entries
|
||||
formset=BaseAlternativeDomainFormSet,
|
||||
)
|
||||
|
||||
|
@ -415,16 +484,14 @@ class DotGovDomainForm(RegistrarForm):
|
|||
def to_database(self, obj):
|
||||
if not self.is_valid():
|
||||
return
|
||||
normalized = Domain.normalize(
|
||||
self.cleaned_data["requested_domain"], "gov", blank=True
|
||||
)
|
||||
if normalized:
|
||||
domain = self.cleaned_data.get("requested_domain", None)
|
||||
if domain:
|
||||
requested_domain = getattr(obj, "requested_domain", None)
|
||||
if requested_domain is not None:
|
||||
requested_domain.name = normalized
|
||||
requested_domain.name = f"{domain}.gov"
|
||||
requested_domain.save()
|
||||
else:
|
||||
requested_domain = Domain.objects.create(name=normalized)
|
||||
requested_domain = Domain.objects.create(name=f"{domain}.gov")
|
||||
obj.requested_domain = requested_domain
|
||||
obj.save()
|
||||
|
||||
|
@ -438,16 +505,12 @@ class DotGovDomainForm(RegistrarForm):
|
|||
values["requested_domain"] = requested_domain.sld
|
||||
return values
|
||||
|
||||
requested_domain = forms.CharField(label="What .gov domain do you want?")
|
||||
|
||||
def clean_requested_domain(self):
|
||||
"""Requested domains need to be legal top-level domains, not subdomains.
|
||||
|
||||
If they end with `.gov`, then we can reasonably take that off. If they have
|
||||
any other dots in them, raise an error.
|
||||
"""
|
||||
requested = self.cleaned_data["requested_domain"]
|
||||
if not requested:
|
||||
"""Validation code for domain names."""
|
||||
try:
|
||||
requested = self.cleaned_data.get("requested_domain", None)
|
||||
validated = Domain.validate(requested)
|
||||
except errors.BlankValueError:
|
||||
# none or empty string
|
||||
raise forms.ValidationError(
|
||||
"Enter the .gov domain you want. Don’t include “www” or “.gov.” For"
|
||||
|
@ -455,20 +518,25 @@ class DotGovDomainForm(RegistrarForm):
|
|||
" the quotes).",
|
||||
code="invalid",
|
||||
)
|
||||
if requested.endswith(".gov"):
|
||||
requested = requested[:-4]
|
||||
if "." in requested:
|
||||
except errors.ExtraDotsError:
|
||||
raise forms.ValidationError(
|
||||
"Enter the .gov domain you want without any periods.",
|
||||
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(
|
||||
"Enter a domain using only letters, "
|
||||
"numbers, or hyphens (though we don't recommend using hyphens).",
|
||||
code="invalid",
|
||||
)
|
||||
return requested
|
||||
return validated
|
||||
|
||||
requested_domain = forms.CharField(label="What .gov domain do you want?")
|
||||
|
||||
|
||||
class PurposeForm(RegistrarForm):
|
||||
|
@ -595,47 +663,26 @@ class OtherContactsForm(RegistrarForm):
|
|||
|
||||
|
||||
class BaseOtherContactsFormSet(RegistrarFormSet):
|
||||
def to_database(self, obj):
|
||||
if not self.is_valid():
|
||||
return
|
||||
obj.save()
|
||||
JOIN = "other_contacts"
|
||||
|
||||
query = obj.other_contacts.order_by("created_at").all()
|
||||
|
||||
# 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:
|
||||
def should_delete(self, cleaned):
|
||||
empty = (isinstance(v, str) and not v.strip() for v in cleaned.values())
|
||||
erased = all(empty)
|
||||
if erased:
|
||||
db_obj.delete()
|
||||
continue
|
||||
for key, value in cleaned.items():
|
||||
setattr(db_obj, key, value)
|
||||
db_obj.save()
|
||||
return all(empty)
|
||||
|
||||
# no matching database object, create it
|
||||
elif db_obj is None and cleaned:
|
||||
obj.other_contacts.create(**cleaned)
|
||||
def to_database(self, obj: DomainApplication):
|
||||
self._to_database(
|
||||
obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create
|
||||
)
|
||||
|
||||
@classmethod
|
||||
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(
|
||||
OtherContactsForm,
|
||||
extra=1,
|
||||
absolute_max=1500,
|
||||
absolute_max=1500, # django default; use `max_num` to limit entries
|
||||
formset=BaseOtherContactsFormSet,
|
||||
)
|
||||
|
||||
|
|
|
@ -6,7 +6,9 @@ from django.core.exceptions import ValidationError
|
|||
from django.db import models
|
||||
from django_fsm import FSMField, transition # type: ignore
|
||||
|
||||
from api.views import in_domains
|
||||
from epp.mock_epp import domain_info, domain_check
|
||||
from registrar.utility import errors
|
||||
|
||||
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
|
||||
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
|
||||
def string_could_be_domain(cls, domain: str | None) -> bool:
|
||||
"""Return True if the string could be a domain name, otherwise False."""
|
||||
|
@ -153,6 +101,29 @@ class Domain(TimeStampedModel):
|
|||
return False
|
||||
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
|
||||
def available(cls, domain: str) -> bool:
|
||||
"""Check if a domain is available.
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<!-- Test page -->
|
||||
{% extends 'application_form.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
|
@ -6,7 +5,7 @@
|
|||
|
||||
<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">
|
||||
{% csrf_token %}
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<!-- Test page -->
|
||||
{% extends 'application_form.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% load static %}
|
||||
|
@ -43,7 +42,7 @@
|
|||
|
||||
{% 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 %}
|
||||
|
||||
<fieldset class="usa-fieldset">
|
||||
|
|
|
@ -1,14 +1,28 @@
|
|||
<!-- Test page -->
|
||||
{% extends 'application_form.html' %}
|
||||
{% load widget_tweaks field_helpers %}
|
||||
{% load static %}
|
||||
|
||||
{% 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 %}
|
||||
<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 }}
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% load widget_tweaks static%}
|
||||
{% load widget_tweaks field_helpers static %}
|
||||
|
||||
{% block form_content %}
|
||||
<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">
|
||||
<li>Be available </li>
|
||||
|
@ -16,78 +17,33 @@
|
|||
<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>
|
||||
<div class="domain-example">
|
||||
<div id="domain-example">
|
||||
{% include "includes/domain_example__city.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||
<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 %}
|
||||
|
||||
{% if forms.0.requested_domain.errors %}
|
||||
<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 %}
|
||||
{% input_with_errors forms.0.requested_domain www_gov=True %}
|
||||
<button type="button" class="usa-button">Check availability</button>
|
||||
|
||||
<h2>Alternative domains</h2>
|
||||
|
||||
<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 }}
|
||||
<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 %}
|
||||
{{ form.alternative_domain|add_label_class:"usa-label" }}
|
||||
<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>
|
||||
{% input_with_errors form.alternative_domain www_gov=True %}
|
||||
{% endfor %}
|
||||
|
||||
<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>
|
||||
</svg><span class="margin-left-05">Add another alternative</span>
|
||||
</button>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% 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 content %}
|
||||
|
@ -12,30 +12,26 @@
|
|||
<main id="main-content" class="grid-container register-form-step">
|
||||
{% if steps.prev %}
|
||||
<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>
|
||||
</svg><span class="margin-left-05">Previous step</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% for form in forms %}
|
||||
{% 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 %}
|
||||
{% comment %}
|
||||
to make sense of this loop, consider that
|
||||
a context variable of `forms` contains all
|
||||
the forms for this page; each of these
|
||||
may be itself a formset and contain additional
|
||||
forms, hence `forms.forms`
|
||||
{% endcomment %}
|
||||
{% for outer in forms %}
|
||||
{% if outer|isformset %}
|
||||
{% for inner in outer.forms %}
|
||||
{% include "includes/form_errors.html" with form=inner %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% include "includes/form_errors.html" with form=outer %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<!-- Test page -->
|
||||
{% extends 'application_form.html' %}
|
||||
{% load widget_tweaks field_helpers %}
|
||||
|
||||
|
@ -16,6 +15,7 @@
|
|||
{% include "includes/required_fields.html" %}
|
||||
</div>
|
||||
|
||||
|
||||
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<!-- Test page -->
|
||||
{% extends 'application_form.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% load dynamic_question_tags %}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<!-- Test page -->
|
||||
{% extends 'application_form.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% load dynamic_question_tags %}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<!-- Test page -->
|
||||
{% extends 'application_form.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% 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>
|
||||
{% 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 %}
|
||||
{{ forms.0.management_form }}
|
||||
{# forms.0 is a formset and this iterates over its forms #}
|
||||
|
@ -28,7 +28,7 @@
|
|||
|
||||
<div>
|
||||
<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>
|
||||
</svg><span class="margin-left-05">Add another contact</span>
|
||||
</button>
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
<!-- Test page -->
|
||||
{% extends 'application_form.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% 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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>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>
|
||||
</div>
|
||||
|
||||
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||
<div class="usa-form-group">
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<!-- Test page -->
|
||||
{% extends 'application_form.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
<!-- Test page -->
|
||||
{% extends 'application_form.html' %}
|
||||
{% load static widget_tweaks namespaced_urls %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% for step in steps.all|slice:":-1" %}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<!-- Test page -->
|
||||
{% extends 'application_form.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% 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>
|
||||
|
||||
<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 %}
|
||||
|
||||
{% if forms.0.security_email.errors %}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<!-- Test page -->
|
||||
{% extends 'application_form.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
|
@ -20,11 +19,11 @@
|
|||
{{ error }}
|
||||
</span>
|
||||
{% 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>
|
||||
{% else %}
|
||||
{{ 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 %}
|
||||
{% endwith %}
|
||||
<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 }}
|
||||
</span>
|
||||
{% 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>
|
||||
{% else %}
|
||||
{{ 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 %}
|
||||
{% endwith %}
|
||||
<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' %}
|
||||
{% load widget_tweaks %}
|
||||
{% load static %}
|
||||
|
@ -16,7 +15,7 @@
|
|||
|
||||
{% 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 %}
|
||||
|
||||
<fieldset class="usa-fieldset">
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<!-- Test page -->
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% 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>
|
||||
{% else %}
|
||||
{{ 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 %}
|
||||
{{ field|add_class:input_class|attr:"required" }}
|
||||
{% else %}
|
||||
{{ field|add_class:input_class }}
|
||||
{% endif %}
|
||||
{% if www_gov %}
|
||||
<span class="padding-top-05 padding-left-2px">.gov </span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<!-- Test page -->
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %} Hello {% endblock %}
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
from django import template
|
||||
from django.forms import BaseFormSet
|
||||
from django.utils.html import format_html
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def isformset(value):
|
||||
return isinstance(value, BaseFormSet)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def radio_buttons_by_value(boundfield):
|
||||
"""
|
||||
|
|
|
@ -5,7 +5,7 @@ from django import template
|
|||
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.
|
||||
|
||||
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}
|
||||
if required:
|
||||
context["required"] = True
|
||||
if www_gov:
|
||||
context["www_gov"] = True
|
||||
return context
|
||||
|
||||
|
||||
@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.
|
||||
|
||||
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.
|
||||
"""
|
||||
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")
|
||||
|
@ -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
|
||||
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})
|
||||
self.assertNotIn("zipcode", form.errors)
|
||||
|
||||
def test_current_site_invalid(self):
|
||||
form = CurrentSitesForm(data={"current_site": "nah"})
|
||||
self.assertEqual(
|
||||
form.errors["current_site"],
|
||||
[
|
||||
"Enter your organization’s website in the required format, like"
|
||||
" www.city.com."
|
||||
],
|
||||
)
|
||||
def test_website_invalid(self):
|
||||
form = CurrentSitesForm(data={"website": "nah"})
|
||||
self.assertEqual(form.errors["website"], ["Enter a valid URL."])
|
||||
|
||||
def test_current_site_valid(self):
|
||||
form = CurrentSitesForm(data={"current_site": "hyphens-rule.gov.uk"})
|
||||
def test_website_valid(self):
|
||||
form = CurrentSitesForm(data={"website": "hyphens-rule.gov.uk"})
|
||||
self.assertEqual(len(form.errors), 0)
|
||||
|
||||
def test_current_site_scheme_valid(self):
|
||||
form = CurrentSitesForm(data={"current_site": "http://hyphens-rule.gov.uk"})
|
||||
def test_website_scheme_valid(self):
|
||||
form = CurrentSitesForm(data={"website": "http://hyphens-rule.gov.uk"})
|
||||
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)
|
||||
|
||||
def test_requested_domain_valid(self):
|
||||
|
|
|
@ -256,7 +256,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
current_sites_page = ao_result.follow()
|
||||
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
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
@ -266,7 +266,8 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# should see results in db
|
||||
application = DomainApplication.objects.get() # there's only one
|
||||
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
|
||||
|
@ -742,12 +743,6 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
|
||||
def test_application_ao_dynamic_text(self):
|
||||
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_form = type_page.form
|
||||
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)
|
||||
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")
|
||||
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):
|
||||
template_name = "application_current_sites.html"
|
||||
forms = [forms.CurrentSitesForm]
|
||||
forms = [forms.CurrentSitesFormSet]
|
||||
|
||||
|
||||
class DotgovDomain(ApplicationWizard):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue