Compare commits

..

19 Commits

Author SHA1 Message Date
Idris Foughali
dc1d8a54c7 Merge cf16a29ad3 into 598f8d034d 2025-12-12 21:55:25 -06:00
ifoughali
cf16a29ad3 Style: removed comment 2025-12-05 15:24:35 +01:00
ifoughali
544c97d923 XMerge branch 'closes-20817-Fix-datasource-sync-broken-when-cron-is-set' of https://github.com/ifoughal/netbox into closes-20817-Fix-datasource-sync-broken-when-cron-is-set 2025-12-05 15:23:43 +01:00
ifoughali
77ee6baa23 refactor: moved status update logic from clean() to save() method 2025-12-05 15:23:38 +01:00
Idris Foughali
09d1049267 Merge branch 'netbox-community:main' into closes-20817-Fix-datasource-sync-broken-when-cron-is-set 2025-12-05 14:51:56 +01:00
Idris Foughali
93e5f919ba Merge branch 'netbox-community:main' into closes-20817-Fix-datasource-sync-broken-when-cron-is-set 2025-12-01 10:07:15 +01:00
ifoughali
929d024003 Merge branch 'closes-20817-Fix-datasource-sync-broken-when-cron-is-set' of https://github.com/ifoughal/netbox into closes-20817-Fix-datasource-sync-broken-when-cron-is-set 2025-11-26 09:00:22 +01:00
ifoughali
e4b614038e revert: re-added queued status set for datasource object 2025-11-26 09:00:17 +01:00
Idris Foughali
3016b1d90b Merge branch 'netbox-community:main' into closes-20817-Fix-datasource-sync-broken-when-cron-is-set 2025-11-26 08:55:12 +01:00
ifoughali
57b47dc1ea style: use != instead of not in for single SYNCING check 2025-11-26 08:05:20 +01:00
ifoughali
da4c669312 Feat: reworked status update logic 2025-11-20 11:27:39 +01:00
ifoughali
71f707b7ac Feat: removed SCHEDULED choice due to redundency with sync interval 2025-11-20 11:26:43 +01:00
ifoughali
e11508dd6c Fix: removed status update from the enqueue method 2025-11-20 10:50:35 +01:00
ifoughali
5b5b5c8909 Revert "Feat: set status as editable field"
This reverts commit b4160ad59b.
2025-11-19 20:18:59 +01:00
Idris Foughali
a49869af42 Feat: removed QUEUED from ready for sync condition 2025-11-19 19:01:01 +00:00
Idris Foughali
2e0ff04f84 Feat: added 2 states for DataSourceStatusChoices 2025-11-19 18:52:27 +00:00
Idris Foughali
bfeba36514 Feat: added status update during save method of DataSourceForm 2025-11-19 18:51:25 +00:00
Idris Foughali
111aca115b Feat: added clean method to set data-source state to Ready or scheduled 2025-11-19 18:51:01 +00:00
Idris Foughali
b4160ad59b Feat: set status as editable field 2025-11-19 18:49:47 +00:00
53 changed files with 7295 additions and 25471 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

@@ -13,6 +13,7 @@ class DataSourceStatusChoices(ChoiceSet):
SYNCING = 'syncing'
COMPLETED = 'completed'
FAILED = 'failed'
READY = 'ready'
CHOICES = (
(NEW, _('New'), 'blue'),
@@ -20,6 +21,7 @@ class DataSourceStatusChoices(ChoiceSet):
(SYNCING, _('Syncing'), 'cyan'),
(COMPLETED, _('Completed'), 'green'),
(FAILED, _('Failed'), 'red'),
(READY, _('Ready'), 'green'),
)

View File

@@ -16,6 +16,7 @@ from utilities.forms import get_field_value
from utilities.forms.fields import CommentField, JSONField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import HTMXSelect
from core.choices import DataSourceStatusChoices
__all__ = (
'ConfigRevisionForm',
@@ -79,14 +80,28 @@ class DataSourceForm(NetBoxModelForm):
if self.instance and self.instance.parameters:
self.fields[field_name].initial = self.instance.parameters.get(name)
def save(self, *args, **kwargs):
def save(self, *args, **kwargs):
parameters = {}
for name in self.fields:
if name.startswith('backend_'):
parameters[name[8:]] = self.cleaned_data[name]
self.instance.parameters = parameters
# Determine initial status based on new/existing instance
if not self.instance.pk:
# New instance
object_status = DataSourceStatusChoices.NEW
else:
# Existing instance
if not self.cleaned_data.get("sync_interval"):
object_status = DataSourceStatusChoices.READY
else:
object_status = self.instance.status
# # Final override only if the user explicitly provided a status
self.instance.status = object_status
return super().save(*args, **kwargs)

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

@@ -111,10 +111,7 @@ class DataSource(JobsMixin, PrimaryModel):
@property
def ready_for_sync(self):
return self.enabled and self.status not in (
DataSourceStatusChoices.QUEUED,
DataSourceStatusChoices.SYNCING
)
return self.enabled and self.status != DataSourceStatusChoices.SYNCING
def clean(self):
super().clean()

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, RESTRICT
from django.db.models import CASCADE
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 not in (CASCADE, RESTRICT):
elif type(relation) is ManyToOneRel and relation.null and relation.on_delete is not CASCADE:
setattr(obj, related_field_name, None)
obj.save()

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

@@ -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
@@ -182,40 +180,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

@@ -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

@@ -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

@@ -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

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

@@ -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-12 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"
@@ -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"
@@ -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
@@ -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
@@ -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 ""
@@ -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"
@@ -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"
@@ -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