Compare commits

...

19 Commits

Author SHA1 Message Date
github-actions
2a391253a5 Update source translation strings
Some checks are pending
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
2025-12-31 05:05:09 +00:00
Jason Novinger
914653d63e Fixes #21045: Allow saving Site with associated Prefix
Some checks failed
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
This was a result of the fix for #20944 optimizing a query to only
include the `id` field with `.only(id)`. Since `Prefix.__init__()`
caches original values from other fields (`_prefix` and `_vrf_id`),
these cached values are `None` at init-time.

This might not normally be a problem, but the sequence of events in
the bug report also end up causing the `handle_prefix_saved` handler
to run, which uses an ORM lookup, (either `net_contained_or_equal`
original`net_contained`) that does not support a query argument of
`None`.
2025-12-30 12:26:48 -05:00
Martin Hauser
3813aad8b1 Fixes #20320: Ensure related interface options availibility in bulk edit (#21006) 2025-12-30 10:17:14 -06:00
Jeremy Stretch
ea5371040e Fixes #20817: Re-enable sync button when disabling scheduled syncing for a data source (#21055) 2025-12-30 10:05:08 -06:00
Unknown
6c824cc48f Fixes #20044: Elevations stuck in light mode (#21037)
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
Co-authored-by: UnknownTy <meaphunter+git@hotmail.com>
Co-authored-by: Jason Novinger <jnovinger@gmail.com>
2025-12-29 16:27:03 -06:00
Jeremy Stretch
f510e40428 Closes #21047: Add compatibility matrix to plugin setup instructions (#21048) 2025-12-29 11:39:51 -06:00
Prince Kumar
860db9590b Fixed #20950: Add missing module and device properties in module-bay (#21005)
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2025-12-23 13:34:06 -06:00
Jeremy Stretch
7c63d001b1 Release v4.4.9 2025-12-23 12:02:30 -05:00
Jeremy Stretch
93119f52c3 Fixes #21032: Avoid subquery in RestrictedQuerySet where unnecessary 2025-12-23 10:15:06 -05:00
github-actions
ee2aa35cba Update source translation strings
Some checks failed
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-12-23 05:04:20 +00:00
bctiemann
7896a48075 Merge pull request #21029 from netbox-community/21011-configrevision-save
Some checks failed
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
Fixes #21011: Avoid updating database when loading active ConfigRevision
2025-12-22 14:19:19 -05:00
bctiemann
eb87c3f304 Merge pull request #21000 from netbox-community/20011-misleading-error-message
Fixes #20011: Provide accurate error for bulk import duplicate IDs
2025-12-22 14:12:36 -05:00
Vincent Simonin
3acbb0a08c Fix on delete cascade entity order (#20949)
* Fix on delete cascade entity order

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

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

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

Fixes #20309

* Wrap "ASDOT" with parentheses in column header

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-12-22 10:06:08 -05:00
Prince Kumar
e5a975176d Fixed #20944: Ensure cached scope fields stay consistent when Region, Site, or Location changes (#20986) 2025-12-22 09:48:43 -05:00
github-actions
83ee4fb593 Update source translation strings
Some checks failed
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2025-12-20 05:02:02 +00:00
bctiemann
db8271c904 Fixes #20114: Preserve parent bay during device bulk import when tags are present (#21019)
Some checks failed
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-12-19 17:05:32 -06:00
Jason Novinger
d95fa8dbb2 Fixes #20011: UI Error msg for duplicate IDs in bulk import 2025-12-17 09:21:17 -06:00
49 changed files with 17974 additions and 16103 deletions

View File

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

View File

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

View File

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

View File

@@ -74,7 +74,7 @@ The plugin source directory contains all the actual Python code and other resour
The `PluginConfig` class is a NetBox-specific wrapper around Django's built-in [`AppConfig`](https://docs.djangoproject.com/en/stable/ref/applications/) class. It is used to declare NetBox plugin functionality within a Python package. Each plugin should provide its own subclass, defining its name, metadata, and default and required configuration parameters. An example is below:
```python
```python title="__init__.py"
from netbox.plugins import PluginConfig
class FooBarConfig(PluginConfig):
@@ -151,7 +151,7 @@ Any additional apps must be installed within the same Python environment as NetB
An example `pyproject.toml` is below:
```
```toml title="pyproject.toml"
# See PEP 518 for the spec of this file
# https://www.python.org/dev/peps/pep-0518/
@@ -179,11 +179,24 @@ classifiers=[
]
requires-python = ">=3.10.0"
```
Many of these are self-explanatory, but for more information, see the [pyproject.toml documentation](https://packaging.python.org/en/latest/specifications/pyproject-toml/).
## Compatibility Matrix
Consider adding a file named `COMPATIBILITY.md` to your plugin project root (alongside `pyproject.toml`). This file should contain a table listing the minimum and maximum supported versions of NetBox (`min_version` and `max_version`) for each release. This serves as a handy reference for users who are upgrading from a previous version of your plugin. An example is shown below:
```markdown title="COMPATIBILITY.md"
# Compatibility Matrix
| Release | Minimum NetBox Version | Maximum NetBox Version |
|---------|------------------------|------------------------|
| 0.2.0 | 4.4.0 | 4.5.x |
| 0.1.1 | 4.3.0 | 4.4.x |
| 0.1.0 | 4.3.0 | 4.4.x |
```
## Create a Virtual Environment
It is strongly recommended to create a Python [virtual environment](https://docs.python.org/3/tutorial/venv.html) for the development of your plugin, as opposed to using system-wide packages. This will afford you complete control over the installed versions of all dependencies and avoid conflict with system packages. This environment can live wherever you'd like;however, it should be excluded from revision control. (A popular convention is to keep all virtual environments in the user's home directory, e.g. `~/.virtualenvs/`.)

View File

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

View File

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

View File

@@ -131,6 +131,19 @@ class DataSource(JobsMixin, PrimaryModel):
'source_url': "URLs for local sources must start with file:// (or specify no scheme)"
})
def save(self, *args, **kwargs):
# If recurring sync is disabled for an existing DataSource, clear any pending sync jobs for it and reset its
# "queued" status
if not self._state.adding and not self.sync_interval:
self.jobs.filter(status=JobStatusChoices.STATUS_PENDING).delete()
if self.status == DataSourceStatusChoices.QUEUED and self.last_synced:
self.status = DataSourceStatusChoices.COMPLETED
elif self.status == DataSourceStatusChoices.QUEUED:
self.status = DataSourceStatusChoices.NEW
super().save(*args, **kwargs)
def to_objectchange(self, action):
objectchange = super().to_objectchange(action)

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ from core.models import ObjectType
from dcim.choices import *
from dcim.models import *
from extras.models import CustomField
from ipam.models import Prefix
from netbox.choices import WeightUnitChoices
from tenancy.models import Tenant
from utilities.data import drange
@@ -1192,3 +1193,14 @@ class VirtualChassisTestCase(TestCase):
device2.vc_position = 1
with self.assertRaises(ValidationError):
device2.full_clean()
class SiteSignalTestCase(TestCase):
@tag('regression')
def test_edit_site_with_prefix_no_vrf(self):
site = Site.objects.create(name='Test Site', slug='test-site')
Prefix.objects.create(prefix='192.0.2.0/24', scope=site, vrf=None)
# Regression test for #21045: should not raise ValueError
site.save()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
from django.db.models import ForeignKey
from django.template import loader
from django.urls.exceptions import NoReverseMatch
from django.utils.translation import gettext_lazy as _
@@ -175,6 +176,21 @@ class BulkEdit(ObjectAction):
permissions_required = {'change'}
template_name = 'buttons/bulk_edit.html'
@classmethod
def get_context(cls, context, model):
url_params = super().get_url_params(context)
# If this is a child object, pass the parent's PK as a URL parameter
if parent := context.get('object'):
for field in model._meta.get_fields():
if isinstance(field, ForeignKey) and field.remote_field.model == parent.__class__:
url_params[field.name] = parent.pk
break
return {
'url_params': url_params,
}
class BulkRename(ObjectAction):
"""

View File

@@ -1,11 +1,10 @@
from unittest import skipIf
from django.conf import settings
from django.test import TestCase
from django.test import RequestFactory, TestCase
from dcim.models import Device
from netbox.object_actions import AddObject, BulkImport
from netbox.tests.dummy_plugin.models import DummyNetBoxModel
from dcim.models import Device, DeviceType, Manufacturer
from netbox.object_actions import AddObject, BulkEdit, BulkImport
class ObjectActionTest(TestCase):
@@ -20,9 +19,11 @@ class ObjectActionTest(TestCase):
url = BulkImport.get_url(obj)
self.assertEqual(url, '/dcim/devices/import/')
@skipIf('netbox.tests.dummy_plugin' not in settings.PLUGINS, "dummy_plugin not in settings.PLUGINS")
@skipIf('netbox.tests.dummy_plugin' not in settings.PLUGINS, 'dummy_plugin not in settings.PLUGINS')
def test_get_url_plugin_model(self):
"""Test URL generation for plugin models includes plugins: namespace"""
from netbox.tests.dummy_plugin.models import DummyNetBoxModel
obj = DummyNetBoxModel()
url = AddObject.get_url(obj)
@@ -30,3 +31,29 @@ class ObjectActionTest(TestCase):
url = BulkImport.get_url(obj)
self.assertEqual(url, '/plugins/dummy-plugin/netboxmodel/import/')
def test_bulk_edit_get_context_child_object(self):
"""
Test that the parent object's PK is included in the context for child objects.
Ensure that BulkEdit.get_context() correctly identifies and
includes the parent object's PK when rendering a child object's
action button.
"""
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
# Mock context containing the parent object (DeviceType)
request = RequestFactory().get('/')
context = {
'request': request,
'object': device_type,
}
# Get context for the child model (Device)
action_context = BulkEdit.get_context(context, Device)
# Verify that 'device_type' (the FK field name) is present in
# url_params with the parent's PK
self.assertIn('url_params', action_context)
self.assertEqual(action_context['url_params'].get('device_type'), device_type.pk)

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-18 05:03+0000\n"
"POT-Creation-Date: 2025-12-31 05:04+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -272,8 +272,8 @@ msgid "ASN (ID)"
msgstr ""
#: netbox/circuits/filtersets.py:79 netbox/circuits/forms/filtersets.py:39
#: netbox/ipam/forms/model_forms.py:166 netbox/ipam/models/asns.py:106
#: netbox/ipam/models/asns.py:123 netbox/ipam/tables/asn.py:41
#: netbox/ipam/forms/model_forms.py:166 netbox/ipam/models/asns.py:137
#: netbox/ipam/models/asns.py:154 netbox/ipam/tables/asn.py:51
#: netbox/templates/ipam/asn.html:20
msgid "ASN"
msgstr ""
@@ -453,8 +453,8 @@ msgstr ""
#: netbox/circuits/forms/model_forms.py:43
#: netbox/circuits/tables/providers.py:32 netbox/dcim/forms/bulk_edit.py:143
#: netbox/dcim/forms/filtersets.py:204 netbox/dcim/forms/model_forms.py:133
#: netbox/dcim/tables/sites.py:108 netbox/ipam/models/asns.py:124
#: netbox/ipam/tables/asn.py:27 netbox/ipam/views.py:269
#: netbox/dcim/tables/sites.py:108 netbox/ipam/models/asns.py:155
#: netbox/ipam/tables/asn.py:37 netbox/ipam/views.py:269
#: netbox/netbox/navigation/menu.py:179 netbox/netbox/navigation/menu.py:182
#: netbox/templates/circuits/provider.html:23
msgid "ASNs"
@@ -1882,7 +1882,7 @@ msgstr ""
#: netbox/dcim/tables/sites.py:40 netbox/dcim/tables/sites.py:74
#: netbox/dcim/tables/sites.py:121 netbox/dcim/tables/sites.py:179
#: netbox/extras/forms/bulk_import.py:303 netbox/extras/tables/tables.py:706
#: netbox/ipam/tables/asn.py:69 netbox/ipam/tables/fhrp.py:34
#: netbox/ipam/tables/asn.py:79 netbox/ipam/tables/fhrp.py:34
#: netbox/ipam/tables/ip.py:83 netbox/ipam/tables/ip.py:227
#: netbox/ipam/tables/ip.py:286 netbox/ipam/tables/ip.py:355
#: netbox/ipam/tables/services.py:25 netbox/ipam/tables/services.py:55
@@ -2551,7 +2551,7 @@ msgstr ""
msgid "Change logging is not supported for this object type ({type})."
msgstr ""
#: netbox/core/models/config.py:21 netbox/core/models/data.py:269
#: netbox/core/models/config.py:21 netbox/core/models/data.py:282
#: netbox/core/models/files.py:29 netbox/core/models/jobs.py:60
#: netbox/extras/models/models.py:839 netbox/extras/models/notifications.py:39
#: netbox/extras/models/notifications.py:195
@@ -2657,58 +2657,58 @@ msgstr ""
msgid "Unknown backend type: {type}"
msgstr ""
#: netbox/core/models/data.py:167
#: netbox/core/models/data.py:180
msgid "Cannot initiate sync; syncing already in progress."
msgstr ""
#: netbox/core/models/data.py:180
#: netbox/core/models/data.py:193
msgid ""
"There was an error initializing the backend. A dependency needs to be "
"installed: "
msgstr ""
#: netbox/core/models/data.py:273 netbox/core/models/files.py:33
#: netbox/core/models/data.py:286 netbox/core/models/files.py:33
#: netbox/netbox/models/features.py:67
msgid "last updated"
msgstr ""
#: netbox/core/models/data.py:283 netbox/dcim/models/cables.py:528
#: netbox/core/models/data.py:296 netbox/dcim/models/cables.py:528
msgid "path"
msgstr ""
#: netbox/core/models/data.py:286
#: netbox/core/models/data.py:299
msgid "File path relative to the data source's root"
msgstr ""
#: netbox/core/models/data.py:290 netbox/ipam/models/ip.py:510
#: netbox/core/models/data.py:303 netbox/ipam/models/ip.py:510
msgid "size"
msgstr ""
#: netbox/core/models/data.py:293
#: netbox/core/models/data.py:306
msgid "hash"
msgstr ""
#: netbox/core/models/data.py:297
#: netbox/core/models/data.py:310
msgid "Length must be 64 hexadecimal characters."
msgstr ""
#: netbox/core/models/data.py:299
#: netbox/core/models/data.py:312
msgid "SHA256 hash of the file data"
msgstr ""
#: netbox/core/models/data.py:313
#: netbox/core/models/data.py:326
msgid "data file"
msgstr ""
#: netbox/core/models/data.py:314
#: netbox/core/models/data.py:327
msgid "data files"
msgstr ""
#: netbox/core/models/data.py:387
#: netbox/core/models/data.py:400
msgid "auto sync record"
msgstr ""
#: netbox/core/models/data.py:388
#: netbox/core/models/data.py:401
msgid "auto sync records"
msgstr ""
@@ -7942,7 +7942,7 @@ msgstr ""
#: netbox/dcim/tables/sites.py:34 netbox/dcim/tables/sites.py:68
#: netbox/extras/forms/filtersets.py:424 netbox/extras/forms/model_forms.py:630
#: netbox/ipam/forms/bulk_edit.py:134 netbox/ipam/forms/model_forms.py:160
#: netbox/ipam/tables/asn.py:66 netbox/netbox/navigation/menu.py:15
#: netbox/ipam/tables/asn.py:76 netbox/netbox/navigation/menu.py:15
#: netbox/netbox/navigation/menu.py:19
msgid "Sites"
msgstr ""
@@ -7987,31 +7987,31 @@ msgstr ""
msgid "Virtual Machines"
msgstr ""
#: netbox/dcim/views.py:3237
#: netbox/dcim/views.py:3238
#, python-brace-format
msgid "Installed device {device} in bay {device_bay}."
msgstr ""
#: netbox/dcim/views.py:3278
#: netbox/dcim/views.py:3279
#, python-brace-format
msgid "Removed device {device} from bay {device_bay}."
msgstr ""
#: netbox/dcim/views.py:3391 netbox/ipam/tables/ip.py:181
#: netbox/dcim/views.py:3392 netbox/ipam/tables/ip.py:181
msgid "Children"
msgstr ""
#: netbox/dcim/views.py:3864
#: netbox/dcim/views.py:3865
#, python-brace-format
msgid "Added member <a href=\"{url}\">{device}</a>"
msgstr ""
#: netbox/dcim/views.py:3909
#: netbox/dcim/views.py:3910
#, python-brace-format
msgid "Unable to remove master device {device} from the virtual chassis."
msgstr ""
#: netbox/dcim/views.py:3920
#: netbox/dcim/views.py:3921
#, python-brace-format
msgid "Removed {device} from virtual chassis {chassis}"
msgstr ""
@@ -10130,7 +10130,7 @@ msgstr ""
msgid "Customer"
msgstr ""
#: netbox/ipam/fields.py:39
#: netbox/ipam/fields.py:40
#, python-brace-format
msgid "Invalid IP address format: {address}"
msgstr ""
@@ -10356,9 +10356,9 @@ msgstr ""
#: netbox/ipam/forms/filtersets.py:151 netbox/ipam/forms/model_forms.py:100
#: netbox/ipam/forms/model_forms.py:113 netbox/ipam/forms/model_forms.py:136
#: netbox/ipam/forms/model_forms.py:155 netbox/ipam/models/asns.py:32
#: netbox/ipam/models/asns.py:101 netbox/ipam/models/ip.py:72
#: netbox/ipam/models/asns.py:132 netbox/ipam/models/ip.py:72
#: netbox/ipam/models/ip.py:88 netbox/ipam/tables/asn.py:20
#: netbox/ipam/tables/asn.py:45 netbox/templates/ipam/aggregate.html:18
#: netbox/ipam/tables/asn.py:55 netbox/templates/ipam/aggregate.html:18
#: netbox/templates/ipam/asn.html:27 netbox/templates/ipam/asnrange.html:19
#: netbox/templates/ipam/rir.html:19
msgid "RIR"
@@ -10834,16 +10834,16 @@ msgstr ""
msgid "ASN ranges"
msgstr ""
#: netbox/ipam/models/asns.py:70
#: netbox/ipam/models/asns.py:63
#, python-brace-format
msgid "Starting ASN ({start}) must be lower than ending ASN ({end})."
msgstr ""
#: netbox/ipam/models/asns.py:102
#: netbox/ipam/models/asns.py:133
msgid "Regional Internet Registry responsible for this AS number space"
msgstr ""
#: netbox/ipam/models/asns.py:107
#: netbox/ipam/models/asns.py:138
msgid "16- or 32-bit autonomous system number"
msgstr ""
@@ -11261,15 +11261,23 @@ msgstr ""
msgid "route targets"
msgstr ""
#: netbox/ipam/tables/asn.py:52
msgid "ASDOT"
#: netbox/ipam/tables/asn.py:26
msgid "Start (ASDOT)"
msgstr ""
#: netbox/ipam/tables/asn.py:57
msgid "Site Count"
#: netbox/ipam/tables/asn.py:31
msgid "End (ASDOT)"
msgstr ""
#: netbox/ipam/tables/asn.py:62
msgid "ASDOT"
msgstr ""
#: netbox/ipam/tables/asn.py:67
msgid "Site Count"
msgstr ""
#: netbox/ipam/tables/asn.py:72
msgid "Provider Count"
msgstr ""
@@ -12279,7 +12287,7 @@ msgstr ""
msgid "Background Tasks"
msgstr ""
#: netbox/netbox/object_actions.py:87
#: netbox/netbox/object_actions.py:88
#: netbox/templates/circuits/inc/circuit_termination.html:10
#: netbox/templates/dcim/manufacturer.html:11
#: netbox/templates/extras/tableconfig_edit.html:29
@@ -12291,12 +12299,12 @@ msgstr ""
msgid "Add"
msgstr ""
#: netbox/netbox/object_actions.py:97
#: netbox/netbox/object_actions.py:98
#: netbox/utilities/templates/buttons/clone.html:4
msgid "Clone"
msgstr ""
#: netbox/netbox/object_actions.py:113
#: netbox/netbox/object_actions.py:114
#: netbox/templates/circuits/inc/circuit_termination.html:15
#: netbox/templates/circuits/inc/circuit_termination_fields.html:37
#: netbox/templates/dcim/inc/panels/inventory_items.html:32
@@ -12309,7 +12317,7 @@ msgstr ""
msgid "Edit"
msgstr ""
#: netbox/netbox/object_actions.py:124
#: netbox/netbox/object_actions.py:125
#: netbox/templates/circuits/inc/circuit_termination.html:23
#: netbox/templates/dcim/inc/panels/inventory_items.html:37
#: netbox/templates/dcim/powerpanel.html:66
@@ -12324,26 +12332,26 @@ msgstr ""
msgid "Delete"
msgstr ""
#: netbox/netbox/object_actions.py:135
#: netbox/netbox/object_actions.py:136
#: netbox/utilities/templatetags/buttons.py:190
msgid "Import"
msgstr ""
#: netbox/netbox/object_actions.py:145
#: netbox/netbox/object_actions.py:146
#: netbox/utilities/templatetags/buttons.py:207
msgid "Export"
msgstr ""
#: netbox/netbox/object_actions.py:173
#: netbox/netbox/object_actions.py:174
#: netbox/utilities/templatetags/buttons.py:227
msgid "Edit Selected"
msgstr ""
#: netbox/netbox/object_actions.py:184
#: netbox/netbox/object_actions.py:200
msgid "Rename Selected"
msgstr ""
#: netbox/netbox/object_actions.py:195
#: netbox/netbox/object_actions.py:211
#: netbox/utilities/templatetags/buttons.py:244
msgid "Delete Selected"
msgstr ""
@@ -12585,69 +12593,75 @@ msgstr ""
msgid "Dummy Plugin"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:122
#: netbox/netbox/views/generic/bulk_views.py:124
#, python-brace-format
msgid ""
"There was an error rendering the selected export template ({template}): "
"{error}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:388
#: netbox/netbox/views/generic/bulk_views.py:390
msgid "Must be a list."
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:398
#: netbox/netbox/views/generic/bulk_views.py:400
msgid "Must be a dictionary."
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:461
#: netbox/netbox/views/generic/bulk_views.py:453
#, python-brace-format
msgid ""
"Duplicate objects found: {model} with ID(s) {ids} appears multiple times"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:475
#, python-brace-format
msgid "Object with ID {id} does not exist"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:546
#: netbox/netbox/views/generic/bulk_views.py:560
#, python-brace-format
msgid "Bulk import {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:562
#: netbox/netbox/views/generic/bulk_views.py:576
#, python-brace-format
msgid "Imported {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:752
#: netbox/netbox/views/generic/bulk_views.py:766
#, python-brace-format
msgid "Bulk edit {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:768
#: netbox/netbox/views/generic/bulk_views.py:782
#, python-brace-format
msgid "Updated {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:801
#: netbox/netbox/views/generic/bulk_views.py:1036
#: netbox/netbox/views/generic/bulk_views.py:1084
#: netbox/netbox/views/generic/bulk_views.py:815
#: netbox/netbox/views/generic/bulk_views.py:1050
#: netbox/netbox/views/generic/bulk_views.py:1098
#, python-brace-format
msgid "No {object_type} were selected."
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:894
#: netbox/netbox/views/generic/bulk_views.py:908
#, python-brace-format
msgid "Renamed {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:964
#: netbox/netbox/views/generic/bulk_views.py:978
#, python-brace-format
msgid "Bulk delete {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:991
#: netbox/netbox/views/generic/bulk_views.py:1005
#, python-brace-format
msgid "Deleted {count} {object_type}"
msgstr ""
#: netbox/netbox/views/generic/bulk_views.py:1008
#: netbox/netbox/views/generic/bulk_views.py:1022
msgid "Deletion failed due to the presence of one or more dependent objects."
msgstr ""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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