From 456cab6ceedba93dff55a72d8a099b30a268415e Mon Sep 17 00:00:00 2001
From: Seamus Johnston
Date: Fri, 3 Feb 2023 14:22:01 -0600
Subject: [PATCH] Enable domain availability checker
---
src/api/views.py | 31 ++++-
src/registrar/assets/js/get-gov.js | 119 +++++++++++++++---
src/registrar/forms/application_wizard.py | 58 +++------
.../templates/application_dotgov_domain.html | 16 ++-
src/registrar/templates/application_form.html | 1 +
5 files changed, 157 insertions(+), 68 deletions(-)
diff --git a/src/api/views.py b/src/api/views.py
index dcda09c69..deaacf0c6 100644
--- a/src/api/views.py
+++ b/src/api/views.py
@@ -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. Don’t 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 isn’t 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"]
+ })
diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js
index 44d0cab57..c9f6d77af 100644
--- a/src/registrar/assets/js/get-gov.js
+++ b/src/registrar/assets/js/get-gov.js
@@ -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=" "` and adds
- * change handlers for each known type.
+ * It looks for elements with `validate=" "` and adds change handlers.
+ *
+ * These handlers know about two other attributes:
+ * - `validate-for=""` creates a button which will run the validator(s) on
+ * - `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);
+ }
})();
diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py
index cec6a777f..b1033a52d 100644
--- a/src/registrar/forms/application_wizard.py
+++ b/src/registrar/forms/application_wizard.py
@@ -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. Don’t 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):
diff --git a/src/registrar/templates/application_dotgov_domain.html b/src/registrar/templates/application_dotgov_domain.html
index 02bc9522b..8d7a2718d 100644
--- a/src/registrar/templates/application_dotgov_domain.html
+++ b/src/registrar/templates/application_dotgov_domain.html
@@ -1,6 +1,5 @@
{% extends 'application_form.html' %}
-{% load static %}
-{% load field_helpers %}
+{% load static field_helpers %}
{% block form_instructions %}
Before requesting a .gov domain, 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.
{% 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 %}
-
-
+
{{ forms.1.management_form }}
@@ -63,7 +65,9 @@
you your first choice? Entering alternative domains is optional.
{% 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 %}
diff --git a/src/registrar/templates/application_form.html b/src/registrar/templates/application_form.html
index c52a5904e..9b1240013 100644
--- a/src/registrar/templates/application_form.html
+++ b/src/registrar/templates/application_form.html
@@ -47,6 +47,7 @@
{% endblock %}