mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-29 17:00:02 +02:00
Write a new Domain interface
This commit is contained in:
parent
dd58f46231
commit
d51a8600d4
6 changed files with 255 additions and 221 deletions
|
@ -28,8 +28,7 @@ information to make connections between registry users and the domains that
|
||||||
they manage. The registrar stores very few fields about a domain except for
|
they manage. The registrar stores very few fields about a domain except for
|
||||||
its name, so it could be straightforward to import the exported list of domains
|
its name, so it could be straightforward to import the exported list of domains
|
||||||
from Verisign's `escrow_domains.daily.dotgov.GOV.txt`. It doesn't appear that
|
from Verisign's `escrow_domains.daily.dotgov.GOV.txt`. It doesn't appear that
|
||||||
that table stores a flag for active or inactive, so every domain in the file
|
that table stores a flag for active or inactive.
|
||||||
can be imported into our system as `is_active=True`.
|
|
||||||
|
|
||||||
An example Django management command that can load the delimited text file
|
An example Django management command that can load the delimited text file
|
||||||
from the daily escrow is in
|
from the daily escrow is in
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Generated by Django 4.2.1 on 2023-05-26 19:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import registrar.models.utility.domain_field
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0022_draftdomain_domainapplication_approved_domain_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name="domain",
|
||||||
|
name="unique_domain_name_in_registry",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="domain",
|
||||||
|
name="is_active",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="domain",
|
||||||
|
name="state",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("created", "Created"),
|
||||||
|
("deleted", "Deleted"),
|
||||||
|
("unknown", "Unknown"),
|
||||||
|
],
|
||||||
|
default="unknown",
|
||||||
|
help_text="Very basic info about the lifecycle of this domain object",
|
||||||
|
max_length=21,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domain",
|
||||||
|
name="name",
|
||||||
|
field=registrar.models.utility.domain_field.DomainField(
|
||||||
|
default=None,
|
||||||
|
help_text="Fully qualified domain name",
|
||||||
|
max_length=253,
|
||||||
|
unique=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,42 +1,44 @@
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
|
|
||||||
from typing import List
|
from datetime import date
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django_fsm import FSMField, transition # type: ignore
|
|
||||||
|
|
||||||
from api.views import in_domains
|
from epplibwrapper import (
|
||||||
from registrar.utility import errors
|
CLIENT as registry,
|
||||||
|
commands,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .utility.domain_field import DomainField
|
||||||
|
from .utility.domain_helper import DomainHelper
|
||||||
from .utility.time_stamped_model import TimeStampedModel
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
|
||||||
|
from .public_contact import PublicContact
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Domain(TimeStampedModel):
|
class Domain(TimeStampedModel, DomainHelper):
|
||||||
"""
|
"""
|
||||||
Manage the lifecycle of domain names.
|
Manage the lifecycle of domain names.
|
||||||
|
|
||||||
The registry is the source of truth for this data and this model exists:
|
The registry is the source of truth for this data and this model exists:
|
||||||
1. To tie ownership information in the registrar to
|
1. To tie ownership information in the registrar to
|
||||||
DNS entries in the registry; and
|
DNS entries in the registry
|
||||||
2. To allow a new registrant to draft DNS entries before their
|
|
||||||
application is approved
|
|
||||||
"""
|
|
||||||
|
|
||||||
class Meta:
|
~~~ HOW TO USE THIS CLASS ~~~
|
||||||
constraints = [
|
|
||||||
# draft domains may share the same name, but
|
A) You can create a Domain object with just a name. `Domain(name="something.gov")`.
|
||||||
# once approved, they must be globally unique
|
B) Saving the Domain object will not contact the registry, as it may be useful
|
||||||
models.UniqueConstraint(
|
to have Domain objects in an `UNKNOWN` pre-created state.
|
||||||
fields=["name"],
|
C) Domain properties are lazily loaded. Accessing `my_domain.expiration_date` will
|
||||||
condition=models.Q(is_active=True),
|
contact the registry, if a cached copy does not exist.
|
||||||
name="unique_domain_name_in_registry",
|
D) Domain creation is lazy. If `my_domain.expiration_date` finds that `my_domain`
|
||||||
),
|
does not exist in the registry, it will ask the registry to create it.
|
||||||
]
|
F) Created is _not_ the same as active aka live on the internet.
|
||||||
|
G) Activation is controlled by the registry. It will happen automatically when the
|
||||||
|
domain meets the required checks.
|
||||||
|
"""
|
||||||
|
|
||||||
class Status(models.TextChoices):
|
class Status(models.TextChoices):
|
||||||
"""
|
"""
|
||||||
|
@ -91,221 +93,193 @@ class Domain(TimeStampedModel):
|
||||||
PENDING_TRANSFER = "pendingTransfer"
|
PENDING_TRANSFER = "pendingTransfer"
|
||||||
PENDING_UPDATE = "pendingUpdate"
|
PENDING_UPDATE = "pendingUpdate"
|
||||||
|
|
||||||
# a domain name is alphanumeric or hyphen, up to 63 characters, doesn't
|
class State(models.TextChoices):
|
||||||
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
|
"""These capture (some of) the states a domain object can be in."""
|
||||||
DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,6}$")
|
|
||||||
|
|
||||||
@classmethod
|
# the normal state of a domain object -- may or may not be active!
|
||||||
def string_could_be_domain(cls, domain: str | None) -> bool:
|
CREATED = "created"
|
||||||
"""Return True if the string could be a domain name, otherwise False."""
|
|
||||||
if not isinstance(domain, str):
|
|
||||||
return False
|
|
||||||
return bool(cls.DOMAIN_REGEX.match(domain))
|
|
||||||
|
|
||||||
@classmethod
|
# previously existed but has been deleted from the registry
|
||||||
def validate(cls, domain: str | None, blank_ok=False) -> str:
|
DELETED = "deleted"
|
||||||
"""Attempt to determine if a domain name could be requested."""
|
|
||||||
if domain is None:
|
# the state is indeterminate
|
||||||
raise errors.BlankValueError()
|
UNKNOWN = "unknown"
|
||||||
if not isinstance(domain, str):
|
|
||||||
raise ValueError("Domain name must be a string")
|
|
||||||
domain = domain.lower().strip()
|
|
||||||
if domain == "":
|
|
||||||
if blank_ok:
|
|
||||||
return domain
|
|
||||||
else:
|
|
||||||
raise errors.BlankValueError()
|
|
||||||
if domain.endswith(".gov"):
|
|
||||||
domain = domain[:-4]
|
|
||||||
if "." in domain:
|
|
||||||
raise errors.ExtraDotsError()
|
|
||||||
if not Domain.string_could_be_domain(domain + ".gov"):
|
|
||||||
raise ValueError()
|
|
||||||
if in_domains(domain):
|
|
||||||
raise errors.DomainUnavailableError()
|
|
||||||
return domain
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def available(cls, domain: str) -> bool:
|
def available(cls, domain: str) -> bool:
|
||||||
"""Check if a domain is available.
|
"""Check if a domain is available."""
|
||||||
|
if not cls.string_could_be_domain(domain):
|
||||||
|
raise ValueError("Not a valid domain: %s" % str(domain))
|
||||||
|
req = commands.CheckDomain([domain])
|
||||||
|
return registry.send(req).res_data[0].avail
|
||||||
|
|
||||||
Not implemented. Returns a dummy value for testing."""
|
@classmethod
|
||||||
return False # domain_check(domain)
|
def registered(cls, domain: str) -> bool:
|
||||||
|
"""Check if a domain is _not_ available."""
|
||||||
|
return not cls.available(domain)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def contacts(self) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
Get a dictionary of registry IDs for the contacts for this domain.
|
||||||
|
|
||||||
|
IDs are provided as strings, e.g.
|
||||||
|
|
||||||
|
{"registrant": "jd1234", "admin": "sh8013",...}
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def creation_date(self) -> date:
|
||||||
|
"""Get the `cr_date` element from the registry."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_transferred_date(self) -> date:
|
||||||
|
"""Get the `tr_date` element from the registry."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_updated_date(self) -> date:
|
||||||
|
"""Get the `up_date` element from the registry."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expiration_date(self) -> date:
|
||||||
|
"""Get or set the `ex_date` element from the registry."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@expiration_date.setter # type: ignore
|
||||||
|
def expiration_date(self, ex_date: date):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def password(self) -> str:
|
||||||
|
"""Get the `auth_info.pw` element from the registry. Not a real password."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nameservers(self) -> list[tuple[str]]:
|
||||||
|
"""
|
||||||
|
Get or set a complete list of nameservers for this domain.
|
||||||
|
|
||||||
|
Hosts are provided as a list of tuples, e.g.
|
||||||
|
|
||||||
|
[("ns1.example.com",), ("ns1.example.gov", "0.0.0.0")]
|
||||||
|
|
||||||
|
Subordinate hosts (something.your-domain.gov) MUST have IP addresses,
|
||||||
|
while non-subordinate hosts MUST NOT.
|
||||||
|
"""
|
||||||
|
# TODO: call EPP to get this info instead of returning fake data.
|
||||||
|
return [
|
||||||
|
("ns1.example.com",),
|
||||||
|
("ns2.example.com",),
|
||||||
|
("ns3.example.com",),
|
||||||
|
]
|
||||||
|
|
||||||
|
@nameservers.setter # type: ignore
|
||||||
|
def nameservers(self, hosts: list[tuple[str]]):
|
||||||
|
# TODO: call EPP to set this info.
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def statuses(self) -> list[str]:
|
||||||
|
"""
|
||||||
|
Get or set the domain `status` elements from the registry.
|
||||||
|
|
||||||
|
A domain's status indicates various properties. See Domain.Status.
|
||||||
|
"""
|
||||||
|
# implementation note: the Status object from EPP stores the string in
|
||||||
|
# a dataclass property `state`, not to be confused with the `state` field here
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@statuses.setter # type: ignore
|
||||||
|
def statuses(self, statuses: list[str]):
|
||||||
|
# TODO: there are a long list of rules in the RFC about which statuses
|
||||||
|
# can be combined; check that here and raise errors for invalid combinations -
|
||||||
|
# some statuses cannot be set by the client at all
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def registrant_contact(self) -> PublicContact:
|
||||||
|
"""Get or set the registrant for this domain."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@registrant_contact.setter # type: ignore
|
||||||
|
def registrant_contact(self, contact: PublicContact):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def administrative_contact(self) -> PublicContact:
|
||||||
|
"""Get or set the admin contact for this domain."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@administrative_contact.setter # type: ignore
|
||||||
|
def administrative_contact(self, contact: PublicContact):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def security_contact(self) -> PublicContact:
|
||||||
|
"""Get or set the security contact for this domain."""
|
||||||
|
# TODO: replace this with a real implementation
|
||||||
|
contact = PublicContact.get_default_security()
|
||||||
|
contact.domain = self
|
||||||
|
contact.email = "mayor@igorville.gov"
|
||||||
|
return contact
|
||||||
|
|
||||||
|
@security_contact.setter # type: ignore
|
||||||
|
def security_contact(self, contact: PublicContact):
|
||||||
|
# TODO: replace this with a real implementation
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def technical_contact(self) -> PublicContact:
|
||||||
|
"""Get or set the tech contact for this domain."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@technical_contact.setter # type: ignore
|
||||||
|
def technical_contact(self, contact: PublicContact):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def is_active(self) -> bool:
|
||||||
|
"""Is the domain live on the inter webs?"""
|
||||||
|
# TODO: implement a check -- should be performant so it can be called for
|
||||||
|
# any number of domains on a status page
|
||||||
|
# this is NOT as simple as checking if Domain.Status.OK is in self.statuses
|
||||||
|
return False
|
||||||
|
|
||||||
def transfer(self):
|
def transfer(self):
|
||||||
"""Going somewhere. Not implemented."""
|
"""Going somewhere. Not implemented."""
|
||||||
pass
|
raise NotImplementedError()
|
||||||
|
|
||||||
def renew(self):
|
def renew(self):
|
||||||
"""Time to renew. Not implemented."""
|
"""Time to renew. Not implemented."""
|
||||||
pass
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _get_property(self, property):
|
def place_client_hold(self):
|
||||||
"""Get some info about a domain."""
|
"""This domain should not be active."""
|
||||||
if not self.is_active:
|
raise NotImplementedError()
|
||||||
return None
|
|
||||||
if not hasattr(self, "info"):
|
|
||||||
try:
|
|
||||||
# get info from registry
|
|
||||||
self.info = {} # domain_info(self.name)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
# TODO: back off error handling
|
|
||||||
return None
|
|
||||||
if hasattr(self, "info"):
|
|
||||||
if property in self.info:
|
|
||||||
return self.info[property]
|
|
||||||
else:
|
|
||||||
raise KeyError(
|
|
||||||
"Requested key %s was not found in registry data." % str(property)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# TODO: return an error if registry cannot be contacted
|
|
||||||
return None
|
|
||||||
|
|
||||||
@transition(field="is_active", source="*", target=True)
|
def remove_client_hold(self):
|
||||||
def activate(self):
|
"""This domain is okay to be active."""
|
||||||
"""This domain should be made live."""
|
raise NotImplementedError()
|
||||||
DomainApplication = apps.get_model("registrar.DomainApplication")
|
|
||||||
if hasattr(self, "domain_application"):
|
|
||||||
if self.domain_application.status != DomainApplication.APPROVED:
|
|
||||||
raise ValueError("Cannot activate. Application must be approved.")
|
|
||||||
if Domain.objects.filter(name=self.name, is_active=True).exists():
|
|
||||||
raise ValueError("Cannot activate. Domain name is already in use.")
|
|
||||||
# TODO: depending on the details of our registry integration
|
|
||||||
# we will either contact the registry and deploy the domain
|
|
||||||
# in this function OR we will verify that it has already been
|
|
||||||
# activated and reject this state transition if it has not
|
|
||||||
pass
|
|
||||||
|
|
||||||
@transition(field="is_active", source="*", target=False)
|
|
||||||
def deactivate(self):
|
|
||||||
"""This domain should not be live."""
|
|
||||||
# there are security concerns to having this function exist
|
|
||||||
# within the codebase; discuss these with the project lead
|
|
||||||
# if there is a feature request to implement this
|
|
||||||
raise Exception("Cannot revoke, contact registry.")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sld(self):
|
|
||||||
"""Get or set the second level domain string."""
|
|
||||||
return self.name.split(".")[0]
|
|
||||||
|
|
||||||
@sld.setter
|
|
||||||
def sld(self, value: str):
|
|
||||||
parts = self.name.split(".")
|
|
||||||
tld = parts[1] if len(parts) > 1 else ""
|
|
||||||
if Domain.string_could_be_domain(f"{value}.{tld}"):
|
|
||||||
self.name = f"{value}.{tld}"
|
|
||||||
else:
|
|
||||||
raise ValidationError("%s is not a valid second level domain" % value)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def tld(self):
|
|
||||||
"""Get or set the top level domain string."""
|
|
||||||
parts = self.name.split(".")
|
|
||||||
return parts[1] if len(parts) > 1 else ""
|
|
||||||
|
|
||||||
@tld.setter
|
|
||||||
def tld(self, value: str):
|
|
||||||
sld = self.name.split(".")[0]
|
|
||||||
if Domain.string_could_be_domain(f"{sld}.{value}"):
|
|
||||||
self.name = f"{sld}.{value}"
|
|
||||||
else:
|
|
||||||
raise ValidationError("%s is not a valid top level domain" % value)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def nameservers(self) -> List[str]:
|
name = DomainField(
|
||||||
"""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)
|
|
||||||
|
|
||||||
def security_email(self) -> str:
|
|
||||||
"""Get the security email for this domain.
|
|
||||||
|
|
||||||
TODO: call EPP to get this info instead of returning fake data.
|
|
||||||
"""
|
|
||||||
return "mayor@igorville.gov"
|
|
||||||
|
|
||||||
def set_security_email(self, new_security_email: str):
|
|
||||||
"""Set the security email for this domain."""
|
|
||||||
# TODO: call EPP to set these values in the registry instead of doing
|
|
||||||
# nothing.
|
|
||||||
logger.warn("TODO: Fake setting security email to %s", new_security_email)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def roid(self):
|
|
||||||
return self._get_property("roid")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status(self):
|
|
||||||
return self._get_property("status")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def registrant(self):
|
|
||||||
return self._get_property("registrant")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sponsor(self):
|
|
||||||
return self._get_property("sponsor")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def creator(self):
|
|
||||||
return self._get_property("creator")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def creation_date(self):
|
|
||||||
return self._get_property("creation_date")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def updator(self):
|
|
||||||
return self._get_property("updator")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def last_update_date(self):
|
|
||||||
return self._get_property("last_update_date")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def expiration_date(self):
|
|
||||||
return self._get_property("expiration_date")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def last_transfer_date(self):
|
|
||||||
return self._get_property("last_transfer_date")
|
|
||||||
|
|
||||||
name = models.CharField(
|
|
||||||
max_length=253,
|
max_length=253,
|
||||||
blank=False,
|
blank=False,
|
||||||
default=None, # prevent saving without a value
|
default=None, # prevent saving without a value
|
||||||
|
unique=True,
|
||||||
help_text="Fully qualified domain name",
|
help_text="Fully qualified domain name",
|
||||||
)
|
)
|
||||||
|
|
||||||
# we use `is_active` rather than `domain_application.status`
|
state = models.CharField(
|
||||||
# because domains may exist without associated applications
|
max_length=21,
|
||||||
is_active = FSMField(
|
choices=State.choices,
|
||||||
choices=[
|
default=State.UNKNOWN,
|
||||||
(True, "Yes"),
|
help_text="Very basic info about the lifecycle of this domain object",
|
||||||
(False, "No"),
|
|
||||||
],
|
|
||||||
default=False,
|
|
||||||
# TODO: how to edit models in Django admin if protected = True
|
|
||||||
protected=False,
|
|
||||||
help_text="Domain is live in the registry",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# ForeignKey on UserDomainRole creates a "permissions" member for
|
# ForeignKey on UserDomainRole creates a "permissions" member for
|
||||||
|
|
|
@ -32,7 +32,7 @@ class PublicContact(TimeStampedModel):
|
||||||
if hasattr(self, "domain"):
|
if hasattr(self, "domain"):
|
||||||
match self.contact_type:
|
match self.contact_type:
|
||||||
case PublicContact.ContactTypeChoices.REGISTRANT:
|
case PublicContact.ContactTypeChoices.REGISTRANT:
|
||||||
self.domain.registrant = self
|
self.domain.registrant_contact = self
|
||||||
case PublicContact.ContactTypeChoices.ADMINISTRATIVE:
|
case PublicContact.ContactTypeChoices.ADMINISTRATIVE:
|
||||||
self.domain.administrative_contact = self
|
self.domain.administrative_contact = self
|
||||||
case PublicContact.ContactTypeChoices.TECHNICAL:
|
case PublicContact.ContactTypeChoices.TECHNICAL:
|
||||||
|
|
13
src/registrar/models/utility/domain_field.py
Normal file
13
src/registrar/models/utility/domain_field.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class DomainField(models.CharField):
|
||||||
|
"""Subclass of CharField to enforce domain name specific requirements."""
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
"""Convert to lowercase during deserialization and during form `clean`."""
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value.lower()
|
||||||
|
return str(value).lower()
|
|
@ -41,7 +41,7 @@ class DomainNameserversView(DomainPermission, FormMixin, DetailView):
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
"""The initial value for the form (which is a formset here)."""
|
"""The initial value for the form (which is a formset here)."""
|
||||||
domain = self.get_object()
|
domain = self.get_object()
|
||||||
return [{"server": server} for server in domain.nameservers()]
|
return [{"server": name} for name, *ip in domain.nameservers]
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
"""Redirect to the overview page for the domain."""
|
"""Redirect to the overview page for the domain."""
|
||||||
|
@ -82,12 +82,13 @@ class DomainNameserversView(DomainPermission, FormMixin, DetailView):
|
||||||
nameservers = []
|
nameservers = []
|
||||||
for form in formset:
|
for form in formset:
|
||||||
try:
|
try:
|
||||||
nameservers.append(form.cleaned_data["server"])
|
as_tuple = (form.cleaned_data["server"],)
|
||||||
|
nameservers.append(as_tuple)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# no server information in this field, skip it
|
# no server information in this field, skip it
|
||||||
pass
|
pass
|
||||||
domain = self.get_object()
|
domain = self.get_object()
|
||||||
domain.set_nameservers(nameservers)
|
domain.nameservers = nameservers
|
||||||
|
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request, "The name servers for this domain have been updated."
|
self.request, "The name servers for this domain have been updated."
|
||||||
|
@ -109,7 +110,7 @@ class DomainSecurityEmailView(DomainPermission, FormMixin, DetailView):
|
||||||
"""The initial value for the form."""
|
"""The initial value for the form."""
|
||||||
domain = self.get_object()
|
domain = self.get_object()
|
||||||
initial = super().get_initial()
|
initial = super().get_initial()
|
||||||
initial["security_email"] = domain.security_email()
|
initial["security_email"] = domain.security_contact.email
|
||||||
return initial
|
return initial
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
|
@ -132,7 +133,9 @@ class DomainSecurityEmailView(DomainPermission, FormMixin, DetailView):
|
||||||
# Set the security email from the form
|
# Set the security email from the form
|
||||||
new_email = form.cleaned_data.get("security_email", "")
|
new_email = form.cleaned_data.get("security_email", "")
|
||||||
domain = self.get_object()
|
domain = self.get_object()
|
||||||
domain.set_security_email(new_email)
|
contact = domain.security_contact
|
||||||
|
contact.email = new_email
|
||||||
|
contact.save()
|
||||||
|
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request, "The security email for this domain have been updated."
|
self.request, "The security email for this domain have been updated."
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue