Compare commits

..

2 Commits

Author SHA1 Message Date
Vincent Simonin
b0f7024dcb Merge 605c61ef5b into f0507d00bf 2025-12-10 16:05:56 +09:00
Vincent Simonin
605c61ef5b 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.
2025-12-08 17:06:13 +01:00
62 changed files with 7352 additions and 25670 deletions

View File

@@ -15,7 +15,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v4.4.9
placeholder: v4.4.8
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.9
placeholder: v4.4.8
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-16-blue" alt="Languages supported" /></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://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.9",
"version": "4.4.8",
"license": {
"name": "Apache v2 License"
}
@@ -158511,7 +158511,6 @@
"fr",
"it",
"ja",
"lv",
"nl",
"pl",
"pt",
@@ -205631,9 +205630,15 @@
"description": {
"type": "string",
"maxLength": 200
},
"devicetype_count": {
"type": "integer",
"format": "int64",
"readOnly": true
}
},
"required": [
"devicetype_count",
"display",
"id",
"name",

View File

@@ -1,31 +1,5 @@
# 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,20 +63,16 @@ class ConfigRevision(models.Model):
return reverse('core:config') # Default config view
return reverse('core:configrevision', args=[self.pk])
def activate(self, update_db=True):
def activate(self):
"""
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)
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)
# 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

@@ -47,6 +47,7 @@ clear_events = Signal()
# Object types
#
@receiver(post_migrate)
def update_object_types(sender, **kwargs):
"""
@@ -133,7 +134,7 @@ def handle_changed_object(sender, instance, **kwargs):
prev_change := ObjectChange.objects.filter(
changed_object_type=ContentType.objects.get_for_model(instance),
changed_object_id=instance.pk,
request_id=request.id
request_id=request.id,
).first()
):
prev_change.postchange_data = objectchange.postchange_data
@@ -172,9 +173,7 @@ def handle_deleted_object(sender, instance, **kwargs):
try:
run_validators(instance, validators)
except ValidationError as e:
raise AbortRequest(
_("Deletion is prevented by a protection rule: {message}").format(message=e)
)
raise AbortRequest(_("Deletion is prevented by a protection rule: {message}").format(message=e))
# Get the current request, or bail if not set
request = current_request.get()
@@ -221,7 +220,12 @@ 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 not in (CASCADE, RESTRICT):
elif (
type(relation) is ManyToOneRel
and relation.null
and relation.on_delete is not CASCADE
and relation.on_delete is not RESTRICT
):
setattr(obj, related_field_name, None)
obj.save()
@@ -256,6 +260,7 @@ def clear_events_queue(sender, **kwargs):
# DataSource handlers
#
@receiver(post_save, sender=DataSource)
def enqueue_sync_job(instance, created, **kwargs):
"""
@@ -267,9 +272,10 @@ def enqueue_sync_job(instance, created, **kwargs):
SyncDataSourceJob.enqueue_once(instance, interval=instance.sync_interval)
elif not created:
# Delete any previously scheduled recurring jobs for this DataSource
for job in SyncDataSourceJob.get_jobs(instance).defer('data').filter(
interval__isnull=False,
status=JobStatusChoices.STATUS_SCHEDULED
for job in (
SyncDataSourceJob.get_jobs(instance)
.defer('data')
.filter(interval__isnull=False, status=JobStatusChoices.STATUS_SCHEDULED)
):
# Call delete() per instance to ensure the associated background task is deleted as well
job.delete()

View File

@@ -350,14 +350,14 @@ class ModuleBaySerializer(NetBoxModelSerializer):
device = DeviceSerializer(nested=True)
module = ModuleSerializer(
nested=True,
fields=('id', 'url', 'display', 'device', 'module_bay'),
fields=('id', 'url', 'display'),
required=False,
allow_null=True,
default=None
)
installed_module = ModuleSerializer(
nested=True,
fields=('id', 'url', 'display', 'device', 'module_bay', 'serial', 'description'),
fields=('id', 'url', 'display', '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')
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count')

View File

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

View File

@@ -1222,8 +1222,6 @@ 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,11 +957,6 @@ 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,12 +315,6 @@ 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,15 +1,13 @@
import logging
from django.db.models.signals import post_delete, post_save
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from dcim.choices import CableEndChoices, LinkStatusChoices
from ipam.models import Prefix
from virtualization.models import Cluster, VMInterface
from wireless.models import WirelessLAN
from virtualization.models import VMInterface
from .models import (
Cable, CablePath, CableTermination, ConsolePort, ConsoleServerPort, Device, DeviceBay, FrontPort, Interface,
InventoryItem, Location, ModuleBay, PathEndpoint, PowerOutlet, PowerPanel, PowerPort, Rack, RearPort, Site,
InventoryItem, ModuleBay, PathEndpoint, PowerOutlet, PowerPanel, PowerPort, Rack, RearPort, Location,
VirtualChassis,
)
from .models.cables import trace_paths
@@ -46,9 +44,6 @@ 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)
@@ -58,12 +53,6 @@ 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)
@@ -182,40 +171,3 @@ 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', 'display', 'id', 'name', 'slug', 'url']
brief_fields = ['description', 'devicetype_count', 'display', 'id', 'name', 'slug', 'url']
create_data = [
{
'name': 'Manufacturer 4',

View File

@@ -841,32 +841,6 @@ 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,32 +2322,6 @@ 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,12 +2454,11 @@ 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 parent_bay:
device_bay = parent_bay
if getattr(obj, 'parent_bay', None):
device_bay = obj.parent_bay
device_bay.installed_device = obj
device_bay.save()

View File

@@ -449,14 +449,7 @@ 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,
for_filterset_form=False,
):
def to_form_field(self, set_initial=True, enforce_required=True, enforce_visibility=True, for_csv_import=False):
"""
Return a form field suitable for setting a CustomField's value for an object.
@@ -464,7 +457,6 @@ 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
@@ -527,7 +519,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 and not for_filterset_form:
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
field_class = DynamicChoiceField
widget_class = APISelect
else:

View File

@@ -16,7 +16,6 @@ __all__ = (
# BGP ASN bounds
BGP_ASN_MIN = 1
BGP_ASN_MAX = 2**32 - 1
BGP_ASN_ASDOT_BASE = 2**16
class BaseIPField(models.Field):
@@ -127,16 +126,3 @@ 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,6 +230,10 @@ 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,6 +55,13 @@ 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()
@@ -65,45 +72,7 @@ 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
@@ -162,20 +131,20 @@ class ASN(ContactsMixin, PrimaryModel):
"""
Return ASDOT notation for AS numbers greater than 16 bits.
"""
return ASNField.to_asdot(self.asn)
if self.asn > 65535:
return f'{self.asn // 65536}.{self.asn % 65536}'
return self.asn
@property
def asn_with_asdot(self):
"""
Return both plain and ASDOT notation, where applicable.
"""
if self.asn >= 65536:
return f'{self.asn} ({self.asn_asdot})'
return str(self.asn)
if self.asn > 65535:
return f'{self.asn} ({self.asn // 65536}.{self.asn % 65536})'
else:
return 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_qs = IPRange.objects.filter(
parent_range = IPRange.objects.filter(
start_address__lte=self.address,
end_address__gte=self.address,
vrf=self.vrf,
mark_populated=True
)
if not self.pk and (parent_range := parent_range_qs.first()):
).first()
if parent_range:
raise ValidationError({
'address': _(
"Cannot create IP address {ip} inside range {range}."

View File

@@ -20,16 +20,6 @@ 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'
)
@@ -40,8 +30,8 @@ class ASNRangeTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = ASNRange
fields = (
'pk', 'name', 'slug', 'rir', 'start', 'start_asdot', 'end', 'end_asdot', 'asn_count', 'tenant',
'tenant_group', 'description', 'tags', 'created', 'last_updated', 'actions',
'pk', 'name', 'slug', 'rir', 'start', 'end', '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,17 +1071,14 @@ 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,82 +564,6 @@ 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,21 +80,22 @@ 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 configuration found in database; proceeding with default values")
logger.debug("No previous configuration found in database; proceeding with default values")
return
logger.debug(f"No active configuration revision found; falling back to most recent (#{revision.pk})")
logger.debug(f"Using fallback configuration revision #{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(update_db=False)
self._populate_from_cache()
revision.activate()
logger.debug("Filled cache with data from latest ConfigRevision")
self._populate_from_cache()
class ConfigItem:

View File

@@ -205,6 +205,4 @@ 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, for_filterset_form=True
)
return customfield.to_form_field(set_initial=False, enforce_required=False, enforce_visibility=False)

View File

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

View File

@@ -1,6 +1,5 @@
import logging
import re
from collections import Counter
from copy import deepcopy
from django.contrib import messages
@@ -34,7 +33,6 @@ 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
@@ -445,18 +443,6 @@ 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.4.1",
"gridstack": "12.3.3",
"htmx.org": "2.0.8",
"query-string": "9.3.1",
"sass": "1.97.1",
"sass": "1.95.0",
"tom-select": "2.4.3",
"typeface-inter": "3.18.1",
"typeface-roboto-mono": "1.1.13"

View File

@@ -28,27 +28,13 @@ function updateElements(targetMode: ColorMode): void {
}
for (const elevation of getElements<HTMLObjectElement>('.rack_elevation')) {
const svg = elevation.firstElementChild ?? null;
if (svg !== null && svg.nodeName == 'svg') {
const svg = elevation.contentDocument?.querySelector('svg') ?? null;
if (svg !== null) {
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.
*
@@ -129,7 +115,6 @@ 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,6 +36,7 @@ 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.4.1:
version "12.4.1"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-12.4.1.tgz#4a44511e5da33016e731f00bee279bed550d4ab9"
integrity sha512-dYBNVEDw2zwnz0bCDouHk8rMclrMoMn4r6rtNyyWSeYsV3RF8QV2KFRTj4c86T2FsZPr3iQv+/LD/ae29FcpHQ==
gridstack@12.3.3:
version "12.3.3"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-12.3.3.tgz#0c4fc3cdf6e1c16e6095bc79ff7240a590d2c200"
integrity sha512-Bboi4gj7HXGnx1VFXQNde4Nwi5srdUSuCCnOSszKhFjBs8EtMEWhsKX02BjIKkErq/FjQUkNUbXUYeQaVMQ0jQ==
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.97.1:
version "1.97.1"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.97.1.tgz#f36e492baf8ccdd08d591b58d3d8b53ea35ab905"
integrity sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==
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==
dependencies:
chokidar "^4.0.0"
immutable "^5.0.2"

View File

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

View File

@@ -23,7 +23,7 @@
</tr>
<tr>
<th scope="row">{% trans "Range" %}</th>
<td>{{ object.range_as_string_with_asdot }}</td>
<td>{{ object.range_as_string }}</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-23 05:04+0000\n"
"POT-Creation-Date: 2025-12-10 05:02+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:485 netbox/ipam/forms/filtersets.py:161
#: netbox/ipam/forms/bulk_import.py:489 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:137
#: netbox/ipam/models/asns.py:154 netbox/ipam/tables/asn.py:51
#: 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/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:155
#: netbox/ipam/tables/asn.py:37 netbox/ipam/views.py:269
#: netbox/dcim/tables/sites.py:108 netbox/ipam/models/asns.py:124
#: netbox/ipam/tables/asn.py:27 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:259
#: netbox/ipam/forms/bulk_import.py:295 netbox/ipam/forms/bulk_import.py:506
#: 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/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: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/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/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:264 netbox/ipam/forms/bulk_import.py:300
#: netbox/ipam/forms/bulk_import.py:511 netbox/ipam/forms/filtersets.py:247
#: 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/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:261 netbox/ipam/forms/bulk_import.py:297
#: netbox/ipam/forms/bulk_import.py:508 netbox/ipam/forms/bulk_import.py:521
#: 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/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: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: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/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:320 netbox/ipam/forms/model_forms.py:291
#: netbox/ipam/forms/bulk_import.py:324 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:492 netbox/ipam/forms/model_forms.py:571
#: netbox/ipam/forms/bulk_import.py:496 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:1282
#: netbox/dcim/models/device_components.py:1280
#: 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:1321
#: netbox/dcim/models/devices.py:580 netbox/dcim/models/devices.py:1207
#: netbox/dcim/models/device_components.py:1319
#: netbox/dcim/models/devices.py:580 netbox/dcim/models/devices.py:1202
#: 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:1133 netbox/dcim/models/devices.py:1202
#: netbox/dcim/models/devices.py:1128 netbox/dcim/models/devices.py:1197
#: 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:79 netbox/ipam/tables/fhrp.py:34
#: netbox/ipam/tables/asn.py:69 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:306 netbox/ipam/forms/filtersets.py:626
#: netbox/ipam/forms/bulk_import.py:310 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:578
#: netbox/dcim/tables/sites.py:147 netbox/ipam/forms/bulk_import.py:582
#: 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:1303
#: netbox/dcim/models/devices.py:1323 netbox/virtualization/filtersets.py:201
#: 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/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:334
#: netbox/ipam/forms/bulk_import.py:338
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:245 netbox/ipam/forms/bulk_import.py:281
#: netbox/ipam/forms/bulk_import.py:249 netbox/ipam/forms/bulk_import.py:285
#: 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:489
#: netbox/dcim/forms/bulk_import.py:1650 netbox/ipam/forms/bulk_import.py:493
#: 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:249 netbox/ipam/forms/bulk_import.py:285
#: netbox/ipam/forms/bulk_import.py:253 netbox/ipam/forms/bulk_import.py:289
#: 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:310
#: netbox/dcim/forms/bulk_import.py:1338 netbox/ipam/forms/bulk_import.py:314
msgid "Parent device of assigned interface (if any)"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1341 netbox/ipam/forms/bulk_import.py:313
#: netbox/dcim/forms/bulk_import.py:1341 netbox/ipam/forms/bulk_import.py:317
#: 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:317
#: netbox/dcim/forms/bulk_import.py:1345 netbox/ipam/forms/bulk_import.py:321
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:324
#: netbox/ipam/forms/bulk_import.py:328
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:462
#: netbox/dcim/forms/mixins.py:117 netbox/ipam/forms/bulk_import.py:466
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:1342
#: netbox/dcim/models/device_components.py:1340
msgid "part ID"
msgstr ""
#: netbox/dcim/models/device_component_templates.py:779
#: netbox/dcim/models/device_components.py:1344
#: netbox/dcim/models/device_components.py:1342
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:524 netbox/ipam/forms/filtersets.py:587
#: netbox/ipam/forms/bulk_import.py:528 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:1245
#: netbox/dcim/models/device_components.py:1243
msgid "device bay"
msgstr ""
#: netbox/dcim/models/device_components.py:1246
#: netbox/dcim/models/device_components.py:1244
msgid "device bays"
msgstr ""
#: netbox/dcim/models/device_components.py:1253
#: netbox/dcim/models/device_components.py:1251
#, python-brace-format
msgid "This type of device ({device_type}) does not support device bays."
msgstr ""
#: netbox/dcim/models/device_components.py:1259
#: netbox/dcim/models/device_components.py:1257
msgid "Cannot install a device into itself."
msgstr ""
#: netbox/dcim/models/device_components.py:1267
#: netbox/dcim/models/device_components.py:1265
#, python-brace-format
msgid ""
"Cannot install the specified device; device is already installed in {bay}."
msgstr ""
#: netbox/dcim/models/device_components.py:1288
#: netbox/dcim/models/device_components.py:1286
msgid "inventory item role"
msgstr ""
#: netbox/dcim/models/device_components.py:1289
#: netbox/dcim/models/device_components.py:1287
msgid "inventory item roles"
msgstr ""
#: netbox/dcim/models/device_components.py:1348
#: netbox/dcim/models/device_components.py:1346
#: 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:1356
#: netbox/dcim/models/device_components.py:1354
#: 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:1357
#: netbox/dcim/models/device_components.py:1355
msgid "A unique tag used to identify this item"
msgstr ""
#: netbox/dcim/models/device_components.py:1360
#: netbox/dcim/models/device_components.py:1358
msgid "discovered"
msgstr ""
#: netbox/dcim/models/device_components.py:1362
#: netbox/dcim/models/device_components.py:1360
msgid "This item was automatically discovered"
msgstr ""
#: netbox/dcim/models/device_components.py:1380
#: netbox/dcim/models/device_components.py:1378
msgid "inventory item"
msgstr ""
#: netbox/dcim/models/device_components.py:1381
#: netbox/dcim/models/device_components.py:1379
msgid "inventory items"
msgstr ""
#: netbox/dcim/models/device_components.py:1389
#: netbox/dcim/models/device_components.py:1387
msgid "Cannot assign self as parent."
msgstr ""
#: netbox/dcim/models/device_components.py:1397
#: netbox/dcim/models/device_components.py:1395
msgid "Parent inventory item does not belong to the same device."
msgstr ""
#: netbox/dcim/models/device_components.py:1403
#: netbox/dcim/models/device_components.py:1401
msgid "Cannot move an inventory item with dependent children"
msgstr ""
#: netbox/dcim/models/device_components.py:1411
#: netbox/dcim/models/device_components.py:1409
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:1223
#: netbox/dcim/models/devices.py:598 netbox/dcim/models/devices.py:1218
#: netbox/virtualization/models/virtualmachines.py:94
msgid "primary IPv4"
msgstr ""
#: netbox/dcim/models/devices.py:606 netbox/dcim/models/devices.py:1231
#: netbox/dcim/models/devices.py:606 netbox/dcim/models/devices.py:1226
#: 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:1138
#: netbox/dcim/models/devices.py:1133
msgid "domain"
msgstr ""
#: netbox/dcim/models/devices.py:1151 netbox/dcim/models/devices.py:1152
#: netbox/dcim/models/devices.py:1146 netbox/dcim/models/devices.py:1147
msgid "virtual chassis"
msgstr ""
#: netbox/dcim/models/devices.py:1164
#: netbox/dcim/models/devices.py:1159
#, python-brace-format
msgid "The selected master ({master}) is not assigned to this virtual chassis."
msgstr ""
#: netbox/dcim/models/devices.py:1179
#: netbox/dcim/models/devices.py:1174
#, 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:1212 netbox/vpn/models/l2vpn.py:42
#: netbox/dcim/models/devices.py:1207 netbox/vpn/models/l2vpn.py:42
msgid "identifier"
msgstr ""
#: netbox/dcim/models/devices.py:1213
#: netbox/dcim/models/devices.py:1208
msgid "Numeric identifier unique to the parent device"
msgstr ""
#: netbox/dcim/models/devices.py:1241 netbox/extras/models/customfields.py:231
#: netbox/dcim/models/devices.py:1236 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:1257
#: netbox/dcim/models/devices.py:1252
msgid "virtual device context"
msgstr ""
#: netbox/dcim/models/devices.py:1258
#: netbox/dcim/models/devices.py:1253
msgid "virtual device contexts"
msgstr ""
#: netbox/dcim/models/devices.py:1287
#: netbox/dcim/models/devices.py:1282
#, python-brace-format
msgid "{ip} is not an IPv{family} address."
msgstr ""
#: netbox/dcim/models/devices.py:1293
#: netbox/dcim/models/devices.py:1288
msgid "Primary IP address must belong to an interface on the assigned device."
msgstr ""
#: netbox/dcim/models/devices.py:1324
#: netbox/dcim/models/devices.py:1319
msgid "MAC addresses"
msgstr ""
#: netbox/dcim/models/devices.py:1356
#: netbox/dcim/models/devices.py:1351
msgid ""
"Cannot unassign MAC Address while it is designated as the primary MAC for an "
"object"
msgstr ""
#: netbox/dcim/models/devices.py:1360
#: netbox/dcim/models/devices.py:1355
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:268 netbox/ipam/forms/bulk_import.py:303
#: netbox/ipam/forms/bulk_import.py:515
#: netbox/ipam/forms/bulk_import.py:272 netbox/ipam/forms/bulk_import.py:307
#: netbox/ipam/forms/bulk_import.py:519
#: 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:597 netbox/ipam/forms/model_forms.py:317
#: netbox/ipam/forms/bulk_import.py:601 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:76 netbox/netbox/navigation/menu.py:15
#: netbox/ipam/tables/asn.py:66 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:3238
#: netbox/dcim/views.py:3237
#, python-brace-format
msgid "Installed device {device} in bay {device_bay}."
msgstr ""
#: netbox/dcim/views.py:3279
#: netbox/dcim/views.py:3278
#, python-brace-format
msgid "Removed device {device} from bay {device_bay}."
msgstr ""
#: netbox/dcim/views.py:3392 netbox/ipam/tables/ip.py:181
#: netbox/dcim/views.py:3391 netbox/ipam/tables/ip.py:181
msgid "Children"
msgstr ""
#: netbox/dcim/views.py:3865
#: netbox/dcim/views.py:3864
#, python-brace-format
msgid "Added member <a href=\"{url}\">{device}</a>"
msgstr ""
#: netbox/dcim/views.py:3910
#: netbox/dcim/views.py:3909
#, python-brace-format
msgid "Unable to remove master device {device} from the virtual chassis."
msgstr ""
#: netbox/dcim/views.py:3921
#: netbox/dcim/views.py:3920
#, 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:496
#: netbox/extras/models/customfields.py:488
msgid "True"
msgstr ""
#: netbox/extras/models/customfields.py:497
#: netbox/extras/models/customfields.py:489
msgid "False"
msgstr ""
#: netbox/extras/models/customfields.py:550
#: netbox/extras/models/customfields.py:598
#: netbox/extras/models/customfields.py:542
#: netbox/extras/models/customfields.py:590
#, python-brace-format
msgid "Values must match this regex: <code>{regex}</code>"
msgstr ""
#: netbox/extras/models/customfields.py:700
#: netbox/extras/models/customfields.py:707
#: netbox/extras/models/customfields.py:692
#: netbox/extras/models/customfields.py:699
msgid "Value must be a string."
msgstr ""
#: netbox/extras/models/customfields.py:702
#: netbox/extras/models/customfields.py:709
#: netbox/extras/models/customfields.py:694
#: netbox/extras/models/customfields.py:701
#, python-brace-format
msgid "Value must match regex '{regex}'"
msgstr ""
#: netbox/extras/models/customfields.py:714
#: netbox/extras/models/customfields.py:706
msgid "Value must be an integer."
msgstr ""
#: netbox/extras/models/customfields.py:717
#: netbox/extras/models/customfields.py:732
#: netbox/extras/models/customfields.py:709
#: netbox/extras/models/customfields.py:724
#, python-brace-format
msgid "Value must be at least {minimum}"
msgstr ""
#: netbox/extras/models/customfields.py:721
#: netbox/extras/models/customfields.py:736
#: netbox/extras/models/customfields.py:713
#: netbox/extras/models/customfields.py:728
#, python-brace-format
msgid "Value must not exceed {maximum}"
msgstr ""
#: netbox/extras/models/customfields.py:729
#: netbox/extras/models/customfields.py:721
msgid "Value must be a decimal."
msgstr ""
#: netbox/extras/models/customfields.py:741
#: netbox/extras/models/customfields.py:733
msgid "Value must be true or false."
msgstr ""
#: netbox/extras/models/customfields.py:749
#: netbox/extras/models/customfields.py:741
msgid "Date values must be in ISO 8601 format (YYYY-MM-DD)."
msgstr ""
#: netbox/extras/models/customfields.py:758
#: netbox/extras/models/customfields.py:750
msgid "Date and time values must be in ISO 8601 format (YYYY-MM-DD HH:MM:SS)."
msgstr ""
#: netbox/extras/models/customfields.py:765
#: netbox/extras/models/customfields.py:757
#, python-brace-format
msgid "Invalid choice ({value}) for choice set {choiceset}."
msgstr ""
#: netbox/extras/models/customfields.py:775
#: netbox/extras/models/customfields.py:767
#, python-brace-format
msgid "Invalid choice(s) ({value}) for choice set {choiceset}."
msgstr ""
#: netbox/extras/models/customfields.py:784
#: netbox/extras/models/customfields.py:776
#, python-brace-format
msgid "Value must be an object ID, not {type}"
msgstr ""
#: netbox/extras/models/customfields.py:790
#: netbox/extras/models/customfields.py:782
#, python-brace-format
msgid "Value must be a list of object IDs, not {type}"
msgstr ""
#: netbox/extras/models/customfields.py:794
#: netbox/extras/models/customfields.py:786
#, python-brace-format
msgid "Found invalid object ID: {id}"
msgstr ""
#: netbox/extras/models/customfields.py:797
#: netbox/extras/models/customfields.py:789
msgid "Required field cannot be empty."
msgstr ""
#: netbox/extras/models/customfields.py:817
#: netbox/extras/models/customfields.py:809
msgid "Base set of predefined choices (optional)"
msgstr ""
#: netbox/extras/models/customfields.py:829
#: netbox/extras/models/customfields.py:821
msgid "Choices are automatically ordered alphabetically"
msgstr ""
#: netbox/extras/models/customfields.py:836
#: netbox/extras/models/customfields.py:828
msgid "custom field choice set"
msgstr ""
#: netbox/extras/models/customfields.py:837
#: netbox/extras/models/customfields.py:829
msgid "custom field choice sets"
msgstr ""
#: netbox/extras/models/customfields.py:879
#: netbox/extras/models/customfields.py:871
msgid "Must define base or extra choices."
msgstr ""
#: netbox/extras/models/customfields.py:903
#: netbox/extras/models/customfields.py:895
#, 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:40
#: netbox/ipam/fields.py:39
#, 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:132 netbox/ipam/models/ip.py:72
#: netbox/ipam/models/asns.py:101 netbox/ipam/models/ip.py:72
#: netbox/ipam/models/ip.py:88 netbox/ipam/tables/asn.py:20
#: netbox/ipam/tables/asn.py:55 netbox/templates/ipam/aggregate.html:18
#: netbox/ipam/tables/asn.py:45 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:443 netbox/ipam/forms/bulk_import.py:561
#: netbox/ipam/forms/bulk_import.py:589 netbox/ipam/forms/filtersets.py:414
#: 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/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:518
#: netbox/ipam/forms/bulk_edit.py:516 netbox/ipam/forms/bulk_import.py:522
#: 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:548
#: netbox/ipam/forms/bulk_edit.py:557 netbox/ipam/forms/bulk_import.py:552
#: 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:327 netbox/ipam/forms/filtersets.py:636
#: netbox/ipam/forms/bulk_import.py:331 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:331
#: netbox/ipam/forms/bulk_import.py:335
msgid "Assigned FHRP Group name"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:335
#: netbox/ipam/forms/bulk_import.py:339
msgid "Make this the primary IP for the assigned device"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:339
#: netbox/ipam/forms/bulk_import.py:343
msgid "Is out-of-band"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:340
#: netbox/ipam/forms/bulk_import.py:344
msgid "Designate this as the out-of-band IP address for the assigned device"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:394
#: netbox/ipam/forms/bulk_import.py:398
msgid "No device or virtual machine specified; cannot set as primary IP"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:398
#: netbox/ipam/forms/bulk_import.py:402
msgid "No device specified; cannot set as out-of-band IP"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:402
#: netbox/ipam/forms/bulk_import.py:406
msgid "Cannot set out-of-band IP for virtual machines"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:406
#: netbox/ipam/forms/bulk_import.py:410
msgid "No interface specified; cannot set as primary IP"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:410
#: netbox/ipam/forms/bulk_import.py:414
msgid "No interface specified; cannot set as out-of-band IP"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:447
#: netbox/ipam/forms/bulk_import.py:451
msgid "Auth type"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:496
#: netbox/ipam/forms/bulk_import.py:500
msgid "Assigned VLAN group"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:528
#: netbox/ipam/forms/bulk_import.py:532
msgid "Service VLAN (for Q-in-Q/802.1ad customer VLANs)"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:551 netbox/ipam/models/vlans.py:369
#: netbox/ipam/forms/bulk_import.py:555 netbox/ipam/models/vlans.py:369
msgid "VLAN translation policy"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:563 netbox/ipam/forms/bulk_import.py:591
#: netbox/ipam/forms/bulk_import.py:567 netbox/ipam/forms/bulk_import.py:595
msgid "IP protocol"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:575
#: netbox/ipam/forms/bulk_import.py:579
msgid "Parent type (app & model)"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:582
#: netbox/ipam/forms/bulk_import.py:586
msgid "Parent object name"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:586
#: netbox/ipam/forms/bulk_import.py:590
msgid "Parent object ID"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:638
#: netbox/ipam/forms/bulk_import.py:642
msgid ""
"One of parent or parent_object_id must be included with parent_object_type"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:651
#: netbox/ipam/forms/bulk_import.py:655
#, 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:63
#: netbox/ipam/models/asns.py:70
#, python-brace-format
msgid "Starting ASN ({start}) must be lower than ending ASN ({end})."
msgstr ""
#: netbox/ipam/models/asns.py:133
#: netbox/ipam/models/asns.py:102
msgid "Regional Internet Registry responsible for this AS number space"
msgstr ""
#: netbox/ipam/models/asns.py:138
#: netbox/ipam/models/asns.py:107
msgid "16- or 32-bit autonomous system number"
msgstr ""
@@ -11261,23 +11261,15 @@ msgstr ""
msgid "route targets"
msgstr ""
#: netbox/ipam/tables/asn.py:26
msgid "Start (ASDOT)"
msgstr ""
#: netbox/ipam/tables/asn.py:31
msgid "End (ASDOT)"
msgstr ""
#: netbox/ipam/tables/asn.py:62
#: netbox/ipam/tables/asn.py:52
msgid "ASDOT"
msgstr ""
#: netbox/ipam/tables/asn.py:67
#: netbox/ipam/tables/asn.py:57
msgid "Site Count"
msgstr ""
#: netbox/ipam/tables/asn.py:72
#: netbox/ipam/tables/asn.py:62
msgid "Provider Count"
msgstr ""
@@ -12531,34 +12523,30 @@ msgid "Japanese"
msgstr ""
#: netbox/netbox/settings.py:830
msgid "Latvian"
msgstr ""
#: netbox/netbox/settings.py:831
msgid "Dutch"
msgstr ""
#: netbox/netbox/settings.py:832
#: netbox/netbox/settings.py:831
msgid "Polish"
msgstr ""
#: netbox/netbox/settings.py:833
#: netbox/netbox/settings.py:832
msgid "Portuguese"
msgstr ""
#: netbox/netbox/settings.py:834
#: netbox/netbox/settings.py:833
msgid "Russian"
msgstr ""
#: netbox/netbox/settings.py:835
#: netbox/netbox/settings.py:834
msgid "Turkish"
msgstr ""
#: netbox/netbox/settings.py:836
#: netbox/netbox/settings.py:835
msgid "Ukrainian"
msgstr ""
#: netbox/netbox/settings.py:837
#: netbox/netbox/settings.py:836
msgid "Chinese"
msgstr ""
@@ -12593,75 +12581,69 @@ msgstr ""
msgid "Dummy Plugin"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:124
#: netbox/netbox/views/generic/bulk_views.py:122
#, python-brace-format
msgid ""
"There was an error rendering the selected export template ({template}): "
"{error}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:390
#: netbox/netbox/views/generic/bulk_views.py:388
msgid "Must be a list."
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:400
#: netbox/netbox/views/generic/bulk_views.py:398
msgid "Must be a dictionary."
msgstr ""
#: 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
#: netbox/netbox/views/generic/bulk_views.py:461
#, python-brace-format
msgid "Object with ID {id} does not exist"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:560
#: netbox/netbox/views/generic/bulk_views.py:546
#, python-brace-format
msgid "Bulk import {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:576
#: netbox/netbox/views/generic/bulk_views.py:562
#, python-brace-format
msgid "Imported {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:766
#: netbox/netbox/views/generic/bulk_views.py:752
#, python-brace-format
msgid "Bulk edit {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:782
#: netbox/netbox/views/generic/bulk_views.py:768
#, python-brace-format
msgid "Updated {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:815
#: netbox/netbox/views/generic/bulk_views.py:1050
#: netbox/netbox/views/generic/bulk_views.py:1098
#: netbox/netbox/views/generic/bulk_views.py:801
#: netbox/netbox/views/generic/bulk_views.py:1036
#: netbox/netbox/views/generic/bulk_views.py:1084
#, python-brace-format
msgid "No {object_type} were selected."
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:908
#: netbox/netbox/views/generic/bulk_views.py:894
#, python-brace-format
msgid "Renamed {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:978
#: netbox/netbox/views/generic/bulk_views.py:964
#, python-brace-format
msgid "Bulk delete {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:1005
#: netbox/netbox/views/generic/bulk_views.py:991
#, python-brace-format
msgid "Deleted {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:1022
#: netbox/netbox/views/generic/bulk_views.py:1008
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

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):
return self
qs = self
# User is anonymous or has not been granted the requisite permission
if user is None or not user.is_authenticated or permission_required not in user.get_all_permissions():
return self.none()
elif user is None or not user.is_authenticated or permission_required not in user.get_all_permissions():
qs = self.none()
# Filter the queryset to include only objects with allowed attributes
constraints = user._object_perm_cache[permission_required]
tokens = {
CONSTRAINT_TOKEN_USER: user,
}
if attrs := qs_filter_from_constraints(constraints, tokens):
else:
tokens = {
CONSTRAINT_TOKEN_USER: user,
}
attrs = qs_filter_from_constraints(user._object_perm_cache[permission_required], 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)
return self.filter(pk__in=allowed_objects)
qs = self.filter(pk__in=allowed_objects)
return self
return qs

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,13 +152,9 @@ class ModelTestCase(TestCase):
elif type(value) is IPNetwork:
model_dict[key] = str(value)
# 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:
field = instance._meta.get_field(key)
# 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.9"
version = "4.4.7"
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.1
mkdocs-material==9.7.0
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.7.0
social-auth-core==4.8.3
social-auth-app-django==5.6.0
social-auth-core==4.8.1
sorl-thumbnail==12.11.0
strawberry-graphql==0.287.3
strawberry-graphql==0.287.2
strawberry-graphql-django==0.70.1
svgwrite==1.4.3
tablib==3.9.0
tzdata==2025.3
tzdata==2025.2