Compare commits

..

7 Commits

Author SHA1 Message Date
Jason Novinger
6eeb382512 Release v4.3.4 (#19887)
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
2025-07-15 12:56:11 -05:00
Jeremy Stretch
e5d6c71171 Fixes #19633: Log all evaluations of invalid event rule conditions (#19885)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
* flush_events() should catch only import errors

* Fixes #19633: Log all evaluations of invalid event rule conditions

* Correct comment
2025-07-15 10:25:25 -05:00
Jeremy Stretch
f777bfee2e Fixes #19876: Remove Markdown rendering from CustomFieldChoiceSet description field (#19877) 2025-07-15 07:55:26 -07:00
bctiemann
8b63eb64c1 Merge pull request #19860 from netbox-community/19839-nested-object-parent-export
Fixes #19839: Enable export of parent assignment for recursively nested objects
2025-07-15 08:42:43 -04:00
Jason Novinger
cff29f9551 Fixes #19413: Group custom fields in filter tab
Replaced manual rendering of custom fields in the filter tab with the
`render_custom_fields` template tag. This change ensures that custom fields are
properly grouped, addressing the issue where they were previously displayed
without their associated groups.
2025-07-15 08:41:38 -04:00
github-actions
a5c0cae112 Update source translation strings 2025-07-15 05:05:26 +00:00
Jeremy Stretch
23cc4f1c41 Fixes #19839: Enable export of parent assignment for recursively nested objects 2025-07-10 12:41:11 -04:00
53 changed files with 4320 additions and 3226 deletions

View File

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

View File

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

View File

@@ -14,6 +14,10 @@ django-debug-toolbar
# https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst # https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst
django-filter django-filter
# Django Debug Toolbar extension for GraphiQL
# https://github.com/flavors/django-graphiql-debug-toolbar/blob/main/CHANGES.rst
django-graphiql-debug-toolbar
# HTMX utilities for Django # HTMX utilities for Django
# https://django-htmx.readthedocs.io/en/latest/changelog.html # https://django-htmx.readthedocs.io/en/latest/changelog.html
django-htmx django-htmx
@@ -108,6 +112,7 @@ nh3
# Fork of PIL (Python Imaging Library) for image processing # Fork of PIL (Python Imaging Library) for image processing
# https://github.com/python-pillow/Pillow/releases # https://github.com/python-pillow/Pillow/releases
# https://pillow.readthedocs.io/en/stable/releasenotes/
Pillow Pillow
# PostgreSQL database adapter for Python # PostgreSQL database adapter for Python
@@ -126,14 +131,14 @@ requests
# https://github.com/rq/rq/blob/master/CHANGES.md # https://github.com/rq/rq/blob/master/CHANGES.md
rq rq
# Social authentication framework
# https://github.com/python-social-auth/social-core/blob/master/CHANGELOG.md
social-auth-core
# Django app for social-auth-core # Django app for social-auth-core
# https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md # https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md
social-auth-app-django social-auth-app-django
# Social authentication framework
# https://github.com/python-social-auth/social-core/blob/master/CHANGELOG.md
social-auth-core
# Strawberry GraphQL # Strawberry GraphQL
# https://github.com/strawberry-graphql/strawberry/blob/main/CHANGELOG.md # https://github.com/strawberry-graphql/strawberry/blob/main/CHANGELOG.md
strawberry-graphql strawberry-graphql

View File

@@ -158,6 +158,7 @@ LOGGING = {
* `netbox.<app>.<model>` - Generic form for model-specific log messages * `netbox.<app>.<model>` - Generic form for model-specific log messages
* `netbox.auth.*` - Authentication events * `netbox.auth.*` - Authentication events
* `netbox.api.views.*` - Views which handle business logic for the REST API * `netbox.api.views.*` - Views which handle business logic for the REST API
* `netbox.event_rules` - Event rules
* `netbox.reports.*` - Report execution (`module.name`) * `netbox.reports.*` - Report execution (`module.name`)
* `netbox.scripts.*` - Custom script execution (`module.name`) * `netbox.scripts.*` - Custom script execution (`module.name`)
* `netbox.views.*` - Views which handle business logic for the web UI * `netbox.views.*` - Views which handle business logic for the web UI

View File

@@ -1,5 +1,27 @@
# NetBox v4.3 # NetBox v4.3
## v4.3.4 (2025-07-15)
### Enhancements
* [#18811](https://github.com/netbox-community/netbox/issues/18811) - Match expanded form IPv6 addresses in global search
* [#19550](https://github.com/netbox-community/netbox/issues/19550) - Enable lazy loading for rack elevations
* [#19571](https://github.com/netbox-community/netbox/issues/19571) - Add a default module type profile for expansion cards
* [#19793](https://github.com/netbox-community/netbox/issues/19793) - Support custom dynamic navigation menu links
* [#19828](https://github.com/netbox-community/netbox/issues/19828) - Expose L2VPN termination in interface GraphQL response
### Bug Fixes
* [#19413](https://github.com/netbox-community/netbox/issues/19413) - Custom fields should be grouped in filter forms
* [#19633](https://github.com/netbox-community/netbox/issues/19633) - Introduce InvalidCondition exception and log all evaluations of invalid event rule conditions
* [#19800](https://github.com/netbox-community/netbox/issues/19800) - Module type bulk import should support profile assignment
* [#19806](https://github.com/netbox-community/netbox/issues/19806) - Introduce JobFailed exception to allow marking background jobs as failed
* [#19827](https://github.com/netbox-community/netbox/issues/19827) - Enforce uniqueness for device role names & slugs
* [#19839](https://github.com/netbox-community/netbox/issues/19839) - Enable export of parent assignment for recursively nested objects
* [#19876](https://github.com/netbox-community/netbox/issues/19876) - Remove Markdown rendering from CustomFieldChoiceSet description field
---
## v4.3.3 (2025-06-26) ## v4.3.3 (2025-06-26)
### Enhancements ### Enhancements

View File

@@ -63,6 +63,10 @@ class DeviceRoleTable(NetBoxTable):
verbose_name=_('Name'), verbose_name=_('Name'),
linkify=True linkify=True
) )
parent = tables.Column(
verbose_name=_('Parent'),
linkify=True,
)
device_count = columns.LinkedCountColumn( device_count = columns.LinkedCountColumn(
viewname='dcim:device_list', viewname='dcim:device_list',
url_params={'role_id': 'pk'}, url_params={'role_id': 'pk'},
@@ -88,8 +92,8 @@ class DeviceRoleTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = models.DeviceRole model = models.DeviceRole
fields = ( fields = (
'pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'config_template', 'description', 'pk', 'id', 'name', 'parent', 'device_count', 'vm_count', 'color', 'vm_role', 'config_template',
'slug', 'tags', 'actions', 'created', 'last_updated', 'description', 'slug', 'tags', 'actions', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description') default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description')

View File

@@ -24,6 +24,10 @@ class RegionTable(ContactsColumnMixin, NetBoxTable):
verbose_name=_('Name'), verbose_name=_('Name'),
linkify=True linkify=True
) )
parent = tables.Column(
verbose_name=_('Parent'),
linkify=True,
)
site_count = columns.LinkedCountColumn( site_count = columns.LinkedCountColumn(
viewname='dcim:site_list', viewname='dcim:site_list',
url_params={'region_id': 'pk'}, url_params={'region_id': 'pk'},
@@ -39,7 +43,7 @@ class RegionTable(ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Region model = Region
fields = ( fields = (
'pk', 'id', 'name', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags', 'pk', 'id', 'name', 'parent', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags',
'created', 'last_updated', 'actions', 'created', 'last_updated', 'actions',
) )
default_columns = ('pk', 'name', 'site_count', 'description') default_columns = ('pk', 'name', 'site_count', 'description')
@@ -54,6 +58,10 @@ class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
verbose_name=_('Name'), verbose_name=_('Name'),
linkify=True linkify=True
) )
parent = tables.Column(
verbose_name=_('Parent'),
linkify=True,
)
site_count = columns.LinkedCountColumn( site_count = columns.LinkedCountColumn(
viewname='dcim:site_list', viewname='dcim:site_list',
url_params={'group_id': 'pk'}, url_params={'group_id': 'pk'},
@@ -69,7 +77,7 @@ class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = SiteGroup model = SiteGroup
fields = ( fields = (
'pk', 'id', 'name', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags', 'pk', 'id', 'name', 'parent', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags',
'created', 'last_updated', 'actions', 'created', 'last_updated', 'actions',
) )
default_columns = ('pk', 'name', 'site_count', 'description') default_columns = ('pk', 'name', 'site_count', 'description')
@@ -135,6 +143,10 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
verbose_name=_('Name'), verbose_name=_('Name'),
linkify=True linkify=True
) )
parent = tables.Column(
verbose_name=_('Parent'),
linkify=True,
)
site = tables.Column( site = tables.Column(
verbose_name=_('Site'), verbose_name=_('Site'),
linkify=True linkify=True
@@ -170,8 +182,8 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Location model = Location
fields = ( fields = (
'pk', 'id', 'name', 'site', 'status', 'facility', 'tenant', 'tenant_group', 'rack_count', 'device_count', 'pk', 'id', 'name', 'parent', 'site', 'status', 'facility', 'tenant', 'tenant_group', 'rack_count',
'description', 'slug', 'comments', 'contacts', 'tags', 'actions', 'created', 'last_updated', 'device_count', 'description', 'slug', 'comments', 'contacts', 'tags', 'actions', 'created', 'last_updated',
'vlangroup_count', 'vlangroup_count',
) )
default_columns = ( default_columns = (

View File

@@ -1,13 +1,14 @@
import functools import functools
import operator
import re import re
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
__all__ = ( __all__ = (
'Condition', 'Condition',
'ConditionSet', 'ConditionSet',
'InvalidCondition',
) )
AND = 'and' AND = 'and'
OR = 'or' OR = 'or'
@@ -19,6 +20,10 @@ def is_ruleset(data):
return type(data) is dict and len(data) == 1 and list(data.keys())[0] in (AND, OR) return type(data) is dict and len(data) == 1 and list(data.keys())[0] in (AND, OR)
class InvalidCondition(Exception):
pass
class Condition: class Condition:
""" """
An individual conditional rule that evaluates a single attribute and its value. An individual conditional rule that evaluates a single attribute and its value.
@@ -61,6 +66,7 @@ class Condition:
self.attr = attr self.attr = attr
self.value = value self.value = value
self.op = op
self.eval_func = getattr(self, f'eval_{op}') self.eval_func = getattr(self, f'eval_{op}')
self.negate = negate self.negate = negate
@@ -70,16 +76,17 @@ class Condition:
""" """
def _get(obj, key): def _get(obj, key):
if isinstance(obj, list): if isinstance(obj, list):
return [dict.get(i, key) for i in obj] return [operator.getitem(item or {}, key) for item in obj]
return operator.getitem(obj or {}, key)
return dict.get(obj, key)
try: try:
value = functools.reduce(_get, self.attr.split('.'), data) value = functools.reduce(_get, self.attr.split('.'), data)
except TypeError: except KeyError:
# Invalid key path raise InvalidCondition(f"Invalid key path: {self.attr}")
value = None try:
result = self.eval_func(value) result = self.eval_func(value)
except TypeError as e:
raise InvalidCondition(f"Invalid data type at '{self.attr}' for '{self.op}' evaluation: {e}")
if self.negate: if self.negate:
return not result return not result

View File

@@ -192,5 +192,5 @@ def flush_events(events):
try: try:
func = import_string(name) func = import_string(name)
func(events) func(events)
except Exception as e: except ImportError as e:
logger.error(_("Cannot import events pipeline {name} error: {error}").format(name=name, error=e)) logger.error(_("Cannot import events pipeline {name} error: {error}").format(name=name, error=e))

View File

@@ -13,7 +13,7 @@ from rest_framework.utils.encoders import JSONEncoder
from core.models import ObjectType from core.models import ObjectType
from extras.choices import * from extras.choices import *
from extras.conditions import ConditionSet from extras.conditions import ConditionSet, InvalidCondition
from extras.constants import * from extras.constants import *
from extras.utils import image_upload from extras.utils import image_upload
from extras.models.mixins import RenderTemplateMixin from extras.models.mixins import RenderTemplateMixin
@@ -142,7 +142,15 @@ class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLogged
if not self.conditions: if not self.conditions:
return True return True
return ConditionSet(self.conditions).eval(data) logger = logging.getLogger('netbox.event_rules')
try:
result = ConditionSet(self.conditions).eval(data)
logger.debug(f'{self.name}: Evaluated as {result}')
return result
except InvalidCondition as e:
logger.error(f"{self.name}: Evaluation failed. {e}")
return False
class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel): class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):

View File

@@ -4,7 +4,7 @@ from django.test import TestCase
from core.events import * from core.events import *
from dcim.choices import SiteStatusChoices from dcim.choices import SiteStatusChoices
from dcim.models import Site from dcim.models import Site
from extras.conditions import Condition, ConditionSet from extras.conditions import Condition, ConditionSet, InvalidCondition
from extras.events import serialize_for_event from extras.events import serialize_for_event
from extras.forms import EventRuleForm from extras.forms import EventRuleForm
from extras.models import EventRule, Webhook from extras.models import EventRule, Webhook
@@ -12,16 +12,11 @@ from extras.models import EventRule, Webhook
class ConditionTestCase(TestCase): class ConditionTestCase(TestCase):
def test_dotted_path_access(self):
c = Condition('a.b.c', 1, 'eq')
self.assertTrue(c.eval({'a': {'b': {'c': 1}}}))
self.assertFalse(c.eval({'a': {'b': {'c': 2}}}))
self.assertFalse(c.eval({'a': {'b': {'x': 1}}}))
def test_undefined_attr(self): def test_undefined_attr(self):
c = Condition('x', 1, 'eq') c = Condition('x', 1, 'eq')
self.assertFalse(c.eval({}))
self.assertTrue(c.eval({'x': 1})) self.assertTrue(c.eval({'x': 1}))
with self.assertRaises(InvalidCondition):
c.eval({})
# #
# Validation tests # Validation tests
@@ -37,10 +32,13 @@ class ConditionTestCase(TestCase):
# dict type is unsupported # dict type is unsupported
Condition('x', 1, dict()) Condition('x', 1, dict())
def test_invalid_op_type(self): def test_invalid_op_types(self):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
# 'gt' supports only numeric values # 'gt' supports only numeric values
Condition('x', 'foo', 'gt') Condition('x', 'foo', 'gt')
with self.assertRaises(ValueError):
# 'in' supports only iterable values
Condition('x', 123, 'in')
# #
# Nested attrs tests # Nested attrs tests
@@ -50,7 +48,10 @@ class ConditionTestCase(TestCase):
c = Condition('x.y.z', 1) c = Condition('x.y.z', 1)
self.assertTrue(c.eval({'x': {'y': {'z': 1}}})) self.assertTrue(c.eval({'x': {'y': {'z': 1}}}))
self.assertFalse(c.eval({'x': {'y': {'z': 2}}})) self.assertFalse(c.eval({'x': {'y': {'z': 2}}}))
self.assertFalse(c.eval({'a': {'b': {'c': 1}}})) with self.assertRaises(InvalidCondition):
c.eval({'x': {'y': None}})
with self.assertRaises(InvalidCondition):
c.eval({'x': {'y': {'a': 1}}})
# #
# Operator tests # Operator tests
@@ -74,23 +75,31 @@ class ConditionTestCase(TestCase):
c = Condition('x', 1, 'gt') c = Condition('x', 1, 'gt')
self.assertTrue(c.eval({'x': 2})) self.assertTrue(c.eval({'x': 2}))
self.assertFalse(c.eval({'x': 1})) self.assertFalse(c.eval({'x': 1}))
with self.assertRaises(InvalidCondition):
c.eval({'x': 'foo'}) # Invalid type
def test_gte(self): def test_gte(self):
c = Condition('x', 1, 'gte') c = Condition('x', 1, 'gte')
self.assertTrue(c.eval({'x': 2})) self.assertTrue(c.eval({'x': 2}))
self.assertTrue(c.eval({'x': 1})) self.assertTrue(c.eval({'x': 1}))
self.assertFalse(c.eval({'x': 0})) self.assertFalse(c.eval({'x': 0}))
with self.assertRaises(InvalidCondition):
c.eval({'x': 'foo'}) # Invalid type
def test_lt(self): def test_lt(self):
c = Condition('x', 2, 'lt') c = Condition('x', 2, 'lt')
self.assertTrue(c.eval({'x': 1})) self.assertTrue(c.eval({'x': 1}))
self.assertFalse(c.eval({'x': 2})) self.assertFalse(c.eval({'x': 2}))
with self.assertRaises(InvalidCondition):
c.eval({'x': 'foo'}) # Invalid type
def test_lte(self): def test_lte(self):
c = Condition('x', 2, 'lte') c = Condition('x', 2, 'lte')
self.assertTrue(c.eval({'x': 1})) self.assertTrue(c.eval({'x': 1}))
self.assertTrue(c.eval({'x': 2})) self.assertTrue(c.eval({'x': 2}))
self.assertFalse(c.eval({'x': 3})) self.assertFalse(c.eval({'x': 3}))
with self.assertRaises(InvalidCondition):
c.eval({'x': 'foo'}) # Invalid type
def test_in(self): def test_in(self):
c = Condition('x', [1, 2, 3], 'in') c = Condition('x', [1, 2, 3], 'in')
@@ -106,6 +115,8 @@ class ConditionTestCase(TestCase):
c = Condition('x', 1, 'contains') c = Condition('x', 1, 'contains')
self.assertTrue(c.eval({'x': [1, 2, 3]})) self.assertTrue(c.eval({'x': [1, 2, 3]}))
self.assertFalse(c.eval({'x': [2, 3, 4]})) self.assertFalse(c.eval({'x': [2, 3, 4]}))
with self.assertRaises(InvalidCondition):
c.eval({'x': 123}) # Invalid type
def test_contains_negated(self): def test_contains_negated(self):
c = Condition('x', 1, 'contains', negate=True) c = Condition('x', 1, 'contains', negate=True)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -23,13 +23,13 @@
}, },
"dependencies": { "dependencies": {
"@mdi/font": "7.4.47", "@mdi/font": "7.4.47",
"@tabler/core": "1.3.2", "@tabler/core": "1.4.0",
"bootstrap": "5.3.7", "bootstrap": "5.3.7",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"flatpickr": "4.6.13", "flatpickr": "4.6.13",
"gridstack": "12.2.1", "gridstack": "12.2.2",
"htmx.org": "2.0.5", "htmx.org": "2.0.6",
"query-string": "9.2.1", "query-string": "9.2.2",
"sass": "1.89.2", "sass": "1.89.2",
"tom-select": "2.4.3", "tom-select": "2.4.3",
"typeface-inter": "3.18.1", "typeface-inter": "3.18.1",
@@ -39,15 +39,15 @@
"@types/bootstrap": "5.2.10", "@types/bootstrap": "5.2.10",
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
"@types/node": "^22.3.0", "@types/node": "^22.3.0",
"@typescript-eslint/eslint-plugin": "^8.1.0", "@typescript-eslint/eslint-plugin": "^8.37.0",
"@typescript-eslint/parser": "^8.1.0", "@typescript-eslint/parser": "^8.37.0",
"esbuild": "^0.25.3", "esbuild": "^0.25.6",
"esbuild-sass-plugin": "^3.3.1", "esbuild-sass-plugin": "^3.3.1",
"eslint": "<9.0", "eslint": "<9.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.3", "eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.30.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.2.1", "eslint-plugin-prettier": "^5.5.1",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"typescript": "<5.5" "typescript": "<5.5"
}, },

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
version: "4.3.3" version: "4.3.4"
edition: "Community" edition: "Community"
published: "2025-06-26" published: "2025-07-15"

View File

@@ -14,7 +14,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Description</th> <th scope="row">Description</th>
<td>{{ object.description|markdown|placeholder }}</td> <td>{{ object.description|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Base Choices</th> <th scope="row">Base Choices</th>

View File

@@ -29,11 +29,7 @@
<div class="hr-text"> <div class="hr-text">
<span>{% trans "Custom Fields" %}</span> <span>{% trans "Custom Fields" %}</span>
</div> </div>
{% for name in filter_form.custom_fields %} {% render_custom_fields filter_form %}
{% with field=filter_form|get_item:name %}
{% render_field field %}
{% endwith %}
{% endfor %}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@@ -19,6 +19,10 @@ class ContactGroupTable(NetBoxTable):
verbose_name=_('Name'), verbose_name=_('Name'),
linkify=True linkify=True
) )
parent = tables.Column(
verbose_name=_('Parent'),
linkify=True,
)
contact_count = columns.LinkedCountColumn( contact_count = columns.LinkedCountColumn(
viewname='tenancy:contact_list', viewname='tenancy:contact_list',
url_params={'group_id': 'pk'}, url_params={'group_id': 'pk'},
@@ -34,7 +38,7 @@ class ContactGroupTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = ContactGroup model = ContactGroup
fields = ( fields = (
'pk', 'name', 'contact_count', 'description', 'comments', 'slug', 'tags', 'created', 'pk', 'name', 'parent', 'contact_count', 'description', 'comments', 'slug', 'tags', 'created',
'last_updated', 'actions', 'last_updated', 'actions',
) )
default_columns = ('pk', 'name', 'contact_count', 'description') default_columns = ('pk', 'name', 'contact_count', 'description')

View File

@@ -16,6 +16,10 @@ class TenantGroupTable(NetBoxTable):
verbose_name=_('Name'), verbose_name=_('Name'),
linkify=True linkify=True
) )
parent = tables.Column(
verbose_name=_('Parent'),
linkify=True,
)
tenant_count = columns.LinkedCountColumn( tenant_count = columns.LinkedCountColumn(
viewname='tenancy:tenant_list', viewname='tenancy:tenant_list',
url_params={'group_id': 'pk'}, url_params={'group_id': 'pk'},
@@ -31,7 +35,7 @@ class TenantGroupTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = TenantGroup model = TenantGroup
fields = ( fields = (
'pk', 'id', 'name', 'tenant_count', 'description', 'comments', 'slug', 'tags', 'created', 'pk', 'id', 'name', 'parent', 'tenant_count', 'description', 'comments', 'slug', 'tags', 'created',
'last_updated', 'actions', 'last_updated', 'actions',
) )
default_columns = ('pk', 'name', 'tenant_count', 'description') default_columns = ('pk', 'name', 'tenant_count', 'description')

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 "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-09 05:04+0000\n" "POT-Creation-Date: 2025-07-15 05:05+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -1461,11 +1461,11 @@ msgstr ""
#: netbox/core/models/jobs.py:87 netbox/dcim/models/cables.py:49 #: netbox/core/models/jobs.py:87 netbox/dcim/models/cables.py:49
#: netbox/dcim/models/device_components.py:456 #: netbox/dcim/models/device_components.py:456
#: netbox/dcim/models/device_components.py:1294 #: netbox/dcim/models/device_components.py:1294
#: netbox/dcim/models/devices.py:533 netbox/dcim/models/devices.py:1138 #: netbox/dcim/models/devices.py:555 netbox/dcim/models/devices.py:1160
#: netbox/dcim/models/modules.py:221 netbox/dcim/models/power.py:94 #: netbox/dcim/models/modules.py:221 netbox/dcim/models/power.py:94
#: netbox/dcim/models/racks.py:294 netbox/dcim/models/sites.py:154 #: netbox/dcim/models/racks.py:294 netbox/dcim/models/sites.py:154
#: netbox/dcim/models/sites.py:270 netbox/ipam/models/ip.py:237 #: netbox/dcim/models/sites.py:270 netbox/ipam/models/ip.py:242
#: netbox/ipam/models/ip.py:511 netbox/ipam/models/ip.py:740 #: netbox/ipam/models/ip.py:521 netbox/ipam/models/ip.py:750
#: netbox/ipam/models/vlans.py:217 netbox/virtualization/models/clusters.py:70 #: netbox/ipam/models/vlans.py:217 netbox/virtualization/models/clusters.py:70
#: netbox/virtualization/models/virtualmachines.py:79 #: netbox/virtualization/models/virtualmachines.py:79
#: netbox/vpn/models/l2vpn.py:36 netbox/vpn/models/tunnels.py:38 #: netbox/vpn/models/l2vpn.py:36 netbox/vpn/models/tunnels.py:38
@@ -1592,8 +1592,8 @@ msgstr ""
#: netbox/circuits/models/providers.py:98 netbox/core/models/data.py:39 #: netbox/circuits/models/providers.py:98 netbox/core/models/data.py:39
#: netbox/core/models/jobs.py:48 #: netbox/core/models/jobs.py:48
#: netbox/dcim/models/device_component_templates.py:43 #: netbox/dcim/models/device_component_templates.py:43
#: netbox/dcim/models/device_components.py:52 netbox/dcim/models/devices.py:477 #: netbox/dcim/models/device_components.py:52 netbox/dcim/models/devices.py:499
#: netbox/dcim/models/devices.py:1070 netbox/dcim/models/devices.py:1133 #: netbox/dcim/models/devices.py:1092 netbox/dcim/models/devices.py:1155
#: netbox/dcim/models/modules.py:32 netbox/dcim/models/power.py:38 #: netbox/dcim/models/modules.py:32 netbox/dcim/models/power.py:38
#: netbox/dcim/models/power.py:89 netbox/dcim/models/racks.py:263 #: netbox/dcim/models/power.py:89 netbox/dcim/models/racks.py:263
#: netbox/dcim/models/sites.py:142 netbox/extras/models/configs.py:33 #: netbox/dcim/models/sites.py:142 netbox/extras/models/configs.py:33
@@ -1683,8 +1683,8 @@ msgstr ""
msgid "virtual circuits" msgid "virtual circuits"
msgstr "" msgstr ""
#: netbox/circuits/models/virtual_circuits.py:133 netbox/ipam/models/ip.py:194 #: netbox/circuits/models/virtual_circuits.py:133 netbox/ipam/models/ip.py:199
#: netbox/ipam/models/ip.py:747 netbox/vpn/models/tunnels.py:109 #: netbox/ipam/models/ip.py:757 netbox/vpn/models/tunnels.py:109
msgid "role" msgid "role"
msgstr "" msgstr ""
@@ -2636,7 +2636,7 @@ msgstr ""
msgid "File path relative to the data source's root" msgid "File path relative to the data source's root"
msgstr "" msgstr ""
#: netbox/core/models/data.py:290 netbox/ipam/models/ip.py:492 #: netbox/core/models/data.py:290 netbox/ipam/models/ip.py:502
msgid "size" msgid "size"
msgstr "" msgstr ""
@@ -3645,7 +3645,7 @@ msgstr ""
#: netbox/dcim/filtersets.py:1164 netbox/dcim/forms/filtersets.py:838 #: netbox/dcim/filtersets.py:1164 netbox/dcim/forms/filtersets.py:838
#: netbox/dcim/forms/filtersets.py:1463 netbox/dcim/forms/filtersets.py:1669 #: netbox/dcim/forms/filtersets.py:1463 netbox/dcim/forms/filtersets.py:1669
#: netbox/dcim/forms/filtersets.py:1674 netbox/dcim/forms/model_forms.py:1887 #: netbox/dcim/forms/filtersets.py:1674 netbox/dcim/forms/model_forms.py:1887
#: netbox/dcim/models/devices.py:1234 netbox/dcim/models/devices.py:1254 #: netbox/dcim/models/devices.py:1256 netbox/dcim/models/devices.py:1276
#: netbox/virtualization/filtersets.py:198 #: netbox/virtualization/filtersets.py:198
#: netbox/virtualization/filtersets.py:270 #: netbox/virtualization/filtersets.py:270
#: netbox/virtualization/forms/filtersets.py:178 #: netbox/virtualization/forms/filtersets.py:178
@@ -3806,8 +3806,8 @@ msgstr ""
#: netbox/ipam/forms/model_forms.py:208 netbox/ipam/forms/model_forms.py:256 #: netbox/ipam/forms/model_forms.py:208 netbox/ipam/forms/model_forms.py:256
#: netbox/ipam/forms/model_forms.py:310 netbox/ipam/forms/model_forms.py:474 #: netbox/ipam/forms/model_forms.py:310 netbox/ipam/forms/model_forms.py:474
#: netbox/ipam/forms/model_forms.py:488 netbox/ipam/forms/model_forms.py:502 #: netbox/ipam/forms/model_forms.py:488 netbox/ipam/forms/model_forms.py:502
#: netbox/ipam/models/ip.py:217 netbox/ipam/models/ip.py:501 #: netbox/ipam/models/ip.py:222 netbox/ipam/models/ip.py:511
#: netbox/ipam/models/ip.py:730 netbox/ipam/models/vrfs.py:61 #: netbox/ipam/models/ip.py:740 netbox/ipam/models/vrfs.py:61
#: netbox/ipam/tables/ip.py:189 netbox/ipam/tables/ip.py:262 #: netbox/ipam/tables/ip.py:189 netbox/ipam/tables/ip.py:262
#: netbox/ipam/tables/ip.py:318 netbox/ipam/tables/ip.py:418 #: netbox/ipam/tables/ip.py:318 netbox/ipam/tables/ip.py:418
#: netbox/templates/dcim/interface.html:152 #: netbox/templates/dcim/interface.html:152
@@ -5417,7 +5417,7 @@ msgstr ""
msgid "Device Role" msgid "Device Role"
msgstr "" msgstr ""
#: netbox/dcim/forms/model_forms.py:594 netbox/dcim/models/devices.py:523 #: netbox/dcim/forms/model_forms.py:594 netbox/dcim/models/devices.py:545
msgid "The lowest-numbered unit occupied by the device" msgid "The lowest-numbered unit occupied by the device"
msgstr "" msgstr ""
@@ -6391,14 +6391,14 @@ msgid "inventory item roles"
msgstr "" msgstr ""
#: netbox/dcim/models/device_components.py:1321 #: netbox/dcim/models/device_components.py:1321
#: netbox/dcim/models/devices.py:486 netbox/dcim/models/modules.py:229 #: netbox/dcim/models/devices.py:508 netbox/dcim/models/modules.py:229
#: netbox/dcim/models/racks.py:310 #: netbox/dcim/models/racks.py:310
#: netbox/virtualization/models/virtualmachines.py:125 #: netbox/virtualization/models/virtualmachines.py:125
msgid "serial number" msgid "serial number"
msgstr "" msgstr ""
#: netbox/dcim/models/device_components.py:1329 #: netbox/dcim/models/device_components.py:1329
#: netbox/dcim/models/devices.py:494 netbox/dcim/models/modules.py:236 #: netbox/dcim/models/devices.py:516 netbox/dcim/models/modules.py:236
#: netbox/dcim/models/racks.py:317 #: netbox/dcim/models/racks.py:317
msgid "asset tag" msgid "asset tag"
msgstr "" msgstr ""
@@ -6494,7 +6494,7 @@ msgid ""
"device type is neither a parent nor a child." "device type is neither a parent nor a child."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:131 netbox/dcim/models/devices.py:539 #: netbox/dcim/models/devices.py:131 netbox/dcim/models/devices.py:561
#: netbox/dcim/models/modules.py:95 netbox/dcim/models/racks.py:321 #: netbox/dcim/models/modules.py:95 netbox/dcim/models/racks.py:321
msgid "airflow" msgid "airflow"
msgstr "" msgstr ""
@@ -6539,261 +6539,269 @@ msgstr ""
msgid "Virtual machines may be assigned to this role" msgid "Virtual machines may be assigned to this role"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:401 #: netbox/dcim/models/devices.py:410
msgid "A top-level device role with this name already exists."
msgstr ""
#: netbox/dcim/models/devices.py:420
msgid "A top-level device role with this slug already exists."
msgstr ""
#: netbox/dcim/models/devices.py:423
msgid "device role" msgid "device role"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:402 #: netbox/dcim/models/devices.py:424
msgid "device roles" msgid "device roles"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:416 #: netbox/dcim/models/devices.py:438
msgid "Optionally limit this platform to devices of a certain manufacturer" msgid "Optionally limit this platform to devices of a certain manufacturer"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:428 #: netbox/dcim/models/devices.py:450
msgid "platform" msgid "platform"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:429 #: netbox/dcim/models/devices.py:451
msgid "platforms" msgid "platforms"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:460 #: netbox/dcim/models/devices.py:482
msgid "The function this device serves" msgid "The function this device serves"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:487 #: netbox/dcim/models/devices.py:509
msgid "Chassis serial number, assigned by the manufacturer" msgid "Chassis serial number, assigned by the manufacturer"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:495 netbox/dcim/models/modules.py:237 #: netbox/dcim/models/devices.py:517 netbox/dcim/models/modules.py:237
msgid "A unique tag used to identify this device" msgid "A unique tag used to identify this device"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:522 #: netbox/dcim/models/devices.py:544
msgid "position (U)" msgid "position (U)"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:530 #: netbox/dcim/models/devices.py:552
msgid "rack face" msgid "rack face"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:551 netbox/dcim/models/devices.py:1154 #: netbox/dcim/models/devices.py:573 netbox/dcim/models/devices.py:1176
#: netbox/virtualization/models/virtualmachines.py:94 #: netbox/virtualization/models/virtualmachines.py:94
msgid "primary IPv4" msgid "primary IPv4"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:559 netbox/dcim/models/devices.py:1162 #: netbox/dcim/models/devices.py:581 netbox/dcim/models/devices.py:1184
#: netbox/virtualization/models/virtualmachines.py:102 #: netbox/virtualization/models/virtualmachines.py:102
msgid "primary IPv6" msgid "primary IPv6"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:567 #: netbox/dcim/models/devices.py:589
msgid "out-of-band IP" msgid "out-of-band IP"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:584 #: netbox/dcim/models/devices.py:606
msgid "VC position" msgid "VC position"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:587 #: netbox/dcim/models/devices.py:609
msgid "Virtual chassis position" msgid "Virtual chassis position"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:590 #: netbox/dcim/models/devices.py:612
msgid "VC priority" msgid "VC priority"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:594 #: netbox/dcim/models/devices.py:616
msgid "Virtual chassis master election priority" msgid "Virtual chassis master election priority"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:597 netbox/dcim/models/sites.py:208 #: netbox/dcim/models/devices.py:619 netbox/dcim/models/sites.py:208
msgid "latitude" msgid "latitude"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:602 netbox/dcim/models/devices.py:610 #: netbox/dcim/models/devices.py:624 netbox/dcim/models/devices.py:632
#: netbox/dcim/models/sites.py:213 netbox/dcim/models/sites.py:221 #: netbox/dcim/models/sites.py:213 netbox/dcim/models/sites.py:221
msgid "GPS coordinate in decimal format (xx.yyyyyy)" msgid "GPS coordinate in decimal format (xx.yyyyyy)"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:605 netbox/dcim/models/sites.py:216 #: netbox/dcim/models/devices.py:627 netbox/dcim/models/sites.py:216
msgid "longitude" msgid "longitude"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:684 #: netbox/dcim/models/devices.py:706
msgid "Device name must be unique per site." msgid "Device name must be unique per site."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:695 #: netbox/dcim/models/devices.py:717
msgid "device" msgid "device"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:696 #: netbox/dcim/models/devices.py:718
msgid "devices" msgid "devices"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:715 #: netbox/dcim/models/devices.py:737
#, python-brace-format #, python-brace-format
msgid "Rack {rack} does not belong to site {site}." msgid "Rack {rack} does not belong to site {site}."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:720 #: netbox/dcim/models/devices.py:742
#, python-brace-format #, python-brace-format
msgid "Location {location} does not belong to site {site}." msgid "Location {location} does not belong to site {site}."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:726 #: netbox/dcim/models/devices.py:748
#, python-brace-format #, python-brace-format
msgid "Rack {rack} does not belong to location {location}." msgid "Rack {rack} does not belong to location {location}."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:733 #: netbox/dcim/models/devices.py:755
msgid "Cannot select a rack face without assigning a rack." msgid "Cannot select a rack face without assigning a rack."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:737 #: netbox/dcim/models/devices.py:759
msgid "Cannot select a rack position without assigning a rack." msgid "Cannot select a rack position without assigning a rack."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:743 #: netbox/dcim/models/devices.py:765
msgid "Position must be in increments of 0.5 rack units." msgid "Position must be in increments of 0.5 rack units."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:747 #: netbox/dcim/models/devices.py:769
msgid "Must specify rack face when defining rack position." msgid "Must specify rack face when defining rack position."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:755 #: netbox/dcim/models/devices.py:777
#, python-brace-format #, python-brace-format
msgid "A 0U device type ({device_type}) cannot be assigned to a rack position." msgid "A 0U device type ({device_type}) cannot be assigned to a rack position."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:766 #: netbox/dcim/models/devices.py:788
msgid "" msgid ""
"Child device types cannot be assigned to a rack face. This is an attribute " "Child device types cannot be assigned to a rack face. This is an attribute "
"of the parent device." "of the parent device."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:773 #: netbox/dcim/models/devices.py:795
msgid "" msgid ""
"Child device types cannot be assigned to a rack position. This is an " "Child device types cannot be assigned to a rack position. This is an "
"attribute of the parent device." "attribute of the parent device."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:787 #: netbox/dcim/models/devices.py:809
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"U{position} is already occupied or does not have sufficient space to " "U{position} is already occupied or does not have sufficient space to "
"accommodate this device type: {device_type} ({u_height}U)" "accommodate this device type: {device_type} ({u_height}U)"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:802 #: netbox/dcim/models/devices.py:824
#, python-brace-format #, python-brace-format
msgid "{ip} is not an IPv4 address." msgid "{ip} is not an IPv4 address."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:814 netbox/dcim/models/devices.py:832 #: netbox/dcim/models/devices.py:836 netbox/dcim/models/devices.py:854
#, python-brace-format #, python-brace-format
msgid "The specified IP address ({ip}) is not assigned to this device." msgid "The specified IP address ({ip}) is not assigned to this device."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:820 #: netbox/dcim/models/devices.py:842
#, python-brace-format #, python-brace-format
msgid "{ip} is not an IPv6 address." msgid "{ip} is not an IPv6 address."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:850 #: netbox/dcim/models/devices.py:872
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"The assigned platform is limited to {platform_manufacturer} device types, " "The assigned platform is limited to {platform_manufacturer} device types, "
"but this device's type belongs to {devicetype_manufacturer}." "but this device's type belongs to {devicetype_manufacturer}."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:861 #: netbox/dcim/models/devices.py:883
#, python-brace-format #, python-brace-format
msgid "The assigned cluster belongs to a different site ({site})" msgid "The assigned cluster belongs to a different site ({site})"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:868 #: netbox/dcim/models/devices.py:890
#, python-brace-format #, python-brace-format
msgid "The assigned cluster belongs to a different location ({location})" msgid "The assigned cluster belongs to a different location ({location})"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:876 #: netbox/dcim/models/devices.py:898
msgid "A device assigned to a virtual chassis must have its position defined." msgid "A device assigned to a virtual chassis must have its position defined."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:882 #: netbox/dcim/models/devices.py:904
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"Device cannot be removed from virtual chassis {virtual_chassis} because it " "Device cannot be removed from virtual chassis {virtual_chassis} because it "
"is currently designated as its master." "is currently designated as its master."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:1075 #: netbox/dcim/models/devices.py:1097
msgid "domain" msgid "domain"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:1088 netbox/dcim/models/devices.py:1089 #: netbox/dcim/models/devices.py:1110 netbox/dcim/models/devices.py:1111
msgid "virtual chassis" msgid "virtual chassis"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:1101 #: netbox/dcim/models/devices.py:1123
#, python-brace-format #, python-brace-format
msgid "The selected master ({master}) is not assigned to this virtual chassis." msgid "The selected master ({master}) is not assigned to this virtual chassis."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:1117 #: netbox/dcim/models/devices.py:1139
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"Unable to delete virtual chassis {self}. There are member interfaces which " "Unable to delete virtual chassis {self}. There are member interfaces which "
"form a cross-chassis LAG interfaces." "form a cross-chassis LAG interfaces."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:1143 netbox/vpn/models/l2vpn.py:42 #: netbox/dcim/models/devices.py:1165 netbox/vpn/models/l2vpn.py:42
msgid "identifier" msgid "identifier"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:1144 #: netbox/dcim/models/devices.py:1166
msgid "Numeric identifier unique to the parent device" msgid "Numeric identifier unique to the parent device"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:1172 netbox/extras/models/customfields.py:227 #: netbox/dcim/models/devices.py:1194 netbox/extras/models/customfields.py:227
#: netbox/extras/models/models.py:109 netbox/extras/models/models.py:767 #: netbox/extras/models/models.py:109 netbox/extras/models/models.py:767
#: netbox/netbox/models/__init__.py:120 netbox/netbox/models/__init__.py:155 #: netbox/netbox/models/__init__.py:120 netbox/netbox/models/__init__.py:155
msgid "comments" msgid "comments"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:1188 #: netbox/dcim/models/devices.py:1210
msgid "virtual device context" msgid "virtual device context"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:1189 #: netbox/dcim/models/devices.py:1211
msgid "virtual device contexts" msgid "virtual device contexts"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:1218 #: netbox/dcim/models/devices.py:1240
#, python-brace-format #, python-brace-format
msgid "{ip} is not an IPv{family} address." msgid "{ip} is not an IPv{family} address."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:1224 #: netbox/dcim/models/devices.py:1246
msgid "Primary IP address must belong to an interface on the assigned device." msgid "Primary IP address must belong to an interface on the assigned device."
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:1255 #: netbox/dcim/models/devices.py:1277
msgid "MAC addresses" msgid "MAC addresses"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:1287 #: netbox/dcim/models/devices.py:1309
msgid "" msgid ""
"Cannot unassign MAC Address while it is designated as the primary MAC for an " "Cannot unassign MAC Address while it is designated as the primary MAC for an "
"object" "object"
msgstr "" msgstr ""
#: netbox/dcim/models/devices.py:1291 #: netbox/dcim/models/devices.py:1313
msgid "" msgid ""
"Cannot reassign MAC Address while it is designated as the primary MAC for an " "Cannot reassign MAC Address while it is designated as the primary MAC for an "
"object" "object"
@@ -8656,7 +8664,7 @@ msgstr ""
#: netbox/extras/models/configs.py:38 netbox/extras/models/models.py:315 #: netbox/extras/models/configs.py:38 netbox/extras/models/models.py:315
#: netbox/extras/models/models.py:480 netbox/extras/models/models.py:559 #: netbox/extras/models/models.py:480 netbox/extras/models/models.py:559
#: netbox/extras/models/search.py:48 netbox/extras/models/tags.py:44 #: netbox/extras/models/search.py:48 netbox/extras/models/tags.py:44
#: netbox/ipam/models/ip.py:188 netbox/netbox/models/mixins.py:16 #: netbox/ipam/models/ip.py:193 netbox/netbox/models/mixins.py:16
msgid "weight" msgid "weight"
msgstr "" msgstr ""
@@ -9903,7 +9911,7 @@ msgstr ""
msgid "IP address (ID)" msgid "IP address (ID)"
msgstr "" msgstr ""
#: netbox/ipam/filtersets.py:1205 netbox/ipam/models/ip.py:798 #: netbox/ipam/filtersets.py:1205 netbox/ipam/models/ip.py:808
msgid "IP address" msgid "IP address"
msgstr "" msgstr ""
@@ -10012,7 +10020,7 @@ msgstr ""
#: netbox/ipam/forms/bulk_edit.py:257 netbox/ipam/forms/bulk_edit.py:307 #: netbox/ipam/forms/bulk_edit.py:257 netbox/ipam/forms/bulk_edit.py:307
#: netbox/ipam/forms/filtersets.py:258 netbox/ipam/forms/filtersets.py:316 #: netbox/ipam/forms/filtersets.py:258 netbox/ipam/forms/filtersets.py:316
#: netbox/ipam/models/ip.py:256 #: netbox/ipam/models/ip.py:261
msgid "Treat as fully utilized" msgid "Treat as fully utilized"
msgstr "" msgstr ""
@@ -10025,7 +10033,7 @@ msgstr ""
msgid "Treat as populated" msgid "Treat as populated"
msgstr "" msgstr ""
#: netbox/ipam/forms/bulk_edit.py:355 netbox/ipam/models/ip.py:782 #: netbox/ipam/forms/bulk_edit.py:355 netbox/ipam/models/ip.py:792
msgid "DNS name" msgid "DNS name"
msgstr "" msgstr ""
@@ -10532,185 +10540,185 @@ msgid ""
"({aggregate})." "({aggregate})."
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:195 #: netbox/ipam/models/ip.py:200
msgid "roles" msgid "roles"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:208 netbox/ipam/models/ip.py:277 #: netbox/ipam/models/ip.py:213 netbox/ipam/models/ip.py:282
msgid "prefix" msgid "prefix"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:209 #: netbox/ipam/models/ip.py:214
msgid "IPv4 or IPv6 network with mask" msgid "IPv4 or IPv6 network with mask"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:238 #: netbox/ipam/models/ip.py:243
msgid "Operational status of this prefix" msgid "Operational status of this prefix"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:246 #: netbox/ipam/models/ip.py:251
msgid "The primary function of this prefix" msgid "The primary function of this prefix"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:249 #: netbox/ipam/models/ip.py:254
msgid "is a pool" msgid "is a pool"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:251 #: netbox/ipam/models/ip.py:256
msgid "All IP addresses within this prefix are considered usable" msgid "All IP addresses within this prefix are considered usable"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:254 netbox/ipam/models/ip.py:531 #: netbox/ipam/models/ip.py:259 netbox/ipam/models/ip.py:541
msgid "mark utilized" msgid "mark utilized"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:278 #: netbox/ipam/models/ip.py:283
msgid "prefixes" msgid "prefixes"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:298 #: netbox/ipam/models/ip.py:303
msgid "Cannot create prefix with /0 mask." msgid "Cannot create prefix with /0 mask."
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:305 netbox/ipam/models/ip.py:881 #: netbox/ipam/models/ip.py:310 netbox/ipam/models/ip.py:896
#, python-brace-format #, python-brace-format
msgid "VRF {vrf}" msgid "VRF {vrf}"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:305 netbox/ipam/models/ip.py:881 #: netbox/ipam/models/ip.py:310 netbox/ipam/models/ip.py:896
msgid "global table" msgid "global table"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:307 #: netbox/ipam/models/ip.py:312
#, python-brace-format #, python-brace-format
msgid "Duplicate prefix found in {table}: {prefix}" msgid "Duplicate prefix found in {table}: {prefix}"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:484 #: netbox/ipam/models/ip.py:494
msgid "start address" msgid "start address"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:485 netbox/ipam/models/ip.py:489 #: netbox/ipam/models/ip.py:495 netbox/ipam/models/ip.py:499
#: netbox/ipam/models/ip.py:722 #: netbox/ipam/models/ip.py:732
msgid "IPv4 or IPv6 address (with mask)" msgid "IPv4 or IPv6 address (with mask)"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:488 #: netbox/ipam/models/ip.py:498
msgid "end address" msgid "end address"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:515 #: netbox/ipam/models/ip.py:525
msgid "Operational status of this range" msgid "Operational status of this range"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:523 #: netbox/ipam/models/ip.py:533
msgid "The primary function of this range" msgid "The primary function of this range"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:526 #: netbox/ipam/models/ip.py:536
msgid "mark populated" msgid "mark populated"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:528 #: netbox/ipam/models/ip.py:538
msgid "Prevent the creation of IP addresses within this range" msgid "Prevent the creation of IP addresses within this range"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:533 #: netbox/ipam/models/ip.py:543
#, python-format #, python-format
msgid "Report space as 100% utilized" msgid "Report space as 100% utilized"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:542 #: netbox/ipam/models/ip.py:552
msgid "IP range" msgid "IP range"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:543 #: netbox/ipam/models/ip.py:553
msgid "IP ranges" msgid "IP ranges"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:556 #: netbox/ipam/models/ip.py:566
msgid "Starting and ending IP address versions must match" msgid "Starting and ending IP address versions must match"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:562 #: netbox/ipam/models/ip.py:572
msgid "Starting and ending IP address masks must match" msgid "Starting and ending IP address masks must match"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:569 #: netbox/ipam/models/ip.py:579
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"Ending address must be greater than the starting address ({start_address})" "Ending address must be greater than the starting address ({start_address})"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:597 #: netbox/ipam/models/ip.py:607
#, python-brace-format #, python-brace-format
msgid "Defined addresses overlap with range {overlapping_range} in VRF {vrf}" msgid "Defined addresses overlap with range {overlapping_range} in VRF {vrf}"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:606 #: netbox/ipam/models/ip.py:616
#, python-brace-format #, python-brace-format
msgid "Defined range exceeds maximum supported size ({max_size})" msgid "Defined range exceeds maximum supported size ({max_size})"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:721 netbox/tenancy/models/contacts.py:76 #: netbox/ipam/models/ip.py:731 netbox/tenancy/models/contacts.py:76
msgid "address" msgid "address"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:744 #: netbox/ipam/models/ip.py:754
msgid "The operational status of this IP" msgid "The operational status of this IP"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:752 #: netbox/ipam/models/ip.py:762
msgid "The functional role of this IP" msgid "The functional role of this IP"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:775 netbox/templates/ipam/ipaddress.html:72 #: netbox/ipam/models/ip.py:785 netbox/templates/ipam/ipaddress.html:72
msgid "NAT (inside)" msgid "NAT (inside)"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:776 #: netbox/ipam/models/ip.py:786
msgid "The IP for which this address is the \"outside\" IP" msgid "The IP for which this address is the \"outside\" IP"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:783 #: netbox/ipam/models/ip.py:793
msgid "Hostname or FQDN (not case-sensitive)" msgid "Hostname or FQDN (not case-sensitive)"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:799 netbox/ipam/models/services.py:86 #: netbox/ipam/models/ip.py:809 netbox/ipam/models/services.py:86
msgid "IP addresses" msgid "IP addresses"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:852 #: netbox/ipam/models/ip.py:867
msgid "Cannot create IP address with /0 mask." msgid "Cannot create IP address with /0 mask."
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:858 #: netbox/ipam/models/ip.py:873
#, python-brace-format #, python-brace-format
msgid "{ip} is a network ID, which may not be assigned to an interface." msgid "{ip} is a network ID, which may not be assigned to an interface."
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:869 #: netbox/ipam/models/ip.py:884
#, python-brace-format #, python-brace-format
msgid "{ip} is a broadcast address, which may not be assigned to an interface." msgid "{ip} is a broadcast address, which may not be assigned to an interface."
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:883 #: netbox/ipam/models/ip.py:898
#, python-brace-format #, python-brace-format
msgid "Duplicate IP address found in {table}: {ipaddress}" msgid "Duplicate IP address found in {table}: {ipaddress}"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:899 #: netbox/ipam/models/ip.py:914
#, python-brace-format #, python-brace-format
msgid "Cannot create IP address {ip} inside range {range}." msgid "Cannot create IP address {ip} inside range {range}."
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:920 #: netbox/ipam/models/ip.py:935
msgid "" msgid ""
"Cannot reassign IP address while it is designated as the primary IP for the " "Cannot reassign IP address while it is designated as the primary IP for the "
"parent object" "parent object"
msgstr "" msgstr ""
#: netbox/ipam/models/ip.py:926 #: netbox/ipam/models/ip.py:941
msgid "Only IPv6 addresses can be assigned SLAAC status" msgid "Only IPv6 addresses can be assigned SLAAC status"
msgstr "" msgstr ""
@@ -11863,16 +11871,16 @@ msgstr ""
msgid "Background Tasks" msgid "Background Tasks"
msgstr "" msgstr ""
#: netbox/netbox/plugins/navigation.py:48 #: netbox/netbox/plugins/navigation.py:55
#: netbox/netbox/plugins/navigation.py:70 #: netbox/netbox/plugins/navigation.py:88
msgid "Permissions must be passed as a tuple or list." msgid "Permissions must be passed as a tuple or list."
msgstr "" msgstr ""
#: netbox/netbox/plugins/navigation.py:52 #: netbox/netbox/plugins/navigation.py:59
msgid "Buttons must be passed as a tuple or list." msgid "Buttons must be passed as a tuple or list."
msgstr "" msgstr ""
#: netbox/netbox/plugins/navigation.py:74 #: netbox/netbox/plugins/navigation.py:92
msgid "Button color must be a choice within ButtonColorChoices." msgid "Button color must be a choice within ButtonColorChoices."
msgstr "" msgstr ""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,10 @@ class WirelessLANGroupTable(NetBoxTable):
verbose_name=_('Name'), verbose_name=_('Name'),
linkify=True linkify=True
) )
parent = tables.Column(
verbose_name=_('Parent'),
linkify=True,
)
wirelesslan_count = columns.LinkedCountColumn( wirelesslan_count = columns.LinkedCountColumn(
viewname='wireless:wirelesslan_list', viewname='wireless:wirelesslan_list',
url_params={'group_id': 'pk'}, url_params={'group_id': 'pk'},
@@ -33,8 +37,8 @@ class WirelessLANGroupTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = WirelessLANGroup model = WirelessLANGroup
fields = ( fields = (
'pk', 'name', 'wirelesslan_count', 'slug', 'description', 'comments', 'tags', 'created', 'last_updated', 'pk', 'name', 'parent', 'slug', 'description', 'comments', 'tags', 'wirelesslan_count', 'created',
'actions', 'last_updated', 'actions',
) )
default_columns = ('pk', 'name', 'wirelesslan_count', 'description') default_columns = ('pk', 'name', 'wirelesslan_count', 'description')

View File

@@ -3,7 +3,7 @@
[project] [project]
name = "netbox" name = "netbox"
version = "4.3.3" version = "4.3.4"
requires-python = ">=3.10" requires-python = ">=3.10"
authors = [ authors = [
{ name = "NetBox Community" } { name = "NetBox Community" }

View File

@@ -1,9 +1,9 @@
Django==5.2.3 Django==5.2.4
django-cors-headers==4.7.0 django-cors-headers==4.7.0
django-debug-toolbar==5.2.0 django-debug-toolbar==5.2.0
django-filter==25.1 django-filter==25.1
django-htmx==1.23.1
django-graphiql-debug-toolbar==0.2.0 django-graphiql-debug-toolbar==0.2.0
django-htmx==1.23.2
django-mptt==0.17.0 django-mptt==0.17.0
django-pglocks==1.0.4 django-pglocks==1.0.4
django-prometheus==2.4.1 django-prometheus==2.4.1
@@ -11,29 +11,29 @@ django-redis==6.0.0
django-rich==2.0.0 django-rich==2.0.0
django-rq==3.0.1 django-rq==3.0.1
django-storages==1.14.6 django-storages==1.14.6
django-taggit==6.1.0
django-tables2==2.7.5 django-tables2==2.7.5
django-taggit==6.1.0
django-timezone-field==7.1 django-timezone-field==7.1
djangorestframework==3.16.0 djangorestframework==3.16.0
drf-spectacular==0.28.0 drf-spectacular==0.28.0
drf-spectacular-sidecar==2025.6.1 drf-spectacular-sidecar==2025.7.1
feedparser==6.0.11 feedparser==6.0.11
gunicorn==23.0.0 gunicorn==23.0.0
Jinja2==3.1.6 Jinja2==3.1.6
jsonschema==4.24.0 jsonschema==4.24.0
Markdown==3.8.2 Markdown==3.8.2
mkdocs-material==9.6.14 mkdocs-material==9.6.15
mkdocstrings[python]==0.29.1 mkdocstrings[python]==0.29.1
netaddr==1.3.0 netaddr==1.3.0
nh3==0.2.21 nh3==0.2.22
Pillow==11.2.1 Pillow==11.3.0
psycopg[c,pool]==3.2.9 psycopg[c,pool]==3.2.9
PyYAML==6.0.2 PyYAML==6.0.2
requests==2.32.4 requests==2.32.4
rq==2.4.0 rq==2.4.0
social-auth-app-django==5.4.3 social-auth-app-django==5.5.1
social-auth-core==4.6.1 social-auth-core==4.7.0
strawberry-graphql==0.275.4 strawberry-graphql==0.276.0
strawberry-graphql-django==0.60.0 strawberry-graphql-django==0.60.0
svgwrite==1.4.3 svgwrite==1.4.3
tablib==3.8.0 tablib==3.8.0