Respond to PR feedback

This commit is contained in:
Seamus Johnston 2023-01-17 09:52:23 -06:00
parent f6c70f88a9
commit 0f87d9ea9a
No known key found for this signature in database
GPG key ID: 2F21225985069105
31 changed files with 392 additions and 355 deletions

View file

@ -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 (

View file

@ -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",

View file

@ -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 organizations 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 organizations 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. Dont include “www” or “.gov.” For" "Enter the .gov domain you want. Dont 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,
) )

View file

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

View file

@ -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 %}

View file

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

View file

@ -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 organizations 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 }}

View file

@ -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>Well try to give you the domain you want. We first need to make sure your request meets our requirements. Well work with you to find the best domain for your organization.</p> <p>Well try to give you the domain you want. We first need to make sure your request meets our requirements. Well 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, well make sure its available and that it meets some of our naming requirements. If your domain passes these initial checks, well 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, well make sure its available and that it meets some of our naming requirements. If your domain passes these initial checks, well 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 youd like if we cant 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 youd like if we cant 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>

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -8,7 +8,7 @@
<p id="instructions">Wed 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">Wed 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>

View file

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

View file

@ -1,4 +1,3 @@
<!-- Test page -->
{% extends 'application_form.html' %} {% extends 'application_form.html' %}
{% load widget_tweaks %} {% load widget_tweaks %}

View file

@ -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" %}

View file

@ -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@&lt;domain.gov&gt;.</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@&lt;domain.gov&gt;.</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 %}

View file

@ -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>

View file

@ -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>

View file

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

View file

@ -1,4 +1,3 @@
<!-- Test page -->
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %} Hello {% endblock %} {% block title %} Hello {% endblock %}

View 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 %}

View file

@ -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 %}

View file

@ -1,4 +1,3 @@
<!-- Test page -->
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %} Hello {% endblock %} {% block title %} Hello {% endblock %}

View file

@ -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):
""" """

View file

@ -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)

View file

@ -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 organizations 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):

View file

@ -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):
""" """

View file

@ -0,0 +1,10 @@
class BlankValueError(ValueError):
pass
class ExtraDotsError(ValueError):
pass
class DomainUnavailableError(ValueError):
pass

View file

@ -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):