mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-16 01:27:03 +02:00
Enable domain availability checker
This commit is contained in:
parent
c2feb1fc16
commit
456cab6cee
5 changed files with 157 additions and 68 deletions
|
@ -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"]
|
||||
})
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue