Compare commits

..

2 Commits

Author SHA1 Message Date
Arthur
0e8023899f #20383 clear rack face if no rack on edit 2026-01-15 09:38:00 -08:00
Arthur
601a7092e0 #20383 clear rack face if no rack on edit 2026-01-15 09:20:38 -08:00
24 changed files with 318 additions and 413 deletions

View File

@@ -30,13 +30,13 @@ jobs:
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
config-file: .github/codeql/codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

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

@@ -44,4 +44,3 @@ class DataFileSerializer(NetBoxModelSerializer):
'id', 'url', 'display_url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
]
brief_fields = ('id', 'url', 'display', 'path')
read_only_fields = ['path', 'last_updated', 'size', 'hash']

View File

@@ -12,7 +12,7 @@ from django.core.validators import RegexValidator
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext as _
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
from netbox.models import PrimaryModel
@@ -128,9 +128,7 @@ class DataSource(JobsMixin, PrimaryModel):
# Ensure URL scheme matches selected type
if self.backend_class.is_local and self.url_scheme not in ('file', ''):
raise ValidationError({
'source_url': _("URLs for local sources must start with {scheme} (or specify no scheme)").format(
scheme='file://'
)
'source_url': "URLs for local sources must start with file:// (or specify no scheme)"
})
def save(self, *args, **kwargs):

View File

@@ -140,6 +140,9 @@ class FrontPortFormMixin(forms.Form):
widget=forms.SelectMultiple(attrs={'size': 8})
)
port_mapping_model = PortMapping
parent_field = 'device'
def clean(self):
super().clean()
@@ -200,22 +203,3 @@ class FrontPortFormMixin(forms.Form):
using=connection,
update_fields=None
)
def _get_rear_port_choices(self, parent_filter, front_port):
"""
Return a list of choices representing each available rear port & position pair on the parent object (identified
by a Q filter), excluding those assigned to the specified instance.
"""
occupied_rear_port_positions = [
f'{mapping.rear_port_id}:{mapping.rear_port_position}'
for mapping in self.port_mapping_model.objects.filter(parent_filter).exclude(front_port=front_port.pk)
]
choices = []
for rear_port in self.rear_port_model.objects.filter(parent_filter):
for i in range(1, rear_port.positions + 1):
pair_id = f'{rear_port.pk}:{i}'
if pair_id not in occupied_rear_port_positions:
pair_label = f'{rear_port.name}:{i}'
choices.append((pair_id, pair_label))
return choices

View File

@@ -722,6 +722,9 @@ class DeviceForm(TenancyForm, PrimaryModelForm):
if position:
self.fields['position'].widget.choices = [(position, f'U{position}')]
# Clear face field when rack is cleared
self.fields['face'].widget.attrs['ts-clear-field'] = 'rack'
class ModuleForm(ModuleCommonForm, PrimaryModelForm):
device = DynamicModelChoiceField(
@@ -1124,8 +1127,9 @@ class FrontPortTemplateForm(FrontPortFormMixin, ModularComponentTemplateForm):
),
)
# Override FrontPortFormMixin attrs
port_mapping_model = PortTemplateMapping
rear_port_model = RearPortTemplate
parent_field = 'device_type'
class Meta:
model = FrontPortTemplate
@@ -1136,14 +1140,13 @@ class FrontPortTemplateForm(FrontPortFormMixin, ModularComponentTemplateForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Populate rear port choices based on parent DeviceType or ModuleType
if device_type_id := self.data.get('device_type') or self.initial.get('device_type'):
parent_filter = Q(device_type=device_type_id)
elif module_type_id := self.data.get('module_type') or self.initial.get('module_type'):
parent_filter = Q(module_type=module_type_id)
device_type = DeviceType.objects.get(pk=device_type_id)
else:
return
self.fields['rear_ports'].choices = self._get_rear_port_choices(parent_filter, self.instance)
# Populate rear port choices
self.fields['rear_ports'].choices = self._get_rear_port_choices(device_type, self.instance)
# Set initial rear port mappings
if self.instance.pk:
@@ -1152,6 +1155,27 @@ class FrontPortTemplateForm(FrontPortFormMixin, ModularComponentTemplateForm):
for mapping in PortTemplateMapping.objects.filter(front_port_id=self.instance.pk)
]
def _get_rear_port_choices(self, device_type, front_port):
"""
Return a list of choices representing each available rear port & position pair on the device type, excluding
those assigned to the specified instance.
"""
occupied_rear_port_positions = [
f'{mapping.rear_port_id}:{mapping.rear_port_position}'
for mapping in device_type.port_mappings.exclude(front_port=front_port.pk)
]
choices = []
for rear_port in RearPortTemplate.objects.filter(device_type=device_type):
for i in range(1, rear_port.positions + 1):
pair_id = f'{rear_port.pk}:{i}'
if pair_id not in occupied_rear_port_positions:
pair_label = f'{rear_port.name}:{i}'
choices.append(
(pair_id, pair_label)
)
return choices
class RearPortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
@@ -1598,9 +1622,6 @@ class FrontPortForm(FrontPortFormMixin, ModularDeviceComponentForm):
),
)
port_mapping_model = PortMapping
rear_port_model = RearPort
class Meta:
model = FrontPort
fields = [
@@ -1611,12 +1632,13 @@ class FrontPortForm(FrontPortFormMixin, ModularDeviceComponentForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Populate rear port choices
if device_id := self.data.get('device') or self.initial.get('device'):
parent_filter = Q(device=device_id)
device = Device.objects.get(pk=device_id)
else:
return
self.fields['rear_ports'].choices = self._get_rear_port_choices(parent_filter, self.instance)
# Populate rear port choices
self.fields['rear_ports'].choices = self._get_rear_port_choices(device, self.instance)
# Set initial rear port mappings
if self.instance.pk:
@@ -1625,6 +1647,27 @@ class FrontPortForm(FrontPortFormMixin, ModularDeviceComponentForm):
for mapping in PortMapping.objects.filter(front_port_id=self.instance.pk)
]
def _get_rear_port_choices(self, device, front_port):
"""
Return a list of choices representing each available rear port & position pair on the device, excluding those
assigned to the specified instance.
"""
occupied_rear_port_positions = [
f'{mapping.rear_port_id}:{mapping.rear_port_position}'
for mapping in device.port_mappings.exclude(front_port=front_port.pk)
]
choices = []
for rear_port in RearPort.objects.filter(device=device):
for i in range(1, rear_port.positions + 1):
pair_id = f'{rear_port.pk}:{i}'
if pair_id not in occupied_rear_port_positions:
pair_label = f'{rear_port.name}:{i}'
choices.append(
(pair_id, pair_label)
)
return choices
class RearPortForm(ModularDeviceComponentForm):
fieldsets = (

View File

@@ -211,16 +211,12 @@ def sync_cached_scope_fields(instance, created, **kwargs):
for model in (Prefix, Cluster, WirelessLAN):
qs = model.objects.filter(**filters)
# Bulk update cached fields to avoid O(N) performance issues with large datasets.
# This does not trigger post_save signals, avoiding spurious change log entries.
objects_to_update = []
for obj in qs:
# Recompute cache using the same logic as save()
obj.cache_related_objects()
objects_to_update.append(obj)
if objects_to_update:
model.objects.bulk_update(
objects_to_update,
['_location', '_site', '_site_group', '_region']
)
obj.save(update_fields=[
'_location',
'_site',
'_site_group',
'_region',
])

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='{}mm')
class RackNumberingPanel(panels.ObjectAttributesPanel):

View File

@@ -1845,7 +1845,6 @@ class ModuleTypeBulkEditView(generic.BulkEditView):
class ModuleTypeBulkRenameView(generic.BulkRenameView):
queryset = ModuleType.objects.all()
filterset = filtersets.ModuleTypeFilterSet
field_name = 'model'
@register_model_view(ModuleType, 'bulk_delete', path='delete', detail=False)

View File

@@ -28,7 +28,7 @@ class ConfigContextProfileSerializer(PrimaryModelSerializer):
)
data_file = DataFileSerializer(
nested=True,
required=False
read_only=True
)
class Meta:
@@ -143,7 +143,7 @@ class ConfigContextSerializer(OwnerMixin, ChangeLogMessageSerializer, ValidatedM
)
data_file = DataFileSerializer(
nested=True,
required=False
read_only=True
)
class Meta:

View File

@@ -1,5 +1,4 @@
import datetime
import hashlib
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
@@ -8,7 +7,7 @@ from rest_framework import status
from core.choices import ManagedFileRootPathChoices
from core.events import *
from core.models import DataFile, DataSource, ObjectType
from core.models import ObjectType
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
from extras.choices import *
from extras.models import *
@@ -732,51 +731,6 @@ class ConfigContextProfileTest(APIViewTestCases.APIViewTestCase):
)
ConfigContextProfile.objects.bulk_create(profiles)
def test_update_data_source_and_data_file(self):
"""
Regression test: Ensure data_source and data_file can be assigned via the API.
This specifically covers PATCHing a ConfigContext with integer IDs for both fields.
"""
self.add_permissions(
'core.view_datafile',
'core.view_datasource',
'extras.view_configcontextprofile',
'extras.change_configcontextprofile',
)
config_context_profile = ConfigContextProfile.objects.first()
# Create a data source and file
datasource = DataSource.objects.create(
name='Data Source 1',
type='local',
source_url='file:///tmp/netbox-datasource/',
)
# Generate a valid dummy YAML file
file_data = b'profile: configcontext\n'
datafile = DataFile.objects.create(
source=datasource,
path='dir1/file1.yml',
last_updated=now(),
size=len(file_data),
hash=hashlib.sha256(file_data).hexdigest(),
data=file_data,
)
url = self._get_detail_url(config_context_profile)
payload = {
'data_source': datasource.pk,
'data_file': datafile.pk,
}
response = self.client.patch(url, payload, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
config_context_profile.refresh_from_db()
self.assertEqual(config_context_profile.data_source_id, datasource.pk)
self.assertEqual(config_context_profile.data_file_id, datafile.pk)
self.assertEqual(response.data['data_source']['id'], datasource.pk)
self.assertEqual(response.data['data_file']['id'], datafile.pk)
class ConfigContextTest(APIViewTestCases.APIViewTestCase):
model = ConfigContext
@@ -858,51 +812,6 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
rendered_context = device.get_config_context()
self.assertEqual(rendered_context['bar'], 456)
def test_update_data_source_and_data_file(self):
"""
Regression test: Ensure data_source and data_file can be assigned via the API.
This specifically covers PATCHing a ConfigContext with integer IDs for both fields.
"""
self.add_permissions(
'core.view_datafile',
'core.view_datasource',
'extras.view_configcontext',
'extras.change_configcontext',
)
config_context = ConfigContext.objects.first()
# Create a data source and file
datasource = DataSource.objects.create(
name='Data Source 1',
type='local',
source_url='file:///tmp/netbox-datasource/',
)
# Generate a valid dummy YAML file
file_data = b'context: config\n'
datafile = DataFile.objects.create(
source=datasource,
path='dir1/file1.yml',
last_updated=now(),
size=len(file_data),
hash=hashlib.sha256(file_data).hexdigest(),
data=file_data,
)
url = self._get_detail_url(config_context)
payload = {
'data_source': datasource.pk,
'data_file': datafile.pk,
}
response = self.client.patch(url, payload, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
config_context.refresh_from_db()
self.assertEqual(config_context.data_source_id, datasource.pk)
self.assertEqual(config_context.data_file_id, datafile.pk)
self.assertEqual(response.data['data_source']['id'], datasource.pk)
self.assertEqual(response.data['data_file']['id'], datafile.pk)
class ConfigTemplateTest(APIViewTestCases.APIViewTestCase):
model = ConfigTemplate

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

@@ -538,7 +538,7 @@ class VLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
FieldSet('qinq_role', 'qinq_svlan_id', name=_('Q-in-Q/802.1ad')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
)
selector_fields = ('filter_id', 'q', 'group_id')
selector_fields = ('filter_id', 'q', 'site_id')
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,

View File

@@ -372,8 +372,8 @@ class IPAddressForm(TenancyForm, PrimaryModelForm):
'virtual_machine_id': instance.assigned_object.virtual_machine.pk,
})
# Disable object assignment fields if the IP address is designated as primary or OOB
if self.initial.get('primary_for_parent') or self.initial.get('oob_for_parent'):
# Disable object assignment fields if the IP address is designated as primary
if self.initial.get('primary_for_parent'):
self.fields['interface'].disabled = True
self.fields['vminterface'].disabled = True
self.fields['fhrpgroup'].disabled = True

View File

@@ -940,13 +940,6 @@ class IPAddress(ContactsMixin, PrimaryModel):
_("Cannot reassign IP address while it is designated as the primary IP for the parent object")
)
# can't use is_oob_ip as self.assigned_object might be changed
if hasattr(original_parent, 'oob_ip') and original_parent.oob_ip_id == self.pk:
if parent != original_parent:
raise ValidationError(
_("Cannot reassign IP address while it is designated as the OOB IP for the parent object")
)
# Validate IP status selection
if self.status == IPAddressStatusChoices.STATUS_SLAAC and self.family != 6:
raise ValidationError({

View File

@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
import strawberry_django
from strawberry import ID
from strawberry_django import ComparisonFilterLookup, FilterLookup
from strawberry_django import FilterLookup
from core.graphql.filter_mixins import ChangeLoggingMixin
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, JournalEntriesFilterMixin, TagsFilterMixin
@@ -23,7 +23,7 @@ __all__ = (
@dataclass
class BaseModelFilter:
id: ComparisonFilterLookup[ID] | None = strawberry_django.filter_field()
id: FilterLookup[ID] | None = strawberry_django.filter_field()
class ChangeLoggedModelFilter(ChangeLoggingMixin, BaseModelFilter):

View File

@@ -164,7 +164,7 @@ class ObjectAttributesPanel(ObjectPanel, metaclass=ObjectAttributesPanelMeta):
"""
label = name[:1].upper() + name[1:]
label = label.replace('_', ' ')
return _(label)
return label
def get_context(self, context):
# Determine which attributes to display in the panel based on only/exclude args

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,4 @@
import TomSelect from 'tom-select';
import { getElements } from '../util';
function handleFormSubmit(): void {
@@ -8,6 +9,37 @@ function handleFormSubmit(): void {
}
}
/**
* Initialize clear-field dependencies.
* When a field with ts-clear-field attribute's parent field is cleared, this field will also be cleared.
*/
function initClearFieldDependencies(): void {
// Find all fields with ts-clear-field attribute
for (const field of getElements<HTMLSelectElement>('[ts-clear-field]')) {
const parentFieldName = field.getAttribute('ts-clear-field');
if (!parentFieldName) continue;
// Find the parent field
const parentField = document.querySelector<HTMLSelectElement>(`[name="${parentFieldName}"]`);
if (!parentField) continue;
// Listen for changes on the parent field
parentField.addEventListener('change', () => {
// If parent field is cleared, also clear this dependent field
if (!parentField.value || parentField.value === '') {
// Check if this field uses TomSelect
const tomselect = (field as HTMLSelectElement & { tomselect?: TomSelect }).tomselect;
if (tomselect) {
tomselect.clear();
} else {
// Regular select field
field.value = '';
}
}
});
}
}
/**
* Attach event listeners to each form's submit/reset buttons.
*/
@@ -28,4 +60,7 @@ export function initFormElements(): void {
});
}
}
// Initialize clear-field dependencies
initClearFieldDependencies();
}

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-13 05:05+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"
@@ -806,7 +806,7 @@ msgstr ""
#: netbox/circuits/forms/model_forms.py:335
#: netbox/dcim/forms/model_forms.py:145 netbox/dcim/forms/model_forms.py:186
#: netbox/dcim/forms/model_forms.py:273 netbox/dcim/forms/model_forms.py:330
#: netbox/dcim/forms/model_forms.py:863 netbox/dcim/forms/model_forms.py:1877
#: netbox/dcim/forms/model_forms.py:863 netbox/dcim/forms/model_forms.py:1917
#: netbox/ipam/forms/bulk_edit.py:380 netbox/ipam/forms/model_forms.py:67
#: netbox/ipam/forms/model_forms.py:84 netbox/ipam/forms/model_forms.py:115
#: netbox/ipam/forms/model_forms.py:136 netbox/ipam/forms/model_forms.py:160
@@ -1012,8 +1012,8 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:262 netbox/dcim/forms/bulk_import.py:1193
#: netbox/dcim/forms/filtersets.py:399 netbox/dcim/forms/filtersets.py:865
#: netbox/dcim/forms/filtersets.py:1872 netbox/dcim/forms/filtersets.py:1912
#: netbox/dcim/forms/model_forms.py:255 netbox/dcim/forms/model_forms.py:1214
#: netbox/dcim/forms/model_forms.py:1697 netbox/dcim/forms/object_import.py:182
#: netbox/dcim/forms/model_forms.py:255 netbox/dcim/forms/model_forms.py:1235
#: netbox/dcim/forms/model_forms.py:1737 netbox/dcim/forms/object_import.py:182
#: netbox/dcim/tables/devices.py:171 netbox/dcim/tables/devices.py:857
#: netbox/dcim/tables/devices.py:983 netbox/dcim/tables/devicetypes.py:317
#: netbox/dcim/tables/racks.py:117 netbox/extras/filtersets.py:708
@@ -1132,9 +1132,9 @@ msgstr ""
#: netbox/circuits/forms/bulk_import.py:258
#: netbox/circuits/forms/model_forms.py:358
#: netbox/circuits/tables/virtual_circuits.py:108
#: netbox/dcim/forms/bulk_import.py:1324 netbox/dcim/forms/model_forms.py:1288
#: netbox/dcim/forms/model_forms.py:1557 netbox/dcim/forms/model_forms.py:1738
#: netbox/dcim/forms/model_forms.py:1773 netbox/dcim/forms/model_forms.py:1898
#: netbox/dcim/forms/bulk_import.py:1324 netbox/dcim/forms/model_forms.py:1309
#: netbox/dcim/forms/model_forms.py:1578 netbox/dcim/forms/model_forms.py:1778
#: netbox/dcim/forms/model_forms.py:1813 netbox/dcim/forms/model_forms.py:1938
#: netbox/dcim/tables/connections.py:65 netbox/dcim/tables/devices.py:1150
#: netbox/ipam/forms/bulk_import.py:319 netbox/ipam/forms/model_forms.py:280
#: netbox/ipam/forms/model_forms.py:289 netbox/ipam/tables/fhrp.py:61
@@ -1852,8 +1852,8 @@ msgstr ""
#: netbox/dcim/forms/filtersets.py:1866 netbox/dcim/forms/filtersets.py:1907
#: netbox/dcim/forms/filtersets.py:2000 netbox/dcim/forms/filtersets.py:2024
#: netbox/dcim/forms/filtersets.py:2048 netbox/dcim/forms/model_forms.py:728
#: netbox/dcim/forms/model_forms.py:943 netbox/dcim/forms/model_forms.py:1355
#: netbox/dcim/forms/model_forms.py:1849 netbox/dcim/forms/model_forms.py:1922
#: netbox/dcim/forms/model_forms.py:943 netbox/dcim/forms/model_forms.py:1376
#: netbox/dcim/forms/model_forms.py:1889 netbox/dcim/forms/model_forms.py:1962
#: netbox/dcim/forms/object_create.py:205 netbox/dcim/tables/connections.py:22
#: netbox/dcim/tables/connections.py:41 netbox/dcim/tables/connections.py:60
#: netbox/dcim/tables/devices.py:291 netbox/dcim/tables/devices.py:386
@@ -2435,7 +2435,7 @@ msgstr ""
msgid "Change logging is not supported for this object type ({type})."
msgstr ""
#: netbox/core/models/config.py:21 netbox/core/models/data.py:284
#: netbox/core/models/config.py:21 netbox/core/models/data.py:282
#: netbox/core/models/files.py:29 netbox/core/models/jobs.py:60
#: netbox/extras/models/models.py:847 netbox/extras/models/notifications.py:39
#: netbox/extras/models/notifications.py:195
@@ -2541,63 +2541,58 @@ msgstr ""
msgid "Unknown backend type: {type}"
msgstr ""
#: netbox/core/models/data.py:131
#, python-brace-format
msgid "URLs for local sources must start with {scheme} (or specify no scheme)"
msgstr ""
#: netbox/core/models/data.py:182
#: netbox/core/models/data.py:180
msgid "Cannot initiate sync; syncing already in progress."
msgstr ""
#: netbox/core/models/data.py:195
#: netbox/core/models/data.py:193
msgid ""
"There was an error initializing the backend. A dependency needs to be "
"installed: "
msgstr ""
#: netbox/core/models/data.py:288 netbox/core/models/files.py:33
#: netbox/core/models/data.py:286 netbox/core/models/files.py:33
#: netbox/netbox/models/features.py:67
msgid "last updated"
msgstr ""
#: netbox/core/models/data.py:298 netbox/dcim/models/cables.py:622
#: netbox/core/models/data.py:296 netbox/dcim/models/cables.py:622
msgid "path"
msgstr ""
#: netbox/core/models/data.py:301
#: netbox/core/models/data.py:299
msgid "File path relative to the data source's root"
msgstr ""
#: netbox/core/models/data.py:305 netbox/ipam/models/ip.py:507
#: netbox/core/models/data.py:303 netbox/ipam/models/ip.py:507
msgid "size"
msgstr ""
#: netbox/core/models/data.py:308
#: netbox/core/models/data.py:306
msgid "hash"
msgstr ""
#: netbox/core/models/data.py:312
#: netbox/core/models/data.py:310
msgid "Length must be 64 hexadecimal characters."
msgstr ""
#: netbox/core/models/data.py:314
#: netbox/core/models/data.py:312
msgid "SHA256 hash of the file data"
msgstr ""
#: netbox/core/models/data.py:328
#: netbox/core/models/data.py:326
msgid "data file"
msgstr ""
#: netbox/core/models/data.py:329
#: netbox/core/models/data.py:327
msgid "data files"
msgstr ""
#: netbox/core/models/data.py:402
#: netbox/core/models/data.py:400
msgid "auto sync record"
msgstr ""
#: netbox/core/models/data.py:403
#: netbox/core/models/data.py:401
msgid "auto sync records"
msgstr ""
@@ -3084,8 +3079,8 @@ msgstr ""
#: netbox/dcim/forms/filtersets.py:768 netbox/dcim/forms/filtersets.py:783
#: netbox/dcim/forms/model_forms.py:81 netbox/dcim/forms/model_forms.py:99
#: netbox/dcim/forms/model_forms.py:176 netbox/dcim/forms/model_forms.py:502
#: netbox/dcim/forms/model_forms.py:523 netbox/dcim/forms/model_forms.py:1206
#: netbox/dcim/forms/model_forms.py:1689 netbox/dcim/forms/object_import.py:177
#: netbox/dcim/forms/model_forms.py:523 netbox/dcim/forms/model_forms.py:1227
#: netbox/dcim/forms/model_forms.py:1729 netbox/dcim/forms/object_import.py:177
#: netbox/dcim/tables/devices.py:702 netbox/dcim/tables/devices.py:916
#: netbox/dcim/tables/devices.py:1003 netbox/dcim/tables/devices.py:1156
#: netbox/ipam/forms/bulk_import.py:578 netbox/ipam/forms/model_forms.py:755
@@ -3217,7 +3212,7 @@ msgstr ""
#: netbox/dcim/choices.py:885 netbox/dcim/choices.py:1351
#: netbox/dcim/forms/bulk_edit.py:1543 netbox/dcim/forms/filtersets.py:1553
#: netbox/dcim/forms/filtersets.py:1678 netbox/dcim/forms/model_forms.py:1105
#: netbox/dcim/forms/model_forms.py:1569 netbox/netbox/navigation/menu.py:147
#: netbox/dcim/forms/model_forms.py:1590 netbox/netbox/navigation/menu.py:147
#: netbox/netbox/navigation/menu.py:151
#: netbox/templates/dcim/interface.html:280
msgid "Wireless"
@@ -3835,7 +3830,7 @@ msgstr ""
#: netbox/dcim/filtersets.py:1242 netbox/dcim/forms/filtersets.py:906
#: netbox/dcim/forms/filtersets.py:1609 netbox/dcim/forms/filtersets.py:1947
#: netbox/dcim/forms/model_forms.py:1895 netbox/dcim/models/devices.py:1307
#: netbox/dcim/forms/model_forms.py:1935 netbox/dcim/models/devices.py:1307
#: netbox/dcim/models/devices.py:1330 netbox/virtualization/filtersets.py:211
#: netbox/virtualization/filtersets.py:284
#: netbox/virtualization/forms/filtersets.py:187
@@ -4001,7 +3996,7 @@ msgstr ""
#: netbox/dcim/filtersets.py:1942 netbox/dcim/forms/bulk_edit.py:1509
#: netbox/dcim/forms/bulk_import.py:1027 netbox/dcim/forms/filtersets.py:1662
#: netbox/dcim/forms/model_forms.py:1535
#: netbox/dcim/forms/model_forms.py:1556
#: netbox/dcim/models/device_components.py:866
#: netbox/dcim/tables/devices.py:660 netbox/ipam/filtersets.py:345
#: netbox/ipam/filtersets.py:356 netbox/ipam/filtersets.py:489
@@ -4060,7 +4055,7 @@ msgid "VLAN Translation Policy (ID)"
msgstr ""
#: netbox/dcim/filtersets.py:1970 netbox/dcim/forms/filtersets.py:1633
#: netbox/dcim/forms/model_forms.py:1552
#: netbox/dcim/forms/model_forms.py:1573
#: netbox/dcim/models/device_components.py:668
#: netbox/ipam/forms/filtersets.py:518 netbox/ipam/forms/model_forms.py:700
#: netbox/templates/ipam/vlantranslationpolicy.html:11
@@ -4114,14 +4109,14 @@ msgstr ""
msgid "Primary MAC address (ID)"
msgstr ""
#: netbox/dcim/filtersets.py:2057 netbox/dcim/forms/model_forms.py:1539
#: netbox/dcim/filtersets.py:2057 netbox/dcim/forms/model_forms.py:1560
#: netbox/virtualization/filtersets.py:295
#: netbox/virtualization/forms/model_forms.py:302
msgid "Primary MAC address"
msgstr ""
#: netbox/dcim/filtersets.py:2079 netbox/dcim/filtersets.py:2091
#: netbox/dcim/forms/filtersets.py:1569 netbox/dcim/forms/model_forms.py:1875
#: netbox/dcim/forms/filtersets.py:1569 netbox/dcim/forms/model_forms.py:1915
#: netbox/templates/dcim/virtualdevicecontext.html:15
msgid "Virtual Device Context"
msgstr ""
@@ -4243,7 +4238,7 @@ msgstr ""
#: netbox/dcim/forms/filtersets.py:1917 netbox/dcim/forms/model_forms.py:211
#: netbox/dcim/forms/model_forms.py:342 netbox/dcim/forms/model_forms.py:354
#: netbox/dcim/forms/model_forms.py:424 netbox/dcim/forms/model_forms.py:528
#: netbox/dcim/forms/model_forms.py:1219 netbox/dcim/forms/model_forms.py:1702
#: netbox/dcim/forms/model_forms.py:1240 netbox/dcim/forms/model_forms.py:1742
#: netbox/dcim/forms/object_import.py:188 netbox/dcim/tables/devices.py:99
#: netbox/dcim/tables/devices.py:174 netbox/dcim/tables/devices.py:986
#: netbox/dcim/tables/devicetypes.py:86 netbox/dcim/tables/devicetypes.py:321
@@ -4421,7 +4416,7 @@ msgstr ""
#: netbox/dcim/forms/filtersets.py:524 netbox/dcim/forms/filtersets.py:667
#: netbox/dcim/forms/filtersets.py:809 netbox/dcim/forms/filtersets.py:1028
#: netbox/dcim/forms/model_forms.py:432 netbox/dcim/forms/model_forms.py:767
#: netbox/dcim/forms/model_forms.py:1770
#: netbox/dcim/forms/model_forms.py:1810
#: netbox/templates/dcim/device_edit.html:22
msgid "Hardware"
msgstr ""
@@ -4447,8 +4442,8 @@ msgstr ""
#: netbox/dcim/forms/bulk_edit.py:531 netbox/dcim/forms/model_forms.py:373
#: netbox/dcim/forms/model_forms.py:1002 netbox/dcim/forms/model_forms.py:1044
#: netbox/dcim/forms/model_forms.py:1071 netbox/dcim/forms/model_forms.py:1099
#: netbox/dcim/forms/model_forms.py:1120 netbox/dcim/forms/model_forms.py:1160
#: netbox/dcim/forms/model_forms.py:1178 netbox/dcim/forms/object_create.py:117
#: netbox/dcim/forms/model_forms.py:1120 netbox/dcim/forms/model_forms.py:1181
#: netbox/dcim/forms/model_forms.py:1199 netbox/dcim/forms/object_create.py:117
#: netbox/dcim/tables/devicetypes.py:83 netbox/templates/dcim/devicebay.html:52
#: netbox/templates/dcim/module.html:61
msgid "Device Type"
@@ -4477,7 +4472,7 @@ msgstr ""
#: netbox/dcim/forms/model_forms.py:431 netbox/dcim/forms/model_forms.py:1003
#: netbox/dcim/forms/model_forms.py:1045 netbox/dcim/forms/model_forms.py:1072
#: netbox/dcim/forms/model_forms.py:1100 netbox/dcim/forms/model_forms.py:1121
#: netbox/dcim/forms/model_forms.py:1161 netbox/dcim/forms/model_forms.py:1179
#: netbox/dcim/forms/model_forms.py:1182 netbox/dcim/forms/model_forms.py:1200
#: netbox/dcim/forms/object_create.py:118 netbox/dcim/tables/modules.py:51
#: netbox/dcim/tables/modules.py:94 netbox/templates/dcim/module.html:92
#: netbox/templates/dcim/modulebay.html:66
@@ -4661,8 +4656,8 @@ msgid "Allocated power draw (watts)"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:1058 netbox/dcim/forms/bulk_import.py:885
#: netbox/dcim/forms/model_forms.py:1060 netbox/dcim/forms/model_forms.py:1425
#: netbox/dcim/forms/model_forms.py:1754 netbox/dcim/forms/object_import.py:56
#: netbox/dcim/forms/model_forms.py:1060 netbox/dcim/forms/model_forms.py:1446
#: netbox/dcim/forms/model_forms.py:1794 netbox/dcim/forms/object_import.py:56
msgid "Power port"
msgstr ""
@@ -4697,7 +4692,7 @@ msgid "Wireless role"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:1268 netbox/dcim/forms/model_forms.py:766
#: netbox/dcim/forms/model_forms.py:1370 netbox/dcim/tables/devices.py:328
#: netbox/dcim/forms/model_forms.py:1391 netbox/dcim/tables/devices.py:328
#: netbox/templates/dcim/consoleport.html:24
#: netbox/templates/dcim/consoleserverport.html:24
#: netbox/templates/dcim/frontport.html:24
@@ -4715,7 +4710,7 @@ msgstr ""
msgid "LAG"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:1415 netbox/dcim/forms/model_forms.py:1452
#: netbox/dcim/forms/bulk_edit.py:1415 netbox/dcim/forms/model_forms.py:1473
msgid "Virtual device contexts"
msgstr ""
@@ -4744,7 +4739,7 @@ msgid "Mode"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:1458 netbox/dcim/forms/bulk_import.py:993
#: netbox/dcim/forms/model_forms.py:1501 netbox/ipam/forms/bulk_import.py:173
#: netbox/dcim/forms/model_forms.py:1522 netbox/ipam/forms/bulk_import.py:173
#: netbox/ipam/forms/filtersets.py:568 netbox/ipam/models/vlans.py:93
#: netbox/virtualization/forms/bulk_edit.py:205
#: netbox/virtualization/forms/bulk_import.py:185
@@ -4753,7 +4748,7 @@ msgid "VLAN group"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:1467 netbox/dcim/forms/bulk_import.py:1000
#: netbox/dcim/forms/model_forms.py:1507 netbox/dcim/tables/devices.py:605
#: netbox/dcim/forms/model_forms.py:1528 netbox/dcim/tables/devices.py:605
#: netbox/virtualization/forms/bulk_edit.py:213
#: netbox/virtualization/forms/bulk_import.py:192
#: netbox/virtualization/forms/model_forms.py:331
@@ -4761,7 +4756,7 @@ msgid "Untagged VLAN"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:1476 netbox/dcim/forms/bulk_import.py:1007
#: netbox/dcim/forms/model_forms.py:1516 netbox/dcim/tables/devices.py:611
#: netbox/dcim/forms/model_forms.py:1537 netbox/dcim/tables/devices.py:611
#: netbox/virtualization/forms/bulk_edit.py:221
#: netbox/virtualization/forms/bulk_import.py:199
#: netbox/virtualization/forms/model_forms.py:340
@@ -4777,18 +4772,18 @@ msgid "Remove tagged VLANs"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:1499 netbox/dcim/forms/bulk_import.py:1020
#: netbox/dcim/forms/model_forms.py:1525
#: netbox/dcim/forms/model_forms.py:1546
#: netbox/virtualization/forms/bulk_import.py:212
#: netbox/virtualization/forms/model_forms.py:349
msgid "Q-in-Q Service VLAN"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:1514 netbox/dcim/forms/model_forms.py:1488
#: netbox/dcim/forms/bulk_edit.py:1514 netbox/dcim/forms/model_forms.py:1509
#: netbox/wireless/forms/filtersets.py:26
msgid "Wireless LAN group"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:1519 netbox/dcim/forms/model_forms.py:1493
#: netbox/dcim/forms/bulk_edit.py:1519 netbox/dcim/forms/model_forms.py:1514
#: netbox/dcim/tables/devices.py:653 netbox/netbox/navigation/menu.py:153
#: netbox/templates/dcim/interface.html:350
#: netbox/wireless/tables/wirelesslan.py:20
@@ -4796,7 +4791,7 @@ msgid "Wireless LANs"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:1528 netbox/dcim/forms/filtersets.py:1550
#: netbox/dcim/forms/model_forms.py:1559 netbox/ipam/forms/bulk_edit.py:224
#: netbox/dcim/forms/model_forms.py:1580 netbox/ipam/forms/bulk_edit.py:224
#: netbox/ipam/forms/bulk_edit.py:310 netbox/ipam/forms/filtersets.py:184
#: netbox/netbox/navigation/menu.py:109
#: netbox/templates/dcim/interface.html:141
@@ -4808,18 +4803,18 @@ msgid "Addressing"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:1529 netbox/dcim/forms/filtersets.py:808
#: netbox/dcim/forms/model_forms.py:1560
#: netbox/dcim/forms/model_forms.py:1581
#: netbox/virtualization/forms/model_forms.py:370
msgid "Operation"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:1530 netbox/dcim/forms/filtersets.py:1551
#: netbox/dcim/forms/filtersets.py:1677 netbox/dcim/forms/model_forms.py:1104
#: netbox/dcim/forms/model_forms.py:1562
#: netbox/dcim/forms/model_forms.py:1583
msgid "PoE"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:1531 netbox/dcim/forms/model_forms.py:1561
#: netbox/dcim/forms/bulk_edit.py:1531 netbox/dcim/forms/model_forms.py:1582
#: netbox/templates/dcim/interface.html:105
#: netbox/virtualization/forms/bulk_edit.py:237
#: netbox/virtualization/forms/model_forms.py:371
@@ -4827,7 +4822,7 @@ msgid "Related Interfaces"
msgstr ""
#: netbox/dcim/forms/bulk_edit.py:1533 netbox/dcim/forms/filtersets.py:1552
#: netbox/dcim/forms/model_forms.py:1565
#: netbox/dcim/forms/model_forms.py:1586
#: netbox/virtualization/forms/bulk_edit.py:240
#: netbox/virtualization/forms/filtersets.py:215
#: netbox/virtualization/forms/model_forms.py:374
@@ -5106,13 +5101,13 @@ msgstr ""
msgid "Electrical phase (for three-phase circuits)"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:939 netbox/dcim/forms/model_forms.py:1463
#: netbox/dcim/forms/bulk_import.py:939 netbox/dcim/forms/model_forms.py:1484
#: netbox/virtualization/forms/bulk_import.py:169
#: netbox/virtualization/forms/model_forms.py:310
msgid "Parent interface"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:946 netbox/dcim/forms/model_forms.py:1471
#: netbox/dcim/forms/bulk_import.py:946 netbox/dcim/forms/model_forms.py:1492
#: netbox/virtualization/forms/bulk_import.py:176
#: netbox/virtualization/forms/model_forms.py:318
msgid "Bridged interface"
@@ -5395,7 +5390,7 @@ msgstr ""
msgid "Single or three-phase"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1729 netbox/dcim/forms/model_forms.py:1855
#: netbox/dcim/forms/bulk_import.py:1729 netbox/dcim/forms/model_forms.py:1895
#: netbox/dcim/ui/panels.py:109
#: netbox/templates/dcim/virtualdevicecontext.html:30
#: netbox/templates/virtualization/virtualmachine.html:56
@@ -5406,7 +5401,7 @@ msgstr ""
msgid "IPv4 address with mask, e.g. 1.2.3.4/24"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1736 netbox/dcim/forms/model_forms.py:1864
#: netbox/dcim/forms/bulk_import.py:1736 netbox/dcim/forms/model_forms.py:1904
#: netbox/dcim/ui/panels.py:114
#: netbox/templates/dcim/virtualdevicecontext.html:41
#: netbox/templates/virtualization/virtualmachine.html:72
@@ -5609,7 +5604,7 @@ msgstr ""
msgid "Mgmt only"
msgstr ""
#: netbox/dcim/forms/filtersets.py:1613 netbox/dcim/forms/model_forms.py:1547
#: netbox/dcim/forms/filtersets.py:1613 netbox/dcim/forms/model_forms.py:1568
#: netbox/dcim/models/device_components.py:791
#: netbox/templates/dcim/interface.html:155
msgid "WWN"
@@ -5710,7 +5705,7 @@ msgstr ""
msgid "Rear ports"
msgstr ""
#: netbox/dcim/forms/mixins.py:155
#: netbox/dcim/forms/mixins.py:158
#, python-brace-format
msgid ""
"The total number of front port positions ({frontport_count}) must match the "
@@ -5782,35 +5777,35 @@ msgid ""
"replaced with the position value when creating a new module."
msgstr ""
#: netbox/dcim/forms/model_forms.py:1231
#: netbox/dcim/forms/model_forms.py:1252
msgid "Console port template"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1239
#: netbox/dcim/forms/model_forms.py:1260
msgid "Console server port template"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1247
#: netbox/dcim/forms/model_forms.py:1268
msgid "Front port template"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1255
#: netbox/dcim/forms/model_forms.py:1276
msgid "Interface template"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1263
#: netbox/dcim/forms/model_forms.py:1284
msgid "Power outlet template"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1271
#: netbox/dcim/forms/model_forms.py:1292
msgid "Power port template"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1279
#: netbox/dcim/forms/model_forms.py:1300
msgid "Rear port template"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1289 netbox/dcim/forms/model_forms.py:1774
#: netbox/dcim/forms/model_forms.py:1310 netbox/dcim/forms/model_forms.py:1814
#: netbox/dcim/tables/connections.py:27
#: netbox/templates/dcim/consoleport.html:17
#: netbox/templates/dcim/consoleserverport.html:73
@@ -5818,14 +5813,14 @@ msgstr ""
msgid "Console Port"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1290 netbox/dcim/forms/model_forms.py:1775
#: netbox/dcim/forms/model_forms.py:1311 netbox/dcim/forms/model_forms.py:1815
#: netbox/templates/dcim/consoleport.html:73
#: netbox/templates/dcim/consoleserverport.html:17
#: netbox/templates/dcim/frontport.html:106
msgid "Console Server Port"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1291 netbox/dcim/forms/model_forms.py:1776
#: netbox/dcim/forms/model_forms.py:1312 netbox/dcim/forms/model_forms.py:1816
#: netbox/templates/circuits/inc/circuit_termination_fields.html:53
#: netbox/templates/dcim/consoleport.html:76
#: netbox/templates/dcim/consoleserverport.html:76
@@ -5837,7 +5832,7 @@ msgstr ""
msgid "Front Port"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1292 netbox/dcim/forms/model_forms.py:1777
#: netbox/dcim/forms/model_forms.py:1313 netbox/dcim/forms/model_forms.py:1817
#: netbox/templates/circuits/inc/circuit_termination_fields.html:54
#: netbox/templates/dcim/consoleport.html:79
#: netbox/templates/dcim/consoleserverport.html:79
@@ -5849,80 +5844,80 @@ msgstr ""
msgid "Rear Port"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1293 netbox/dcim/forms/model_forms.py:1778
#: netbox/dcim/forms/model_forms.py:1314 netbox/dcim/forms/model_forms.py:1818
#: netbox/dcim/tables/connections.py:46 netbox/dcim/tables/devices.py:526
#: netbox/templates/dcim/poweroutlet.html:58
#: netbox/templates/dcim/powerport.html:17
msgid "Power Port"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1294 netbox/dcim/forms/model_forms.py:1779
#: netbox/dcim/forms/model_forms.py:1315 netbox/dcim/forms/model_forms.py:1819
#: netbox/templates/dcim/poweroutlet.html:17
#: netbox/templates/dcim/powerport.html:77
msgid "Power Outlet"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1296 netbox/dcim/forms/model_forms.py:1781
#: netbox/dcim/forms/model_forms.py:1317 netbox/dcim/forms/model_forms.py:1821
msgid "Component Assignment"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1342 netbox/dcim/forms/model_forms.py:1828
#: netbox/dcim/forms/model_forms.py:1363 netbox/dcim/forms/model_forms.py:1868
msgid "An InventoryItem can only be assigned to a single component."
msgstr ""
#: netbox/dcim/forms/model_forms.py:1479
#: netbox/dcim/forms/model_forms.py:1500
msgid "LAG interface"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1502
#: netbox/dcim/forms/model_forms.py:1523
msgid "Filter VLANs available for assignment by group."
msgstr ""
#: netbox/dcim/forms/model_forms.py:1671
#: netbox/dcim/forms/model_forms.py:1711
msgid "Child Device"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1672
#: netbox/dcim/forms/model_forms.py:1712
msgid ""
"Child devices must first be created and assigned to the site and rack of the "
"parent device."
msgstr ""
#: netbox/dcim/forms/model_forms.py:1714
#: netbox/dcim/forms/model_forms.py:1754
msgid "Console port"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1722
#: netbox/dcim/forms/model_forms.py:1762
msgid "Console server port"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1730 netbox/dcim/forms/object_import.py:140
#: netbox/dcim/forms/model_forms.py:1770 netbox/dcim/forms/object_import.py:140
msgid "Front port"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1746
#: netbox/dcim/forms/model_forms.py:1786
msgid "Power outlet"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1762 netbox/dcim/forms/object_import.py:145
#: netbox/dcim/forms/model_forms.py:1802 netbox/dcim/forms/object_import.py:145
msgid "Rear port"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1768
#: netbox/dcim/forms/model_forms.py:1808
#: netbox/templates/dcim/inventoryitem.html:17
msgid "Inventory Item"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1837
#: netbox/dcim/forms/model_forms.py:1877
#: netbox/templates/dcim/inventoryitemrole.html:15
msgid "Inventory Item Role"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1907
#: netbox/dcim/forms/model_forms.py:1947
msgid "VM Interface"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1923 netbox/ipam/forms/filtersets.py:638
#: netbox/dcim/forms/model_forms.py:1963 netbox/ipam/forms/filtersets.py:638
#: netbox/ipam/forms/model_forms.py:323 netbox/ipam/tables/vlans.py:171
#: netbox/templates/virtualization/virtualdisk.html:21
#: netbox/templates/virtualization/virtualmachine.html:12
@@ -5939,7 +5934,7 @@ msgstr ""
msgid "Virtual Machine"
msgstr ""
#: netbox/dcim/forms/model_forms.py:1962
#: netbox/dcim/forms/model_forms.py:2002
msgid "A MAC address can only be assigned to a single object."
msgstr ""
@@ -7679,7 +7674,7 @@ msgstr ""
#: netbox/dcim/tables/devices.py:252 netbox/dcim/tables/devices.py:1125
#: netbox/dcim/tables/devicetypes.py:131 netbox/dcim/views.py:1412
#: netbox/dcim/views.py:1749 netbox/dcim/views.py:2578
#: netbox/dcim/views.py:1749 netbox/dcim/views.py:2577
#: netbox/netbox/navigation/menu.py:95 netbox/netbox/navigation/menu.py:259
#: netbox/templates/dcim/buttons/bulk_add_components.html:38
#: netbox/templates/dcim/device/base.html:37
@@ -7726,7 +7721,7 @@ msgstr ""
#: netbox/dcim/tables/devices.py:333 netbox/dcim/tables/devicetypes.py:52
#: netbox/dcim/tables/devicetypes.py:146 netbox/dcim/views.py:1487
#: netbox/dcim/views.py:2664 netbox/netbox/navigation/menu.py:104
#: netbox/dcim/views.py:2663 netbox/netbox/navigation/menu.py:104
#: netbox/templates/dcim/buttons/bulk_add_components.html:66
#: netbox/templates/dcim/device/base.html:52
#: netbox/templates/dcim/devicetype/base.html:49
@@ -7860,7 +7855,7 @@ msgid "Device Count"
msgstr ""
#: netbox/dcim/tables/devicetypes.py:119 netbox/dcim/views.py:1352
#: netbox/dcim/views.py:1689 netbox/dcim/views.py:2513
#: netbox/dcim/views.py:1689 netbox/dcim/views.py:2512
#: netbox/netbox/navigation/menu.py:98
#: netbox/templates/dcim/buttons/bulk_add_components.html:10
#: netbox/templates/dcim/device/base.html:25
@@ -7871,7 +7866,7 @@ msgid "Console Ports"
msgstr ""
#: netbox/dcim/tables/devicetypes.py:122 netbox/dcim/views.py:1367
#: netbox/dcim/views.py:1704 netbox/dcim/views.py:2529
#: netbox/dcim/views.py:1704 netbox/dcim/views.py:2528
#: netbox/netbox/navigation/menu.py:99
#: netbox/templates/dcim/buttons/bulk_add_components.html:17
#: netbox/templates/dcim/device/base.html:28
@@ -7882,7 +7877,7 @@ msgid "Console Server Ports"
msgstr ""
#: netbox/dcim/tables/devicetypes.py:125 netbox/dcim/views.py:1382
#: netbox/dcim/views.py:1719 netbox/dcim/views.py:2545
#: netbox/dcim/views.py:1719 netbox/dcim/views.py:2544
#: netbox/netbox/navigation/menu.py:100
#: netbox/templates/dcim/buttons/bulk_add_components.html:24
#: netbox/templates/dcim/device/base.html:31
@@ -7893,7 +7888,7 @@ msgid "Power Ports"
msgstr ""
#: netbox/dcim/tables/devicetypes.py:128 netbox/dcim/views.py:1397
#: netbox/dcim/views.py:1734 netbox/dcim/views.py:2561
#: netbox/dcim/views.py:1734 netbox/dcim/views.py:2560
#: netbox/netbox/navigation/menu.py:101
#: netbox/templates/dcim/buttons/bulk_add_components.html:31
#: netbox/templates/dcim/device/base.html:34
@@ -7904,7 +7899,7 @@ msgid "Power Outlets"
msgstr ""
#: netbox/dcim/tables/devicetypes.py:134 netbox/dcim/views.py:1427
#: netbox/dcim/views.py:1764 netbox/dcim/views.py:2600
#: netbox/dcim/views.py:1764 netbox/dcim/views.py:2599
#: netbox/netbox/navigation/menu.py:96
#: netbox/templates/dcim/device/base.html:40
#: netbox/templates/dcim/devicetype/base.html:37
@@ -7914,7 +7909,7 @@ msgid "Front Ports"
msgstr ""
#: netbox/dcim/tables/devicetypes.py:137 netbox/dcim/views.py:1442
#: netbox/dcim/views.py:1779 netbox/dcim/views.py:2616
#: netbox/dcim/views.py:1779 netbox/dcim/views.py:2615
#: netbox/netbox/navigation/menu.py:97
#: netbox/templates/dcim/buttons/bulk_add_components.html:45
#: netbox/templates/dcim/device/base.html:43
@@ -7925,7 +7920,7 @@ msgid "Rear Ports"
msgstr ""
#: netbox/dcim/tables/devicetypes.py:140 netbox/dcim/views.py:1472
#: netbox/dcim/views.py:2648 netbox/netbox/navigation/menu.py:103
#: netbox/dcim/views.py:2647 netbox/netbox/navigation/menu.py:103
#: netbox/templates/dcim/buttons/bulk_add_components.html:52
#: netbox/templates/dcim/device/base.html:49
#: netbox/templates/dcim/devicetype/base.html:46
@@ -7933,7 +7928,7 @@ msgid "Device Bays"
msgstr ""
#: netbox/dcim/tables/devicetypes.py:143 netbox/dcim/views.py:1457
#: netbox/dcim/views.py:1794 netbox/dcim/views.py:2632
#: netbox/dcim/views.py:1794 netbox/dcim/views.py:2631
#: netbox/netbox/navigation/menu.py:102
#: netbox/templates/dcim/buttons/bulk_add_components.html:59
#: netbox/templates/dcim/device/base.html:46
@@ -8072,13 +8067,13 @@ msgstr ""
msgid "Reservations"
msgstr ""
#: netbox/dcim/views.py:2459 netbox/netbox/navigation/menu.py:213
#: netbox/dcim/views.py:2458 netbox/netbox/navigation/menu.py:213
#: netbox/templates/ipam/ipaddress.html:118
#: netbox/templates/virtualization/virtualmachine.html:160
msgid "Application Services"
msgstr ""
#: netbox/dcim/views.py:2677 netbox/extras/forms/filtersets.py:427
#: netbox/dcim/views.py:2676 netbox/extras/forms/filtersets.py:427
#: netbox/extras/forms/model_forms.py:691
#: netbox/templates/extras/configcontext.html:10
#: netbox/virtualization/forms/model_forms.py:225
@@ -8086,41 +8081,41 @@ msgstr ""
msgid "Config Context"
msgstr ""
#: netbox/dcim/views.py:2688 netbox/virtualization/views.py:410
#: netbox/dcim/views.py:2687 netbox/virtualization/views.py:410
msgid "Render Config"
msgstr ""
#: netbox/dcim/views.py:2701 netbox/extras/tables/tables.py:713
#: netbox/dcim/views.py:2700 netbox/extras/tables/tables.py:713
#: netbox/netbox/navigation/menu.py:256 netbox/netbox/navigation/menu.py:258
#: netbox/virtualization/views.py:224
msgid "Virtual Machines"
msgstr ""
#: netbox/dcim/views.py:3510
#: netbox/dcim/views.py:3509
#, python-brace-format
msgid "Installed device {device} in bay {device_bay}."
msgstr ""
#: netbox/dcim/views.py:3551
#: netbox/dcim/views.py:3550
#, python-brace-format
msgid "Removed device {device} from bay {device_bay}."
msgstr ""
#: netbox/dcim/views.py:3664 netbox/ipam/tables/ip.py:178
#: netbox/dcim/views.py:3663 netbox/ipam/tables/ip.py:178
msgid "Children"
msgstr ""
#: netbox/dcim/views.py:4137
#: netbox/dcim/views.py:4136
#, python-brace-format
msgid "Added member <a href=\"{url}\">{device}</a>"
msgstr ""
#: netbox/dcim/views.py:4182
#: netbox/dcim/views.py:4181
#, python-brace-format
msgid "Unable to remove master device {device} from the virtual chassis."
msgstr ""
#: netbox/dcim/views.py:4193
#: netbox/dcim/views.py:4192
#, python-brace-format
msgid "Removed {device} from virtual chassis {chassis}"
msgstr ""
@@ -11245,13 +11240,7 @@ msgid ""
"parent object"
msgstr ""
#: netbox/ipam/models/ip.py:947
msgid ""
"Cannot reassign IP address while it is designated as the OOB IP for the "
"parent object"
msgstr ""
#: netbox/ipam/models/ip.py:953
#: netbox/ipam/models/ip.py:946
msgid "Only IPv6 addresses can be assigned SLAAC status"
msgstr ""
@@ -12500,8 +12489,8 @@ msgstr ""
msgid "Delete Selected"
msgstr ""
#: netbox/netbox/plugins/navigation.py:53
#: netbox/netbox/plugins/navigation.py:89
#: netbox/netbox/plugins/navigation.py:55
#: netbox/netbox/plugins/navigation.py:88
msgid "Permissions must be passed as a tuple or list."
msgstr ""
@@ -12509,7 +12498,7 @@ msgstr ""
msgid "Buttons must be passed as a tuple or list."
msgstr ""
#: netbox/netbox/plugins/navigation.py:95
#: netbox/netbox/plugins/navigation.py:92
msgid "Button color must be a choice within ButtonColorChoices."
msgstr ""
@@ -16587,7 +16576,7 @@ msgstr ""
msgid "Missing required value for static query param: '{static_params}'"
msgstr ""
#: netbox/utilities/forms/widgets/modifiers.py:141
#: netbox/utilities/forms/widgets/modifiers.py:111
msgid "(automatically set)"
msgstr ""

View File

@@ -1,8 +1,6 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from utilities.forms.widgets.apiselect import APISelect, APISelectMultiple
__all__ = (
'FilterModifierWidget',
'MODIFIER_EMPTY_FALSE',
@@ -96,37 +94,9 @@ class FilterModifierWidget(forms.Widget):
# to the original widget before rendering
self.original_widget.attrs.update(self.attrs)
# For APISelect/APISelectMultiple widgets, temporarily clear choices to prevent queryset evaluation
original_choices = None
if isinstance(self.original_widget, (APISelect, APISelectMultiple)):
original_choices = self.original_widget.choices
# Only keep selected choices to preserve 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
]
else:
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 = []
# Get context from the original widget
original_context = self.original_widget.get_context(name, value, attrs)
# Restore original choices if we modified them
if original_choices is not None:
self.original_widget.choices = original_choices
# Build our wrapper context
context = super().get_context(name, value, attrs)
context['widget']['original_widget'] = original_context['widget']

16
scripts/git-hooks/pre-commit Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
# TODO: Remove this file in NetBox v4.3
# This script has been maintained to ease transition to the pre-commit tool.
exec 1>&2
EXIT=0
RED='\033[0;31m'
YELLOW='\033[0;33m'
NOCOLOR='\033[0m'
printf "${YELLOW}The pre-commit hook script is obsolete. Please use pre-commit instead:${NOCOLOR}\n"
printf " pip install pre-commit\n"
printf " pre-commit install${NOCOLOR}\n"
exit 1