Enable domain availability checker

This commit is contained in:
Seamus Johnston 2023-02-03 14:22:01 -06:00
parent c2feb1fc16
commit 456cab6cee
No known key found for this signature in database
GPG key ID: 2F21225985069105
5 changed files with 157 additions and 68 deletions

View file

@ -1,7 +1,5 @@
"""Internal API views""" """Internal API views"""
from django.apps import apps from django.apps import apps
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
@ -17,6 +15,19 @@ DOMAIN_FILE_URL = (
) )
DOMAIN_API_MESSAGES = {
"required": "Enter the .gov domain you want. Dont include “www” or “.gov.”"
" For example, if you want www.city.gov, you would enter “city”"
" (without the quotes).",
"extra_dots": "Enter the .gov domain you want without any periods.",
"unavailable": "That domain isnt available. Try entering another one."
" Contact us if you need help coming up with a domain.",
"invalid": "Enter a domain using only letters,"
" numbers, or hyphens (though we don't recommend using hyphens).",
"success": "That domain is available!",
}
# this file doesn't change that often, nor is it that big, so cache the result # this file doesn't change that often, nor is it that big, so cache the result
# in memory for ten minutes # in memory for ten minutes
@ttl_cache(ttl=600) @ttl_cache(ttl=600)
@ -72,6 +83,18 @@ def available(request, domain=""):
Domain.string_could_be_domain(domain) Domain.string_could_be_domain(domain)
or Domain.string_could_be_domain(domain + ".gov") or Domain.string_could_be_domain(domain + ".gov")
): ):
raise BadRequest("Invalid request.") return JsonResponse({
"available": False,
"message": DOMAIN_API_MESSAGES["invalid"]
})
# a domain is available if it is NOT in the list of current domains # a domain is available if it is NOT in the list of current domains
return JsonResponse({"available": not in_domains(domain)}) if in_domains(domain):
return JsonResponse({
"available": False,
"message": DOMAIN_API_MESSAGES["unavailable"]
})
else:
return JsonResponse({
"available": True,
"message": DOMAIN_API_MESSAGES["success"]
})

View file

@ -9,6 +9,11 @@
var DEFAULT_ERROR = "Please check this field for errors."; var DEFAULT_ERROR = "Please check this field for errors.";
var INFORMATIVE = "info";
var WARNING = "warning";
var ERROR = "error";
var SUCCESS = "success";
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>> // <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
// Helper functions. // Helper functions.
@ -89,44 +94,114 @@ function toggleInputValidity(el, valid, msg=DEFAULT_ERROR) {
el.classList.remove('usa-input--success'); el.classList.remove('usa-input--success');
el.setAttribute("aria-invalid", "true"); el.setAttribute("aria-invalid", "true");
el.setCustomValidity(msg); el.setCustomValidity(msg);
// this is here for testing: in actual use, we might not want to
// visually display these errors until the user tries to submit
el.classList.add('usa-input--error'); el.classList.add('usa-input--error');
} }
} }
function _checkDomainAvailability(e) { /** Display (or hide) a message beneath an element. */
function inlineToast(el, id, style, msg) {
if (!el.id && !id) {
console.error("Elements must have an `id` to show an inline toast.");
return;
}
let toast = document.getElementById((el.id || id) + "--toast");
if (style) {
if (!toast) {
// create and insert the message div
toast = document.createElement("div");
const toastBody = document.createElement("div");
const p = document.createElement("p");
toast.setAttribute("id", (el.id || id) + "--toast");
toast.className = `usa-alert usa-alert--${style} usa-alert--slim`;
toastBody.classList.add("usa-alert__body");
p.classList.add("usa-alert__text");
p.innerText = msg;
toastBody.appendChild(p);
toast.appendChild(toastBody);
el.parentNode.insertBefore(toast, el.nextSibling);
} else {
// update and show the existing message div
toast.className = `usa-alert usa-alert--${style} usa-alert--slim`;
toast.querySelector("div p").innerText = msg;
makeVisible(toast);
}
} else {
if (toast) makeHidden(toast);
}
}
function _checkDomainAvailability(el) {
const callback = (response) => { const callback = (response) => {
toggleInputValidity(e.target, (response && response.available)); toggleInputValidity(el, (response && response.available), msg=response.message);
if (e.target.validity.valid) { announce(el.id, response.message);
e.target.classList.add('usa-input--success'); if (el.validity.valid) {
// do other stuff, like display a toast? el.classList.add('usa-input--success');
// use of `parentElement` due to .gov inputs being wrapped in www/.gov decoration
inlineToast(el.parentElement, el.id, SUCCESS, response.message);
} else {
inlineToast(el.parentElement, el.id, ERROR, response.message);
} }
} }
fetchJSON(`available/${e.target.value}`, callback); fetchJSON(`available/${el.value}`, callback);
} }
/** Call the API to see if the domain is good. */ /** Call the API to see if the domain is good. */
const checkDomainAvailability = debounce(_checkDomainAvailability); const checkDomainAvailability = debounce(_checkDomainAvailability);
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>> /** Hides the toast message and clears the aira live region. */
// Event handlers. function clearDomainAvailability(el) {
el.classList.remove('usa-input--success');
announce(el.id, "");
// use of `parentElement` due to .gov inputs being wrapped in www/.gov decoration
inlineToast(el.parentElement, el.id);
}
/** Runs all the validators associated with this element. */
/** On input change, handles running any associated validators. */ function runValidators(el) {
function handleInputValidation(e) { const attribute = el.getAttribute("validate") || "";
const attribute = e.target.getAttribute("validate") || "";
if (!attribute.length) return; if (!attribute.length) return;
const validators = attribute.split(" "); const validators = attribute.split(" ");
let isInvalid = false; let isInvalid = false;
for (const validator of validators) { for (const validator of validators) {
switch (validator) { switch (validator) {
case "domain": case "domain":
checkDomainAvailability(e); checkDomainAvailability(el);
break; break;
} }
} }
toggleInputValidity(e.target, !isInvalid); toggleInputValidity(el, !isInvalid);
}
/** Clears all the validators associated with this element. */
function clearValidators(el) {
const attribute = el.getAttribute("validate") || "";
if (!attribute.length) return;
const validators = attribute.split(" ");
for (const validator of validators) {
switch (validator) {
case "domain":
clearDomainAvailability(el);
break;
}
}
toggleInputValidity(el, true);
}
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
// Event handlers.
/** On input change, handles running any associated validators. */
function handleInputValidation(e) {
clearValidators(e.target);
if (e.target.hasAttribute("auto-validate")) runValidators(e.target);
}
/** On button click, handles running any associated validators. */
function handleValidationClick(e) {
const attribute = e.target.getAttribute("validate-for") || "";
if (!attribute.length) return;
const input = document.getElementById(attribute);
runValidators(input);
} }
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>> // <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
@ -135,8 +210,12 @@ function handleInputValidation(e) {
/** /**
* An IIFE that will attach validators to inputs. * An IIFE that will attach validators to inputs.
* *
* It looks for elements with `validate="<type> <type>"` and adds * It looks for elements with `validate="<type> <type>"` and adds change handlers.
* change handlers for each known type. *
* These handlers know about two other attributes:
* - `validate-for="<id>"` creates a button which will run the validator(s) on <id>
* - `auto-validate` will run validator(s) when the user stops typing (otherwise,
* they will only run when a user clicks the button with `validate-for`)
*/ */
(function validatorsInit() { (function validatorsInit() {
"use strict"; "use strict";
@ -144,6 +223,10 @@ function handleInputValidation(e) {
for(const input of needsValidation) { for(const input of needsValidation) {
input.addEventListener('input', handleInputValidation); input.addEventListener('input', handleInputValidation);
} }
const activatesValidation = document.querySelectorAll('[validate-for]');
for(const button of activatesValidation) {
button.addEventListener('click', handleValidationClick);
}
})(); })();

View file

@ -7,6 +7,8 @@ from phonenumber_field.formfields import PhoneNumberField # type: ignore
from django import forms from django import forms
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from api.views import DOMAIN_API_MESSAGES
from registrar.models import Contact, DomainApplication, Domain from registrar.models import Contact, DomainApplication, Domain
from registrar.utility import errors from registrar.utility import errors
@ -414,36 +416,25 @@ CurrentSitesFormSet = forms.formset_factory(
formset=BaseCurrentSitesFormSet, formset=BaseCurrentSitesFormSet,
) )
class AlternativeDomainForm(RegistrarForm): class AlternativeDomainForm(RegistrarForm):
alternative_domain = forms.CharField(
required=False,
label="Alternative domain",
)
def clean_alternative_domain(self): def clean_alternative_domain(self):
"""Validation code for domain names.""" """Validation code for domain names."""
try: try:
requested = self.cleaned_data.get("alternative_domain", None) requested = self.cleaned_data.get("alternative_domain", None)
validated = Domain.validate(requested, blank_ok=True) validated = Domain.validate(requested, blank_ok=True)
except errors.ExtraDotsError: except errors.ExtraDotsError:
raise forms.ValidationError( raise forms.ValidationError(code="extra_dots")
"Please enter a domain without any periods.",
code="invalid",
)
except errors.DomainUnavailableError: except errors.DomainUnavailableError:
raise forms.ValidationError( raise forms.ValidationError(code="unavailable")
"ERROR MESSAGE GOES HERE",
code="invalid",
)
except ValueError: except ValueError:
raise forms.ValidationError( raise forms.ValidationError(code="invalid")
"Please enter a valid domain name using only letters, "
"numbers, and hyphens",
code="invalid",
)
return validated return validated
alternative_domain = forms.CharField(
required=False,
label="Alternative domain",
error_messages=DOMAIN_API_MESSAGES
)
class BaseAlternativeDomainFormSet(RegistrarFormSet): class BaseAlternativeDomainFormSet(RegistrarFormSet):
JOIN = "alternative_domains" JOIN = "alternative_domains"
@ -517,32 +508,19 @@ class DotGovDomainForm(RegistrarForm):
requested = self.cleaned_data.get("requested_domain", None) requested = self.cleaned_data.get("requested_domain", None)
validated = Domain.validate(requested) validated = Domain.validate(requested)
except errors.BlankValueError: except errors.BlankValueError:
# none or empty string raise forms.ValidationError(code="required")
raise forms.ValidationError(
"Enter the .gov domain you want. Dont include “www” or “.gov.” For"
" example, if you want www.city.gov, you would enter “city” (without"
" the quotes).",
code="invalid",
)
except errors.ExtraDotsError: except errors.ExtraDotsError:
raise forms.ValidationError( raise forms.ValidationError(code="extra_dots")
"Enter the .gov domain you want without any periods.",
code="invalid",
)
except errors.DomainUnavailableError: except errors.DomainUnavailableError:
raise forms.ValidationError( raise forms.ValidationError(code="unavailable")
"ERROR MESSAGE GOES HERE",
code="invalid",
)
except ValueError: except ValueError:
raise forms.ValidationError( raise forms.ValidationError(code="invalid")
"Enter a domain using only letters, "
"numbers, or hyphens (though we don't recommend using hyphens).",
code="invalid",
)
return validated return validated
requested_domain = forms.CharField(label="What .gov domain do you want?") requested_domain = forms.CharField(
label="What .gov domain do you want?",
error_messages=DOMAIN_API_MESSAGES
)
class PurposeForm(RegistrarForm): class PurposeForm(RegistrarForm):

View file

@ -1,6 +1,5 @@
{% extends 'application_form.html' %} {% extends 'application_form.html' %}
{% load static %} {% load static field_helpers %}
{% load field_helpers %}
{% block form_instructions %} {% block form_instructions %}
<p>Before requesting a .gov domain, <a href="{% url 'todo' %}">please make sure it <p>Before requesting a .gov domain, <a href="{% url 'todo' %}">please make sure it
@ -29,7 +28,6 @@
{% block form_fields %} {% block form_fields %}
{% csrf_token %}
{{ forms.0.management_form }} {{ forms.0.management_form }}
@ -44,12 +42,16 @@
complete and submit the rest of this form.</p> complete and submit the rest of this form.</p>
{% with attr_aria_describedby="domain_instructions domain_instructions2" %} {% with attr_aria_describedby="domain_instructions domain_instructions2" %}
{# attr_validate / validate="domain" invokes code in get-gov.js #}
{% with www_gov=True attr_validate="domain" %} {% with www_gov=True attr_validate="domain" %}
{% input_with_errors forms.0.requested_domain %} {% input_with_errors forms.0.requested_domain %}
{% endwith %} {% endwith %}
{% endwith %} {% endwith %}
<button type="button" class="usa-button">Check availability</button> <button
type="button"
class="usa-button"
validate-for="{{ forms.0.requested_domain.auto_id }}"
>Check availability</button>
</fieldset> </fieldset>
{{ forms.1.management_form }} {{ forms.1.management_form }}
@ -63,7 +65,9 @@
you your first choice? Entering alternative domains is optional.</p> you your first choice? Entering alternative domains is optional.</p>
{% with attr_aria_describedby="alt_domain_instructions" %} {% with attr_aria_describedby="alt_domain_instructions" %}
{% with www_gov=True attr_validate="domain" %} {# attr_validate / validate="domain" invokes code in get-gov.js #}
{# attr_auto_validate likewise triggers behavior in get-gov.js #}
{% with www_gov=True attr_validate="domain" attr_auto_validate=True %}
{% for form in forms.1 %} {% for form in forms.1 %}
{% input_with_errors form.alternative_domain %} {% input_with_errors form.alternative_domain %}
{% endfor %} {% endfor %}

View file

@ -47,6 +47,7 @@
{% endblock %} {% endblock %}
<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 %}
{% block form_fields %}{% endblock %} {% block form_fields %}{% endblock %}