mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-06 01:35:22 +02:00
Merge pull request #575 from cisagov/nmb/nameservers
Form to change nameservers for approved domains
This commit is contained in:
commit
b319f8c429
14 changed files with 266 additions and 5 deletions
|
@ -230,4 +230,34 @@ function handleValidationClick(e) {
|
|||
})();
|
||||
|
||||
|
||||
/**
|
||||
* An IIFE that attaches a click handler for our dynamic nameservers form
|
||||
*
|
||||
* Only does something on a single page, but it should be fast enough to run
|
||||
* it everywhere.
|
||||
*/
|
||||
(function prepareForms() {
|
||||
let serverForm = document.querySelectorAll(".server-form")
|
||||
let container = document.querySelector("#form-container")
|
||||
let addButton = document.querySelector("#add-form")
|
||||
let totalForms = document.querySelector("#id_form-TOTAL_FORMS")
|
||||
|
||||
let formNum = serverForm.length-1
|
||||
addButton.addEventListener('click', addForm)
|
||||
|
||||
function addForm(e){
|
||||
let newForm = serverForm[2].cloneNode(true)
|
||||
let formNumberRegex = RegExp(`form-(\\d){1}-`,'g')
|
||||
let formLabelRegex = RegExp(`Name server (\\d){1}`, 'g')
|
||||
let formExampleRegex = RegExp(`ns(\\d){1}`, 'g')
|
||||
|
||||
formNum++
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum}-`)
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `Name server ${formNum+1}`)
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum+1}`)
|
||||
container.insertBefore(newForm, addButton)
|
||||
newForm.querySelector("input").value = ""
|
||||
|
||||
totalForms.setAttribute('value', `${formNum+1}`)
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -78,6 +78,11 @@ urlpatterns = [
|
|||
),
|
||||
path("domain/<int:pk>", views.DomainView.as_view(), name="domain"),
|
||||
path("domain/<int:pk>/users", views.DomainUsersView.as_view(), name="domain-users"),
|
||||
path(
|
||||
"domain/<int:pk>/nameservers",
|
||||
views.DomainNameserversView.as_view(),
|
||||
name="domain-nameservers",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/users/add",
|
||||
views.DomainAddUserView.as_view(),
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
from .application_wizard import *
|
||||
from .domain import DomainAddUserForm
|
||||
from .domain import DomainAddUserForm, NameserverFormset
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""Forms for domain management."""
|
||||
|
||||
from django import forms
|
||||
from django.forms import formset_factory
|
||||
|
||||
|
||||
class DomainAddUserForm(forms.Form):
|
||||
|
@ -8,3 +9,16 @@ class DomainAddUserForm(forms.Form):
|
|||
"""Form for adding a user to a domain."""
|
||||
|
||||
email = forms.EmailField(label="Email")
|
||||
|
||||
|
||||
class DomainNameserverForm(forms.Form):
|
||||
|
||||
"""Form for changing nameservers."""
|
||||
|
||||
server = forms.CharField(label="Name server")
|
||||
|
||||
|
||||
NameserverFormset = formset_factory(
|
||||
DomainNameserverForm,
|
||||
extra=1,
|
||||
)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import logging
|
||||
import re
|
||||
|
||||
from typing import List
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
|
@ -215,6 +217,24 @@ class Domain(TimeStampedModel):
|
|||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def nameservers(self) -> List[str]:
|
||||
"""A list of the nameservers for this domain.
|
||||
|
||||
TODO: call EPP to get this info instead of returning fake data.
|
||||
"""
|
||||
return [
|
||||
# reserved example domain
|
||||
"ns1.example.com",
|
||||
"ns2.example.com",
|
||||
"ns3.example.com",
|
||||
]
|
||||
|
||||
def set_nameservers(self, new_nameservers: List[str]):
|
||||
"""Set the nameservers for this domain."""
|
||||
# TODO: call EPP to set these values in the registry instead of doing
|
||||
# nothing.
|
||||
logger.warn("TODO: Fake setting nameservers to %s", new_nameservers)
|
||||
|
||||
@property
|
||||
def roid(self):
|
||||
return self._get_property("roid")
|
||||
|
|
|
@ -4,5 +4,6 @@
|
|||
{# hint: spacing in the class string matters #}
|
||||
class="{{ uswds_input_class }}{% if classes %} {{ classes }}{% endif %}"
|
||||
{% if widget.value != None %}value="{{ widget.value|stringformat:'s' }}"{% endif %}
|
||||
{% if sublabel_text %}aria-describedby="{{ widget.attrs.id }}__sublabel"{% endif %}
|
||||
{% include "django/forms/widgets/attrs.html" %}
|
||||
/>
|
||||
/>
|
||||
|
|
52
src/registrar/templates/domain_nameservers.html
Normal file
52
src/registrar/templates/domain_nameservers.html
Normal file
|
@ -0,0 +1,52 @@
|
|||
{% extends "domain_base.html" %}
|
||||
{% load static field_helpers%}
|
||||
|
||||
{% block title %}Domain name servers | {{ domain.name }} | {% endblock %}
|
||||
|
||||
{% block domain_content %}
|
||||
{# this is right after the messages block in the parent template #}
|
||||
{% for form in formset %}
|
||||
{% include "includes/form_errors.html" with form=form %}
|
||||
{% endfor %}
|
||||
|
||||
<h1>Domain name servers</h1>
|
||||
|
||||
<p>Before your domain can be used we'll need information about your domain
|
||||
name servers.</p>
|
||||
|
||||
<p><a class="usa-link" href="{% url "todo" %}">Get help with domain servers.</a></p>
|
||||
|
||||
{% include "includes/required_fields.html" %}
|
||||
|
||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
||||
{% csrf_token %}
|
||||
{{ formset.management_form }}
|
||||
|
||||
{% for form in formset %}
|
||||
<div class="server-form">
|
||||
{% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" %}
|
||||
{% if forloop.counter <= 2 %}
|
||||
{% with attr_required=True %}
|
||||
{% input_with_errors form.server %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% input_with_errors form.server %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<button type="button" class="usa-button usa-button--unstyled display-block" id="add-form">
|
||||
<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 name server</span>
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
>Save</button>
|
||||
</form>
|
||||
|
||||
{% endblock %} {# domain_content #}
|
|
@ -13,7 +13,7 @@
|
|||
</li>
|
||||
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'todo' as url %}
|
||||
{% url 'domain-nameservers' pk=domain.id as url %}
|
||||
<a href="{{ url }}"
|
||||
{% if request.path == url %}class="usa-current"{% endif %}
|
||||
>
|
||||
|
|
|
@ -28,6 +28,10 @@ error messages, if necessary.
|
|||
{% include "django/forms/label.html" %}
|
||||
{% endif %}
|
||||
|
||||
{% if sublabel_text %}
|
||||
<p id="{{ widget.attrs.id }}__sublabel" class="text-base margin-top-2px margin-bottom-1">{{ sublabel_text }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if field.errors %}
|
||||
<div id="{{ widget.attrs.id }}__error-message">
|
||||
{% for error in field.errors %}
|
||||
|
@ -71,4 +75,4 @@ error messages, if necessary.
|
|||
</span>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
@ -6,3 +6,9 @@ from django.template.defaulttags import register
|
|||
@register.filter
|
||||
def get_item(dictionary, key):
|
||||
return dictionary.get(key)
|
||||
|
||||
|
||||
@register.filter
|
||||
def concat(arg1, arg2):
|
||||
"""concatenate arg1 & arg2"""
|
||||
return str(arg1) + str(arg2)
|
||||
|
|
|
@ -1058,6 +1058,11 @@ class TestDomainPermissions(TestWithDomainPermissions):
|
|||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_no_domain_role(self):
|
||||
"""Logged in but no role gets 403 Forbidden."""
|
||||
self.client.force_login(self.user)
|
||||
|
@ -1079,6 +1084,12 @@ class TestDomainPermissions(TestWithDomainPermissions):
|
|||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
with less_console_noise():
|
||||
response = self.client.get(
|
||||
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
class TestDomainDetail(TestWithDomainPermissions, WebTest):
|
||||
def setUp(self):
|
||||
|
@ -1222,6 +1233,55 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
|
|||
home_page = self.app.get(reverse("home"))
|
||||
self.assertContains(home_page, self.domain.name)
|
||||
|
||||
def test_domain_nameservers(self):
|
||||
"""Can load domain's nameservers page."""
|
||||
page = self.client.get(
|
||||
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertContains(page, "Domain name servers")
|
||||
|
||||
def test_domain_nameservers_form(self):
|
||||
"""Can change domain's nameservers.
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
nameservers_page = self.app.get(
|
||||
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
with less_console_noise(): # swallow log warning message
|
||||
result = nameservers_page.form.submit()
|
||||
# form submission was a post, response should be a redirect
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(
|
||||
result["Location"],
|
||||
reverse("domain-nameservers", kwargs={"pk": self.domain.id}),
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
page = result.follow()
|
||||
self.assertContains(page, "The name servers for this domain have been updated")
|
||||
|
||||
def test_domain_nameservers_form_invalid(self):
|
||||
"""Can change domain's nameservers.
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
nameservers_page = self.app.get(
|
||||
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# first two nameservers are required, so if we empty one out we should
|
||||
# get a form error
|
||||
nameservers_page.form["form-0-server"] = ""
|
||||
with less_console_noise(): # swallow logged warning message
|
||||
result = nameservers_page.form.submit()
|
||||
# form submission was a post with an error, response should be a 200
|
||||
# error text appears twice, once at the top of the page, once around
|
||||
# the field.
|
||||
self.assertContains(result, "This field is required", count=2, status_code=200)
|
||||
|
||||
|
||||
class TestApplicationStatus(TestWithUser, WebTest):
|
||||
def setUp(self):
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from .application import *
|
||||
from .domain import (
|
||||
DomainView,
|
||||
DomainNameserversView,
|
||||
DomainUsersView,
|
||||
DomainAddUserView,
|
||||
DomainInvitationDeleteView,
|
||||
|
|
|
@ -12,7 +12,7 @@ from django.views.generic.edit import DeleteView, FormMixin
|
|||
|
||||
from registrar.models import Domain, DomainInvitation, User, UserDomainRole
|
||||
|
||||
from ..forms import DomainAddUserForm
|
||||
from ..forms import DomainAddUserForm, NameserverFormset
|
||||
from ..utility.email import send_templated_email, EmailSendingError
|
||||
from .utility import DomainPermission
|
||||
|
||||
|
@ -29,6 +29,73 @@ class DomainView(DomainPermission, DetailView):
|
|||
context_object_name = "domain"
|
||||
|
||||
|
||||
class DomainNameserversView(DomainPermission, FormMixin, DetailView):
|
||||
|
||||
"""Domain nameserver editing view."""
|
||||
|
||||
model = Domain
|
||||
template_name = "domain_nameservers.html"
|
||||
context_object_name = "domain"
|
||||
form_class = NameserverFormset
|
||||
|
||||
def get_initial(self):
|
||||
"""The initial value for the form (which is a formset here)."""
|
||||
domain = self.get_object()
|
||||
return [{"server": server} for server in domain.nameservers()]
|
||||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the overview page for the domain."""
|
||||
return reverse("domain-nameservers", kwargs={"pk": self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Adjust context from FormMixin for formsets."""
|
||||
context = super().get_context_data(**kwargs)
|
||||
# use "formset" instead of "form" for the key
|
||||
context["formset"] = context.pop("form")
|
||||
return context
|
||||
|
||||
def get_form(self, **kwargs):
|
||||
"""Override the labels and required fields every time we get a formset."""
|
||||
formset = super().get_form(**kwargs)
|
||||
for i, form in enumerate(formset):
|
||||
form.fields["server"].label += f" {i+1}"
|
||||
if i < 2:
|
||||
form.fields["server"].required = True
|
||||
else:
|
||||
form.fields["server"].required = False
|
||||
return formset
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Formset submission posts to this view."""
|
||||
self.object = self.get_object()
|
||||
formset = self.get_form()
|
||||
|
||||
if formset.is_valid():
|
||||
return self.form_valid(formset)
|
||||
else:
|
||||
return self.form_invalid(formset)
|
||||
|
||||
def form_valid(self, formset):
|
||||
"""The formset is valid, perform something with it."""
|
||||
|
||||
# Set the nameservers from the formset
|
||||
nameservers = []
|
||||
for form in formset:
|
||||
try:
|
||||
nameservers.append(form.cleaned_data["server"])
|
||||
except KeyError:
|
||||
# no server information in this field, skip it
|
||||
pass
|
||||
domain = self.get_object()
|
||||
domain.set_nameservers(nameservers)
|
||||
|
||||
messages.success(
|
||||
self.request, "The name servers for this domain have been updated"
|
||||
)
|
||||
# superclass has the redirect
|
||||
return super().form_valid(formset)
|
||||
|
||||
|
||||
class DomainUsersView(DomainPermission, DetailView):
|
||||
|
||||
"""User management page in the domain details."""
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
10038 OUTOFSCOPE http://app:8080/(robots.txt|sitemap.xml|TODO|edit/)
|
||||
10038 OUTOFSCOPE http://app:8080/users
|
||||
10038 OUTOFSCOPE http://app:8080/users/add
|
||||
10038 OUTOFSCOPE http://app:8080/nameservers
|
||||
10038 OUTOFSCOPE http://app:8080/delete
|
||||
10038 OUTOFSCOPE http://app:8080/withdraw
|
||||
10038 OUTOFSCOPE http://app:8080/withdrawconfirmed
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue