Compare commits

..

11 Commits

Author SHA1 Message Date
Aditya Sharma
040a2ae9a9 Enable specifying mask length when creating IP addresses via available-ips endpoint (#21193)
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
CI / build (20.x, 3.14) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
* Enable specifying mask length when creating IP addresses via available-ips endpoint

Fixes #21144

Allow clients to specify an arbitrary mask length when creating IP addresses
from a parent prefix or range using the 'next available' REST API endpoint.

Changes:
- Updated AvailableIPAddressesView to use PrefixLengthSerializer as write_serializer_class
- Enhanced PrefixLengthSerializer to support both 'prefix' and 'parent' context keys
- Added validation to ensure requested prefix_length >= parent mask_length
- Updated prep_object_data to use requested prefix_length if provided, otherwise fall back to parent mask_length for backwards compatibility
- Updated API schema documentation to reflect PrefixLengthSerializer usage

This enables use cases like creating loopback IP addresses with /32 mask length
from a parent prefix with a shorter mask length.

* Refine available-ips prefix length handling

Keep PrefixLengthSerializer strict for available-prefixes and introduce
AvailableIPRequestSerializer for the available-ips endpoint, where
prefix_length is optional and validated against the parent prefix/range.

* Revert PrefixLengthSerializer to original strict state

PrefixLengthSerializer should remain required and strict for the
available-prefixes endpoint. The optional prefix_length functionality
for available-ips is handled by AvailableIPRequestSerializer.

* Add API test; misc cleanup

---------

Co-authored-by: adionit7 <adionit7@users.noreply.github.com>
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2026-01-20 11:20:02 -05:00
Martin Hauser
39f11f28fb fix(core): Cache table existence for ObjectType checks
Introduces a cached `_table_exists` flag to avoid repeated database
introspection queries for `core_objecttype`.
Improves performance during ObjectType lookups and reduces
redundant query overhead.

Fixes #21231
2026-01-20 11:15:14 -05:00
Jeremy Stretch
62b9025a9e Fixes #21181: Handle AuthenticationFailed exception on /media endpoint (#21224) 2026-01-20 08:07:18 -08:00
Jeremy Stretch
21091f22e6 Closes #21234: Add #20966 to the changelog for v4.4.9 (#21236) 2026-01-20 09:22:03 -06:00
github-actions
3efa23cf8f Update source translation strings
Some checks failed
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-01-20 05:07:49 +00:00
bctiemann
0f62137957 Merge pull request #21199 from netbox-community/21178-change-rack-dimensions-display-to-be-more-consistent
Some checks failed
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
Fixes #21178: Use localized “millimeters” for rack mounting depth (follow-up)
2026-01-19 14:14:24 -05:00
Martin Hauser
7858ccb712 feat(extras): Add AVIF support for image attachments
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Extends allowed image file formats to include AVIF for better modern
format support. Introduces a constants mapping for image formats to
centralize file type definitions. Updates form widgets and utilities
to leverage the new constants, enabling more flexible and consistent
image handling.

Fixes #21039
2026-01-19 09:56:06 -05:00
Martin Hauser
6b7b38ee0a fix(users): Refactor object permission query logic
Simplifies the `OBJECTPERMISSION_OBJECT_TYPES` definition by adjusting
query filters and introducing new conditions for specific app labels
and models.

Fixes #21051
2026-01-19 09:30:36 -05:00
matthew-242
c8f17e06a2 Add support to filter on cached relations _location, _region, _site and _site_group to ScopedFilterMixin (#21162) 2026-01-19 09:09:03 -05:00
Jeremy Stretch
edace6aff4 Fixes #21166: Fix support for filtering on unsigned 32-bit integer values in GraphQL API (#21186)
* Fixes #21166: Fix support for filtering on unsigned 32-bit integer values in GraphQL API

* tunnel_id should also use BigIntegerLookup
2026-01-19 08:54:39 -05:00
Martin Hauser
3d1f18d6dd fix(dcim): Localize mounting depth format string
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
Replaces the fixed format string for `mounting_depth` with a localized
version using `gettext_lazy`. This ensures proper translation of the
unit label for internationalization purposes.

Fixes #21178
2026-01-16 19:53:49 +01:00
19 changed files with 325 additions and 218 deletions

View File

@@ -18,17 +18,7 @@ They can also be used as a mechanism for validating the integrity of data within
Custom scripts are Python code which exists outside the NetBox code base, so they can be updated and changed without interfering with the core NetBox installation. And because they're completely custom, there is no inherent limitation on what a script can accomplish.
!!! danger "Only install trusted scripts"
Custom scripts have unrestricted access to change anything in the database and are inherently unsafe and should only be installed and run from trusted sources. You should also review and set permissions for who can run scripts if the script can modify any data.
!!! tip "Permissions for Custom Scripts"
A user can be granted permissions on all Custom Scripts via the "Managed File" object-level permission. To further restrict a user to only be able to access certain scripts, create an additional permission on the "Script" object type, with appropriate queryset-style constraints matching fields available on Script. For example:
```json
{
"name__in": [
"MyScript"
]
}
```
Custom scripts have unrestricted access to change anything in the databse and are inherently unsafe and should only be installed and run from trusted sources. You should also review and set permissions for who can run scripts if the script can modify any data.
## Writing Custom Scripts

View File

@@ -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

View File

@@ -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__

View File

@@ -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:

View File

@@ -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):

View File

@@ -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'

View File

@@ -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())))}
),
}

View File

@@ -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('\\', '/')

View File

@@ -24,11 +24,9 @@ from extras.utils import SharedObjectViewMixin
from netbox.object_actions import *
from netbox.views import generic
from netbox.views.generic.mixins import TableMixin
from users.models import ObjectPermission
from utilities.forms import ConfirmationForm, get_field_value
from utilities.htmx import htmx_partial, htmx_maybe_redirect_current_page
from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.permissions import qs_filter_from_constraints
from utilities.query import count_related
from utilities.querydict import normalize_querydict
from utilities.request import copy_safe_request
@@ -1443,24 +1441,12 @@ class ScriptListView(ContentTypePermissionRequiredMixin, View):
return 'extras.view_script'
def get(self, request):
# Permissions for the Scripts page are given via the "Managed File" object permission. To further restrict
# users to access only specified scripts, create permissions on the "Script" object with appropriate
# queryset-style constraints matching fields available on Script.
script_modules = ScriptModule.objects.restrict(request.user).prefetch_related(
'data_source', 'data_file', 'jobs'
)
script_ct = ContentType.objects.get_for_model(Script)
script_permissions = qs_filter_from_constraints(
ObjectPermission.objects.filter(
users=self.request.user, object_types=script_ct
).values_list("constraints", flat=True)
)
available_scripts = Script.objects.filter(script_permissions, module__in=script_modules)
context = {
'model': ScriptModule,
'script_modules': script_modules,
'available_scripts': available_scripts,
}
# Use partial template for dashboard widgets

View File

@@ -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()

View File

@@ -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)

View File

@@ -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()
)

View File

@@ -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):
"""

View File

@@ -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()

View File

@@ -38,83 +38,81 @@
</thead>
<tbody>
{% for script in scripts %}
{% if script in available_scripts %}
{% with last_job=script.get_latest_jobs|first %}
<tr>
<td>
{% if script.is_executable %}
<a href="{% url 'extras:script' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
{% else %}
<a href="{% url 'extras:script_jobs' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
<span class="text-danger">
<i class="mdi mdi-alert" title="{% trans "Script is no longer present in the source file" %}"></i>
</span>
{% endif %}
</td>
<td>{{ script.python_class.description|markdown|placeholder }}</td>
{% if last_job %}
<td>
<a href="{% url 'extras:script_result' job_pk=last_job.pk %}">{{ last_job.created|isodatetime }}</a>
</td>
<td>
{% badge last_job.get_status_display last_job.get_status_color %}
</td>
{% with last_job=script.get_latest_jobs|first %}
<tr>
<td>
{% if script.is_executable %}
<a href="{% url 'extras:script' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
{% else %}
<td class="text-muted">{% trans "Never" %}</td>
<td>{{ ''|placeholder }}</td>
<a href="{% url 'extras:script_jobs' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
<span class="text-danger">
<i class="mdi mdi-alert" title="{% trans "Script is no longer present in the source file" %}"></i>
</span>
{% endif %}
</td>
<td>{{ script.python_class.description|markdown|placeholder }}</td>
{% if last_job %}
<td>
{% if request.user|can_run:script and script.is_executable %}
<div class="float-end d-print-none">
<form action="{% url 'extras:script' script.pk %}" method="post">
{% if script.python_class.commit_default %}
<input type="checkbox" name="_commit" hidden checked>
{% endif %}
{% csrf_token %}
<button type="submit" name="_run" class="btn btn-primary{% if embedded %} btn-sm{% endif %}">
{% if last_job %}
<i class="mdi mdi-replay"></i> {% if not embedded %}{% trans "Run Again" %}{% endif %}
{% else %}
<i class="mdi mdi-play"></i> {% if not embedded %}{% trans "Run Script" %}{% endif %}
{% endif %}
</button>
</form>
</div>
{% endif %}
<a href="{% url 'extras:script_result' job_pk=last_job.pk %}">{{ last_job.created|isodatetime }}</a>
</td>
</tr>
{% if last_job and not embedded %}
{% for test_name, data in last_job.data.tests.items %}
<tr>
<td colspan="4" class="method">
<span class="ps-3">{{ test_name }}</span>
</td>
<td class="text-end text-nowrap script-stats">
<span class="badge text-bg-success">{{ data.success }}</span>
<span class="badge text-bg-info">{{ data.info }}</span>
<span class="badge text-bg-warning">{{ data.warning }}</span>
<span class="badge text-bg-danger">{{ data.failure }}</span>
</td>
</tr>
{% endfor %}
{% elif last_job and not last_job.data.log and not embedded %}
{# legacy #}
{% for method, stats in last_job.data.items %}
<tr>
<td colspan="4" class="method">
<span class="ps-3">{{ method }}</span>
</td>
<td class="text-end text-nowrap report-stats">
<span class="badge bg-success">{{ stats.success }}</span>
<span class="badge bg-info">{{ stats.info }}</span>
<span class="badge bg-warning">{{ stats.warning }}</span>
<span class="badge bg-danger">{{ stats.failure }}</span>
</td>
</tr>
{% endfor %}
<td>
{% badge last_job.get_status_display last_job.get_status_color %}
</td>
{% else %}
<td class="text-muted">{% trans "Never" %}</td>
<td>{{ ''|placeholder }}</td>
{% endif %}
{% endwith %}
{% endif %}
<td>
{% if request.user|can_run:script and script.is_executable %}
<div class="float-end d-print-none">
<form action="{% url 'extras:script' script.pk %}" method="post">
{% if script.python_class.commit_default %}
<input type="checkbox" name="_commit" hidden checked>
{% endif %}
{% csrf_token %}
<button type="submit" name="_run" class="btn btn-primary{% if embedded %} btn-sm{% endif %}">
{% if last_job %}
<i class="mdi mdi-replay"></i> {% if not embedded %}{% trans "Run Again" %}{% endif %}
{% else %}
<i class="mdi mdi-play"></i> {% if not embedded %}{% trans "Run Script" %}{% endif %}
{% endif %}
</button>
</form>
</div>
{% endif %}
</td>
</tr>
{% if last_job and not embedded %}
{% for test_name, data in last_job.data.tests.items %}
<tr>
<td colspan="4" class="method">
<span class="ps-3">{{ test_name }}</span>
</td>
<td class="text-end text-nowrap script-stats">
<span class="badge text-bg-success">{{ data.success }}</span>
<span class="badge text-bg-info">{{ data.info }}</span>
<span class="badge text-bg-warning">{{ data.warning }}</span>
<span class="badge text-bg-danger">{{ data.failure }}</span>
</td>
</tr>
{% endfor %}
{% elif last_job and not last_job.data.log and not embedded %}
{# legacy #}
{% for method, stats in last_job.data.items %}
<tr>
<td colspan="4" class="method">
<span class="ps-3">{{ method }}</span>
</td>
<td class="text-end text-nowrap report-stats">
<span class="badge bg-success">{{ stats.success }}</span>
<span class="badge bg-info">{{ stats.info }}</span>
<span class="badge bg-warning">{{ stats.warning }}</span>
<span class="badge bg-danger">{{ stats.failure }}</span>
</td>
</tr>
{% endfor %}
{% endif %}
{% endwith %}
{% endfor %}
</tbody>
</table>

View File

@@ -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 ""

View File

@@ -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'

View File

@@ -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)

View File

@@ -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 = (