Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions netbox_dns/api/serializers_/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class Meta:
"tenant",
"ipam_ip_address",
"absolute_value",
"expiration_date",
"expired",
)

brief_fields = (
Expand All @@ -57,6 +59,7 @@ class Meta:
"description",
"managed",
"active",
"expired",
)

url = serializers.HyperlinkedIdentityField(
Expand Down Expand Up @@ -102,3 +105,7 @@ class Meta:
required=False,
allow_null=True,
)
expired = serializers.SerializerMethodField()

def get_expired(self, instance):
return instance.is_expired
11 changes: 11 additions & 0 deletions netbox_dns/filtersets/record.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import django_filters
import netaddr
from django.db.models import Q
from django.utils.timezone import datetime

from ipam.models import IPAddress
from netbox.filtersets import PrimaryModelFilterSet
Expand Down Expand Up @@ -73,6 +74,10 @@ class Meta:
method="filter_ip_address",
)
active = django_filters.BooleanFilter()
expiration_date = django_filters.DateFromToRangeFilter()
expired = django_filters.BooleanFilter(
method="filter_expired",
)

def filter_ip_address(self, queryset, name, value):
if not value:
Expand All @@ -88,6 +93,12 @@ def filter_ip_address(self, queryset, name, value):
except (netaddr.AddrFormatError, ValueError):
return queryset.none()

def filter_expired(self, queryset, name, value):
if value:
return queryset.filter(expiration_date__lt=datetime.now())

return queryset.exclude(expiration_date__lt=datetime.now())

def search(self, queryset, name, value):
if not value.strip():
return queryset
Expand Down
28 changes: 27 additions & 1 deletion netbox_dns/forms/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
TagFilterField,
)
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import BulkEditNullBooleanSelect
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker

__all__ = (
"RecordForm",
Expand All @@ -46,6 +46,7 @@ class Meta:
"value",
"status",
"ttl",
"expiration_date",
"disable_ptr",
"tenant_group",
"tenant",
Expand All @@ -57,6 +58,10 @@ class Meta:
"ttl": _("TTL"),
}

widgets = {
"expiration_date": DatePicker,
}

fieldsets = (
FieldSet(
"name",
Expand All @@ -68,6 +73,7 @@ class Meta:
"status",
"ttl",
"disable_ptr",
"expiration_date",
name=_("Record"),
),
FieldSet(
Expand Down Expand Up @@ -141,6 +147,8 @@ class RecordFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
"ttl",
"disable_ptr",
"active",
"expiration_date_before",
"expiration_date_after",
name=_("Attributes"),
),
FieldSet(
Expand Down Expand Up @@ -176,6 +184,16 @@ class RecordFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES),
label=_("Disable PTR"),
)
expiration_date_before = forms.DateField(
required=False,
label=_("Expiration Date Before"),
widget=DatePicker,
)
expiration_date_after = forms.DateField(
required=False,
label=_("Expiration Date After"),
widget=DatePicker,
)
status = forms.MultipleChoiceField(
choices=RecordStatusChoices,
required=False,
Expand Down Expand Up @@ -221,6 +239,7 @@ class Meta:
"value",
"ttl",
"disable_ptr",
"expiration_date",
"tenant",
"tags",
)
Expand Down Expand Up @@ -302,6 +321,7 @@ class RecordBulkEditForm(PrimaryModelBulkEditForm):
"status",
"ttl",
"disable_ptr",
"expiration_date",
name=_("Attributes"),
),
FieldSet(
Expand All @@ -315,6 +335,7 @@ class RecordBulkEditForm(PrimaryModelBulkEditForm):
"description",
"ttl",
"tenant",
"expiration_date",
)

zone = DynamicModelChoiceField(
Expand Down Expand Up @@ -355,3 +376,8 @@ class RecordBulkEditForm(PrimaryModelBulkEditForm):
required=False,
label=_("Tenant"),
)
expiration_date = forms.DateField(
required=False,
label=_("Expiration Date"),
widget=DatePicker,
)
4 changes: 3 additions & 1 deletion netbox_dns/graphql/filters/record.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from datetime import date
from typing import TYPE_CHECKING, Annotated

import strawberry
import strawberry_django
from strawberry.scalars import ID
from strawberry_django import FilterLookup
from strawberry_django import DateFilterLookup, FilterLookup

try:
from strawberry_django import StrFilterLookup
Expand Down Expand Up @@ -58,6 +59,7 @@ class NetBoxDNSRecordFilter(
| None
) = strawberry_django.filter_field()
value: StrFilterLookup[str] | None = strawberry_django.filter_field()
expiration_date: DateFilterLookup[date] | None = strawberry_django.filter_field()
disable_ptr: FilterLookup[bool] | None = strawberry_django.filter_field()
status: (
Annotated[
Expand Down
17 changes: 17 additions & 0 deletions netbox_dns/migrations/0032_record_expiration_date.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 6.0.5 on 2026-06-07 10:07

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("netbox_dns", "0031_record_netbox_dns_record_name_and_more"),
]

operations = [
migrations.AddField(
model_name="record",
name="expiration_date",
field=models.DateField(blank=True, null=True),
),
]
14 changes: 14 additions & 0 deletions netbox_dns/models/record.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ipaddress
from datetime import date

import dns
import netaddr
Expand Down Expand Up @@ -144,6 +145,7 @@ class Meta:
"disable_ptr",
"description",
"tenant",
"expiration_date",
)

def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -258,6 +260,11 @@ def __str__(self):
null=True,
blank=True,
)
expiration_date = models.DateField(
verbose_name=_("Expiration Date"),
blank=True,
null=True,
)

@property
def cleanup_ptr_record(self):
Expand Down Expand Up @@ -307,6 +314,13 @@ def is_active(self):
and self.zone.status in ZONE_ACTIVE_STATUS_LIST
)

@property
def is_expired(self):
if self.expiration_date is None:
return False

return self.expiration_date < date.today()

@property
def is_address_record(self):
return self.type in (RecordTypeChoices.A, RecordTypeChoices.AAAA)
Expand Down
1 change: 1 addition & 0 deletions netbox_dns/tables/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class Meta(PrimaryModelTable.Meta):
fields = (
"status",
"description",
"expiration_date",
)

default_columns = (
Expand Down
94 changes: 53 additions & 41 deletions netbox_dns/templates/netbox_dns/record.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@
{% block content %}
<div class="row">
{% if object.managed %}
<div class="col col-md-12">
<div class="col col-md-12">
{% else %}
<div class="col col-md-8">
<div class="col col-md-8">
{% endif %}
<div class="card">
<div class="card">
<h5 class="card-header">{% trans "Record" %}</h5>
<table class="table table-hover attr-table">
<tr>
Expand Down Expand Up @@ -117,6 +117,18 @@ <h5 class="card-header">{% trans "Record" %}</h5>
<td>{% checkmark object.disable_ptr %}</td>
</tr>
{% endif %}
{% if object.expiration_date %}
<tr>
<th scope="row">{% trans "Expiration Date" %}</th>
<td>{{ object.expiration_date|isodate }}</td>
</tr>
{% if expiration_warning %}
<tr class="text-warning">
<th scope="row">{% trans "Warning" %}</th>
<td>{{ expiration_warning }}</td>
</tr>
{% endif %}
{% endif %}
{% if object.ptr_record %}
<tr>
<th scope="row">{% trans "PTR Record" %}</th>
Expand Down Expand Up @@ -149,52 +161,52 @@ <h5 class="card-header">{% trans "Record" %}</h5>
</tr>
</table>
</div>
{% if rrset_record_table %}
<div class="card">
{% if rrset_record_table.rows|length == 1 %}
<h5 class="card-header">{% trans "Other Record in the RRSET" %}</h5>
{% else %}
<h5 class="card-header">{% trans "Other Records in the RRSET" %}</h5>
{% endif %}
<div class="table-responsive">
{% render_table rrset_record_table 'inc/table.html' %}
</div>
{% if rrset_record_table %}
<div class="card">
{% if rrset_record_table.rows|length == 1 %}
<h5 class="card-header">{% trans "Other Record in the RRSET" %}</h5>
{% else %}
<h5 class="card-header">{% trans "Other Records in the RRSET" %}</h5>
{% endif %}
<div class="table-responsive">
{% render_table rrset_record_table 'inc/table.html' %}
</div>
{% endif %}
{% if cname_target_table %}
<div class="card">
{% if cname_target_table.rows|length == 1 %}
<h2 class="card-header">{% trans "CNAME Target" %}</h2>
{% else %}
<h2 class="card-header">{% trans "CNAME Targets" %}</h2>
{% endif %}
<div class="table-responsive">
{% render_table cname_target_table 'inc/table.html' %}
</div>
</div>
{% endif %}
{% if cname_target_table %}
<div class="card">
{% if cname_target_table.rows|length == 1 %}
<h2 class="card-header">{% trans "CNAME Target" %}</h2>
{% else %}
<h2 class="card-header">{% trans "CNAME Targets" %}</h2>
{% endif %}
<div class="table-responsive">
{% render_table cname_target_table 'inc/table.html' %}
</div>
{% elif cname_table %}
<div class="card">
{% if cname_table.rows|length == 1 %}
<h2 class="card-header">{% trans "CNAME" %}</h2>
{% else %}
<h2 class="card-header">{% trans "CNAMEs" %}</h2>
{% endif %}
<div class="table-responsive">
{% render_table cname_table 'inc/table.html' %}
</div>
</div>
{% elif cname_table %}
<div class="card">
{% if cname_table.rows|length == 1 %}
<h2 class="card-header">{% trans "CNAME" %}</h2>
{% else %}
<h2 class="card-header">{% trans "CNAMEs" %}</h2>
{% endif %}
<div class="table-responsive">
{% render_table cname_table 'inc/table.html' %}
</div>
{% endif %}
{% if not object.managed %}
</div>
{% endif %}
{% if not object.managed %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
{% endif %}
{% endif %}
</div>
{% if not object.managed %}
<div class="col col-md-4">
{% include 'inc/panels/tags.html' %}
</div>
{% plugin_right_page object %}
<div class="col col-md-4">
{% include 'inc/panels/tags.html' %}
</div>
{% plugin_right_page object %}
{% endif %}
</div>
<div class="row">
Expand Down
Loading