mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-16 00:32:18 -06:00
Compare commits
1 Commits
main
...
21140-pane
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bda369623c |
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -30,13 +30,13 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v4
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
config-file: .github/codeql/codeql-config.yml
|
config-file: .github/codeql/codeql-config.yml
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v4
|
uses: github/codeql-action/analyze@v3
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
|||||||
@@ -44,4 +44,3 @@ class DataFileSerializer(NetBoxModelSerializer):
|
|||||||
'id', 'url', 'display_url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
|
'id', 'url', 'display_url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'path')
|
brief_fields = ('id', 'url', 'display', 'path')
|
||||||
read_only_fields = ['path', 'last_updated', 'size', 'hash']
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from django.core.validators import RegexValidator
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
||||||
from netbox.models import PrimaryModel
|
from netbox.models import PrimaryModel
|
||||||
@@ -128,9 +128,7 @@ class DataSource(JobsMixin, PrimaryModel):
|
|||||||
# Ensure URL scheme matches selected type
|
# Ensure URL scheme matches selected type
|
||||||
if self.backend_class.is_local and self.url_scheme not in ('file', ''):
|
if self.backend_class.is_local and self.url_scheme not in ('file', ''):
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'source_url': _("URLs for local sources must start with {scheme} (or specify no scheme)").format(
|
'source_url': "URLs for local sources must start with file:// (or specify no scheme)"
|
||||||
scheme='file://'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -38,15 +38,6 @@ class ScopedFilterMixin:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ComponentModelFilterMixin:
|
class ComponentModelFilterMixin:
|
||||||
_site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
|
||||||
strawberry_django.filter_field(name='site')
|
|
||||||
)
|
|
||||||
_location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
|
||||||
strawberry_django.filter_field(name='location')
|
|
||||||
)
|
|
||||||
_rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
|
||||||
strawberry_django.filter_field(name='rack')
|
|
||||||
)
|
|
||||||
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||||
device_id: ID | None = strawberry_django.filter_field()
|
device_id: ID | None = strawberry_django.filter_field()
|
||||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||||
|
|||||||
@@ -211,16 +211,12 @@ def sync_cached_scope_fields(instance, created, **kwargs):
|
|||||||
for model in (Prefix, Cluster, WirelessLAN):
|
for model in (Prefix, Cluster, WirelessLAN):
|
||||||
qs = model.objects.filter(**filters)
|
qs = model.objects.filter(**filters)
|
||||||
|
|
||||||
# Bulk update cached fields to avoid O(N) performance issues with large datasets.
|
|
||||||
# This does not trigger post_save signals, avoiding spurious change log entries.
|
|
||||||
objects_to_update = []
|
|
||||||
for obj in qs:
|
for obj in qs:
|
||||||
# Recompute cache using the same logic as save()
|
# Recompute cache using the same logic as save()
|
||||||
obj.cache_related_objects()
|
obj.cache_related_objects()
|
||||||
objects_to_update.append(obj)
|
obj.save(update_fields=[
|
||||||
|
'_location',
|
||||||
if objects_to_update:
|
'_site',
|
||||||
model.objects.bulk_update(
|
'_site_group',
|
||||||
objects_to_update,
|
'_region',
|
||||||
['_location', '_site', '_site_group', '_region']
|
])
|
||||||
)
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class RackDimensionsPanel(panels.ObjectAttributesPanel):
|
|||||||
outer_width = attrs.NumericAttr('outer_width', unit_accessor='get_outer_unit_display')
|
outer_width = attrs.NumericAttr('outer_width', unit_accessor='get_outer_unit_display')
|
||||||
outer_height = attrs.NumericAttr('outer_height', unit_accessor='get_outer_unit_display')
|
outer_height = attrs.NumericAttr('outer_height', unit_accessor='get_outer_unit_display')
|
||||||
outer_depth = attrs.NumericAttr('outer_depth', unit_accessor='get_outer_unit_display')
|
outer_depth = attrs.NumericAttr('outer_depth', unit_accessor='get_outer_unit_display')
|
||||||
mounting_depth = attrs.TextAttr('mounting_depth', format_string='{} mm')
|
mounting_depth = attrs.TextAttr('mounting_depth', format_string='{}mm')
|
||||||
|
|
||||||
|
|
||||||
class RackNumberingPanel(panels.ObjectAttributesPanel):
|
class RackNumberingPanel(panels.ObjectAttributesPanel):
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class ConfigContextProfileSerializer(PrimaryModelSerializer):
|
|||||||
)
|
)
|
||||||
data_file = DataFileSerializer(
|
data_file = DataFileSerializer(
|
||||||
nested=True,
|
nested=True,
|
||||||
required=False
|
read_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -143,7 +143,7 @@ class ConfigContextSerializer(OwnerMixin, ChangeLogMessageSerializer, ValidatedM
|
|||||||
)
|
)
|
||||||
data_file = DataFileSerializer(
|
data_file = DataFileSerializer(
|
||||||
nested=True,
|
nested=True,
|
||||||
required=False
|
read_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -8,7 +7,7 @@ from rest_framework import status
|
|||||||
|
|
||||||
from core.choices import ManagedFileRootPathChoices
|
from core.choices import ManagedFileRootPathChoices
|
||||||
from core.events import *
|
from core.events import *
|
||||||
from core.models import DataFile, DataSource, ObjectType
|
from core.models import ObjectType
|
||||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
|
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
@@ -732,51 +731,6 @@ class ConfigContextProfileTest(APIViewTestCases.APIViewTestCase):
|
|||||||
)
|
)
|
||||||
ConfigContextProfile.objects.bulk_create(profiles)
|
ConfigContextProfile.objects.bulk_create(profiles)
|
||||||
|
|
||||||
def test_update_data_source_and_data_file(self):
|
|
||||||
"""
|
|
||||||
Regression test: Ensure data_source and data_file can be assigned via the API.
|
|
||||||
|
|
||||||
This specifically covers PATCHing a ConfigContext with integer IDs for both fields.
|
|
||||||
"""
|
|
||||||
self.add_permissions(
|
|
||||||
'core.view_datafile',
|
|
||||||
'core.view_datasource',
|
|
||||||
'extras.view_configcontextprofile',
|
|
||||||
'extras.change_configcontextprofile',
|
|
||||||
)
|
|
||||||
config_context_profile = ConfigContextProfile.objects.first()
|
|
||||||
|
|
||||||
# Create a data source and file
|
|
||||||
datasource = DataSource.objects.create(
|
|
||||||
name='Data Source 1',
|
|
||||||
type='local',
|
|
||||||
source_url='file:///tmp/netbox-datasource/',
|
|
||||||
)
|
|
||||||
# Generate a valid dummy YAML file
|
|
||||||
file_data = b'profile: configcontext\n'
|
|
||||||
datafile = DataFile.objects.create(
|
|
||||||
source=datasource,
|
|
||||||
path='dir1/file1.yml',
|
|
||||||
last_updated=now(),
|
|
||||||
size=len(file_data),
|
|
||||||
hash=hashlib.sha256(file_data).hexdigest(),
|
|
||||||
data=file_data,
|
|
||||||
)
|
|
||||||
|
|
||||||
url = self._get_detail_url(config_context_profile)
|
|
||||||
payload = {
|
|
||||||
'data_source': datasource.pk,
|
|
||||||
'data_file': datafile.pk,
|
|
||||||
}
|
|
||||||
response = self.client.patch(url, payload, format='json', **self.header)
|
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
config_context_profile.refresh_from_db()
|
|
||||||
self.assertEqual(config_context_profile.data_source_id, datasource.pk)
|
|
||||||
self.assertEqual(config_context_profile.data_file_id, datafile.pk)
|
|
||||||
self.assertEqual(response.data['data_source']['id'], datasource.pk)
|
|
||||||
self.assertEqual(response.data['data_file']['id'], datafile.pk)
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = ConfigContext
|
model = ConfigContext
|
||||||
@@ -858,51 +812,6 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|||||||
rendered_context = device.get_config_context()
|
rendered_context = device.get_config_context()
|
||||||
self.assertEqual(rendered_context['bar'], 456)
|
self.assertEqual(rendered_context['bar'], 456)
|
||||||
|
|
||||||
def test_update_data_source_and_data_file(self):
|
|
||||||
"""
|
|
||||||
Regression test: Ensure data_source and data_file can be assigned via the API.
|
|
||||||
|
|
||||||
This specifically covers PATCHing a ConfigContext with integer IDs for both fields.
|
|
||||||
"""
|
|
||||||
self.add_permissions(
|
|
||||||
'core.view_datafile',
|
|
||||||
'core.view_datasource',
|
|
||||||
'extras.view_configcontext',
|
|
||||||
'extras.change_configcontext',
|
|
||||||
)
|
|
||||||
config_context = ConfigContext.objects.first()
|
|
||||||
|
|
||||||
# Create a data source and file
|
|
||||||
datasource = DataSource.objects.create(
|
|
||||||
name='Data Source 1',
|
|
||||||
type='local',
|
|
||||||
source_url='file:///tmp/netbox-datasource/',
|
|
||||||
)
|
|
||||||
# Generate a valid dummy YAML file
|
|
||||||
file_data = b'context: config\n'
|
|
||||||
datafile = DataFile.objects.create(
|
|
||||||
source=datasource,
|
|
||||||
path='dir1/file1.yml',
|
|
||||||
last_updated=now(),
|
|
||||||
size=len(file_data),
|
|
||||||
hash=hashlib.sha256(file_data).hexdigest(),
|
|
||||||
data=file_data,
|
|
||||||
)
|
|
||||||
|
|
||||||
url = self._get_detail_url(config_context)
|
|
||||||
payload = {
|
|
||||||
'data_source': datasource.pk,
|
|
||||||
'data_file': datafile.pk,
|
|
||||||
}
|
|
||||||
response = self.client.patch(url, payload, format='json', **self.header)
|
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
config_context.refresh_from_db()
|
|
||||||
self.assertEqual(config_context.data_source_id, datasource.pk)
|
|
||||||
self.assertEqual(config_context.data_file_id, datafile.pk)
|
|
||||||
self.assertEqual(response.data['data_source']['id'], datasource.pk)
|
|
||||||
self.assertEqual(response.data['data_file']['id'], datafile.pk)
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigTemplateTest(APIViewTestCases.APIViewTestCase):
|
class ConfigTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = ConfigTemplate
|
model = ConfigTemplate
|
||||||
|
|||||||
@@ -372,8 +372,8 @@ class IPAddressForm(TenancyForm, PrimaryModelForm):
|
|||||||
'virtual_machine_id': instance.assigned_object.virtual_machine.pk,
|
'virtual_machine_id': instance.assigned_object.virtual_machine.pk,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Disable object assignment fields if the IP address is designated as primary or OOB
|
# Disable object assignment fields if the IP address is designated as primary
|
||||||
if self.initial.get('primary_for_parent') or self.initial.get('oob_for_parent'):
|
if self.initial.get('primary_for_parent'):
|
||||||
self.fields['interface'].disabled = True
|
self.fields['interface'].disabled = True
|
||||||
self.fields['vminterface'].disabled = True
|
self.fields['vminterface'].disabled = True
|
||||||
self.fields['fhrpgroup'].disabled = True
|
self.fields['fhrpgroup'].disabled = True
|
||||||
|
|||||||
@@ -940,13 +940,6 @@ class IPAddress(ContactsMixin, PrimaryModel):
|
|||||||
_("Cannot reassign IP address while it is designated as the primary IP for the parent object")
|
_("Cannot reassign IP address while it is designated as the primary IP for the parent object")
|
||||||
)
|
)
|
||||||
|
|
||||||
# can't use is_oob_ip as self.assigned_object might be changed
|
|
||||||
if hasattr(original_parent, 'oob_ip') and original_parent.oob_ip_id == self.pk:
|
|
||||||
if parent != original_parent:
|
|
||||||
raise ValidationError(
|
|
||||||
_("Cannot reassign IP address while it is designated as the OOB IP for the parent object")
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate IP status selection
|
# Validate IP status selection
|
||||||
if self.status == IPAddressStatusChoices.STATUS_SLAAC and self.family != 6:
|
if self.status == IPAddressStatusChoices.STATUS_SLAAC and self.family != 6:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
import strawberry_django
|
import strawberry_django
|
||||||
from strawberry import ID
|
from strawberry import ID
|
||||||
from strawberry_django import ComparisonFilterLookup, FilterLookup
|
from strawberry_django import FilterLookup
|
||||||
|
|
||||||
from core.graphql.filter_mixins import ChangeLoggingMixin
|
from core.graphql.filter_mixins import ChangeLoggingMixin
|
||||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, JournalEntriesFilterMixin, TagsFilterMixin
|
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, JournalEntriesFilterMixin, TagsFilterMixin
|
||||||
@@ -23,7 +23,7 @@ __all__ = (
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BaseModelFilter:
|
class BaseModelFilter:
|
||||||
id: ComparisonFilterLookup[ID] | None = strawberry_django.filter_field()
|
id: FilterLookup[ID] | None = strawberry_django.filter_field()
|
||||||
|
|
||||||
|
|
||||||
class ChangeLoggedModelFilter(ChangeLoggingMixin, BaseModelFilter):
|
class ChangeLoggedModelFilter(ChangeLoggingMixin, BaseModelFilter):
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ class PluginMenuItem:
|
|||||||
Alternatively, a pre-generated url can be set on the object which will be rendered literally.
|
Alternatively, a pre-generated url can be set on the object which will be rendered literally.
|
||||||
Buttons are each specified as a list of PluginMenuButton instances.
|
Buttons are each specified as a list of PluginMenuButton instances.
|
||||||
"""
|
"""
|
||||||
|
permissions = []
|
||||||
|
buttons = []
|
||||||
_url = None
|
_url = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -52,14 +54,10 @@ class PluginMenuItem:
|
|||||||
if type(permissions) not in (list, tuple):
|
if type(permissions) not in (list, tuple):
|
||||||
raise TypeError(_("Permissions must be passed as a tuple or list."))
|
raise TypeError(_("Permissions must be passed as a tuple or list."))
|
||||||
self.permissions = permissions
|
self.permissions = permissions
|
||||||
else:
|
|
||||||
self.permissions = []
|
|
||||||
if buttons is not None:
|
if buttons is not None:
|
||||||
if type(buttons) not in (list, tuple):
|
if type(buttons) not in (list, tuple):
|
||||||
raise TypeError(_("Buttons must be passed as a tuple or list."))
|
raise TypeError(_("Buttons must be passed as a tuple or list."))
|
||||||
self.buttons = buttons
|
self.buttons = buttons
|
||||||
else:
|
|
||||||
self.buttons = []
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
@@ -76,6 +74,7 @@ class PluginMenuButton:
|
|||||||
ButtonColorChoices.
|
ButtonColorChoices.
|
||||||
"""
|
"""
|
||||||
color = ButtonColorChoices.DEFAULT
|
color = ButtonColorChoices.DEFAULT
|
||||||
|
permissions = []
|
||||||
_url = None
|
_url = None
|
||||||
|
|
||||||
def __init__(self, link, title, icon_class, color=None, permissions=None):
|
def __init__(self, link, title, icon_class, color=None, permissions=None):
|
||||||
@@ -88,8 +87,6 @@ class PluginMenuButton:
|
|||||||
if type(permissions) not in (list, tuple):
|
if type(permissions) not in (list, tuple):
|
||||||
raise TypeError(_("Permissions must be passed as a tuple or list."))
|
raise TypeError(_("Permissions must be passed as a tuple or list."))
|
||||||
self.permissions = permissions
|
self.permissions = permissions
|
||||||
else:
|
|
||||||
self.permissions = []
|
|
||||||
if color is not None:
|
if color is not None:
|
||||||
if color not in ButtonColorChoices.values():
|
if color not in ButtonColorChoices.values():
|
||||||
raise ValueError(_("Button color must be a choice within ButtonColorChoices."))
|
raise ValueError(_("Button color must be a choice within ButtonColorChoices."))
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from netbox.tests.dummy_plugin import config as dummy_config
|
|||||||
from netbox.tests.dummy_plugin.data_backends import DummyBackend
|
from netbox.tests.dummy_plugin.data_backends import DummyBackend
|
||||||
from netbox.tests.dummy_plugin.jobs import DummySystemJob
|
from netbox.tests.dummy_plugin.jobs import DummySystemJob
|
||||||
from netbox.tests.dummy_plugin.webhook_callbacks import set_context
|
from netbox.tests.dummy_plugin.webhook_callbacks import set_context
|
||||||
from netbox.plugins.navigation import PluginMenu, PluginMenuItem, PluginMenuButton
|
from netbox.plugins.navigation import PluginMenu
|
||||||
from netbox.plugins.utils import get_plugin_config
|
from netbox.plugins.utils import get_plugin_config
|
||||||
from netbox.graphql.schema import Query
|
from netbox.graphql.schema import Query
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
@@ -227,46 +227,3 @@ class PluginTest(TestCase):
|
|||||||
Test the registration of webhook callbacks.
|
Test the registration of webhook callbacks.
|
||||||
"""
|
"""
|
||||||
self.assertIn(set_context, registry['webhook_callbacks'])
|
self.assertIn(set_context, registry['webhook_callbacks'])
|
||||||
|
|
||||||
|
|
||||||
class PluginNavigationTest(TestCase):
|
|
||||||
|
|
||||||
def test_plugin_menu_item_independent_permissions(self):
|
|
||||||
item1 = PluginMenuItem(link='test1', link_text='Test 1')
|
|
||||||
item1.permissions.append('leaked_permission')
|
|
||||||
|
|
||||||
item2 = PluginMenuItem(link='test2', link_text='Test 2')
|
|
||||||
|
|
||||||
self.assertIsNot(item1.permissions, item2.permissions)
|
|
||||||
self.assertEqual(item1.permissions, ['leaked_permission'])
|
|
||||||
self.assertEqual(item2.permissions, [])
|
|
||||||
|
|
||||||
def test_plugin_menu_item_independent_buttons(self):
|
|
||||||
item1 = PluginMenuItem(link='test1', link_text='Test 1')
|
|
||||||
button = PluginMenuButton(link='button1', title='Button 1', icon_class='mdi-test')
|
|
||||||
item1.buttons.append(button)
|
|
||||||
|
|
||||||
item2 = PluginMenuItem(link='test2', link_text='Test 2')
|
|
||||||
|
|
||||||
self.assertIsNot(item1.buttons, item2.buttons)
|
|
||||||
self.assertEqual(len(item1.buttons), 1)
|
|
||||||
self.assertEqual(item1.buttons[0], button)
|
|
||||||
self.assertEqual(item2.buttons, [])
|
|
||||||
|
|
||||||
def test_plugin_menu_button_independent_permissions(self):
|
|
||||||
button1 = PluginMenuButton(link='button1', title='Button 1', icon_class='mdi-test')
|
|
||||||
button1.permissions.append('leaked_permission')
|
|
||||||
|
|
||||||
button2 = PluginMenuButton(link='button2', title='Button 2', icon_class='mdi-test')
|
|
||||||
|
|
||||||
self.assertIsNot(button1.permissions, button2.permissions)
|
|
||||||
self.assertEqual(button1.permissions, ['leaked_permission'])
|
|
||||||
self.assertEqual(button2.permissions, [])
|
|
||||||
|
|
||||||
def test_explicit_permissions_remain_independent(self):
|
|
||||||
item1 = PluginMenuItem(link='test1', link_text='Test 1', permissions=['explicit_permission'])
|
|
||||||
item2 = PluginMenuItem(link='test2', link_text='Test 2', permissions=['different_permission'])
|
|
||||||
|
|
||||||
self.assertIsNot(item1.permissions, item2.permissions)
|
|
||||||
self.assertEqual(item1.permissions, ['explicit_permission'])
|
|
||||||
self.assertEqual(item2.permissions, ['different_permission'])
|
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ class ObjectAttributesPanel(ObjectPanel, metaclass=ObjectAttributesPanelMeta):
|
|||||||
"""
|
"""
|
||||||
label = name[:1].upper() + name[1:]
|
label = name[:1].upper() + name[1:]
|
||||||
label = label.replace('_', ' ')
|
label = label.replace('_', ' ')
|
||||||
return label
|
return _(label)
|
||||||
|
|
||||||
def get_context(self, context):
|
def get_context(self, context):
|
||||||
# Determine which attributes to display in the panel based on only/exclude args
|
# Determine which attributes to display in the panel based on only/exclude args
|
||||||
|
|||||||
@@ -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: 2026-01-16 05:04+0000\n"
|
"POT-Creation-Date: 2026-01-13 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"
|
||||||
@@ -2435,7 +2435,7 @@ msgstr ""
|
|||||||
msgid "Change logging is not supported for this object type ({type})."
|
msgid "Change logging is not supported for this object type ({type})."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/core/models/config.py:21 netbox/core/models/data.py:284
|
#: 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/core/models/files.py:29 netbox/core/models/jobs.py:60
|
||||||
#: netbox/extras/models/models.py:847 netbox/extras/models/notifications.py:39
|
#: netbox/extras/models/models.py:847 netbox/extras/models/notifications.py:39
|
||||||
#: netbox/extras/models/notifications.py:195
|
#: netbox/extras/models/notifications.py:195
|
||||||
@@ -2541,63 +2541,58 @@ msgstr ""
|
|||||||
msgid "Unknown backend type: {type}"
|
msgid "Unknown backend type: {type}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/core/models/data.py:131
|
#: netbox/core/models/data.py:180
|
||||||
#, python-brace-format
|
|
||||||
msgid "URLs for local sources must start with {scheme} (or specify no scheme)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: netbox/core/models/data.py:182
|
|
||||||
msgid "Cannot initiate sync; syncing already in progress."
|
msgid "Cannot initiate sync; syncing already in progress."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/core/models/data.py:195
|
#: netbox/core/models/data.py:193
|
||||||
msgid ""
|
msgid ""
|
||||||
"There was an error initializing the backend. A dependency needs to be "
|
"There was an error initializing the backend. A dependency needs to be "
|
||||||
"installed: "
|
"installed: "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/core/models/data.py:288 netbox/core/models/files.py:33
|
#: netbox/core/models/data.py:286 netbox/core/models/files.py:33
|
||||||
#: netbox/netbox/models/features.py:67
|
#: netbox/netbox/models/features.py:67
|
||||||
msgid "last updated"
|
msgid "last updated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/core/models/data.py:298 netbox/dcim/models/cables.py:622
|
#: netbox/core/models/data.py:296 netbox/dcim/models/cables.py:622
|
||||||
msgid "path"
|
msgid "path"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/core/models/data.py:301
|
#: netbox/core/models/data.py:299
|
||||||
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:305 netbox/ipam/models/ip.py:507
|
#: netbox/core/models/data.py:303 netbox/ipam/models/ip.py:507
|
||||||
msgid "size"
|
msgid "size"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/core/models/data.py:308
|
#: netbox/core/models/data.py:306
|
||||||
msgid "hash"
|
msgid "hash"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/core/models/data.py:312
|
#: netbox/core/models/data.py:310
|
||||||
msgid "Length must be 64 hexadecimal characters."
|
msgid "Length must be 64 hexadecimal characters."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/core/models/data.py:314
|
#: netbox/core/models/data.py:312
|
||||||
msgid "SHA256 hash of the file data"
|
msgid "SHA256 hash of the file data"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/core/models/data.py:328
|
#: netbox/core/models/data.py:326
|
||||||
msgid "data file"
|
msgid "data file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/core/models/data.py:329
|
#: netbox/core/models/data.py:327
|
||||||
msgid "data files"
|
msgid "data files"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/core/models/data.py:402
|
#: netbox/core/models/data.py:400
|
||||||
msgid "auto sync record"
|
msgid "auto sync record"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/core/models/data.py:403
|
#: netbox/core/models/data.py:401
|
||||||
msgid "auto sync records"
|
msgid "auto sync records"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -11245,13 +11240,7 @@ msgid ""
|
|||||||
"parent object"
|
"parent object"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/ipam/models/ip.py:947
|
#: netbox/ipam/models/ip.py:946
|
||||||
msgid ""
|
|
||||||
"Cannot reassign IP address while it is designated as the OOB IP for the "
|
|
||||||
"parent object"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: netbox/ipam/models/ip.py:953
|
|
||||||
msgid "Only IPv6 addresses can be assigned SLAAC status"
|
msgid "Only IPv6 addresses can be assigned SLAAC status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -12500,8 +12489,8 @@ msgstr ""
|
|||||||
msgid "Delete Selected"
|
msgid "Delete Selected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/plugins/navigation.py:53
|
#: netbox/netbox/plugins/navigation.py:55
|
||||||
#: netbox/netbox/plugins/navigation.py:89
|
#: 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 ""
|
||||||
|
|
||||||
@@ -12509,7 +12498,7 @@ msgstr ""
|
|||||||
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:95
|
#: 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 ""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user