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"""
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
@ -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
# in memory for ten minutes
@ttl_cache(ttl=600)
@ -72,6 +83,18 @@ def available(request, domain=""):
Domain.string_could_be_domain(domain)
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
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 INFORMATIVE = "info";
var WARNING = "warning";
var ERROR = "error";
var SUCCESS = "success";
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
// Helper functions.
@ -89,44 +94,114 @@ function toggleInputValidity(el, valid, msg=DEFAULT_ERROR) {
el.classList.remove('usa-input--success');
el.setAttribute("aria-invalid", "true");
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');
}
}
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) => {
toggleInputValidity(e.target, (response && response.available));
if (e.target.validity.valid) {
e.target.classList.add('usa-input--success');
// do other stuff, like display a toast?
toggleInputValidity(el, (response && response.available), msg=response.message);
announce(el.id, response.message);
if (el.validity.valid) {
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. */
const checkDomainAvailability = debounce(_checkDomainAvailability);
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
// Event handlers.
/** Hides the toast message and clears the aira live region. */
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);
}
/** On input change, handles running any associated validators. */
function handleInputValidation(e) {
const attribute = e.target.getAttribute("validate") || "";
/** Runs all the validators associated with this element. */
function runValidators(el) {
const attribute = el.getAttribute("validate") || "";
if (!attribute.length) return;
const validators = attribute.split(" ");
let isInvalid = false;
for (const validator of validators) {
switch (validator) {
case "domain":
checkDomainAvailability(e);
checkDomainAvailability(el);
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.
*
* It looks for elements with `validate="<type> <type>"` and adds
* change handlers for each known type.
* It looks for elements with `validate="<type> <type>"` and adds change handlers.
*
* 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() {
"use strict";
@ -144,6 +223,10 @@ function handleInputValidation(e) {
for(const input of needsValidation) {
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.core.validators import RegexValidator
from api.views import DOMAIN_API_MESSAGES
from registrar.models import Contact, DomainApplication, Domain
from registrar.utility import errors
@ -414,36 +416,25 @@ CurrentSitesFormSet = forms.formset_factory(
formset=BaseCurrentSitesFormSet,
)
class AlternativeDomainForm(RegistrarForm):
alternative_domain = forms.CharField(
required=False,
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",
)
raise forms.ValidationError(code="extra_dots")
except errors.DomainUnavailableError:
raise forms.ValidationError(
"ERROR MESSAGE GOES HERE",
code="invalid",
)
raise forms.ValidationError(code="unavailable")
except ValueError:
raise forms.ValidationError(
"Please enter a valid domain name using only letters, "
"numbers, and hyphens",
code="invalid",
)
raise forms.ValidationError(code="invalid")
return validated
alternative_domain = forms.CharField(
required=False,
label="Alternative domain",
error_messages=DOMAIN_API_MESSAGES
)
class BaseAlternativeDomainFormSet(RegistrarFormSet):
JOIN = "alternative_domains"
@ -517,32 +508,19 @@ class DotGovDomainForm(RegistrarForm):
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. Dont include “www” or “.gov.” For"
" example, if you want www.city.gov, you would enter “city” (without"
" the quotes).",
code="invalid",
)
raise forms.ValidationError(code="required")
except errors.ExtraDotsError:
raise forms.ValidationError(
"Enter the .gov domain you want without any periods.",
code="invalid",
)
raise forms.ValidationError(code="extra_dots")
except errors.DomainUnavailableError:
raise forms.ValidationError(
"ERROR MESSAGE GOES HERE",
code="invalid",
)
raise forms.ValidationError(code="unavailable")
except ValueError:
raise forms.ValidationError(
"Enter a domain using only letters, "
"numbers, or hyphens (though we don't recommend using hyphens).",
code="invalid",
)
raise forms.ValidationError(code="invalid")
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):

View file

@ -1,6 +1,5 @@
{% extends 'application_form.html' %}
{% load static %}
{% load field_helpers %}
{% load static field_helpers %}
{% block form_instructions %}
<p>Before requesting a .gov domain, <a href="{% url 'todo' %}">please make sure it
@ -29,7 +28,6 @@
{% block form_fields %}
{% csrf_token %}
{{ forms.0.management_form }}
@ -44,12 +42,16 @@
complete and submit the rest of this form.</p>
{% 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" %}
{% input_with_errors forms.0.requested_domain %}
{% 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>
{{ forms.1.management_form }}
@ -63,7 +65,9 @@
you your first choice? Entering alternative domains is optional.</p>
{% 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 %}
{% input_with_errors form.alternative_domain %}
{% endfor %}

View file

@ -47,6 +47,7 @@
{% endblock %}
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
{% csrf_token %}
{% block form_fields %}{% endblock %}