mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-31 01:27:45 -06:00
Compare commits
19 Commits
19506-comp
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a391253a5 | ||
|
|
914653d63e | ||
|
|
3813aad8b1 | ||
|
|
ea5371040e | ||
|
|
6c824cc48f | ||
|
|
f510e40428 | ||
|
|
860db9590b | ||
|
|
7c63d001b1 | ||
|
|
93119f52c3 | ||
|
|
ee2aa35cba | ||
|
|
7896a48075 | ||
|
|
eb87c3f304 | ||
|
|
3acbb0a08c | ||
|
|
f67cc47def | ||
|
|
f7219e0672 | ||
|
|
e5a975176d | ||
|
|
83ee4fb593 | ||
|
|
db8271c904 | ||
|
|
d95fa8dbb2 |
@@ -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
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/`.)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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',
|
||||
])
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}'
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
10
netbox/project-static/dist/netbox.js
vendored
10
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
6
netbox/project-static/dist/netbox.js.map
vendored
6
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version: "4.4.8"
|
||||
version: "4.4.9"
|
||||
edition: "Community"
|
||||
published: "2025-12-09"
|
||||
published: "2025-12-23"
|
||||
|
||||
@@ -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
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -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
BIN
netbox/translations/lv/LC_MESSAGES/django.mo
Normal file
BIN
netbox/translations/lv/LC_MESSAGES/django.mo
Normal file
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
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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user