diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 48c14a2daf0..5e936c5ec16 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
- placeholder: v3.6.5
+ placeholder: v3.6.6
validations:
required: true
- type: dropdown
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
index 0525659aee9..34103e61615 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yaml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
- placeholder: v3.6.5
+ placeholder: v3.6.6
validations:
required: true
- type: dropdown
diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md
index 646c2019ea1..6f81e45261c 100644
--- a/docs/release-notes/version-3.6.md
+++ b/docs/release-notes/version-3.6.md
@@ -1,5 +1,28 @@
# NetBox v3.6
+## v3.6.6 (2023-11-29)
+
+### Enhancements
+
+* [#13735](https://github.com/netbox-community/netbox/issues/13735) - Show complete region hierarchy in UI for all relevant objects
+
+### Bug Fixes
+
+* [#14056](https://github.com/netbox-community/netbox/issues/14056) - Record a pre-change snapshot when bulk editing objects via CSV
+* [#14187](https://github.com/netbox-community/netbox/issues/14187) - Raise a validation error when attempting to create a duplicate script or report
+* [#14199](https://github.com/netbox-community/netbox/issues/14199) - Fix jobs list for reports with a custom name
+* [#14239](https://github.com/netbox-community/netbox/issues/14239) - Fix CustomFieldChoiceSet search filter
+* [#14242](https://github.com/netbox-community/netbox/issues/14242) - Enable export templates for contact assignments
+* [#14299](https://github.com/netbox-community/netbox/issues/14299) - Webhook timestamps should be in proper ISO 8601 format
+* [#14325](https://github.com/netbox-community/netbox/issues/14325) - Fix numeric ordering of service ports
+* [#14339](https://github.com/netbox-community/netbox/issues/14339) - Correctly hash local user password when set via REST API
+* [#14343](https://github.com/netbox-community/netbox/issues/14343) - Fix ordering ASN table by ASDOT column
+* [#14346](https://github.com/netbox-community/netbox/issues/14346) - Fix running reports via REST API
+* [#14349](https://github.com/netbox-community/netbox/issues/14349) - Fix custom validation support for remote data sources
+* [#14363](https://github.com/netbox-community/netbox/issues/14363) - Fix bulk editing of interfaces assigned to VM with no cluster
+
+---
+
## v3.6.5 (2023-11-09)
### Enhancements
diff --git a/netbox/core/models/data.py b/netbox/core/models/data.py
index 54a43c7ef9f..9e41e84461d 100644
--- a/netbox/core/models/data.py
+++ b/netbox/core/models/data.py
@@ -122,6 +122,7 @@ def ready_for_sync(self):
)
def clean(self):
+ super().clean()
# Ensure URL scheme matches selected type
if self.type == DataSourceTypeChoices.LOCAL and self.url_scheme not in ('file', ''):
diff --git a/netbox/core/models/files.py b/netbox/core/models/files.py
index 38d82463eca..a9e0e7f0069 100644
--- a/netbox/core/models/files.py
+++ b/netbox/core/models/files.py
@@ -2,6 +2,7 @@
import os
from django.conf import settings
+from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext as _
@@ -84,6 +85,14 @@ def sync_data(self):
self.file_path = os.path.basename(self.data_path)
self.data_file.write_to_disk(self.full_path, overwrite=True)
+ def clean(self):
+ super().clean()
+
+ # Ensure that the file root and path make a unique pair
+ if self._meta.model.objects.filter(file_root=self.file_root, file_path=self.file_path).exclude(pk=self.pk).exists():
+ raise ValidationError(
+ f"A {self._meta.verbose_name.lower()} with this file path already exists ({self.file_root}/{self.file_path}).")
+
def delete(self, *args, **kwargs):
# Delete file from disk
try:
diff --git a/netbox/core/models/jobs.py b/netbox/core/models/jobs.py
index 61b0e64fab0..d52cbe165ca 100644
--- a/netbox/core/models/jobs.py
+++ b/netbox/core/models/jobs.py
@@ -229,7 +229,7 @@ def trigger_webhooks(self, event):
model_name=self.object_type.model,
event=event,
data=self.data,
- timestamp=str(timezone.now()),
+ timestamp=timezone.now().isoformat(),
username=self.user.username,
retry=get_rq_retry()
)
diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py
index f518275e0ed..830982e7455 100644
--- a/netbox/extras/api/views.py
+++ b/netbox/extras/api/views.py
@@ -283,7 +283,7 @@ def run(self, request, pk):
# Retrieve and run the Report. This will create a new Job.
module, report_cls = self._get_report(pk)
- report = report_cls()
+ report = report_cls
input_serializer = serializers.ReportInputSerializer(
data=request.data,
context={'report': report}
diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py
index fec06726357..b33e7048843 100644
--- a/netbox/extras/filtersets.py
+++ b/netbox/extras/filtersets.py
@@ -122,8 +122,7 @@ def search(self, queryset, name, value):
return queryset
return queryset.filter(
Q(name__icontains=value) |
- Q(description__icontains=value) |
- Q(extra_choices__contains=value)
+ Q(description__icontains=value)
)
def filter_by_choice(self, queryset, name, value):
diff --git a/netbox/extras/views.py b/netbox/extras/views.py
index 55b73d29d3a..2c59c52350b 100644
--- a/netbox/extras/views.py
+++ b/netbox/extras/views.py
@@ -1073,7 +1073,7 @@ def get(self, request, module, name):
jobs = Job.objects.filter(
object_type=object_type,
object_id=module.pk,
- name=report.name
+ name=report.class_name
)
jobs_table = JobTable(
diff --git a/netbox/extras/webhooks.py b/netbox/extras/webhooks.py
index 1fc869ee8e3..a22f73c27e4 100644
--- a/netbox/extras/webhooks.py
+++ b/netbox/extras/webhooks.py
@@ -115,7 +115,7 @@ def flush_webhooks(queue):
event=data['event'],
data=data['data'],
snapshots=data['snapshots'],
- timestamp=str(timezone.now()),
+ timestamp=timezone.now().isoformat(),
username=data['username'],
request_id=data['request_id'],
retry=get_rq_retry()
diff --git a/netbox/ipam/tables/asn.py b/netbox/ipam/tables/asn.py
index 6bb15523ec7..bbe38dc1a33 100644
--- a/netbox/ipam/tables/asn.py
+++ b/netbox/ipam/tables/asn.py
@@ -48,6 +48,7 @@ class ASNTable(TenancyColumnsMixin, NetBoxTable):
asn_asdot = tables.Column(
accessor=tables.A('asn_asdot'),
linkify=True,
+ order_by=tables.A('asn'),
verbose_name=_('ASDOT')
)
site_count = columns.LinkedCountColumn(
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index eac1c3c3777..9072dd36466 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -25,7 +25,7 @@
# Environment setup
#
-VERSION = '3.6.5'
+VERSION = '3.6.6'
# Hostname
HOSTNAME = platform.node()
diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py
index 676e3f5afe3..c5a08c80a2d 100644
--- a/netbox/netbox/views/generic/bulk_views.py
+++ b/netbox/netbox/views/generic/bulk_views.py
@@ -394,6 +394,10 @@ def create_and_update_objects(self, form, request):
form.add_error('data', f"Row {i}: Object with ID {object_id} does not exist")
raise ValidationError('')
+ # Take a snapshot for change logging
+ if instance.pk and hasattr(instance, 'snapshot'):
+ instance.snapshot()
+
# Instantiate the model form for the object
model_form_kwargs = {
'data': record,
diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html
index 5fa6a3314a3..39e78c81b7f 100644
--- a/netbox/templates/dcim/device.html
+++ b/netbox/templates/dcim/device.html
@@ -5,6 +5,7 @@
{% load helpers %}
{% load plugins %}
{% load i18n %}
+{% load mptt %}
{% block content %}
@@ -15,16 +16,7 @@
{% trans "Region" %} |
-
- {% if object.site.region %}
- {% for region in object.site.region.get_ancestors %}
- {{ region|linkify }} /
- {% endfor %}
- {{ object.site.region|linkify }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
+ {% nested_tree object.site.region %} |
{% trans "Site" %} |
@@ -32,16 +24,7 @@
{% trans "Location" %} |
-
- {% if object.location %}
- {% for location in object.location.get_ancestors %}
- {{ location|linkify }} /
- {% endfor %}
- {{ object.location|linkify }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
+ {% nested_tree object.location %} |
{% trans "Rack" %} |
diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html
index 671c7ab2eb2..857061d00b9 100644
--- a/netbox/templates/dcim/rack.html
+++ b/netbox/templates/dcim/rack.html
@@ -4,6 +4,7 @@
{% load static %}
{% load plugins %}
{% load i18n %}
+{% load mptt %}
{% block content %}
@@ -15,26 +16,18 @@