mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-20 10:38:44 -06:00
Compare commits
12 Commits
15801-vlan
...
21160-foll
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d166aa10d | ||
|
|
040a2ae9a9 | ||
|
|
39f11f28fb | ||
|
|
62b9025a9e | ||
|
|
21091f22e6 | ||
|
|
3efa23cf8f | ||
|
|
0f62137957 | ||
|
|
7858ccb712 | ||
|
|
6b7b38ee0a | ||
|
|
c8f17e06a2 | ||
|
|
edace6aff4 | ||
|
|
3d1f18d6dd |
@@ -40,6 +40,7 @@
|
||||
* [#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`
|
||||
* [#20966](https://github.com/netbox-community/netbox/issues/20966) - Fix UI rendering issue when scrolling list of object types in permissions form
|
||||
* [#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
|
||||
|
||||
@@ -35,6 +35,10 @@ class ObjectTypeQuerySet(models.QuerySet):
|
||||
|
||||
class ObjectTypeManager(models.Manager):
|
||||
|
||||
# TODO: Remove this in NetBox v5.0
|
||||
# Cache the result of introspection to avoid repeated queries.
|
||||
_table_exists = False
|
||||
|
||||
def get_queryset(self):
|
||||
return ObjectTypeQuerySet(self.model, using=self._db)
|
||||
|
||||
@@ -69,10 +73,12 @@ class ObjectTypeManager(models.Manager):
|
||||
# TODO: Remove this in NetBox v5.0
|
||||
# If the ObjectType table has not yet been provisioned (e.g. because we're in a pre-v4.4 migration),
|
||||
# fall back to ContentType.
|
||||
if 'core_objecttype' not in connection.introspection.table_names():
|
||||
ct = ContentType.objects.get_for_model(model, for_concrete_model=for_concrete_model)
|
||||
ct.features = get_model_features(ct.model_class())
|
||||
return ct
|
||||
if not ObjectTypeManager._table_exists:
|
||||
if 'core_objecttype' not in connection.introspection.table_names():
|
||||
ct = ContentType.objects.get_for_model(model, for_concrete_model=for_concrete_model)
|
||||
ct.features = get_model_features(ct.model_class())
|
||||
return ct
|
||||
ObjectTypeManager._table_exists = True
|
||||
|
||||
if not inspect.isclass(model):
|
||||
model = model.__class__
|
||||
|
||||
@@ -13,6 +13,7 @@ if TYPE_CHECKING:
|
||||
from netbox.graphql.filter_lookups import IntegerLookup
|
||||
from extras.graphql.filters import ConfigTemplateFilter
|
||||
from ipam.graphql.filters import VLANFilter, VLANTranslationPolicyFilter
|
||||
from dcim.graphql.filters import LocationFilter, RegionFilter, SiteFilter, SiteGroupFilter
|
||||
from .filters import *
|
||||
|
||||
__all__ = (
|
||||
@@ -35,6 +36,20 @@ class ScopedFilterMixin:
|
||||
)
|
||||
scope_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
# Cached relations
|
||||
_location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='location')
|
||||
)
|
||||
_region: Annotated['RegionFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='region')
|
||||
)
|
||||
_site_group: Annotated['SiteGroupFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='site_group')
|
||||
)
|
||||
_site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='site')
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComponentModelFilterMixin:
|
||||
|
||||
@@ -31,7 +31,7 @@ class RackDimensionsPanel(panels.ObjectAttributesPanel):
|
||||
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_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=_('{} millimeters'))
|
||||
|
||||
|
||||
class RackNumberingPanel(panels.ObjectAttributesPanel):
|
||||
|
||||
@@ -4,6 +4,17 @@ from extras.choices import LogLevelChoices
|
||||
# Custom fields
|
||||
CUSTOMFIELD_EMPTY_VALUES = (None, '', [])
|
||||
|
||||
# ImageAttachment
|
||||
IMAGE_ATTACHMENT_IMAGE_FORMATS = {
|
||||
'avif': 'image/avif',
|
||||
'bmp': 'image/bmp',
|
||||
'gif': 'image/gif',
|
||||
'jpeg': 'image/jpeg',
|
||||
'jpg': 'image/jpeg',
|
||||
'png': 'image/png',
|
||||
'webp': 'image/webp',
|
||||
}
|
||||
|
||||
# Template Export
|
||||
DEFAULT_MIME_TYPE = 'text/plain; charset=utf-8'
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from core.forms.mixins import SyncedDataMixin
|
||||
from core.models import ObjectType
|
||||
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
||||
from extras.constants import IMAGE_ATTACHMENT_IMAGE_FORMATS
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
from netbox.events import get_event_type_choices
|
||||
@@ -784,8 +785,11 @@ class ImageAttachmentForm(forms.ModelForm):
|
||||
fields = [
|
||||
'image', 'name', 'description',
|
||||
]
|
||||
help_texts = {
|
||||
'name': _("If no name is specified, the file name will be used.")
|
||||
# Explicitly set 'image/avif' to support AVIF selection in Firefox
|
||||
widgets = {
|
||||
'image': forms.ClearableFileInput(
|
||||
attrs={'accept': ','.join(sorted(set(IMAGE_ATTACHMENT_IMAGE_FORMATS.values())))}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from taggit.managers import _TaggableManager
|
||||
|
||||
from netbox.context import current_request
|
||||
|
||||
from .constants import IMAGE_ATTACHMENT_IMAGE_FORMATS
|
||||
from .validators import CustomValidator
|
||||
|
||||
__all__ = (
|
||||
@@ -78,7 +79,7 @@ def image_upload(instance, filename):
|
||||
"""
|
||||
upload_dir = 'image-attachments'
|
||||
default_filename = 'unnamed'
|
||||
allowed_img_extensions = ('bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp')
|
||||
allowed_img_extensions = IMAGE_ATTACHMENT_IMAGE_FORMATS.keys()
|
||||
|
||||
# Normalize Windows paths and create a Path object.
|
||||
normalized_filename = str(filename).replace('\\', '/')
|
||||
|
||||
@@ -19,6 +19,7 @@ from ..field_serializers import IPAddressField, IPNetworkField
|
||||
__all__ = (
|
||||
'AggregateSerializer',
|
||||
'AvailableIPSerializer',
|
||||
'AvailableIPRequestSerializer',
|
||||
'AvailablePrefixSerializer',
|
||||
'IPAddressSerializer',
|
||||
'IPRangeSerializer',
|
||||
@@ -147,6 +148,43 @@ class IPRangeSerializer(PrimaryModelSerializer):
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
class AvailableIPRequestSerializer(serializers.Serializer):
|
||||
"""
|
||||
Request payload for creating IP addresses from the available-ips endpoint.
|
||||
"""
|
||||
prefix_length = serializers.IntegerField(required=False)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
data = super().to_internal_value(data)
|
||||
|
||||
prefix_length = data.get('prefix_length')
|
||||
if prefix_length is None:
|
||||
# No override requested; the parent prefix/range mask length will be used.
|
||||
return data
|
||||
|
||||
parent = self.context.get('parent')
|
||||
if parent is None:
|
||||
return data
|
||||
|
||||
# Validate the requested prefix length
|
||||
if prefix_length < parent.mask_length:
|
||||
raise serializers.ValidationError({
|
||||
'prefix_length': 'Prefix length must be greater than or equal to the parent mask length ({})'.format(
|
||||
parent.mask_length
|
||||
)
|
||||
})
|
||||
elif parent.family == 4 and prefix_length > 32:
|
||||
raise serializers.ValidationError({
|
||||
'prefix_length': 'Invalid prefix length ({}) for IPv6'.format(prefix_length)
|
||||
})
|
||||
elif parent.family == 6 and prefix_length > 128:
|
||||
raise serializers.ValidationError({
|
||||
'prefix_length': 'Invalid prefix length ({}) for IPv4'.format(prefix_length)
|
||||
})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class IPAddressSerializer(PrimaryModelSerializer):
|
||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||
address = IPAddressField()
|
||||
|
||||
@@ -400,7 +400,7 @@ class AvailablePrefixesView(AvailableObjectsView):
|
||||
class AvailableIPAddressesView(AvailableObjectsView):
|
||||
queryset = IPAddress.objects.all()
|
||||
read_serializer_class = serializers.AvailableIPSerializer
|
||||
write_serializer_class = serializers.AvailableIPSerializer
|
||||
write_serializer_class = serializers.AvailableIPRequestSerializer
|
||||
advisory_lock_key = 'available-ips'
|
||||
|
||||
def get_available_objects(self, parent, limit=None):
|
||||
@@ -421,8 +421,9 @@ class AvailableIPAddressesView(AvailableObjectsView):
|
||||
def prep_object_data(self, requested_objects, available_objects, parent):
|
||||
available_ips = iter(available_objects)
|
||||
for i, request_data in enumerate(requested_objects):
|
||||
prefix_length = request_data.pop('prefix_length', None) or parent.mask_length
|
||||
request_data.update({
|
||||
'address': f'{next(available_ips)}/{parent.mask_length}',
|
||||
'address': f'{next(available_ips)}/{prefix_length}',
|
||||
'vrf': parent.vrf.pk if parent.vrf else None,
|
||||
})
|
||||
|
||||
@@ -435,7 +436,7 @@ class AvailableIPAddressesView(AvailableObjectsView):
|
||||
@extend_schema(
|
||||
methods=["post"],
|
||||
responses={201: serializers.IPAddressSerializer(many=True)},
|
||||
request=serializers.IPAddressSerializer(many=True),
|
||||
request=serializers.AvailableIPRequestSerializer(many=True),
|
||||
)
|
||||
def post(self, request, pk):
|
||||
return super().post(request, pk)
|
||||
|
||||
@@ -20,7 +20,7 @@ from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
|
||||
from virtualization.models import VMInterface
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from netbox.graphql.filter_lookups import IntegerLookup, IntegerRangeArrayLookup
|
||||
from netbox.graphql.filter_lookups import BigIntegerLookup, IntegerLookup, IntegerRangeArrayLookup
|
||||
from circuits.graphql.filters import ProviderFilter
|
||||
from core.graphql.filters import ContentTypeFilter
|
||||
from dcim.graphql.filters import SiteFilter
|
||||
@@ -53,7 +53,7 @@ __all__ = (
|
||||
class ASNFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rir_id: ID | None = strawberry_django.filter_field()
|
||||
asn: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
asn: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
sites: (
|
||||
@@ -70,10 +70,10 @@ class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilter):
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rir_id: ID | None = strawberry_django.filter_field()
|
||||
start: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
start: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
end: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
end: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import Interface
|
||||
from dcim.tables.template_code import INTERFACE_LINKTERMINATION, LINKTERMINATION
|
||||
from ipam.models import *
|
||||
from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
|
||||
from tenancy.tables import TenancyColumnsMixin, TenantColumn
|
||||
@@ -160,26 +159,11 @@ class VLANDevicesTable(VLANMembersTable):
|
||||
actions = columns.ActionsColumn(
|
||||
actions=('edit',)
|
||||
)
|
||||
link_peer = columns.TemplateColumn(
|
||||
accessor='link_peers',
|
||||
template_code=LINKTERMINATION,
|
||||
orderable=False,
|
||||
verbose_name=_('Link Peers'),
|
||||
)
|
||||
|
||||
# Override PathEndpointTable.connection to accommodate virtual circuits
|
||||
connection = columns.TemplateColumn(
|
||||
accessor='_path__destinations',
|
||||
template_code=INTERFACE_LINKTERMINATION,
|
||||
orderable=False,
|
||||
verbose_name=_('Connection'),
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Interface
|
||||
fields = ('device', 'name', 'link_peer', 'connection', 'tagged', 'actions')
|
||||
default_columns = ('device', 'name', 'connection', 'tagged', 'actions')
|
||||
exclude = ('id',)
|
||||
fields = ('device', 'name', 'tagged', 'actions')
|
||||
exclude = ('id', )
|
||||
|
||||
|
||||
class VLANVirtualMachinesTable(VLANMembersTable):
|
||||
|
||||
@@ -595,6 +595,31 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(len(response.data), 8)
|
||||
|
||||
def test_create_available_ip_with_mask(self):
|
||||
"""
|
||||
Test the creation of an available IP address with a specific prefix length.
|
||||
"""
|
||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/24'))
|
||||
url = reverse('ipam-api:prefix-available-ips', kwargs={'pk': prefix.pk})
|
||||
self.add_permissions('ipam.view_prefix', 'ipam.add_ipaddress')
|
||||
|
||||
# Create an available IP with a specific prefix length
|
||||
data = {
|
||||
'prefix_length': 32,
|
||||
'description': 'Test IP 1',
|
||||
}
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(response.data['address'], '192.0.2.1/32')
|
||||
self.assertEqual(response.data['description'], data['description'])
|
||||
|
||||
# Attempt to create an available IP with a prefix length less than its parent prefix
|
||||
data = {
|
||||
'prefix_length': 23, # Prefix is a /24
|
||||
}
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@tag('regression')
|
||||
def test_graphql_tenant_prefixes_contains_nested_skips_invalid(self):
|
||||
"""
|
||||
|
||||
@@ -19,8 +19,11 @@ from strawberry_django import (
|
||||
process_filters,
|
||||
)
|
||||
|
||||
from netbox.graphql.scalars import BigInt
|
||||
|
||||
__all__ = (
|
||||
'ArrayLookup',
|
||||
'BigIntegerLookup',
|
||||
'FloatArrayLookup',
|
||||
'FloatLookup',
|
||||
'IntegerArrayLookup',
|
||||
@@ -78,6 +81,29 @@ class IntegerLookup:
|
||||
return process_filters(filters=filters, queryset=queryset, info=info, prefix=prefix)
|
||||
|
||||
|
||||
@strawberry.input(one_of=True, description='Lookup for BigInteger fields. Only one of the lookup fields can be set.')
|
||||
class BigIntegerLookup:
|
||||
filter_lookup: FilterLookup[BigInt] | None = strawberry_django.filter_field()
|
||||
range_lookup: RangeLookup[BigInt] | None = strawberry_django.filter_field()
|
||||
comparison_lookup: ComparisonFilterLookup[BigInt] | None = strawberry_django.filter_field()
|
||||
|
||||
def get_filter(self):
|
||||
for field in self.__strawberry_definition__.fields:
|
||||
value = getattr(self, field.name, None)
|
||||
if value is not strawberry.UNSET:
|
||||
return value
|
||||
return None
|
||||
|
||||
@strawberry_django.filter_field
|
||||
def filter(self, info: Info, queryset: QuerySet, prefix: DirectiveValue[str] = '') -> Tuple[QuerySet, Q]:
|
||||
filters = self.get_filter()
|
||||
|
||||
if not filters:
|
||||
return queryset, Q()
|
||||
|
||||
return process_filters(filters=filters, queryset=queryset, info=info, prefix=prefix)
|
||||
|
||||
|
||||
@strawberry.input(one_of=True, description='Lookup for Float fields. Only one of the lookup fields can be set.')
|
||||
class FloatLookup:
|
||||
filter_lookup: FilterLookup[float] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-17 05:02+0000\n"
|
||||
"POT-Creation-Date: 2026-01-20 05:07+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"
|
||||
@@ -1279,7 +1279,7 @@ msgid "Term Side"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/circuits/forms/filtersets.py:287 netbox/dcim/forms/bulk_edit.py:1537
|
||||
#: netbox/extras/forms/model_forms.py:696 netbox/ipam/forms/filtersets.py:149
|
||||
#: netbox/extras/forms/model_forms.py:697 netbox/ipam/forms/filtersets.py:149
|
||||
#: netbox/ipam/forms/filtersets.py:627 netbox/ipam/forms/model_forms.py:326
|
||||
#: netbox/templates/dcim/macaddress.html:25
|
||||
#: netbox/templates/extras/configcontext.html:36
|
||||
@@ -2172,7 +2172,7 @@ msgstr ""
|
||||
msgid "Sync interval"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/forms/bulk_edit.py:33 netbox/extras/forms/model_forms.py:306
|
||||
#: netbox/core/forms/bulk_edit.py:33 netbox/extras/forms/model_forms.py:307
|
||||
#: netbox/templates/extras/savedfilter.html:56
|
||||
#: netbox/vpn/forms/filtersets.py:102 netbox/vpn/forms/filtersets.py:132
|
||||
#: netbox/vpn/forms/filtersets.py:156 netbox/vpn/forms/filtersets.py:175
|
||||
@@ -2187,10 +2187,10 @@ msgid "Ignore rules"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/forms/filtersets.py:30 netbox/core/forms/model_forms.py:100
|
||||
#: netbox/extras/forms/model_forms.py:267
|
||||
#: netbox/extras/forms/model_forms.py:603
|
||||
#: netbox/extras/forms/model_forms.py:692
|
||||
#: netbox/extras/forms/model_forms.py:745 netbox/extras/tables/tables.py:218
|
||||
#: netbox/extras/forms/model_forms.py:268
|
||||
#: netbox/extras/forms/model_forms.py:604
|
||||
#: netbox/extras/forms/model_forms.py:693
|
||||
#: netbox/extras/forms/model_forms.py:746 netbox/extras/tables/tables.py:218
|
||||
#: netbox/extras/tables/tables.py:588 netbox/extras/tables/tables.py:618
|
||||
#: netbox/extras/tables/tables.py:660 netbox/templates/core/datasource.html:31
|
||||
#: netbox/templates/core/inc/datafile_panel.html:7
|
||||
@@ -2290,7 +2290,7 @@ msgid "Before"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/forms/filtersets.py:155 netbox/core/tables/change_logging.py:29
|
||||
#: netbox/extras/forms/model_forms.py:476
|
||||
#: netbox/extras/forms/model_forms.py:477
|
||||
#: netbox/templates/core/objectchange.html:46
|
||||
#: netbox/templates/extras/eventrule.html:71
|
||||
msgid "Action"
|
||||
@@ -2360,8 +2360,8 @@ msgid "Pagination"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/forms/model_forms.py:166 netbox/extras/forms/bulk_edit.py:96
|
||||
#: netbox/extras/forms/filtersets.py:49 netbox/extras/forms/model_forms.py:121
|
||||
#: netbox/extras/forms/model_forms.py:134
|
||||
#: netbox/extras/forms/filtersets.py:49 netbox/extras/forms/model_forms.py:122
|
||||
#: netbox/extras/forms/model_forms.py:135
|
||||
#: netbox/templates/core/inc/config_data.html:93
|
||||
msgid "Validation"
|
||||
msgstr ""
|
||||
@@ -4176,9 +4176,9 @@ msgid "Power panel (ID)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_create.py:40 netbox/extras/forms/filtersets.py:515
|
||||
#: netbox/extras/forms/model_forms.py:596
|
||||
#: netbox/extras/forms/model_forms.py:681
|
||||
#: netbox/extras/forms/model_forms.py:733 netbox/extras/ui/panels.py:69
|
||||
#: netbox/extras/forms/model_forms.py:597
|
||||
#: netbox/extras/forms/model_forms.py:682
|
||||
#: netbox/extras/forms/model_forms.py:734 netbox/extras/ui/panels.py:69
|
||||
#: netbox/netbox/forms/bulk_import.py:26 netbox/netbox/forms/mixins.py:113
|
||||
#: netbox/netbox/tables/columns.py:490
|
||||
#: netbox/templates/circuits/inc/circuit_termination.html:29
|
||||
@@ -4319,7 +4319,7 @@ msgstr ""
|
||||
#: netbox/extras/forms/bulk_edit.py:315 netbox/extras/forms/bulk_edit.py:341
|
||||
#: netbox/extras/forms/bulk_import.py:275 netbox/extras/forms/filtersets.py:71
|
||||
#: netbox/extras/forms/filtersets.py:175 netbox/extras/forms/filtersets.py:279
|
||||
#: netbox/extras/forms/filtersets.py:315 netbox/extras/forms/model_forms.py:574
|
||||
#: netbox/extras/forms/filtersets.py:315 netbox/extras/forms/model_forms.py:575
|
||||
#: netbox/ipam/forms/bulk_edit.py:159 netbox/templates/dcim/moduletype.html:51
|
||||
#: netbox/templates/extras/configcontext.html:17
|
||||
#: netbox/templates/extras/customlink.html:25
|
||||
@@ -4455,7 +4455,7 @@ msgid "Device Type"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_edit.py:540 netbox/dcim/forms/model_forms.py:400
|
||||
#: netbox/dcim/views.py:1578 netbox/extras/forms/model_forms.py:591
|
||||
#: netbox/dcim/views.py:1578 netbox/extras/forms/model_forms.py:592
|
||||
msgid "Schema"
|
||||
msgstr ""
|
||||
|
||||
@@ -4464,7 +4464,7 @@ msgstr ""
|
||||
#: netbox/dcim/forms/bulk_import.py:1452 netbox/dcim/forms/filtersets.py:679
|
||||
#: netbox/dcim/forms/filtersets.py:1197 netbox/dcim/forms/model_forms.py:406
|
||||
#: netbox/dcim/forms/model_forms.py:419 netbox/dcim/tables/modules.py:42
|
||||
#: netbox/extras/forms/filtersets.py:437 netbox/extras/forms/model_forms.py:616
|
||||
#: netbox/extras/forms/filtersets.py:437 netbox/extras/forms/model_forms.py:617
|
||||
#: netbox/extras/tables/tables.py:615 netbox/templates/account/base.html:7
|
||||
#: netbox/templates/dcim/cable.html:23 netbox/templates/dcim/moduletype.html:27
|
||||
#: netbox/templates/extras/configcontext.html:21
|
||||
@@ -5600,7 +5600,7 @@ msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/filtersets.py:1572 netbox/extras/forms/bulk_edit.py:421
|
||||
#: netbox/extras/forms/bulk_import.py:298 netbox/extras/forms/filtersets.py:616
|
||||
#: netbox/extras/forms/model_forms.py:794 netbox/extras/tables/tables.py:743
|
||||
#: netbox/extras/forms/model_forms.py:798 netbox/extras/tables/tables.py:743
|
||||
#: netbox/templates/extras/journalentry.html:30
|
||||
msgid "Kind"
|
||||
msgstr ""
|
||||
@@ -5745,7 +5745,7 @@ msgid ""
|
||||
"hyphen."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/model_forms.py:402 netbox/extras/forms/model_forms.py:593
|
||||
#: netbox/dcim/forms/model_forms.py:402 netbox/extras/forms/model_forms.py:594
|
||||
msgid "Enter a valid JSON schema to define supported attributes."
|
||||
msgstr ""
|
||||
|
||||
@@ -7610,7 +7610,7 @@ msgid "VMs"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/devices.py:103 netbox/dcim/tables/devices.py:223
|
||||
#: netbox/extras/forms/model_forms.py:744
|
||||
#: netbox/extras/forms/model_forms.py:745
|
||||
#: netbox/templates/dcim/devicerole.html:48
|
||||
#: netbox/templates/dcim/platform.html:45
|
||||
#: netbox/templates/extras/configtemplate.html:10
|
||||
@@ -7842,7 +7842,7 @@ msgid "Module Types"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/devicetypes.py:57 netbox/extras/forms/filtersets.py:485
|
||||
#: netbox/extras/forms/model_forms.py:651 netbox/extras/tables/tables.py:703
|
||||
#: netbox/extras/forms/model_forms.py:652 netbox/extras/tables/tables.py:703
|
||||
#: netbox/netbox/navigation/menu.py:78
|
||||
msgid "Platforms"
|
||||
msgstr ""
|
||||
@@ -8000,7 +8000,7 @@ msgid "Space"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/sites.py:21 netbox/dcim/tables/sites.py:40
|
||||
#: netbox/extras/forms/filtersets.py:465 netbox/extras/forms/model_forms.py:631
|
||||
#: netbox/extras/forms/filtersets.py:465 netbox/extras/forms/model_forms.py:632
|
||||
#: netbox/ipam/forms/bulk_edit.py:112 netbox/ipam/forms/model_forms.py:154
|
||||
#: netbox/ipam/tables/asn.py:76 netbox/netbox/navigation/menu.py:15
|
||||
#: netbox/netbox/navigation/menu.py:19
|
||||
@@ -8015,6 +8015,10 @@ msgstr ""
|
||||
msgid "Test case must set peer_termination_type"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/ui/panels.py:34
|
||||
msgid "{} millimeters"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/ui/panels.py:53 netbox/dcim/ui/panels.py:96
|
||||
#: netbox/virtualization/forms/filtersets.py:198
|
||||
msgid "Serial number"
|
||||
@@ -8079,7 +8083,7 @@ msgid "Application Services"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:2677 netbox/extras/forms/filtersets.py:427
|
||||
#: netbox/extras/forms/model_forms.py:691
|
||||
#: netbox/extras/forms/model_forms.py:692
|
||||
#: netbox/templates/extras/configcontext.html:10
|
||||
#: netbox/virtualization/forms/model_forms.py:225
|
||||
#: netbox/virtualization/views.py:399
|
||||
@@ -8315,13 +8319,13 @@ msgstr ""
|
||||
msgid "White"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/choices.py:249 netbox/extras/forms/model_forms.py:433
|
||||
#: netbox/extras/forms/model_forms.py:510
|
||||
#: netbox/extras/choices.py:249 netbox/extras/forms/model_forms.py:434
|
||||
#: netbox/extras/forms/model_forms.py:511
|
||||
#: netbox/templates/extras/webhook.html:10
|
||||
msgid "Webhook"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/choices.py:250 netbox/extras/forms/model_forms.py:498
|
||||
#: netbox/extras/choices.py:250 netbox/extras/forms/model_forms.py:499
|
||||
#: netbox/templates/extras/script/base.html:29
|
||||
msgid "Script"
|
||||
msgstr ""
|
||||
@@ -8501,7 +8505,7 @@ msgstr ""
|
||||
msgid "Tenant group (slug)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/filtersets.py:779 netbox/extras/forms/model_forms.py:579
|
||||
#: netbox/extras/filtersets.py:779 netbox/extras/forms/model_forms.py:580
|
||||
#: netbox/templates/extras/tag.html:11
|
||||
msgid "Tag"
|
||||
msgstr ""
|
||||
@@ -8558,7 +8562,7 @@ msgid "Validation regex"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/bulk_edit.py:95 netbox/extras/forms/filtersets.py:48
|
||||
#: netbox/extras/forms/model_forms.py:81
|
||||
#: netbox/extras/forms/model_forms.py:82
|
||||
#: netbox/templates/extras/customfield.html:70
|
||||
msgid "Behavior"
|
||||
msgstr ""
|
||||
@@ -8623,7 +8627,7 @@ msgid "CA file path"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/bulk_edit.py:289 netbox/extras/forms/bulk_import.py:231
|
||||
#: netbox/extras/forms/model_forms.py:457
|
||||
#: netbox/extras/forms/model_forms.py:458
|
||||
msgid "Event types"
|
||||
msgstr ""
|
||||
|
||||
@@ -8642,12 +8646,12 @@ msgstr ""
|
||||
#: netbox/extras/forms/bulk_import.py:225
|
||||
#: netbox/extras/forms/bulk_import.py:279 netbox/extras/forms/filtersets.py:54
|
||||
#: netbox/extras/forms/filtersets.py:156 netbox/extras/forms/filtersets.py:260
|
||||
#: netbox/extras/forms/filtersets.py:296 netbox/extras/forms/model_forms.py:52
|
||||
#: netbox/extras/forms/model_forms.py:224
|
||||
#: netbox/extras/forms/model_forms.py:256
|
||||
#: netbox/extras/forms/model_forms.py:299
|
||||
#: netbox/extras/forms/model_forms.py:452
|
||||
#: netbox/extras/forms/model_forms.py:569 netbox/users/forms/model_forms.py:323
|
||||
#: netbox/extras/forms/filtersets.py:296 netbox/extras/forms/model_forms.py:53
|
||||
#: netbox/extras/forms/model_forms.py:225
|
||||
#: netbox/extras/forms/model_forms.py:257
|
||||
#: netbox/extras/forms/model_forms.py:300
|
||||
#: netbox/extras/forms/model_forms.py:453
|
||||
#: netbox/extras/forms/model_forms.py:570 netbox/users/forms/model_forms.py:323
|
||||
msgid "Object types"
|
||||
msgstr ""
|
||||
|
||||
@@ -8665,9 +8669,9 @@ msgid "Field data type (e.g. text, integer, etc.)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/bulk_import.py:48 netbox/extras/forms/filtersets.py:243
|
||||
#: netbox/extras/forms/filtersets.py:356 netbox/extras/forms/model_forms.py:325
|
||||
#: netbox/extras/forms/model_forms.py:384
|
||||
#: netbox/extras/forms/model_forms.py:421
|
||||
#: netbox/extras/forms/filtersets.py:356 netbox/extras/forms/model_forms.py:326
|
||||
#: netbox/extras/forms/model_forms.py:385
|
||||
#: netbox/extras/forms/model_forms.py:422
|
||||
#: netbox/tenancy/forms/filtersets.py:111
|
||||
msgid "Object type"
|
||||
msgstr ""
|
||||
@@ -8733,8 +8737,8 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/bulk_import.py:195
|
||||
#: netbox/extras/forms/model_forms.py:291
|
||||
#: netbox/extras/forms/model_forms.py:772
|
||||
#: netbox/extras/forms/model_forms.py:292
|
||||
#: netbox/extras/forms/model_forms.py:773
|
||||
msgid "Must specify either local content or a data file"
|
||||
msgstr ""
|
||||
|
||||
@@ -8779,7 +8783,7 @@ msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/bulk_import.py:316
|
||||
#: netbox/extras/forms/model_forms.py:400 netbox/netbox/navigation/menu.py:414
|
||||
#: netbox/extras/forms/model_forms.py:401 netbox/netbox/navigation/menu.py:414
|
||||
#: netbox/templates/extras/notificationgroup.html:41
|
||||
#: netbox/templates/users/group.html:29 netbox/templates/users/owner.html:46
|
||||
#: netbox/users/forms/filtersets.py:181 netbox/users/forms/model_forms.py:262
|
||||
@@ -8794,7 +8798,7 @@ msgid "User names separated by commas, encased with double quotes"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/bulk_import.py:323
|
||||
#: netbox/extras/forms/model_forms.py:395 netbox/netbox/navigation/menu.py:295
|
||||
#: netbox/extras/forms/model_forms.py:396 netbox/netbox/navigation/menu.py:295
|
||||
#: netbox/netbox/navigation/menu.py:434
|
||||
#: netbox/templates/extras/notificationgroup.html:31
|
||||
#: netbox/templates/tenancy/contact.html:21
|
||||
@@ -8816,7 +8820,7 @@ msgstr ""
|
||||
msgid "Type Options"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:59 netbox/extras/forms/model_forms.py:61
|
||||
#: netbox/extras/forms/filtersets.py:59 netbox/extras/forms/model_forms.py:62
|
||||
msgid "Related object type"
|
||||
msgstr ""
|
||||
|
||||
@@ -8824,7 +8828,7 @@ msgstr ""
|
||||
msgid "Field type"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:133 netbox/extras/forms/model_forms.py:162
|
||||
#: netbox/extras/forms/filtersets.py:133 netbox/extras/forms/model_forms.py:163
|
||||
#: netbox/extras/tables/tables.py:97
|
||||
#: netbox/templates/generic/bulk_import.html:185
|
||||
msgid "Choices"
|
||||
@@ -8832,14 +8836,14 @@ msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:189 netbox/extras/forms/filtersets.py:406
|
||||
#: netbox/extras/forms/filtersets.py:428 netbox/extras/forms/filtersets.py:528
|
||||
#: netbox/extras/forms/model_forms.py:686 netbox/templates/core/job.html:69
|
||||
#: netbox/extras/forms/model_forms.py:687 netbox/templates/core/job.html:69
|
||||
#: netbox/templates/extras/eventrule.html:84
|
||||
msgid "Data"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:190 netbox/extras/forms/filtersets.py:529
|
||||
#: netbox/extras/forms/model_forms.py:269
|
||||
#: netbox/extras/forms/model_forms.py:747
|
||||
#: netbox/extras/forms/model_forms.py:270
|
||||
#: netbox/extras/forms/model_forms.py:748
|
||||
msgid "Rendering"
|
||||
msgstr ""
|
||||
|
||||
@@ -8867,37 +8871,37 @@ msgstr ""
|
||||
msgid "Allowed object type"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:455 netbox/extras/forms/model_forms.py:621
|
||||
#: netbox/extras/forms/filtersets.py:455 netbox/extras/forms/model_forms.py:622
|
||||
#: netbox/netbox/navigation/menu.py:17
|
||||
msgid "Regions"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:460 netbox/extras/forms/model_forms.py:626
|
||||
#: netbox/extras/forms/filtersets.py:460 netbox/extras/forms/model_forms.py:627
|
||||
msgid "Site groups"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:470 netbox/extras/forms/model_forms.py:636
|
||||
#: netbox/extras/forms/filtersets.py:470 netbox/extras/forms/model_forms.py:637
|
||||
#: netbox/netbox/navigation/menu.py:20
|
||||
msgid "Locations"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:475 netbox/extras/forms/model_forms.py:641
|
||||
#: netbox/extras/forms/filtersets.py:475 netbox/extras/forms/model_forms.py:642
|
||||
msgid "Device types"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:480 netbox/extras/forms/model_forms.py:646
|
||||
#: netbox/extras/forms/filtersets.py:480 netbox/extras/forms/model_forms.py:647
|
||||
msgid "Roles"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:490 netbox/extras/forms/model_forms.py:656
|
||||
#: netbox/extras/forms/filtersets.py:490 netbox/extras/forms/model_forms.py:657
|
||||
msgid "Cluster types"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:495 netbox/extras/forms/model_forms.py:661
|
||||
#: netbox/extras/forms/filtersets.py:495 netbox/extras/forms/model_forms.py:662
|
||||
msgid "Cluster groups"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:500 netbox/extras/forms/model_forms.py:666
|
||||
#: netbox/extras/forms/filtersets.py:500 netbox/extras/forms/model_forms.py:667
|
||||
#: netbox/netbox/navigation/menu.py:264 netbox/netbox/navigation/menu.py:266
|
||||
#: netbox/templates/virtualization/clustertype.html:30
|
||||
#: netbox/virtualization/tables/clusters.py:23
|
||||
@@ -8905,183 +8909,179 @@ msgstr ""
|
||||
msgid "Clusters"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:505 netbox/extras/forms/model_forms.py:671
|
||||
#: netbox/extras/forms/filtersets.py:505 netbox/extras/forms/model_forms.py:672
|
||||
msgid "Tenant groups"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:54
|
||||
#: netbox/extras/forms/model_forms.py:55
|
||||
msgid "The type(s) of object that have this custom field"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:57
|
||||
#: netbox/extras/forms/model_forms.py:58
|
||||
msgid "Default value"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:63
|
||||
#: netbox/extras/forms/model_forms.py:64
|
||||
msgid "Type of the related object (for object/multi-object fields only)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:66
|
||||
#: netbox/extras/forms/model_forms.py:67
|
||||
#: netbox/templates/extras/customfield.html:60
|
||||
msgid "Related object filter"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:68
|
||||
#: netbox/extras/forms/model_forms.py:69
|
||||
msgid "Specify query parameters as a JSON object."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:78
|
||||
#: netbox/extras/forms/model_forms.py:79
|
||||
#: netbox/templates/extras/customfield.html:10
|
||||
msgid "Custom Field"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:90
|
||||
#: netbox/extras/forms/model_forms.py:91
|
||||
msgid ""
|
||||
"The type of data stored in this field. For object/multi-object fields, "
|
||||
"select the related object type below."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:93
|
||||
#: netbox/extras/forms/model_forms.py:94
|
||||
msgid ""
|
||||
"This will be displayed as help text for the form field. Markdown is "
|
||||
"supported."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:148
|
||||
#: netbox/extras/forms/model_forms.py:149
|
||||
msgid "Related Object"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:175
|
||||
#: netbox/extras/forms/model_forms.py:176
|
||||
msgid ""
|
||||
"Enter one choice per line. An optional label may be specified for each "
|
||||
"choice by appending it with a colon. Example:"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:231
|
||||
#: netbox/extras/forms/model_forms.py:232
|
||||
#: netbox/templates/extras/customlink.html:10
|
||||
msgid "Custom Link"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:233
|
||||
#: netbox/extras/forms/model_forms.py:234
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:245
|
||||
#: netbox/extras/forms/model_forms.py:246
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Jinja2 template code for the link text. Reference the object as {example}. "
|
||||
"Links which render as empty text will not be displayed."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:249
|
||||
#: netbox/extras/forms/model_forms.py:250
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Jinja2 template code for the link URL. Reference the object as {example}."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:260
|
||||
#: netbox/extras/forms/model_forms.py:738
|
||||
#: netbox/extras/forms/model_forms.py:261
|
||||
#: netbox/extras/forms/model_forms.py:739
|
||||
msgid "Template code"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:266
|
||||
#: netbox/extras/forms/model_forms.py:267
|
||||
#: netbox/templates/extras/exporttemplate.html:12
|
||||
msgid "Export Template"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:284
|
||||
#: netbox/extras/forms/model_forms.py:765
|
||||
#: netbox/extras/forms/model_forms.py:285
|
||||
#: netbox/extras/forms/model_forms.py:766
|
||||
msgid "Template content is populated from the remote source selected below."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:305 netbox/netbox/forms/mixins.py:92
|
||||
#: netbox/extras/forms/model_forms.py:306 netbox/netbox/forms/mixins.py:92
|
||||
#: netbox/templates/extras/savedfilter.html:10
|
||||
msgid "Saved Filter"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:331
|
||||
#: netbox/extras/forms/model_forms.py:332
|
||||
#: netbox/templates/account/preferences.html:50
|
||||
#: netbox/templates/extras/tableconfig.html:62
|
||||
msgid "Ordering"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:333
|
||||
#: netbox/extras/forms/model_forms.py:334
|
||||
msgid ""
|
||||
"Enter a comma-separated list of column names. Prepend a name with a hyphen "
|
||||
"to reverse the order."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:342 netbox/utilities/forms/forms.py:164
|
||||
#: netbox/extras/forms/model_forms.py:343 netbox/utilities/forms/forms.py:164
|
||||
msgid "Available Columns"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:349 netbox/utilities/forms/forms.py:172
|
||||
#: netbox/extras/forms/model_forms.py:350 netbox/utilities/forms/forms.py:172
|
||||
msgid "Selected Columns"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:414
|
||||
#: netbox/extras/forms/model_forms.py:415
|
||||
msgid "A notification group specify at least one user or group."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:436
|
||||
#: netbox/extras/forms/model_forms.py:437
|
||||
#: netbox/templates/extras/webhook.html:23
|
||||
msgid "HTTP Request"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:438
|
||||
#: netbox/extras/forms/model_forms.py:439
|
||||
#: netbox/templates/extras/webhook.html:44
|
||||
msgid "SSL"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:460
|
||||
#: netbox/extras/forms/model_forms.py:461
|
||||
msgid "Action choice"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:465
|
||||
#: netbox/extras/forms/model_forms.py:466
|
||||
msgid "Enter conditions in <a href=\"https://json.org/\">JSON</a> format."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:469
|
||||
#: netbox/extras/forms/model_forms.py:470
|
||||
msgid ""
|
||||
"Enter parameters to pass to the action in <a href=\"https://json.org/"
|
||||
"\">JSON</a> format."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:474
|
||||
#: netbox/extras/forms/model_forms.py:475
|
||||
#: netbox/templates/extras/eventrule.html:10
|
||||
msgid "Event Rule"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:475
|
||||
#: netbox/extras/forms/model_forms.py:476
|
||||
msgid "Triggers"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:522
|
||||
#: netbox/extras/forms/model_forms.py:523
|
||||
msgid "Notification group"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:602
|
||||
#: netbox/extras/forms/model_forms.py:603
|
||||
#: netbox/templates/extras/configcontextprofile.html:10
|
||||
msgid "Config Context Profile"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:676 netbox/netbox/navigation/menu.py:26
|
||||
#: netbox/extras/forms/model_forms.py:677 netbox/netbox/navigation/menu.py:26
|
||||
#: netbox/tenancy/tables/tenants.py:18
|
||||
msgid "Tenants"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:720
|
||||
#: netbox/extras/forms/model_forms.py:721
|
||||
msgid "Data is populated from the remote source selected below."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:726
|
||||
#: netbox/extras/forms/model_forms.py:727
|
||||
msgid "Must specify either local data or a data file"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:788
|
||||
msgid "If no name is specified, the file name will be used."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/reports.py:17 netbox/extras/forms/scripts.py:25
|
||||
msgid "Schedule at"
|
||||
msgstr ""
|
||||
|
||||
@@ -3,9 +3,10 @@ import string
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
OBJECTPERMISSION_OBJECT_TYPES = Q(
|
||||
~Q(app_label__in=['account', 'admin', 'auth', 'contenttypes', 'sessions', 'taggit', 'users']) |
|
||||
Q(app_label='users', model__in=['objectpermission', 'token', 'group', 'user', 'owner'])
|
||||
OBJECTPERMISSION_OBJECT_TYPES = (
|
||||
(Q(public=True) & ~Q(app_label='core', model='objecttype'))
|
||||
| Q(app_label='core', model__in=['managedfile'])
|
||||
| Q(app_label='extras', model__in=['scriptmodule', 'taggeditem'])
|
||||
)
|
||||
|
||||
CONSTRAINT_TOKEN_USER = '$user'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from utilities.forms.widgets.apiselect import APISelect, APISelectMultiple
|
||||
@@ -101,21 +102,27 @@ class FilterModifierWidget(forms.Widget):
|
||||
if isinstance(self.original_widget, (APISelect, APISelectMultiple)):
|
||||
original_choices = self.original_widget.choices
|
||||
|
||||
# Only keep selected choices to preserve current selection in HTML
|
||||
# Only keep selected choices to preserve the current selection in HTML
|
||||
if value:
|
||||
values = value if isinstance(value, (list, tuple)) else [value]
|
||||
|
||||
if hasattr(original_choices, 'queryset'):
|
||||
queryset = original_choices.queryset
|
||||
selected_objects = queryset.filter(pk__in=values)
|
||||
# Build minimal choice list with just the selected values
|
||||
self.original_widget.choices = [
|
||||
(obj.pk, str(obj)) for obj in selected_objects
|
||||
]
|
||||
# Extract valid PKs (exclude special null choice string)
|
||||
pk_values = [v for v in values if v != settings.FILTERS_NULL_CHOICE_VALUE]
|
||||
|
||||
# Build a minimal choice list with just the selected values
|
||||
choices = []
|
||||
if pk_values:
|
||||
selected_objects = original_choices.queryset.filter(pk__in=pk_values)
|
||||
choices = [(obj.pk, str(obj)) for obj in selected_objects]
|
||||
|
||||
# Re-add the "None" option if it was selected via the null choice value
|
||||
if settings.FILTERS_NULL_CHOICE_VALUE in values:
|
||||
choices.append((settings.FILTERS_NULL_CHOICE_VALUE, settings.FILTERS_NULL_CHOICE_LABEL))
|
||||
|
||||
self.original_widget.choices = choices
|
||||
else:
|
||||
self.original_widget.choices = [
|
||||
choice for choice in original_choices if choice[0] in values
|
||||
]
|
||||
self.original_widget.choices = [choice for choice in original_choices if choice[0] in values]
|
||||
else:
|
||||
# No selection - render empty select element
|
||||
self.original_widget.choices = []
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.http import QueryDict
|
||||
from django.template import Context
|
||||
@@ -14,6 +15,7 @@ from utilities.forms.fields import TagFilterField
|
||||
from utilities.forms.mixins import FilterModifierMixin
|
||||
from utilities.forms.widgets import FilterModifierWidget
|
||||
from utilities.templatetags.helpers import applied_filters
|
||||
from tenancy.models import Tenant
|
||||
|
||||
|
||||
# Test model for FilterModifierMixin tests
|
||||
@@ -99,6 +101,51 @@ class FilterModifierWidgetTest(TestCase):
|
||||
self.assertEqual(context['widget']['current_modifier'], 'exact') # Defaults to exact, JS updates from URL
|
||||
self.assertEqual(context['widget']['current_value'], 'test')
|
||||
|
||||
def test_get_context_handles_null_selection(self):
|
||||
"""Widget should preserve the 'null' choice when rendering."""
|
||||
|
||||
null_value = settings.FILTERS_NULL_CHOICE_VALUE
|
||||
null_label = settings.FILTERS_NULL_CHOICE_LABEL
|
||||
|
||||
# Simulate a query for objects with no tenant assigned (?tenant_id=null)
|
||||
query_params = QueryDict(f'tenant_id={null_value}')
|
||||
form = DeviceFilterForm(query_params)
|
||||
|
||||
# Rendering the field triggers FilterModifierWidget.get_context()
|
||||
try:
|
||||
html = form['tenant_id'].as_widget()
|
||||
except ValueError as e:
|
||||
# ValueError: Field 'id' expected a number but got 'null'
|
||||
self.fail(f"FilterModifierWidget raised ValueError on 'null' selection: {e}")
|
||||
|
||||
# Verify the "None" option is rendered so user selection is preserved in the UI
|
||||
self.assertIn(f'value="{null_value}"', html)
|
||||
self.assertIn(null_label, html)
|
||||
|
||||
def test_get_context_handles_mixed_selection(self):
|
||||
"""Widget should preserve both real objects and the 'null' choice together."""
|
||||
|
||||
null_value = settings.FILTERS_NULL_CHOICE_VALUE
|
||||
|
||||
# Create a tenant to simulate a real object
|
||||
tenant = Tenant.objects.create(name='Tenant A', slug='tenant-a')
|
||||
|
||||
# Simulate a selection containing both a real PK and the null sentinel
|
||||
query_params = QueryDict('', mutable=True)
|
||||
query_params.setlist('tenant_id', [str(tenant.pk), null_value])
|
||||
form = DeviceFilterForm(query_params)
|
||||
|
||||
# Rendering the field triggers FilterModifierWidget.get_context()
|
||||
try:
|
||||
html = form['tenant_id'].as_widget()
|
||||
except ValueError as e:
|
||||
# ValueError: Field 'id' expected a number but got 'null'
|
||||
self.fail(f"FilterModifierWidget raised ValueError on 'null' selection: {e}")
|
||||
|
||||
# Verify both the real object and the null option are present in the output
|
||||
self.assertIn(f'value="{tenant.pk}"', html)
|
||||
self.assertIn(f'value="{null_value}"', html)
|
||||
|
||||
def test_widget_renders_modifier_dropdown_and_input(self):
|
||||
"""Widget should render modifier dropdown alongside original input."""
|
||||
widget = FilterModifierWidget(
|
||||
|
||||
@@ -5,9 +5,11 @@ from django.conf import settings
|
||||
from django.contrib.auth.mixins import AccessMixin
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.models import QuerySet
|
||||
from django.http import HttpResponseForbidden
|
||||
from django.urls import reverse
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
|
||||
from netbox.api.authentication import TokenAuthentication
|
||||
from netbox.plugins import PluginConfig
|
||||
@@ -50,10 +52,12 @@ class TokenConditionalLoginRequiredMixin(ConditionalLoginRequiredMixin):
|
||||
# Attempt to authenticate the user using a DRF token, if provided
|
||||
if settings.LOGIN_REQUIRED and not request.user.is_authenticated:
|
||||
authenticator = TokenAuthentication()
|
||||
auth_info = authenticator.authenticate(request)
|
||||
if auth_info is not None:
|
||||
request.user = auth_info[0] # User object
|
||||
request.auth = auth_info[1]
|
||||
try:
|
||||
if (auth_info := authenticator.authenticate(request)) is not None:
|
||||
request.user = auth_info[0] # User object
|
||||
request.auth = auth_info[1]
|
||||
except AuthenticationFailed:
|
||||
return HttpResponseForbidden("Invalid token")
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from vpn import models
|
||||
if TYPE_CHECKING:
|
||||
from core.graphql.filters import ContentTypeFilter
|
||||
from ipam.graphql.filters import IPAddressFilter, RouteTargetFilter
|
||||
from netbox.graphql.filter_lookups import IntegerLookup
|
||||
from netbox.graphql.filter_lookups import BigIntegerLookup, IntegerLookup
|
||||
from .enums import *
|
||||
|
||||
__all__ = (
|
||||
@@ -75,7 +75,7 @@ class TunnelFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
ipsec_profile: Annotated['IPSecProfileFilter', strawberry.lazy('vpn.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
tunnel_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
tunnel_id: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
terminations: Annotated['TunnelTerminationFilter', strawberry.lazy('vpn.graphql.filters')] | None = (
|
||||
@@ -187,7 +187,7 @@ class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
type: BaseFilterLookup[Annotated['L2VPNTypeEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
identifier: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
identifier: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
import_targets: Annotated['RouteTargetFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
|
||||
Reference in New Issue
Block a user