Compare commits

...

33 Commits

Author SHA1 Message Date
Jason Novinger
c39f2c7de5 Change rack bg back and re-build static dist files 2025-12-29 15:50:14 -06:00
UnknownTy
68e995d551 Fixes netbox-community#20044: Elevations stuck in light mode 2025-12-25 02:14:21 +00:00
Prince Kumar
860db9590b Fixed #20950: Add missing module and device properties in module-bay (#21005)
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2025-12-23 13:34:06 -06:00
Jeremy Stretch
7c63d001b1 Release v4.4.9 2025-12-23 12:02:30 -05:00
Jeremy Stretch
93119f52c3 Fixes #21032: Avoid subquery in RestrictedQuerySet where unnecessary 2025-12-23 10:15:06 -05:00
github-actions
ee2aa35cba Update source translation strings
Some checks failed
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-12-23 05:04:20 +00:00
bctiemann
7896a48075 Merge pull request #21029 from netbox-community/21011-configrevision-save
Some checks failed
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
Fixes #21011: Avoid updating database when loading active ConfigRevision
2025-12-22 14:19:19 -05:00
bctiemann
eb87c3f304 Merge pull request #21000 from netbox-community/20011-misleading-error-message
Fixes #20011: Provide accurate error for bulk import duplicate IDs
2025-12-22 14:12:36 -05:00
Vincent Simonin
3acbb0a08c Fix on delete cascade entity order (#20949)
* Fix on delete cascade entity order

Since [#20708](https://github.com/netbox-community/netbox/pull/20708)
relation with a on delete RESTRICT are not deleted in the proper order.
Then the error `violate not-null constraint` occurs and breaks the
delete cascade feature.

* Revert unrelated and simplify changes
2025-12-22 13:19:02 -05:00
Jeremy Stretch
f67cc47def Fixes #21011: Avoid updating database when loading active ConfigRevision 2025-12-22 11:00:04 -05:00
Martin Hauser
f7219e0672 Closes #20309: Add ASDOT notation support for ASN ranges (#21004)
* feat(ipam): Add ASDOT notation support for ASN ranges

Introduces ASDOT notation for ASN Ranges to improve readability of large
AS numbers. Adds `start_asdot` and `end_asdot` properties, columns, and
display logic for ASN ranges in the UI.

Fixes #20309

* Wrap "ASDOT" with parentheses in column header

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-12-22 10:06:08 -05:00
Prince Kumar
e5a975176d Fixed #20944: Ensure cached scope fields stay consistent when Region, Site, or Location changes (#20986) 2025-12-22 09:48:43 -05:00
github-actions
83ee4fb593 Update source translation strings
Some checks failed
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2025-12-20 05:02:02 +00:00
bctiemann
db8271c904 Fixes #20114: Preserve parent bay during device bulk import when tags are present (#21019)
Some checks failed
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-12-19 17:05:32 -06:00
github-actions
5a24f99c9d Update source translation strings
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2025-12-18 05:03:18 +00:00
Jeremy Stretch
9318c91405 Closes #20720: Add support for Latvian translations (#21003) 2025-12-17 15:20:04 -06:00
Martin Hauser
5c6aaf2388 Closes #20900: Allow multiple choices in CustomField select filter fields (#20992) 2025-12-17 14:32:46 -06:00
Jason Novinger
265f375595 Fixes #20876: Allow editing IPAddress in IPRange marked populated 2025-12-17 13:03:45 -05:00
Jason Novinger
d95fa8dbb2 Fixes #20011: UI Error msg for duplicate IDs in bulk import 2025-12-17 09:21:17 -06:00
bctiemann
2699149016 Merge pull request #20963 from pheus/20491-normalize-arrayfield-values-to-inclusive-pairs-for-api-tests
Fixes #20491: Normalize numeric range array fields for API test comparisons
2025-12-16 15:40:44 -05:00
vo42
f371004809 Fixes #20969: Fix FrontPortTemplateFilterSet rear_port_id queryset. (#20987)
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2025-12-16 11:23:18 -08:00
github-actions
ad29402b87 Update source translation strings
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2025-12-13 05:02:00 +00:00
Jason Novinger
598f8d034d Fixes #20912: Clear ModuleBay parent when module assignment removed (#20974)
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-12-12 13:31:59 -08:00
Arthur Hanson
ec13a79907 Fixes #20875: Fix updating of denormalized fields for component models (#20956) 2025-12-12 13:29:34 -06:00
github-actions
21f4036782 Update source translation strings
Some checks are pending
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-12-12 05:03:16 +00:00
bctiemann
ce3738572c Merge pull request #20967 from netbox-community/20966-remove-stick-scroll
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
Fixes #20966: Fix broken optgroup stickiness in ObjectType multiselect
2025-12-11 19:44:16 -05:00
bctiemann
cbb979934e Merge pull request #20958 from netbox-community/17976-manufacturer-devicetype_count
Fixes #17976: Remove devicetype_count from nested manufacturer to correct OpenAPI schema
2025-12-11 19:42:26 -05:00
bctiemann
642d83a4c6 Merge pull request #20937 from netbox-community/20560-bulk-import-prefix
Fixes #20560: Fix VLAN disambiguation in prefix bulk import
2025-12-11 19:40:59 -05:00
Jason Novinger
a06c12c6b8 Fixes #20966: Fix broken optgroup stickiness in ObjectType multiselect 2025-12-11 08:59:16 -06:00
Martin Hauser
60fce84c96 feat(ipam): Normalize numeric ranges in API output
Adds logic to handle numeric range fields in API responses by
converting them into inclusive `[low, high]` pairs for consistent
behavior. Updates test cases with `vid_ranges` fields to reflect the
changes.

Closes #20491
2025-12-10 21:11:23 +01:00
Jeremy Stretch
59afa0b41d Fix test 2025-12-10 09:01:11 -05:00
Jeremy Stretch
14b246cb8a Fixes #17976: Remove devicetype_count from nested manufacturer to correct OpenAPI schema 2025-12-10 08:23:48 -05:00
Jason Novinger
9ae53fc232 Fixes #20560: Fix VLAN disambiguation in prefix bulk import 2025-12-05 16:39:28 -06:00
62 changed files with 25665 additions and 7341 deletions

View File

@@ -15,7 +15,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v4.4.8
placeholder: v4.4.9
validations:
required: true
- type: dropdown

View File

@@ -27,7 +27,7 @@ body:
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v4.4.8
placeholder: v4.4.9
validations:
required: true
- type: dropdown

View File

@@ -5,7 +5,7 @@
<a href="https://github.com/netbox-community/netbox/blob/main/LICENSE.txt"><img src="https://img.shields.io/badge/license-Apache_2.0-blue.svg" alt="License" /></a>
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://img.shields.io/github/contributors/netbox-community/netbox?color=blue" alt="Contributors" /></a>
<a href="https://github.com/netbox-community/netbox/stargazers"><img src="https://img.shields.io/github/stars/netbox-community/netbox?style=flat" alt="GitHub stars" /></a>
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-15-blue" alt="Languages supported" /></a>
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-16-blue" alt="Languages supported" /></a>
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/actions/workflows/ci.yml/badge.svg" alt="CI status" /></a>
<p>
<strong><a href="https://netboxlabs.com/community/">NetBox Community</a></strong> |

View File

@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "NetBox REST API",
"version": "4.4.8",
"version": "4.4.9",
"license": {
"name": "Apache v2 License"
}
@@ -158511,6 +158511,7 @@
"fr",
"it",
"ja",
"lv",
"nl",
"pl",
"pt",
@@ -205630,15 +205631,9 @@
"description": {
"type": "string",
"maxLength": 200
},
"devicetype_count": {
"type": "integer",
"format": "int64",
"readOnly": true
}
},
"required": [
"devicetype_count",
"display",
"id",
"name",

View File

@@ -1,5 +1,31 @@
# NetBox v4.4
## v4.4.9 (2025-12-23)
### Enhancements
* [#20309](https://github.com/netbox-community/netbox/issues/20309) - Support ASDOT notation for ASN ranges
* [#20720](https://github.com/netbox-community/netbox/issues/20720) - Add Latvian translations
* [#20900](https://github.com/netbox-community/netbox/issues/20900) - Allow filtering custom choice fields by multiple values in the UI
### Bug Fixes
* [#17976](https://github.com/netbox-community/netbox/issues/17976) - Remove `devicetype_count` from nested manufacturer to correct OpenAPI schema
* [#20011](https://github.com/netbox-community/netbox/issues/20011) - Provide a clear message when encountering duplicate object IDs during bulk import
* [#20114](https://github.com/netbox-community/netbox/issues/20114) - Preserve `parent_bay` during device bulk import when tags are present
* [#20491](https://github.com/netbox-community/netbox/issues/20491) - Improve handling of numeric ranges in tests
* [#20873](https://github.com/netbox-community/netbox/issues/20873) - Fix `AttributeError` exception triggered by event rules associated with an object that supports file attachments
* [#20875](https://github.com/netbox-community/netbox/issues/20875) - Ensure that parent object relations are cached (for filtering) on device/module components during instantiation
* [#20876](https://github.com/netbox-community/netbox/issues/20876) - Allow editing an IP address that resides within a range marked as populated
* [#20912](https://github.com/netbox-community/netbox/issues/20912) - Fix inconsistent clearing of `module` field on ModuleBay
* [#20944](https://github.com/netbox-community/netbox/issues/20944) - Ensure cached scope is updated on child objects when a parent region/site/location is changed
* [#20948](https://github.com/netbox-community/netbox/issues/20948) - Handle the deletion of related objects with `on_delete=RESTRICT` the same as `CASCADE`
* [#20969](https://github.com/netbox-community/netbox/issues/20969) - Fix querying of front port templates by `rear_port_id`
* [#21011](https://github.com/netbox-community/netbox/issues/21011) - Avoid writing to the database when loading active ConfigRevision
* [#21032](https://github.com/netbox-community/netbox/issues/21032) - Avoid SQL subquery in RestrictedQuerySet where unnecessary
---
## v4.4.8 (2025-12-09)
### Enhancements

View File

@@ -63,16 +63,20 @@ class ConfigRevision(models.Model):
return reverse('core:config') # Default config view
return reverse('core:configrevision', args=[self.pk])
def activate(self):
def activate(self, update_db=True):
"""
Cache the configuration data.
Parameters:
update_db: Mark the ConfigRevision as active in the database (default: True)
"""
cache.set('config', self.data, None)
cache.set('config_version', self.pk, None)
# Set all instances of ConfigRevision to false and set this instance to true
ConfigRevision.objects.all().update(active=False)
ConfigRevision.objects.filter(pk=self.pk).update(active=True)
if update_db:
# Set all instances of ConfigRevision to false and set this instance to true
ConfigRevision.objects.all().update(active=False)
ConfigRevision.objects.filter(pk=self.pk).update(active=True)
activate.alters_data = True

View File

@@ -3,7 +3,7 @@ from threading import local
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db.models import CASCADE
from django.db.models import CASCADE, RESTRICT
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel
from django.db.models.signals import m2m_changed, post_migrate, post_save, pre_delete
from django.dispatch import receiver, Signal
@@ -221,7 +221,7 @@ def handle_deleted_object(sender, instance, **kwargs):
obj.snapshot() # Ensure the change record includes the "before" state
if type(relation) is ManyToManyRel:
getattr(obj, related_field_name).remove(instance)
elif type(relation) is ManyToOneRel and relation.null and relation.on_delete is not CASCADE:
elif type(relation) is ManyToOneRel and relation.null and relation.on_delete not in (CASCADE, RESTRICT):
setattr(obj, related_field_name, None)
obj.save()

View File

@@ -350,14 +350,14 @@ class ModuleBaySerializer(NetBoxModelSerializer):
device = DeviceSerializer(nested=True)
module = ModuleSerializer(
nested=True,
fields=('id', 'url', 'display'),
fields=('id', 'url', 'display', 'device', 'module_bay'),
required=False,
allow_null=True,
default=None
)
installed_module = ModuleSerializer(
nested=True,
fields=('id', 'url', 'display', 'serial', 'description'),
fields=('id', 'url', 'display', 'device', 'module_bay', 'serial', 'description'),
required=False,
allow_null=True
)

View File

@@ -20,4 +20,4 @@ class ManufacturerSerializer(NetBoxModelSerializer):
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields',
'created', 'last_updated', 'devicetype_count', 'inventoryitem_count', 'platform_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count')
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')

View File

@@ -875,7 +875,7 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
null_value=None
)
rear_port_id = django_filters.ModelMultipleChoiceFilter(
queryset=RearPort.objects.all()
queryset=RearPortTemplate.objects.all()
)
class Meta:

View File

@@ -1222,6 +1222,8 @@ class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel):
def save(self, *args, **kwargs):
if self.module:
self.parent = self.module.module_bay
else:
self.parent = None
super().save(*args, **kwargs)

View File

@@ -957,6 +957,11 @@ class Device(
if cf_defaults := CustomField.objects.get_defaults_for_model(model):
for component in components:
component.custom_field_data = cf_defaults
# Set denormalized references
for component in components:
component._site = self.site
component._location = self.location
component._rack = self.rack
components = model.objects.bulk_create(components)
# Prefetch related objects to minimize queries needed during post_save
prefetch_fields = get_prefetchable_fields(model)

View File

@@ -315,6 +315,12 @@ class Module(PrimaryModel, ConfigContextModel):
for component in create_instances:
component.custom_field_data = cf_defaults
# Set denormalized references
for component in create_instances:
component._site = self.device.site
component._location = self.device.location
component._rack = self.device.rack
if component_model is not ModuleBay:
component_model.objects.bulk_create(create_instances)
# Emit the post_save signal for each newly created object

View File

@@ -1,13 +1,15 @@
import logging
from django.db.models.signals import post_save, post_delete
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from dcim.choices import CableEndChoices, LinkStatusChoices
from virtualization.models import VMInterface
from ipam.models import Prefix
from virtualization.models import Cluster, VMInterface
from wireless.models import WirelessLAN
from .models import (
Cable, CablePath, CableTermination, ConsolePort, ConsoleServerPort, Device, DeviceBay, FrontPort, Interface,
InventoryItem, ModuleBay, PathEndpoint, PowerOutlet, PowerPanel, PowerPort, Rack, RearPort, Location,
InventoryItem, Location, ModuleBay, PathEndpoint, PowerOutlet, PowerPanel, PowerPort, Rack, RearPort, Site,
VirtualChassis,
)
from .models.cables import trace_paths
@@ -44,6 +46,9 @@ def handle_location_site_change(instance, created, **kwargs):
Device.objects.filter(location__in=locations).update(site=instance.site)
PowerPanel.objects.filter(location__in=locations).update(site=instance.site)
CableTermination.objects.filter(_location__in=locations).update(_site=instance.site)
# Update component models for devices in these locations
for model in COMPONENT_MODELS:
model.objects.filter(device__location__in=locations).update(_site=instance.site)
@receiver(post_save, sender=Rack)
@@ -53,6 +58,12 @@ def handle_rack_site_change(instance, created, **kwargs):
"""
if not created:
Device.objects.filter(rack=instance).update(site=instance.site, location=instance.location)
# Update component models for devices in this rack
for model in COMPONENT_MODELS:
model.objects.filter(device__rack=instance).update(
_site=instance.site,
_location=instance.location,
)
@receiver(post_save, sender=Device)
@@ -171,3 +182,40 @@ def update_mac_address_interface(instance, created, raw, **kwargs):
if created and not raw and instance.primary_mac_address:
instance.primary_mac_address.assigned_object = instance
instance.primary_mac_address.save()
@receiver(post_save, sender=Location)
@receiver(post_save, sender=Site)
def sync_cached_scope_fields(instance, created, **kwargs):
"""
Rebuild cached scope fields for all CachedScopeMixin-based models
affected by a change in a Region, SiteGroup, Site, or Location.
This method is safe to run for objects created in the past and does
not rely on incremental updates. Cached fields are recomputed from
authoritative relationships.
"""
if created:
return
if isinstance(instance, Location):
filters = {'_location': instance}
elif isinstance(instance, Site):
filters = {'_site': instance}
else:
return
# These models are explicitly listed because they all subclass CachedScopeMixin
# and therefore require their cached scope fields to be recomputed.
for model in (Prefix, Cluster, WirelessLAN):
qs = model.objects.filter(**filters)
for obj in qs.only('id'):
# Recompute cache using the same logic as save()
obj.cache_related_objects()
obj.save(update_fields=[
'_location',
'_site',
'_site_group',
'_region',
])

View File

@@ -531,7 +531,7 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase):
class ManufacturerTest(APIViewTestCases.APIViewTestCase):
model = Manufacturer
brief_fields = ['description', 'devicetype_count', 'display', 'id', 'name', 'slug', 'url']
brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url']
create_data = [
{
'name': 'Manufacturer 4',

View File

@@ -841,6 +841,32 @@ class ModuleBayTestCase(TestCase):
nested_bay = module.modulebays.get(name='SFP A-21')
self.assertEqual(nested_bay.label, 'A-21')
@tag('regression') # #20912
def test_module_bay_parent_cleared_when_module_removed(self):
"""Test that the parent field is properly cleared when a module bay's module assignment is removed"""
device = Device.objects.first()
manufacturer = Manufacturer.objects.first()
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Test Module Type')
bay1 = ModuleBay.objects.create(device=device, name='Test Bay 1')
bay2 = ModuleBay.objects.create(device=device, name='Test Bay 2')
# Install a module in bay1
module1 = Module.objects.create(device=device, module_bay=bay1, module_type=module_type)
# Assign bay2 to module1 and verify parent is now set to bay1 (module1's bay)
bay2.module = module1
bay2.save()
bay2.refresh_from_db()
self.assertEqual(bay2.parent, bay1)
self.assertEqual(bay2.module, module1)
# Clear the module assignment (return bay2 to device level) Verify parent is cleared
bay2.module = None
bay2.save()
bay2.refresh_from_db()
self.assertIsNone(bay2.parent)
self.assertIsNone(bay2.module)
class CableTestCase(TestCase):

View File

@@ -2322,6 +2322,32 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
url = reverse('dcim:device_inventory', kwargs={'pk': device.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_bulk_import_duplicate_ids_error_message(self):
device = Device.objects.first()
csv_data = (
"id,role",
f"{device.pk},Device Role 1",
f"{device.pk},Device Role 2",
)
self.add_permissions('dcim.add_device', 'dcim.change_device')
response = self.client.post(
self._get_url('bulk_import'),
{
'data': '\n'.join(csv_data),
'format': ImportFormatChoices.CSV,
'csv_delimiter': CSVDelimiterChoices.AUTO,
},
follow=True
)
self.assertEqual(response.status_code, 200)
self.assertIn(
f'Duplicate objects found: Device with ID(s) {device.pk} appears multiple times',
response.content.decode('utf-8')
)
class ModuleTestCase(
# Module does not support bulk renaming (no name field) or

View File

@@ -2454,11 +2454,12 @@ class DeviceBulkImportView(generic.BulkImportView):
model_form = forms.DeviceImportForm
def save_object(self, object_form, request):
parent_bay = getattr(object_form.instance, 'parent_bay', None)
obj = object_form.save()
# For child devices, save the reverse relation to the parent device bay
if getattr(obj, 'parent_bay', None):
device_bay = obj.parent_bay
if parent_bay:
device_bay = parent_bay
device_bay.installed_device = obj
device_bay.save()

View File

@@ -449,7 +449,14 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
return model.objects.filter(pk__in=value)
return value
def to_form_field(self, set_initial=True, enforce_required=True, enforce_visibility=True, for_csv_import=False):
def to_form_field(
self,
set_initial=True,
enforce_required=True,
enforce_visibility=True,
for_csv_import=False,
for_filterset_form=False,
):
"""
Return a form field suitable for setting a CustomField's value for an object.
@@ -457,6 +464,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
enforce_visibility: Honor the value of CustomField.ui_visible. Set to False for filtering.
for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
for_filterset_form: Return a form field suitable for use in a FilterSet form.
"""
initial = self.default if set_initial else None
required = self.required if enforce_required else False
@@ -519,7 +527,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
field_class = CSVMultipleChoiceField
field = field_class(choices=choices, required=required, initial=initial)
else:
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
if self.type == CustomFieldTypeChoices.TYPE_SELECT and not for_filterset_form:
field_class = DynamicChoiceField
widget_class = APISelect
else:

View File

@@ -16,6 +16,7 @@ __all__ = (
# BGP ASN bounds
BGP_ASN_MIN = 1
BGP_ASN_MAX = 2**32 - 1
BGP_ASN_ASDOT_BASE = 2**16
class BaseIPField(models.Field):
@@ -126,3 +127,16 @@ class ASNField(models.BigIntegerField):
}
defaults.update(**kwargs)
return super().formfield(**defaults)
@staticmethod
def to_asdot(value) -> str:
"""
Return ASDOT notation for AS numbers greater than 16 bits.
"""
if value is None:
return ''
if value >= BGP_ASN_ASDOT_BASE:
hi, lo = divmod(value, BGP_ASN_ASDOT_BASE)
return f'{hi}.{lo}'
return str(value)

View File

@@ -230,10 +230,6 @@ class PrefixImportForm(ScopedImportForm, NetBoxModelImportForm):
query |= Q(**{
f"site__{self.fields['vlan_site'].to_field_name}": vlan_site
})
# Don't Forget to include VLANs without a site in the filter
query |= Q(**{
f"site__{self.fields['vlan_site'].to_field_name}__isnull": True
})
if vlan_group:
query &= Q(**{

View File

@@ -55,13 +55,6 @@ class ASNRange(OrganizationalModel):
def __str__(self):
return f'{self.name} ({self.range_as_string()})'
@property
def range(self):
return range(self.start, self.end + 1)
def range_as_string(self):
return f'{self.start}-{self.end}'
def clean(self):
super().clean()
@@ -72,7 +65,45 @@ class ASNRange(OrganizationalModel):
)
)
@property
def range(self):
"""
Return a range of integers representing the ASN range.
"""
return range(self.start, self.end + 1)
@property
def start_asdot(self):
"""
Return ASDOT notation for AS numbers greater than 16 bits.
"""
return ASNField.to_asdot(self.start)
@property
def end_asdot(self):
"""
Return ASDOT notation for AS numbers greater than 16 bits.
"""
return ASNField.to_asdot(self.end)
def range_as_string(self):
"""
Return a string representation of the ASN range.
"""
return f'{self.start}-{self.end}'
def range_as_string_with_asdot(self):
"""
Return a string representation of the ASN range, including ASDOT notation.
"""
if self.end >= 65536:
return f'{self.range_as_string()} ({self.start_asdot}-{self.end_asdot})'
return self.range_as_string()
def get_child_asns(self):
"""
Return all child ASNs (ASNs within the range).
"""
return ASN.objects.filter(
asn__gte=self.start,
asn__lte=self.end
@@ -131,20 +162,20 @@ class ASN(ContactsMixin, PrimaryModel):
"""
Return ASDOT notation for AS numbers greater than 16 bits.
"""
if self.asn > 65535:
return f'{self.asn // 65536}.{self.asn % 65536}'
return self.asn
return ASNField.to_asdot(self.asn)
@property
def asn_with_asdot(self):
"""
Return both plain and ASDOT notation, where applicable.
"""
if self.asn > 65535:
return f'{self.asn} ({self.asn // 65536}.{self.asn % 65536})'
else:
return self.asn
if self.asn >= 65536:
return f'{self.asn} ({self.asn_asdot})'
return str(self.asn)
@property
def prefixed_name(self):
"""
Return the ASN with ASDOT notation prefixed with "AS".
"""
return f'AS{self.asn_with_asdot}'

View File

@@ -910,13 +910,13 @@ class IPAddress(ContactsMixin, PrimaryModel):
})
# Disallow the creation of IPAddresses within an IPRange with mark_populated=True
parent_range = IPRange.objects.filter(
parent_range_qs = IPRange.objects.filter(
start_address__lte=self.address,
end_address__gte=self.address,
vrf=self.vrf,
mark_populated=True
).first()
if parent_range:
)
if not self.pk and (parent_range := parent_range_qs.first()):
raise ValidationError({
'address': _(
"Cannot create IP address {ip} inside range {range}."

View File

@@ -20,6 +20,16 @@ class ASNRangeTable(TenancyColumnsMixin, NetBoxTable):
verbose_name=_('RIR'),
linkify=True
)
start_asdot = tables.Column(
accessor=tables.A('start_asdot'),
order_by=tables.A('start'),
verbose_name=_('Start (ASDOT)')
)
end_asdot = tables.Column(
accessor=tables.A('end_asdot'),
order_by=tables.A('end'),
verbose_name=_('End (ASDOT)')
)
tags = columns.TagColumn(
url_name='ipam:asnrange_list'
)
@@ -30,8 +40,8 @@ class ASNRangeTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = ASNRange
fields = (
'pk', 'name', 'slug', 'rir', 'start', 'end', 'asn_count', 'tenant', 'tenant_group', 'description', 'tags',
'created', 'last_updated', 'actions',
'pk', 'name', 'slug', 'rir', 'start', 'start_asdot', 'end', 'end_asdot', 'asn_count', 'tenant',
'tenant_group', 'description', 'tags', 'created', 'last_updated', 'actions',
)
default_columns = ('pk', 'name', 'rir', 'start', 'end', 'tenant', 'asn_count', 'description')

View File

@@ -1071,14 +1071,17 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
{
'name': 'VLAN Group 4',
'slug': 'vlan-group-4',
'vid_ranges': [[1, 4094]]
},
{
'name': 'VLAN Group 5',
'slug': 'vlan-group-5',
'vid_ranges': [[1, 4094]]
},
{
'name': 'VLAN Group 6',
'slug': 'vlan-group-6',
'vid_ranges': [[1, 4094]]
},
]
bulk_update_data = {

View File

@@ -564,6 +564,82 @@ vlan: 102
self.assertEqual(prefix.vlan.vid, 102)
self.assertEqual(prefix.scope, site)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_prefix_import_with_vlan_site_multiple_vlans_same_vid(self):
"""
Test import when multiple VLANs exist with the same vid but different sites.
Ref: #20560
"""
site1 = Site.objects.get(name='Site 1')
site2 = Site.objects.get(name='Site 2')
# Create VLANs with the same vid but different sites
vlan1 = VLAN.objects.create(vid=1, name='VLAN1-Site1', site=site1)
VLAN.objects.create(vid=1, name='VLAN1-Site2', site=site2) # Create ambiguity
# Import prefix with vlan_site specified
IMPORT_DATA = f"""
prefix: 10.11.0.0/22
status: active
scope_type: dcim.site
scope_id: {site1.pk}
vlan_site: {site1.name}
vlan: 1
description: LOC02-MGMT
"""
# Add all required permissions to the test user
self.add_permissions('ipam.view_prefix', 'ipam.add_prefix')
form_data = {
'data': IMPORT_DATA,
'format': 'yaml'
}
response = self.client.post(reverse('ipam:prefix_bulk_import'), data=form_data, follow=True)
self.assertHttpStatus(response, 200)
# Verify the prefix was created with the correct VLAN
prefix = Prefix.objects.get(prefix='10.11.0.0/22')
self.assertEqual(prefix.vlan, vlan1)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_prefix_import_with_vlan_site_and_global_vlan(self):
"""
Test import when a global VLAN (no site) and site-specific VLAN exist with same vid.
When vlan_site is specified, should prefer the site-specific VLAN.
Ref: #20560
"""
site1 = Site.objects.get(name='Site 1')
# Create a global VLAN (no site) and a site-specific VLAN with the same vid
VLAN.objects.create(vid=10, name='VLAN10-Global', site=None) # Create ambiguity
vlan_site = VLAN.objects.create(vid=10, name='VLAN10-Site1', site=site1)
# Import prefix with vlan_site specified
IMPORT_DATA = f"""
prefix: 10.12.0.0/22
status: active
scope_type: dcim.site
scope_id: {site1.pk}
vlan_site: {site1.name}
vlan: 10
description: Test Site-Specific VLAN
"""
# Add all required permissions to the test user
self.add_permissions('ipam.view_prefix', 'ipam.add_prefix')
form_data = {
'data': IMPORT_DATA,
'format': 'yaml'
}
response = self.client.post(reverse('ipam:prefix_bulk_import'), data=form_data, follow=True)
self.assertHttpStatus(response, 200)
# Verify the prefix was created with the site-specific VLAN (not the global one)
prefix = Prefix.objects.get(prefix='10.12.0.0/22')
self.assertEqual(prefix.vlan, vlan_site)
class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = IPRange

View File

@@ -80,22 +80,21 @@ class Config:
try:
# Enforce the creation date as the ordering parameter
revision = ConfigRevision.objects.get(active=True)
logger.debug(f"Loaded active configuration revision #{revision.pk}")
logger.debug(f"Loaded active configuration revision (#{revision.pk})")
except (ConfigRevision.DoesNotExist, ConfigRevision.MultipleObjectsReturned):
logger.debug("No active configuration revision found - falling back to most recent")
revision = ConfigRevision.objects.order_by('-created').first()
if revision is None:
logger.debug("No previous configuration found in database; proceeding with default values")
logger.debug("No configuration found in database; proceeding with default values")
return
logger.debug(f"Using fallback configuration revision #{revision.pk}")
logger.debug(f"No active configuration revision found; falling back to most recent (#{revision.pk})")
except DatabaseError:
# The database may not be available yet (e.g. when running a management command)
logger.warning("Skipping config initialization (database unavailable)")
return
revision.activate()
logger.debug("Filled cache with data from latest ConfigRevision")
revision.activate(update_db=False)
self._populate_from_cache()
logger.debug("Filled cache with data from latest ConfigRevision")
class ConfigItem:

View File

@@ -205,4 +205,6 @@ class NetBoxModelFilterSetForm(CustomFieldsMixin, SavedFiltersMixin, forms.Form)
)
def _get_form_field(self, customfield):
return customfield.to_form_field(set_initial=False, enforce_required=False, enforce_visibility=False)
return customfield.to_form_field(
set_initial=False, enforce_required=False, enforce_visibility=False, for_filterset_form=True
)

View File

@@ -827,6 +827,7 @@ LANGUAGES = (
('fr', _('French')),
('it', _('Italian')),
('ja', _('Japanese')),
('lv', _('Latvian')),
('nl', _('Dutch')),
('pl', _('Polish')),
('pt', _('Portuguese')),

View File

@@ -1,5 +1,6 @@
import logging
import re
from collections import Counter
from copy import deepcopy
from django.contrib import messages
@@ -33,6 +34,7 @@ from utilities.jobs import is_background_request, process_request_as_job
from utilities.permissions import get_permission_for_model
from utilities.query import reapply_model_ordering
from utilities.request import safe_for_redirect
from utilities.string import title
from utilities.tables import get_table_configs
from utilities.views import GetReturnURLMixin, get_action_url
from .base import BaseMultiObjectView
@@ -443,6 +445,18 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
# Prefetch objects to be updated, if any
prefetch_ids = [int(record['id']) for record in records if record.get('id')]
# check for duplicate IDs
duplicate_pks = [pk for pk, count in Counter(prefetch_ids).items() if count > 1]
if duplicate_pks:
error_msg = _(
"Duplicate objects found: {model} with ID(s) {ids} appears multiple times"
).format(
model=title(self.queryset.model._meta.verbose_name),
ids=', '.join(str(pk) for pk in sorted(duplicate_pks))
)
raise ValidationError(error_msg)
prefetched_objects = {
obj.pk: obj
for obj in self.queryset.model.objects.filter(id__in=prefetch_ids)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -27,10 +27,10 @@
"bootstrap": "5.3.8",
"clipboard": "2.0.11",
"flatpickr": "4.6.13",
"gridstack": "12.3.3",
"gridstack": "12.4.1",
"htmx.org": "2.0.8",
"query-string": "9.3.1",
"sass": "1.95.0",
"sass": "1.97.1",
"tom-select": "2.4.3",
"typeface-inter": "3.18.1",
"typeface-roboto-mono": "1.1.13"

View File

@@ -28,13 +28,27 @@ function updateElements(targetMode: ColorMode): void {
}
for (const elevation of getElements<HTMLObjectElement>('.rack_elevation')) {
const svg = elevation.contentDocument?.querySelector('svg') ?? null;
if (svg !== null) {
const svg = elevation.firstElementChild ?? null;
if (svg !== null && svg.nodeName == 'svg') {
svg.setAttribute(`data-bs-theme`, targetMode);
}
}
}
/**
* Set the color mode to light of elevations after an htmx call.
* Pulls current color mode from document
*
* @param event htmx listener event details. See: https://htmx.org/events/#htmx:afterSwap
*/
function updateElevations(evt: CustomEvent, ): void {
const swappedElement = evt.detail.elt
if (swappedElement.nodeName == 'svg') {
const currentMode = localStorage.getItem(COLOR_MODE_KEY);
swappedElement.setAttribute('data-bs-theme', currentMode)
}
}
/**
* Call all functions necessary to update the color mode across the UI.
*
@@ -115,6 +129,7 @@ function initColorModeToggle(): void {
*/
export function initColorMode(): void {
window.addEventListener('load', defaultColorMode);
window.addEventListener('htmx:afterSwap', updateElevations as EventListener); // Uses a custom event from HTMX
for (const func of [initColorModeToggle]) {
func();
}

View File

@@ -36,7 +36,6 @@ form.object-edit {
// Make optgroup labels sticky when scrolling through select elements
select[multiple] {
optgroup {
position: sticky;
top: 0;
background-color: var(--bs-body-bg);
font-style: normal;

View File

@@ -2178,10 +2178,10 @@ graphql@16.10.0:
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c"
integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==
gridstack@12.3.3:
version "12.3.3"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-12.3.3.tgz#0c4fc3cdf6e1c16e6095bc79ff7240a590d2c200"
integrity sha512-Bboi4gj7HXGnx1VFXQNde4Nwi5srdUSuCCnOSszKhFjBs8EtMEWhsKX02BjIKkErq/FjQUkNUbXUYeQaVMQ0jQ==
gridstack@12.4.1:
version "12.4.1"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-12.4.1.tgz#4a44511e5da33016e731f00bee279bed550d4ab9"
integrity sha512-dYBNVEDw2zwnz0bCDouHk8rMclrMoMn4r6rtNyyWSeYsV3RF8QV2KFRTj4c86T2FsZPr3iQv+/LD/ae29FcpHQ==
has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2"
@@ -3190,10 +3190,10 @@ safe-regex-test@^1.1.0:
es-errors "^1.3.0"
is-regex "^1.2.1"
sass@1.95.0:
version "1.95.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.95.0.tgz#3a3a4d4d954313ab50eaf16f6e2548a2f6ec0811"
integrity sha512-9QMjhLq+UkOg/4bb8Lt8A+hJZvY3t+9xeZMKSBtBEgxrXA3ed5Ts4NDreUkYgJP1BTmrscQE/xYhf7iShow6lw==
sass@1.97.1:
version "1.97.1"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.97.1.tgz#f36e492baf8ccdd08d591b58d3d8b53ea35ab905"
integrity sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==
dependencies:
chokidar "^4.0.0"
immutable "^5.0.2"

View File

@@ -1,3 +1,3 @@
version: "4.4.8"
version: "4.4.9"
edition: "Community"
published: "2025-12-09"
published: "2025-12-23"

View File

@@ -23,7 +23,7 @@
</tr>
<tr>
<th scope="row">{% trans "Range" %}</th>
<td>{{ object.range_as_string }}</td>
<td>{{ object.range_as_string_with_asdot }}</td>
</tr>
<tr>
<th scope="row">{% trans "Tenant" %}</th>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-10 05:02+0000\n"
"POT-Creation-Date: 2025-12-23 05:04+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -231,7 +231,7 @@ msgstr ""
#: netbox/dcim/tables/power.py:93 netbox/dcim/tables/racks.py:125
#: netbox/dcim/tables/racks.py:215 netbox/dcim/tables/sites.py:151
#: netbox/extras/filtersets.py:662 netbox/ipam/forms/bulk_edit.py:479
#: netbox/ipam/forms/bulk_import.py:489 netbox/ipam/forms/filtersets.py:161
#: netbox/ipam/forms/bulk_import.py:485 netbox/ipam/forms/filtersets.py:161
#: netbox/ipam/forms/filtersets.py:236 netbox/ipam/forms/filtersets.py:457
#: netbox/ipam/forms/filtersets.py:552 netbox/ipam/forms/model_forms.py:673
#: netbox/ipam/tables/vlans.py:90 netbox/ipam/tables/vlans.py:200
@@ -272,8 +272,8 @@ msgid "ASN (ID)"
msgstr ""
#: netbox/circuits/filtersets.py:79 netbox/circuits/forms/filtersets.py:39
#: netbox/ipam/forms/model_forms.py:166 netbox/ipam/models/asns.py:106
#: netbox/ipam/models/asns.py:123 netbox/ipam/tables/asn.py:41
#: netbox/ipam/forms/model_forms.py:166 netbox/ipam/models/asns.py:137
#: netbox/ipam/models/asns.py:154 netbox/ipam/tables/asn.py:51
#: netbox/templates/ipam/asn.html:20
msgid "ASN"
msgstr ""
@@ -453,8 +453,8 @@ msgstr ""
#: netbox/circuits/forms/model_forms.py:43
#: netbox/circuits/tables/providers.py:32 netbox/dcim/forms/bulk_edit.py:143
#: netbox/dcim/forms/filtersets.py:204 netbox/dcim/forms/model_forms.py:133
#: netbox/dcim/tables/sites.py:108 netbox/ipam/models/asns.py:124
#: netbox/ipam/tables/asn.py:27 netbox/ipam/views.py:269
#: netbox/dcim/tables/sites.py:108 netbox/ipam/models/asns.py:155
#: netbox/ipam/tables/asn.py:37 netbox/ipam/views.py:269
#: netbox/netbox/navigation/menu.py:179 netbox/netbox/navigation/menu.py:182
#: netbox/templates/circuits/provider.html:23
msgid "ASNs"
@@ -784,8 +784,8 @@ msgstr ""
#: netbox/dcim/tables/sites.py:96 netbox/dcim/tables/sites.py:155
#: netbox/ipam/forms/bulk_edit.py:240 netbox/ipam/forms/bulk_edit.py:290
#: netbox/ipam/forms/bulk_edit.py:343 netbox/ipam/forms/bulk_edit.py:501
#: netbox/ipam/forms/bulk_import.py:195 netbox/ipam/forms/bulk_import.py:263
#: netbox/ipam/forms/bulk_import.py:299 netbox/ipam/forms/bulk_import.py:510
#: netbox/ipam/forms/bulk_import.py:195 netbox/ipam/forms/bulk_import.py:259
#: netbox/ipam/forms/bulk_import.py:295 netbox/ipam/forms/bulk_import.py:506
#: netbox/ipam/forms/filtersets.py:219 netbox/ipam/forms/filtersets.py:297
#: netbox/ipam/forms/filtersets.py:379 netbox/ipam/forms/filtersets.py:564
#: netbox/ipam/forms/model_forms.py:512 netbox/ipam/tables/ip.py:184
@@ -866,8 +866,8 @@ msgstr ""
#: netbox/ipam/forms/bulk_import.py:41 netbox/ipam/forms/bulk_import.py:70
#: netbox/ipam/forms/bulk_import.py:98 netbox/ipam/forms/bulk_import.py:118
#: netbox/ipam/forms/bulk_import.py:138 netbox/ipam/forms/bulk_import.py:167
#: netbox/ipam/forms/bulk_import.py:256 netbox/ipam/forms/bulk_import.py:292
#: netbox/ipam/forms/bulk_import.py:472 netbox/ipam/forms/bulk_import.py:503
#: netbox/ipam/forms/bulk_import.py:252 netbox/ipam/forms/bulk_import.py:288
#: netbox/ipam/forms/bulk_import.py:468 netbox/ipam/forms/bulk_import.py:499
#: netbox/ipam/forms/filtersets.py:50 netbox/ipam/forms/filtersets.py:70
#: netbox/ipam/forms/filtersets.py:102 netbox/ipam/forms/filtersets.py:123
#: netbox/ipam/forms/filtersets.py:146 netbox/ipam/forms/filtersets.py:182
@@ -1106,8 +1106,8 @@ msgstr ""
#: netbox/extras/filtersets.py:689 netbox/ipam/forms/bulk_edit.py:245
#: netbox/ipam/forms/bulk_edit.py:295 netbox/ipam/forms/bulk_edit.py:348
#: netbox/ipam/forms/bulk_edit.py:506 netbox/ipam/forms/bulk_import.py:200
#: netbox/ipam/forms/bulk_import.py:268 netbox/ipam/forms/bulk_import.py:304
#: netbox/ipam/forms/bulk_import.py:515 netbox/ipam/forms/filtersets.py:247
#: netbox/ipam/forms/bulk_import.py:264 netbox/ipam/forms/bulk_import.py:300
#: netbox/ipam/forms/bulk_import.py:511 netbox/ipam/forms/filtersets.py:247
#: netbox/ipam/forms/filtersets.py:305 netbox/ipam/forms/filtersets.py:384
#: netbox/ipam/forms/filtersets.py:572 netbox/ipam/forms/model_forms.py:195
#: netbox/ipam/forms/model_forms.py:221 netbox/ipam/forms/model_forms.py:260
@@ -1160,8 +1160,8 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:365 netbox/dcim/forms/bulk_import.py:597
#: netbox/dcim/forms/bulk_import.py:757 netbox/dcim/forms/bulk_import.py:1250
#: netbox/dcim/forms/bulk_import.py:1681 netbox/ipam/forms/bulk_import.py:197
#: netbox/ipam/forms/bulk_import.py:265 netbox/ipam/forms/bulk_import.py:301
#: netbox/ipam/forms/bulk_import.py:512 netbox/ipam/forms/bulk_import.py:525
#: netbox/ipam/forms/bulk_import.py:261 netbox/ipam/forms/bulk_import.py:297
#: netbox/ipam/forms/bulk_import.py:508 netbox/ipam/forms/bulk_import.py:521
#: netbox/virtualization/forms/bulk_import.py:62
#: netbox/virtualization/forms/bulk_import.py:93
#: netbox/vpn/forms/bulk_import.py:39 netbox/vpn/forms/bulk_import.py:266
@@ -1178,9 +1178,9 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:1740 netbox/ipam/forms/bulk_import.py:45
#: netbox/ipam/forms/bulk_import.py:74 netbox/ipam/forms/bulk_import.py:102
#: netbox/ipam/forms/bulk_import.py:122 netbox/ipam/forms/bulk_import.py:142
#: netbox/ipam/forms/bulk_import.py:171 netbox/ipam/forms/bulk_import.py:260
#: netbox/ipam/forms/bulk_import.py:296 netbox/ipam/forms/bulk_import.py:476
#: netbox/ipam/forms/bulk_import.py:507
#: netbox/ipam/forms/bulk_import.py:171 netbox/ipam/forms/bulk_import.py:256
#: netbox/ipam/forms/bulk_import.py:292 netbox/ipam/forms/bulk_import.py:472
#: netbox/ipam/forms/bulk_import.py:503
#: netbox/virtualization/forms/bulk_import.py:76
#: netbox/virtualization/forms/bulk_import.py:130
#: netbox/vpn/forms/bulk_import.py:63 netbox/wireless/forms/bulk_import.py:61
@@ -1224,7 +1224,7 @@ msgstr ""
#: netbox/dcim/forms/model_forms.py:1571 netbox/dcim/forms/model_forms.py:1738
#: netbox/dcim/forms/model_forms.py:1773 netbox/dcim/forms/model_forms.py:1903
#: netbox/dcim/tables/connections.py:65 netbox/dcim/tables/devices.py:1169
#: netbox/ipam/forms/bulk_import.py:324 netbox/ipam/forms/model_forms.py:291
#: netbox/ipam/forms/bulk_import.py:320 netbox/ipam/forms/model_forms.py:291
#: netbox/ipam/forms/model_forms.py:300 netbox/ipam/tables/fhrp.py:64
#: netbox/ipam/tables/ip.py:330 netbox/ipam/tables/vlans.py:148
#: netbox/templates/circuits/inc/circuit_termination_fields.html:52
@@ -1389,7 +1389,7 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:104 netbox/dcim/forms/model_forms.py:126
#: netbox/dcim/tables/sites.py:103 netbox/extras/forms/filtersets.py:582
#: netbox/ipam/filtersets.py:982 netbox/ipam/forms/bulk_edit.py:488
#: netbox/ipam/forms/bulk_import.py:496 netbox/ipam/forms/model_forms.py:571
#: netbox/ipam/forms/bulk_import.py:492 netbox/ipam/forms/model_forms.py:571
#: netbox/ipam/tables/fhrp.py:67 netbox/ipam/tables/vlans.py:94
#: netbox/ipam/tables/vlans.py:205
#: netbox/templates/circuits/circuitgroupassignment.html:22
@@ -1441,7 +1441,7 @@ msgstr ""
#: netbox/dcim/models/device_components.py:517
#: netbox/dcim/models/device_components.py:1063
#: netbox/dcim/models/device_components.py:1134
#: netbox/dcim/models/device_components.py:1280
#: netbox/dcim/models/device_components.py:1282
#: netbox/dcim/models/devices.py:382 netbox/dcim/models/racks.py:227
#: netbox/extras/models/tags.py:29
msgid "color"
@@ -1469,8 +1469,8 @@ msgstr ""
#: netbox/circuits/models/virtual_circuits.py:59 netbox/core/models/data.py:52
#: netbox/core/models/jobs.py:95 netbox/dcim/models/cables.py:51
#: netbox/dcim/models/device_components.py:488
#: netbox/dcim/models/device_components.py:1319
#: netbox/dcim/models/devices.py:580 netbox/dcim/models/devices.py:1202
#: netbox/dcim/models/device_components.py:1321
#: netbox/dcim/models/devices.py:580 netbox/dcim/models/devices.py:1207
#: netbox/dcim/models/modules.py:209 netbox/dcim/models/power.py:94
#: netbox/dcim/models/racks.py:294 netbox/dcim/models/racks.py:677
#: netbox/dcim/models/sites.py:157 netbox/dcim/models/sites.py:281
@@ -1604,7 +1604,7 @@ msgstr ""
#: netbox/core/models/jobs.py:56
#: netbox/dcim/models/device_component_templates.py:44
#: netbox/dcim/models/device_components.py:53 netbox/dcim/models/devices.py:524
#: netbox/dcim/models/devices.py:1128 netbox/dcim/models/devices.py:1197
#: netbox/dcim/models/devices.py:1133 netbox/dcim/models/devices.py:1202
#: netbox/dcim/models/modules.py:31 netbox/dcim/models/power.py:38
#: netbox/dcim/models/power.py:89 netbox/dcim/models/racks.py:263
#: netbox/dcim/models/sites.py:145 netbox/extras/models/configs.py:36
@@ -1882,7 +1882,7 @@ msgstr ""
#: netbox/dcim/tables/sites.py:40 netbox/dcim/tables/sites.py:74
#: netbox/dcim/tables/sites.py:121 netbox/dcim/tables/sites.py:179
#: netbox/extras/forms/bulk_import.py:303 netbox/extras/tables/tables.py:706
#: netbox/ipam/tables/asn.py:69 netbox/ipam/tables/fhrp.py:34
#: netbox/ipam/tables/asn.py:79 netbox/ipam/tables/fhrp.py:34
#: netbox/ipam/tables/ip.py:83 netbox/ipam/tables/ip.py:227
#: netbox/ipam/tables/ip.py:286 netbox/ipam/tables/ip.py:355
#: netbox/ipam/tables/services.py:25 netbox/ipam/tables/services.py:55
@@ -1984,7 +1984,7 @@ msgstr ""
#: netbox/dcim/tables/devices.py:862 netbox/dcim/tables/devices.py:921
#: netbox/dcim/tables/devices.py:989 netbox/dcim/tables/devices.py:1118
#: netbox/dcim/tables/modules.py:87 netbox/extras/forms/filtersets.py:389
#: netbox/ipam/forms/bulk_import.py:310 netbox/ipam/forms/filtersets.py:626
#: netbox/ipam/forms/bulk_import.py:306 netbox/ipam/forms/filtersets.py:626
#: netbox/ipam/forms/model_forms.py:334 netbox/ipam/tables/vlans.py:159
#: netbox/templates/circuits/virtualcircuittermination.html:56
#: netbox/templates/dcim/consoleport.html:20
@@ -3185,7 +3185,7 @@ msgstr ""
#: netbox/dcim/tables/devices.py:719 netbox/dcim/tables/devices.py:929
#: netbox/dcim/tables/devices.py:1016 netbox/dcim/tables/devices.py:1175
#: netbox/dcim/tables/sites.py:28 netbox/dcim/tables/sites.py:62
#: netbox/dcim/tables/sites.py:147 netbox/ipam/forms/bulk_import.py:582
#: netbox/dcim/tables/sites.py:147 netbox/ipam/forms/bulk_import.py:578
#: netbox/ipam/forms/model_forms.py:770 netbox/ipam/tables/fhrp.py:59
#: netbox/ipam/tables/ip.py:336 netbox/ipam/tables/services.py:45
#: netbox/templates/dcim/devicerole.html:34
@@ -3817,8 +3817,8 @@ msgstr ""
#: netbox/dcim/filtersets.py:1197 netbox/dcim/forms/filtersets.py:855
#: netbox/dcim/forms/filtersets.py:1483 netbox/dcim/forms/filtersets.py:1699
#: netbox/dcim/forms/model_forms.py:1900 netbox/dcim/models/devices.py:1298
#: netbox/dcim/models/devices.py:1318 netbox/virtualization/filtersets.py:201
#: netbox/dcim/forms/model_forms.py:1900 netbox/dcim/models/devices.py:1303
#: netbox/dcim/models/devices.py:1323 netbox/virtualization/filtersets.py:201
#: netbox/virtualization/filtersets.py:273
#: netbox/virtualization/forms/filtersets.py:178
#: netbox/virtualization/forms/filtersets.py:231
@@ -3963,7 +3963,7 @@ msgid "Is assigned"
msgstr ""
#: netbox/dcim/filtersets.py:1826 netbox/dcim/forms/bulk_import.py:1355
#: netbox/ipam/forms/bulk_import.py:338
#: netbox/ipam/forms/bulk_import.py:334
msgid "Is primary"
msgstr ""
@@ -3991,7 +3991,7 @@ msgstr ""
#: netbox/ipam/filtersets.py:579 netbox/ipam/filtersets.py:590
#: netbox/ipam/forms/bulk_edit.py:226 netbox/ipam/forms/bulk_edit.py:282
#: netbox/ipam/forms/bulk_edit.py:329 netbox/ipam/forms/bulk_import.py:160
#: netbox/ipam/forms/bulk_import.py:249 netbox/ipam/forms/bulk_import.py:285
#: netbox/ipam/forms/bulk_import.py:245 netbox/ipam/forms/bulk_import.py:281
#: netbox/ipam/forms/filtersets.py:69 netbox/ipam/forms/filtersets.py:180
#: netbox/ipam/forms/filtersets.py:332 netbox/ipam/forms/model_forms.py:66
#: netbox/ipam/forms/model_forms.py:209 netbox/ipam/forms/model_forms.py:257
@@ -4856,7 +4856,7 @@ msgid "available options"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:138 netbox/dcim/forms/bulk_import.py:633
#: netbox/dcim/forms/bulk_import.py:1650 netbox/ipam/forms/bulk_import.py:493
#: netbox/dcim/forms/bulk_import.py:1650 netbox/ipam/forms/bulk_import.py:489
#: netbox/virtualization/forms/bulk_import.py:69
#: netbox/virtualization/forms/bulk_import.py:100
msgid "Assigned site"
@@ -5168,7 +5168,7 @@ msgid "Assigned Q-in-Q Service VLAN ID (filtered by VLAN group)"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1028 netbox/ipam/forms/bulk_import.py:164
#: netbox/ipam/forms/bulk_import.py:253 netbox/ipam/forms/bulk_import.py:289
#: netbox/ipam/forms/bulk_import.py:249 netbox/ipam/forms/bulk_import.py:285
#: netbox/ipam/forms/filtersets.py:210 netbox/ipam/forms/filtersets.py:293
#: netbox/ipam/forms/filtersets.py:360
#: netbox/virtualization/forms/bulk_import.py:220
@@ -5247,11 +5247,11 @@ msgstr ""
msgid "Component type must be specified when component name is specified"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1338 netbox/ipam/forms/bulk_import.py:314
#: netbox/dcim/forms/bulk_import.py:1338 netbox/ipam/forms/bulk_import.py:310
msgid "Parent device of assigned interface (if any)"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1341 netbox/ipam/forms/bulk_import.py:317
#: netbox/dcim/forms/bulk_import.py:1341 netbox/ipam/forms/bulk_import.py:313
#: netbox/virtualization/filtersets.py:259
#: netbox/virtualization/filtersets.py:310
#: netbox/virtualization/forms/bulk_edit.py:182
@@ -5265,12 +5265,12 @@ msgstr ""
msgid "Virtual machine"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1345 netbox/ipam/forms/bulk_import.py:321
#: netbox/dcim/forms/bulk_import.py:1345 netbox/ipam/forms/bulk_import.py:317
msgid "Parent VM of assigned interface (if any)"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1352 netbox/ipam/filtersets.py:1035
#: netbox/ipam/forms/bulk_import.py:328
#: netbox/ipam/forms/bulk_import.py:324
msgid "Assigned interface"
msgstr ""
@@ -5654,7 +5654,7 @@ msgstr ""
msgid "Please select a {scope_type}."
msgstr ""
#: netbox/dcim/forms/mixins.py:117 netbox/ipam/forms/bulk_import.py:466
#: netbox/dcim/forms/mixins.py:117 netbox/ipam/forms/bulk_import.py:462
msgid "Scope type (app & model)"
msgstr ""
@@ -6277,12 +6277,12 @@ msgid ""
msgstr ""
#: netbox/dcim/models/device_component_templates.py:777
#: netbox/dcim/models/device_components.py:1340
#: netbox/dcim/models/device_components.py:1342
msgid "part ID"
msgstr ""
#: netbox/dcim/models/device_component_templates.py:779
#: netbox/dcim/models/device_components.py:1342
#: netbox/dcim/models/device_components.py:1344
msgid "Manufacturer-assigned part identifier"
msgstr ""
@@ -6404,7 +6404,7 @@ msgstr ""
#: netbox/dcim/models/device_components.py:605
#: netbox/dcim/tables/devices.py:631 netbox/ipam/forms/bulk_edit.py:521
#: netbox/ipam/forms/bulk_import.py:528 netbox/ipam/forms/filtersets.py:587
#: netbox/ipam/forms/bulk_import.py:524 netbox/ipam/forms/filtersets.py:587
#: netbox/ipam/forms/model_forms.py:694 netbox/ipam/tables/vlans.py:109
#: netbox/templates/dcim/interface.html:86 netbox/templates/ipam/vlan.html:77
#: netbox/templates/virtualization/vminterface.html:60
@@ -6631,83 +6631,83 @@ msgstr ""
msgid "A module bay cannot belong to a module installed within it."
msgstr ""
#: netbox/dcim/models/device_components.py:1243
#: netbox/dcim/models/device_components.py:1245
msgid "device bay"
msgstr ""
#: netbox/dcim/models/device_components.py:1244
#: netbox/dcim/models/device_components.py:1246
msgid "device bays"
msgstr ""
#: netbox/dcim/models/device_components.py:1251
#: netbox/dcim/models/device_components.py:1253
#, python-brace-format
msgid "This type of device ({device_type}) does not support device bays."
msgstr ""
#: netbox/dcim/models/device_components.py:1257
#: netbox/dcim/models/device_components.py:1259
msgid "Cannot install a device into itself."
msgstr ""
#: netbox/dcim/models/device_components.py:1265
#: netbox/dcim/models/device_components.py:1267
#, python-brace-format
msgid ""
"Cannot install the specified device; device is already installed in {bay}."
msgstr ""
#: netbox/dcim/models/device_components.py:1286
#: netbox/dcim/models/device_components.py:1288
msgid "inventory item role"
msgstr ""
#: netbox/dcim/models/device_components.py:1287
#: netbox/dcim/models/device_components.py:1289
msgid "inventory item roles"
msgstr ""
#: netbox/dcim/models/device_components.py:1346
#: netbox/dcim/models/device_components.py:1348
#: netbox/dcim/models/devices.py:533 netbox/dcim/models/modules.py:217
#: netbox/dcim/models/racks.py:310
#: netbox/virtualization/models/virtualmachines.py:125
msgid "serial number"
msgstr ""
#: netbox/dcim/models/device_components.py:1354
#: netbox/dcim/models/device_components.py:1356
#: netbox/dcim/models/devices.py:541 netbox/dcim/models/modules.py:224
#: netbox/dcim/models/racks.py:317
msgid "asset tag"
msgstr ""
#: netbox/dcim/models/device_components.py:1355
#: netbox/dcim/models/device_components.py:1357
msgid "A unique tag used to identify this item"
msgstr ""
#: netbox/dcim/models/device_components.py:1358
#: netbox/dcim/models/device_components.py:1360
msgid "discovered"
msgstr ""
#: netbox/dcim/models/device_components.py:1360
#: netbox/dcim/models/device_components.py:1362
msgid "This item was automatically discovered"
msgstr ""
#: netbox/dcim/models/device_components.py:1378
#: netbox/dcim/models/device_components.py:1380
msgid "inventory item"
msgstr ""
#: netbox/dcim/models/device_components.py:1379
#: netbox/dcim/models/device_components.py:1381
msgid "inventory items"
msgstr ""
#: netbox/dcim/models/device_components.py:1387
#: netbox/dcim/models/device_components.py:1389
msgid "Cannot assign self as parent."
msgstr ""
#: netbox/dcim/models/device_components.py:1395
#: netbox/dcim/models/device_components.py:1397
msgid "Parent inventory item does not belong to the same device."
msgstr ""
#: netbox/dcim/models/device_components.py:1401
#: netbox/dcim/models/device_components.py:1403
msgid "Cannot move an inventory item with dependent children"
msgstr ""
#: netbox/dcim/models/device_components.py:1409
#: netbox/dcim/models/device_components.py:1411
msgid "Cannot assign inventory item to component on another device"
msgstr ""
@@ -6867,12 +6867,12 @@ msgstr ""
msgid "rack face"
msgstr ""
#: netbox/dcim/models/devices.py:598 netbox/dcim/models/devices.py:1218
#: netbox/dcim/models/devices.py:598 netbox/dcim/models/devices.py:1223
#: netbox/virtualization/models/virtualmachines.py:94
msgid "primary IPv4"
msgstr ""
#: netbox/dcim/models/devices.py:606 netbox/dcim/models/devices.py:1226
#: netbox/dcim/models/devices.py:606 netbox/dcim/models/devices.py:1231
#: netbox/virtualization/models/virtualmachines.py:102
msgid "primary IPv6"
msgstr ""
@@ -7020,68 +7020,68 @@ msgid ""
"is currently designated as its master."
msgstr ""
#: netbox/dcim/models/devices.py:1133
#: netbox/dcim/models/devices.py:1138
msgid "domain"
msgstr ""
#: netbox/dcim/models/devices.py:1146 netbox/dcim/models/devices.py:1147
#: netbox/dcim/models/devices.py:1151 netbox/dcim/models/devices.py:1152
msgid "virtual chassis"
msgstr ""
#: netbox/dcim/models/devices.py:1159
#: netbox/dcim/models/devices.py:1164
#, python-brace-format
msgid "The selected master ({master}) is not assigned to this virtual chassis."
msgstr ""
#: netbox/dcim/models/devices.py:1174
#: netbox/dcim/models/devices.py:1179
#, python-brace-format
msgid ""
"Unable to delete virtual chassis {self}. There are member interfaces which "
"form a cross-chassis LAG interfaces."
msgstr ""
#: netbox/dcim/models/devices.py:1207 netbox/vpn/models/l2vpn.py:42
#: netbox/dcim/models/devices.py:1212 netbox/vpn/models/l2vpn.py:42
msgid "identifier"
msgstr ""
#: netbox/dcim/models/devices.py:1208
#: netbox/dcim/models/devices.py:1213
msgid "Numeric identifier unique to the parent device"
msgstr ""
#: netbox/dcim/models/devices.py:1236 netbox/extras/models/customfields.py:231
#: netbox/dcim/models/devices.py:1241 netbox/extras/models/customfields.py:231
#: netbox/extras/models/models.py:111 netbox/extras/models/models.py:800
#: netbox/netbox/models/__init__.py:120 netbox/netbox/models/__init__.py:155
msgid "comments"
msgstr ""
#: netbox/dcim/models/devices.py:1252
#: netbox/dcim/models/devices.py:1257
msgid "virtual device context"
msgstr ""
#: netbox/dcim/models/devices.py:1253
#: netbox/dcim/models/devices.py:1258
msgid "virtual device contexts"
msgstr ""
#: netbox/dcim/models/devices.py:1282
#: netbox/dcim/models/devices.py:1287
#, python-brace-format
msgid "{ip} is not an IPv{family} address."
msgstr ""
#: netbox/dcim/models/devices.py:1288
#: netbox/dcim/models/devices.py:1293
msgid "Primary IP address must belong to an interface on the assigned device."
msgstr ""
#: netbox/dcim/models/devices.py:1319
#: netbox/dcim/models/devices.py:1324
msgid "MAC addresses"
msgstr ""
#: netbox/dcim/models/devices.py:1351
#: netbox/dcim/models/devices.py:1356
msgid ""
"Cannot unassign MAC Address while it is designated as the primary MAC for an "
"object"
msgstr ""
#: netbox/dcim/models/devices.py:1355
#: netbox/dcim/models/devices.py:1360
msgid ""
"Cannot reassign MAC Address while it is designated as the primary MAC for an "
"object"
@@ -7324,8 +7324,8 @@ msgid "Locally-assigned identifier"
msgstr ""
#: netbox/dcim/models/racks.py:305 netbox/ipam/forms/bulk_import.py:204
#: netbox/ipam/forms/bulk_import.py:272 netbox/ipam/forms/bulk_import.py:307
#: netbox/ipam/forms/bulk_import.py:519
#: netbox/ipam/forms/bulk_import.py:268 netbox/ipam/forms/bulk_import.py:303
#: netbox/ipam/forms/bulk_import.py:515
#: netbox/virtualization/forms/bulk_import.py:123
msgid "Functional role"
msgstr ""
@@ -7576,7 +7576,7 @@ msgid "U Height"
msgstr ""
#: netbox/dcim/tables/devices.py:210 netbox/dcim/tables/devices.py:1128
#: netbox/ipam/forms/bulk_import.py:601 netbox/ipam/forms/model_forms.py:317
#: netbox/ipam/forms/bulk_import.py:597 netbox/ipam/forms/model_forms.py:317
#: netbox/ipam/forms/model_forms.py:330 netbox/ipam/tables/ip.py:314
#: netbox/ipam/tables/ip.py:381 netbox/ipam/tables/ip.py:391
#: netbox/ipam/tables/ip.py:414 netbox/templates/ipam/ipaddress.html:11
@@ -7942,7 +7942,7 @@ msgstr ""
#: netbox/dcim/tables/sites.py:34 netbox/dcim/tables/sites.py:68
#: netbox/extras/forms/filtersets.py:424 netbox/extras/forms/model_forms.py:630
#: netbox/ipam/forms/bulk_edit.py:134 netbox/ipam/forms/model_forms.py:160
#: netbox/ipam/tables/asn.py:66 netbox/netbox/navigation/menu.py:15
#: netbox/ipam/tables/asn.py:76 netbox/netbox/navigation/menu.py:15
#: netbox/netbox/navigation/menu.py:19
msgid "Sites"
msgstr ""
@@ -7987,31 +7987,31 @@ msgstr ""
msgid "Virtual Machines"
msgstr ""
#: netbox/dcim/views.py:3237
#: netbox/dcim/views.py:3238
#, python-brace-format
msgid "Installed device {device} in bay {device_bay}."
msgstr ""
#: netbox/dcim/views.py:3278
#: netbox/dcim/views.py:3279
#, python-brace-format
msgid "Removed device {device} from bay {device_bay}."
msgstr ""
#: netbox/dcim/views.py:3391 netbox/ipam/tables/ip.py:181
#: netbox/dcim/views.py:3392 netbox/ipam/tables/ip.py:181
msgid "Children"
msgstr ""
#: netbox/dcim/views.py:3864
#: netbox/dcim/views.py:3865
#, python-brace-format
msgid "Added member <a href=\"{url}\">{device}</a>"
msgstr ""
#: netbox/dcim/views.py:3909
#: netbox/dcim/views.py:3910
#, python-brace-format
msgid "Unable to remove master device {device} from the virtual chassis."
msgstr ""
#: netbox/dcim/views.py:3920
#: netbox/dcim/views.py:3921
#, python-brace-format
msgid "Removed {device} from virtual chassis {chassis}"
msgstr ""
@@ -9267,113 +9267,113 @@ msgstr ""
msgid "Filter must be defined as a dictionary mapping attributes to values."
msgstr ""
#: netbox/extras/models/customfields.py:488
#: netbox/extras/models/customfields.py:496
msgid "True"
msgstr ""
#: netbox/extras/models/customfields.py:489
#: netbox/extras/models/customfields.py:497
msgid "False"
msgstr ""
#: netbox/extras/models/customfields.py:542
#: netbox/extras/models/customfields.py:590
#: netbox/extras/models/customfields.py:550
#: netbox/extras/models/customfields.py:598
#, python-brace-format
msgid "Values must match this regex: <code>{regex}</code>"
msgstr ""
#: netbox/extras/models/customfields.py:692
#: netbox/extras/models/customfields.py:699
#: netbox/extras/models/customfields.py:700
#: netbox/extras/models/customfields.py:707
msgid "Value must be a string."
msgstr ""
#: netbox/extras/models/customfields.py:694
#: netbox/extras/models/customfields.py:701
#: netbox/extras/models/customfields.py:702
#: netbox/extras/models/customfields.py:709
#, python-brace-format
msgid "Value must match regex '{regex}'"
msgstr ""
#: netbox/extras/models/customfields.py:706
#: netbox/extras/models/customfields.py:714
msgid "Value must be an integer."
msgstr ""
#: netbox/extras/models/customfields.py:709
#: netbox/extras/models/customfields.py:724
#: netbox/extras/models/customfields.py:717
#: netbox/extras/models/customfields.py:732
#, python-brace-format
msgid "Value must be at least {minimum}"
msgstr ""
#: netbox/extras/models/customfields.py:713
#: netbox/extras/models/customfields.py:728
#: netbox/extras/models/customfields.py:721
#: netbox/extras/models/customfields.py:736
#, python-brace-format
msgid "Value must not exceed {maximum}"
msgstr ""
#: netbox/extras/models/customfields.py:721
#: netbox/extras/models/customfields.py:729
msgid "Value must be a decimal."
msgstr ""
#: netbox/extras/models/customfields.py:733
#: netbox/extras/models/customfields.py:741
msgid "Value must be true or false."
msgstr ""
#: netbox/extras/models/customfields.py:741
#: netbox/extras/models/customfields.py:749
msgid "Date values must be in ISO 8601 format (YYYY-MM-DD)."
msgstr ""
#: netbox/extras/models/customfields.py:750
#: netbox/extras/models/customfields.py:758
msgid "Date and time values must be in ISO 8601 format (YYYY-MM-DD HH:MM:SS)."
msgstr ""
#: netbox/extras/models/customfields.py:757
#: netbox/extras/models/customfields.py:765
#, python-brace-format
msgid "Invalid choice ({value}) for choice set {choiceset}."
msgstr ""
#: netbox/extras/models/customfields.py:767
#: netbox/extras/models/customfields.py:775
#, python-brace-format
msgid "Invalid choice(s) ({value}) for choice set {choiceset}."
msgstr ""
#: netbox/extras/models/customfields.py:776
#: netbox/extras/models/customfields.py:784
#, python-brace-format
msgid "Value must be an object ID, not {type}"
msgstr ""
#: netbox/extras/models/customfields.py:782
#: netbox/extras/models/customfields.py:790
#, python-brace-format
msgid "Value must be a list of object IDs, not {type}"
msgstr ""
#: netbox/extras/models/customfields.py:786
#: netbox/extras/models/customfields.py:794
#, python-brace-format
msgid "Found invalid object ID: {id}"
msgstr ""
#: netbox/extras/models/customfields.py:789
#: netbox/extras/models/customfields.py:797
msgid "Required field cannot be empty."
msgstr ""
#: netbox/extras/models/customfields.py:809
#: netbox/extras/models/customfields.py:817
msgid "Base set of predefined choices (optional)"
msgstr ""
#: netbox/extras/models/customfields.py:821
#: netbox/extras/models/customfields.py:829
msgid "Choices are automatically ordered alphabetically"
msgstr ""
#: netbox/extras/models/customfields.py:828
#: netbox/extras/models/customfields.py:836
msgid "custom field choice set"
msgstr ""
#: netbox/extras/models/customfields.py:829
#: netbox/extras/models/customfields.py:837
msgid "custom field choice sets"
msgstr ""
#: netbox/extras/models/customfields.py:871
#: netbox/extras/models/customfields.py:879
msgid "Must define base or extra choices."
msgstr ""
#: netbox/extras/models/customfields.py:895
#: netbox/extras/models/customfields.py:903
#, python-brace-format
msgid ""
"Cannot remove choice {choice} as there are {model} objects which reference "
@@ -10130,7 +10130,7 @@ msgstr ""
msgid "Customer"
msgstr ""
#: netbox/ipam/fields.py:39
#: netbox/ipam/fields.py:40
#, python-brace-format
msgid "Invalid IP address format: {address}"
msgstr ""
@@ -10356,9 +10356,9 @@ msgstr ""
#: netbox/ipam/forms/filtersets.py:151 netbox/ipam/forms/model_forms.py:100
#: netbox/ipam/forms/model_forms.py:113 netbox/ipam/forms/model_forms.py:136
#: netbox/ipam/forms/model_forms.py:155 netbox/ipam/models/asns.py:32
#: netbox/ipam/models/asns.py:101 netbox/ipam/models/ip.py:72
#: netbox/ipam/models/asns.py:132 netbox/ipam/models/ip.py:72
#: netbox/ipam/models/ip.py:88 netbox/ipam/tables/asn.py:20
#: netbox/ipam/tables/asn.py:45 netbox/templates/ipam/aggregate.html:18
#: netbox/ipam/tables/asn.py:55 netbox/templates/ipam/aggregate.html:18
#: netbox/templates/ipam/asn.html:27 netbox/templates/ipam/asnrange.html:19
#: netbox/templates/ipam/rir.html:19
msgid "RIR"
@@ -10419,8 +10419,8 @@ msgid "DNS name"
msgstr ""
#: netbox/ipam/forms/bulk_edit.py:376 netbox/ipam/forms/bulk_edit.py:573
#: netbox/ipam/forms/bulk_import.py:447 netbox/ipam/forms/bulk_import.py:565
#: netbox/ipam/forms/bulk_import.py:593 netbox/ipam/forms/filtersets.py:414
#: netbox/ipam/forms/bulk_import.py:443 netbox/ipam/forms/bulk_import.py:561
#: netbox/ipam/forms/bulk_import.py:589 netbox/ipam/forms/filtersets.py:414
#: netbox/ipam/forms/filtersets.py:604 netbox/templates/ipam/fhrpgroup.html:22
#: netbox/templates/ipam/inc/panels/fhrp_groups.html:24
#: netbox/templates/ipam/service.html:34
@@ -10464,7 +10464,7 @@ msgstr ""
msgid "VLAN ID ranges"
msgstr ""
#: netbox/ipam/forms/bulk_edit.py:516 netbox/ipam/forms/bulk_import.py:522
#: netbox/ipam/forms/bulk_edit.py:516 netbox/ipam/forms/bulk_import.py:518
#: netbox/ipam/forms/filtersets.py:579 netbox/ipam/models/vlans.py:250
#: netbox/ipam/tables/vlans.py:106
msgid "Q-in-Q role"
@@ -10478,7 +10478,7 @@ msgstr ""
msgid "Site & Group"
msgstr ""
#: netbox/ipam/forms/bulk_edit.py:557 netbox/ipam/forms/bulk_import.py:552
#: netbox/ipam/forms/bulk_edit.py:557 netbox/ipam/forms/bulk_import.py:548
#: netbox/ipam/forms/model_forms.py:726 netbox/ipam/tables/vlans.py:259
#: netbox/templates/ipam/vlantranslationrule.html:14
#: netbox/vpn/forms/model_forms.py:322 netbox/vpn/forms/model_forms.py:359
@@ -10523,86 +10523,86 @@ msgstr ""
msgid "Scope ID"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:331 netbox/ipam/forms/filtersets.py:636
#: netbox/ipam/forms/bulk_import.py:327 netbox/ipam/forms/filtersets.py:636
#: netbox/ipam/forms/model_forms.py:306 netbox/ipam/forms/model_forms.py:336
#: netbox/ipam/forms/model_forms.py:517 netbox/templates/ipam/fhrpgroup.html:19
msgid "FHRP Group"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:335
#: netbox/ipam/forms/bulk_import.py:331
msgid "Assigned FHRP Group name"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:339
#: netbox/ipam/forms/bulk_import.py:335
msgid "Make this the primary IP for the assigned device"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:343
#: netbox/ipam/forms/bulk_import.py:339
msgid "Is out-of-band"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:344
#: netbox/ipam/forms/bulk_import.py:340
msgid "Designate this as the out-of-band IP address for the assigned device"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:398
#: netbox/ipam/forms/bulk_import.py:394
msgid "No device or virtual machine specified; cannot set as primary IP"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:402
#: netbox/ipam/forms/bulk_import.py:398
msgid "No device specified; cannot set as out-of-band IP"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:406
#: netbox/ipam/forms/bulk_import.py:402
msgid "Cannot set out-of-band IP for virtual machines"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:410
#: netbox/ipam/forms/bulk_import.py:406
msgid "No interface specified; cannot set as primary IP"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:414
#: netbox/ipam/forms/bulk_import.py:410
msgid "No interface specified; cannot set as out-of-band IP"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:451
#: netbox/ipam/forms/bulk_import.py:447
msgid "Auth type"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:500
#: netbox/ipam/forms/bulk_import.py:496
msgid "Assigned VLAN group"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:532
#: netbox/ipam/forms/bulk_import.py:528
msgid "Service VLAN (for Q-in-Q/802.1ad customer VLANs)"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:555 netbox/ipam/models/vlans.py:369
#: netbox/ipam/forms/bulk_import.py:551 netbox/ipam/models/vlans.py:369
msgid "VLAN translation policy"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:567 netbox/ipam/forms/bulk_import.py:595
#: netbox/ipam/forms/bulk_import.py:563 netbox/ipam/forms/bulk_import.py:591
msgid "IP protocol"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:579
#: netbox/ipam/forms/bulk_import.py:575
msgid "Parent type (app & model)"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:586
#: netbox/ipam/forms/bulk_import.py:582
msgid "Parent object name"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:590
#: netbox/ipam/forms/bulk_import.py:586
msgid "Parent object ID"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:642
#: netbox/ipam/forms/bulk_import.py:638
msgid ""
"One of parent or parent_object_id must be included with parent_object_type"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:655
#: netbox/ipam/forms/bulk_import.py:651
#, python-brace-format
msgid "{ip} is not assigned to this parent."
msgstr ""
@@ -10834,16 +10834,16 @@ msgstr ""
msgid "ASN ranges"
msgstr ""
#: netbox/ipam/models/asns.py:70
#: netbox/ipam/models/asns.py:63
#, python-brace-format
msgid "Starting ASN ({start}) must be lower than ending ASN ({end})."
msgstr ""
#: netbox/ipam/models/asns.py:102
#: netbox/ipam/models/asns.py:133
msgid "Regional Internet Registry responsible for this AS number space"
msgstr ""
#: netbox/ipam/models/asns.py:107
#: netbox/ipam/models/asns.py:138
msgid "16- or 32-bit autonomous system number"
msgstr ""
@@ -11261,15 +11261,23 @@ msgstr ""
msgid "route targets"
msgstr ""
#: netbox/ipam/tables/asn.py:52
msgid "ASDOT"
#: netbox/ipam/tables/asn.py:26
msgid "Start (ASDOT)"
msgstr ""
#: netbox/ipam/tables/asn.py:57
msgid "Site Count"
#: netbox/ipam/tables/asn.py:31
msgid "End (ASDOT)"
msgstr ""
#: netbox/ipam/tables/asn.py:62
msgid "ASDOT"
msgstr ""
#: netbox/ipam/tables/asn.py:67
msgid "Site Count"
msgstr ""
#: netbox/ipam/tables/asn.py:72
msgid "Provider Count"
msgstr ""
@@ -12523,30 +12531,34 @@ msgid "Japanese"
msgstr ""
#: netbox/netbox/settings.py:830
msgid "Dutch"
msgid "Latvian"
msgstr ""
#: netbox/netbox/settings.py:831
msgid "Polish"
msgid "Dutch"
msgstr ""
#: netbox/netbox/settings.py:832
msgid "Portuguese"
msgid "Polish"
msgstr ""
#: netbox/netbox/settings.py:833
msgid "Russian"
msgid "Portuguese"
msgstr ""
#: netbox/netbox/settings.py:834
msgid "Turkish"
msgid "Russian"
msgstr ""
#: netbox/netbox/settings.py:835
msgid "Ukrainian"
msgid "Turkish"
msgstr ""
#: netbox/netbox/settings.py:836
msgid "Ukrainian"
msgstr ""
#: netbox/netbox/settings.py:837
msgid "Chinese"
msgstr ""
@@ -12581,69 +12593,75 @@ msgstr ""
msgid "Dummy Plugin"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:122
#: netbox/netbox/views/generic/bulk_views.py:124
#, python-brace-format
msgid ""
"There was an error rendering the selected export template ({template}): "
"{error}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:388
#: netbox/netbox/views/generic/bulk_views.py:390
msgid "Must be a list."
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:398
#: netbox/netbox/views/generic/bulk_views.py:400
msgid "Must be a dictionary."
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:461
#: netbox/netbox/views/generic/bulk_views.py:453
#, python-brace-format
msgid ""
"Duplicate objects found: {model} with ID(s) {ids} appears multiple times"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:475
#, python-brace-format
msgid "Object with ID {id} does not exist"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:546
#: netbox/netbox/views/generic/bulk_views.py:560
#, python-brace-format
msgid "Bulk import {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:562
#: netbox/netbox/views/generic/bulk_views.py:576
#, python-brace-format
msgid "Imported {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:752
#: netbox/netbox/views/generic/bulk_views.py:766
#, python-brace-format
msgid "Bulk edit {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:768
#: netbox/netbox/views/generic/bulk_views.py:782
#, python-brace-format
msgid "Updated {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:801
#: netbox/netbox/views/generic/bulk_views.py:1036
#: netbox/netbox/views/generic/bulk_views.py:1084
#: netbox/netbox/views/generic/bulk_views.py:815
#: netbox/netbox/views/generic/bulk_views.py:1050
#: netbox/netbox/views/generic/bulk_views.py:1098
#, python-brace-format
msgid "No {object_type} were selected."
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:894
#: netbox/netbox/views/generic/bulk_views.py:908
#, python-brace-format
msgid "Renamed {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:964
#: netbox/netbox/views/generic/bulk_views.py:978
#, python-brace-format
msgid "Bulk delete {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:991
#: netbox/netbox/views/generic/bulk_views.py:1005
#, python-brace-format
msgid "Deleted {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:1008
#: netbox/netbox/views/generic/bulk_views.py:1022
msgid "Deletion failed due to the presence of one or more dependent objects."
msgstr ""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -50,21 +50,21 @@ class RestrictedQuerySet(QuerySet):
# Bypass restriction for superusers and exempt views
if user and user.is_superuser or permission_is_exempt(permission_required):
qs = self
return self
# User is anonymous or has not been granted the requisite permission
elif user is None or not user.is_authenticated or permission_required not in user.get_all_permissions():
qs = self.none()
if user is None or not user.is_authenticated or permission_required not in user.get_all_permissions():
return self.none()
# Filter the queryset to include only objects with allowed attributes
else:
tokens = {
CONSTRAINT_TOKEN_USER: user,
}
attrs = qs_filter_from_constraints(user._object_perm_cache[permission_required], tokens)
constraints = user._object_perm_cache[permission_required]
tokens = {
CONSTRAINT_TOKEN_USER: user,
}
if attrs := qs_filter_from_constraints(constraints, tokens):
# #8715: Avoid duplicates when JOIN on many-to-many fields without using DISTINCT.
# DISTINCT acts globally on the entire request, which may not be desirable.
allowed_objects = self.model.objects.filter(attrs)
qs = self.filter(pk__in=allowed_objects)
return self.filter(pk__in=allowed_objects)
return qs
return self

View File

@@ -141,8 +141,8 @@ class ModelTestCase(TestCase):
elif value and type(field) is GenericForeignKey:
model_dict[key] = value.pk
# Handle API output
elif api:
# Replace ContentType numeric IDs with <app_label>.<model>
if type(getattr(instance, key)) in (ContentType, ObjectType):
object_type = ObjectType.objects.get(pk=value)
@@ -152,9 +152,13 @@ class ModelTestCase(TestCase):
elif type(value) is IPNetwork:
model_dict[key] = str(value)
else:
field = instance._meta.get_field(key)
# Normalize arrays of numeric ranges (e.g. VLAN IDs or port ranges).
# DB uses canonical half-open [lo, hi) via NumericRange; API uses inclusive [lo, hi].
# Convert to inclusive pairs for stable API comparisons.
elif type(field) is ArrayField and issubclass(type(field.base_field), RangeField):
model_dict[key] = [[r.lower, r.upper - 1] for r in value]
else:
# Convert ArrayFields to CSV strings
if type(field) is ArrayField:
if getattr(field.base_field, 'choices', None):

View File

@@ -3,7 +3,7 @@
[project]
name = "netbox"
version = "4.4.7"
version = "4.4.9"
requires-python = ">=3.10"
description = "The premier source of truth powering network automation."
readme = "README.md"

View File

@@ -23,7 +23,7 @@ gunicorn==23.0.0
Jinja2==3.1.6
jsonschema==4.25.1
Markdown==3.10
mkdocs-material==9.7.0
mkdocs-material==9.7.1
mkdocstrings==1.0.0
mkdocstrings-python==2.0.1
netaddr==1.3.0
@@ -33,11 +33,11 @@ psycopg[c,pool]==3.3.2
PyYAML==6.0.3
requests==2.32.5
rq==2.6.1
social-auth-app-django==5.6.0
social-auth-core==4.8.1
social-auth-app-django==5.7.0
social-auth-core==4.8.3
sorl-thumbnail==12.11.0
strawberry-graphql==0.287.2
strawberry-graphql==0.287.3
strawberry-graphql-django==0.70.1
svgwrite==1.4.3
tablib==3.9.0
tzdata==2025.2
tzdata==2025.3