Compare commits

..

10 Commits

Author SHA1 Message Date
Jeremy Stretch
77102e9f7f Genericize cache for use by other models 2026-01-26 10:39:10 -05:00
Jeremy Stretch
75193ee5fc Initial work on #21259 2026-01-23 16:28:07 -05:00
Arthur Hanson
a9a300197a Clear Rack Face when clear Rack (#21182)
* #20383 clear rack face if no rack on edit

* #20383 clear rack face if no rack on edit

* review changes

* review changes
2026-01-23 12:26:27 -05:00
Jeremy Stretch
3dcca73ecc Fixes #21249: Avoid unneeded user query when no event rules are present (#21250) 2026-01-23 09:44:54 -06:00
bctiemann
4b4c542dce Add truncate_middle filter for middle-ellipsis on long filenames (#21253) 2026-01-22 09:40:48 -08:00
github-actions
077d9b1129 Update source translation strings 2026-01-22 05:07:49 +00:00
Aditya Sharma
e81ccb9be6 Fixes #21214: Clean up AutoSyncRecord when detaching from DataSource (#21219)
Co-authored-by: adionit7 <adionit7@users.noreply.github.com>
2026-01-21 16:38:27 -06:00
Jeremy Stretch
bc83d04c8f Introduce performance issue template (#21247) 2026-01-21 16:34:01 -06:00
Matthew Papaleo
339ad455e4 Support for max_length and max_depth standardised for prefix_list, aggreate/prefixes and prefix/prefixes 2026-01-21 10:02:06 -05:00
github-actions
f24376cfab Update source translation strings 2026-01-21 05:07:22 +00:00
26 changed files with 320 additions and 118 deletions

View File

@@ -0,0 +1,43 @@
---
name: 🏁 Performance
type: Performance
description: An opportunity to improve application performance
labels: ["netbox", "type: performance", "status: needs triage"]
body:
- type: input
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v4.5.1
validations:
required: true
- type: dropdown
attributes:
label: Python Version
description: What version of Python are you currently running?
options:
- "3.12"
- "3.13"
- "3.14"
validations:
required: true
- type: checkboxes
attributes:
label: Area(s) of Concern
description: Which application interface(s) are affected?
options:
- label: User Interface
- label: REST API
- label: GraphQL API
- label: Python ORM
- label: Other
validations:
required: true
- type: textarea
attributes:
label: Details
description: >
Describe in detail the operations being performed and the indications of a performance issue.
Include any relevant testing parameters, benchmarks, and expected results.
validations:
required: true

View File

@@ -9,6 +9,7 @@ from django.db import connection, models
from django.db.models import Q
from django.utils.translation import gettext as _
from netbox.context import query_cache
from netbox.plugins import PluginConfig
from netbox.registry import registry
from utilities.string import title
@@ -70,6 +71,12 @@ class ObjectTypeManager(models.Manager):
"""
from netbox.models.features import get_model_features, model_is_public
# Check the request cache before hitting the database
cache = query_cache.get()
if cache is not None:
if ot := cache['object_types'].get((model._meta.model, for_concrete_model)):
return ot
# 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.
@@ -96,6 +103,10 @@ class ObjectTypeManager(models.Manager):
features=get_model_features(model),
)[0]
# Populate the request cache to avoid redundant lookups
if cache is not None:
cache['object_types'][(model._meta.model, for_concrete_model)] = ot
return ot
def get_for_models(self, *models, for_concrete_models=True):

View File

@@ -20,7 +20,9 @@ from utilities.forms.fields import (
DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField,
)
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
from utilities.forms.widgets import (
APISelect, ClearableFileInput, ClearableSelect, HTMXSelect, NumberWithOptions, SelectWithPK,
)
from utilities.jsonschema import JSONSchemaProperty
from virtualization.models import Cluster, VMInterface
from wireless.models import WirelessLAN, WirelessLANGroup
@@ -592,6 +594,14 @@ class DeviceForm(TenancyForm, PrimaryModelForm):
},
)
)
face = forms.ChoiceField(
label=_('Face'),
choices=add_blank_choice(DeviceFaceChoices),
required=False,
widget=ClearableSelect(
requires_fields=['rack']
)
)
device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all(),

View File

@@ -86,7 +86,7 @@ def enqueue_event(queue, instance, request, event_type):
def process_event_rules(event_rules, object_type, event_type, data, username=None, snapshots=None, request=None):
user = User.objects.get(username=username) if username else None
user = None # To be resolved from the username if needed
for event_rule in event_rules:
@@ -134,6 +134,10 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
# Resolve the script from action parameters
script = event_rule.action_object.python_class()
# Retrieve the User if not already resolved
if user is None:
user = User.objects.get(username=username)
# Enqueue a Job to record the script's execution
from extras.jobs import ScriptJob
params = {

View File

@@ -43,7 +43,7 @@ IMAGEATTACHMENT_IMAGE = """
<a href="{{ record.image.url }}" target="_blank" class="image-preview" data-bs-placement="top">
<i class="mdi mdi-image"></i></a>
{% endif %}
<a href="{{ record.get_absolute_url }}">{{ record }}</a>
<a href="{{ record.get_absolute_url }}">{{ record.filename|truncate_middle:16 }}</a>
"""
NOTIFICATION_ICON = """

View File

@@ -6,7 +6,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile
from django.forms import ValidationError
from django.test import tag, TestCase
from core.models import DataSource, ObjectType
from core.models import AutoSyncRecord, DataSource, ObjectType
from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup
from extras.models import ConfigContext, ConfigContextProfile, ConfigTemplate, ImageAttachment, Tag, TaggedItem
from tenancy.models import Tenant, TenantGroup
@@ -754,3 +754,53 @@ class ConfigTemplateTest(TestCase):
@tag('regression')
def test_config_template_with_data_source_nested_templates(self):
self.assertEqual(self.BASE_TEMPLATE, self.main_config_template.render({}))
@tag('regression')
def test_autosyncrecord_cleanup_on_detach(self):
"""Test that AutoSyncRecord is deleted when detaching from DataSource."""
with tempfile.TemporaryDirectory() as temp_dir:
templates_dir = Path(temp_dir) / "templates"
templates_dir.mkdir(parents=True, exist_ok=True)
self._create_template_file(templates_dir, 'test.j2', 'Test content')
data_source = DataSource(
name="Test DataSource for Detach",
type="local",
source_url=str(templates_dir),
)
data_source.save()
data_source.sync()
data_file = data_source.datafiles.filter(path__endswith='test.j2').first()
# Create a ConfigTemplate with data_file and auto_sync_enabled
config_template = ConfigTemplate(
name="TestTemplateForDetach",
data_file=data_file,
auto_sync_enabled=True
)
config_template.clean()
config_template.save()
# Verify AutoSyncRecord was created
object_type = ObjectType.objects.get_for_model(ConfigTemplate)
autosync_records = AutoSyncRecord.objects.filter(
object_type=object_type,
object_id=config_template.pk
)
self.assertEqual(autosync_records.count(), 1, "AutoSyncRecord should be created")
# Detach from DataSource
config_template.data_file = None
config_template.data_source = None
config_template.auto_sync_enabled = False
config_template.clean()
config_template.save()
# Verify AutoSyncRecord was deleted
autosync_records = AutoSyncRecord.objects.filter(
object_type=object_type,
object_id=config_template.pk
)
self.assertEqual(autosync_records.count(), 0, "AutoSyncRecord should be deleted after detaching")

View File

@@ -3,8 +3,10 @@ from contextvars import ContextVar
__all__ = (
'current_request',
'events_queue',
'query_cache',
)
current_request = ContextVar('current_request', default=None)
events_queue = ContextVar('events_queue', default=dict())
query_cache = ContextVar('query_cache', default=None)

View File

@@ -1,6 +1,7 @@
from collections import defaultdict
from contextlib import contextmanager
from netbox.context import current_request, events_queue
from netbox.context import current_request, events_queue, query_cache
from netbox.utils import register_request_processor
from extras.events import flush_events
@@ -16,6 +17,7 @@ def event_tracking(request):
"""
current_request.set(request)
events_queue.set({})
query_cache.set(defaultdict(dict))
yield
@@ -26,3 +28,4 @@ def event_tracking(request):
# Clear context vars
current_request.set(None)
events_queue.set({})
query_cache.set(None)

View File

@@ -569,7 +569,6 @@ class SyncedDataMixin(models.Model):
)
else:
AutoSyncRecord.objects.filter(
datafile=self.data_file,
object_type=object_type,
object_id=self.pk
).delete()
@@ -582,7 +581,6 @@ class SyncedDataMixin(models.Model):
# Delete AutoSyncRecord
object_type = ObjectType.objects.get_for_model(self)
AutoSyncRecord.objects.filter(
datafile=self.data_file,
object_type=object_type,
object_id=self.pk
).delete()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,40 @@
import TomSelect from 'tom-select';
import { getElements } from '../util';
/**
* Initialize clear-field dependencies.
* When a required field is cleared, dependent fields with data-requires-fields attribute will also be cleared.
*/
export function initClearField(): void {
// Find all fields with data-requires-fields attribute
for (const field of getElements<HTMLSelectElement>('[data-requires-fields]')) {
const requiredFieldsAttr = field.getAttribute('data-requires-fields');
if (!requiredFieldsAttr) continue;
// Parse the comma-separated list of required field names
const requiredFields = requiredFieldsAttr.split(',').map(name => name.trim());
// Set up listeners for each required field
for (const requiredFieldName of requiredFields) {
const requiredField = document.querySelector<HTMLSelectElement>(
`[name="${requiredFieldName}"]`,
);
if (!requiredField) continue;
// Listen for changes on the required field
requiredField.addEventListener('change', () => {
// If required field is cleared, also clear this dependent field
if (!requiredField.value || requiredField.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 = '';
}
}
});
}
}
}

View File

@@ -1,9 +1,10 @@
import { initClearField } from './clearField';
import { initFormElements } from './elements';
import { initFilterModifiers } from './filterModifiers';
import { initSpeedSelector } from './speedSelector';
export function initForms(): void {
for (const func of [initFormElements, initSpeedSelector, initFilterModifiers]) {
for (const func of [initFormElements, initSpeedSelector, initFilterModifiers, initClearField]) {
func();
}
}

View File

@@ -3,6 +3,8 @@
{% block extra_controls %}
{% include 'ipam/inc/toggle_available.html' %}
{% include 'ipam/inc/max_depth.html' %}
{% include 'ipam/inc/max_length.html' %}
{% if perms.ipam.add_prefix and first_available_prefix %}
<a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}" class="btn btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Prefix" %}

View File

@@ -0,0 +1,20 @@
{% load i18n %}
{% load helpers %}
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="max_depth" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{% trans "Max Depth" %}{% if "depth__lte" in request.GET %}: {{ request.GET.depth__lte }}{% endif %}
</button>
<ul class="dropdown-menu" aria-labelledby="max_depth">
{% if request.GET.depth__lte %}
<li>
<a class="dropdown-item" href="{{ request.path }}{% querystring request depth__lte=None page=1 %}">{% trans "Clear" %}</a>
</li>
{% endif %}
{% for i in 16|as_range %}
<li><a class="dropdown-item" href="{{ request.path }}{% querystring request depth__lte=i page=1 %}">
{{ i }} {% if request.GET.depth__lte == i %}<i class="mdi mdi-check-bold"></i>{% endif %}
</a></li>
{% endfor %}
</ul>
</div>

View File

@@ -0,0 +1,20 @@
{% load i18n %}
{% load helpers %}
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="max_length" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{% trans "Max Length" %}{% if "mask_length__lte" in request.GET %}: {{ request.GET.mask_length__lte }}{% endif %}
</button>
<ul class="dropdown-menu" aria-labelledby="max_length">
{% if request.GET.mask_length__lte %}
<li>
<a class="dropdown-item" href="{{ request.path }}{% querystring request mask_length__lte=None page=1 %}">{% trans "Clear" %}</a>
</li>
{% endif %}
{% for i in "4,8,12,16,20,24,28,32,40,48,56,64"|split %}
<li><a class="dropdown-item" href="{{ request.path }}{% querystring request mask_length__lte=i page=1 %}">
{{ i }} {% if request.GET.mask_length__lte == i %}<i class="mdi mdi-check-bold"></i>{% endif %}
</a></li>
{% endfor %}
</ul>
</div>

View File

@@ -3,6 +3,8 @@
{% block extra_controls %}
{% include 'ipam/inc/toggle_available.html' %}
{% include 'ipam/inc/max_depth.html' %}
{% include 'ipam/inc/max_length.html' %}
{% if perms.ipam.add_prefix and first_available_prefix %}
<a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}&vrf={{ object.vrf.pk }}&site={{ object.site.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Prefix" %}

View File

@@ -6,38 +6,6 @@
<button class="btn btn-outline-secondary toggle-depth" type="button">
{% trans "Hide Depth Indicators" %}
</button>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="max_depth" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{% trans "Max Depth" %}{% if "depth__lte" in request.GET %}: {{ request.GET.depth__lte }}{% endif %}
</button>
<ul class="dropdown-menu" aria-labelledby="max_depth">
{% if request.GET.depth__lte %}
<li>
<a class="dropdown-item" href="{% url 'ipam:prefix_list' %}{% querystring request depth__lte=None page=1 %}">{% trans "Clear" %}</a>
</li>
{% endif %}
{% for i in 16|as_range %}
<li><a class="dropdown-item" href="{% url 'ipam:prefix_list' %}{% querystring request depth__lte=i page=1 %}">
{{ i }} {% if request.GET.depth__lte == i %}<i class="mdi mdi-check-bold"></i>{% endif %}
</a></li>
{% endfor %}
</ul>
</div>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="max_length" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{% trans "Max Length" %}{% if "mask_length__lte" in request.GET %}: {{ request.GET.mask_length__lte }}{% endif %}
</button>
<ul class="dropdown-menu" aria-labelledby="max_length">
{% if request.GET.mask_length__lte %}
<li>
<a class="dropdown-item" href="{% url 'ipam:prefix_list' %}{% querystring request mask_length__lte=None page=1 %}">{% trans "Clear" %}</a>
</li>
{% endif %}
{% for i in "4,8,12,16,20,24,28,32,40,48,56,64"|split %}
<li><a class="dropdown-item" href="{% url 'ipam:prefix_list' %}{% querystring request mask_length__lte=i page=1 %}">
{{ i }} {% if request.GET.mask_length__lte == i %}<i class="mdi mdi-check-bold"></i>{% endif %}
</a></li>
{% endfor %}
</ul>
</div>
{% include 'ipam/inc/max_depth.html' %}
{% include 'ipam/inc/max_length.html' %}
{% endblock %}

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-20 05:07+0000\n"
"POT-Creation-Date: 2026-01-22 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:697 netbox/ipam/forms/filtersets.py:149
#: netbox/extras/forms/model_forms.py:693 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
@@ -1901,7 +1901,7 @@ msgstr ""
msgid "Device"
msgstr ""
#: netbox/core/api/views.py:51
#: netbox/core/api/views.py:50
msgid "This user does not have permission to synchronize this data source."
msgstr ""
@@ -2188,9 +2188,9 @@ msgstr ""
#: netbox/core/forms/filtersets.py:30 netbox/core/forms/model_forms.py:100
#: 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/forms/model_forms.py:600
#: netbox/extras/forms/model_forms.py:689
#: netbox/extras/forms/model_forms.py:742 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
@@ -2685,11 +2685,11 @@ msgid ""
"enqueue() cannot be called with values for both schedule_at and immediate."
msgstr ""
#: netbox/core/models/object_types.py:188
#: netbox/core/models/object_types.py:194
msgid "object type"
msgstr ""
#: netbox/core/models/object_types.py:189 netbox/extras/models/models.py:57
#: netbox/core/models/object_types.py:195 netbox/extras/models/models.py:57
msgid "object types"
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:597
#: netbox/extras/forms/model_forms.py:682
#: netbox/extras/forms/model_forms.py:734 netbox/extras/ui/panels.py:69
#: netbox/extras/forms/model_forms.py:593
#: netbox/extras/forms/model_forms.py:678
#: netbox/extras/forms/model_forms.py:730 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
@@ -4317,9 +4317,8 @@ msgstr ""
#: netbox/extras/forms/bulk_edit.py:57 netbox/extras/forms/bulk_edit.py:137
#: netbox/extras/forms/bulk_edit.py:191 netbox/extras/forms/bulk_edit.py:219
#: 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:575
#: 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/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 +4454,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:592
#: netbox/dcim/views.py:1578 netbox/extras/forms/model_forms.py:588
msgid "Schema"
msgstr ""
@@ -4464,7 +4463,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:617
#: netbox/extras/forms/filtersets.py:437 netbox/extras/forms/model_forms.py:613
#: 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
@@ -5599,8 +5598,8 @@ msgid "Connection"
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:798 netbox/extras/tables/tables.py:743
#: netbox/extras/forms/bulk_import.py:294 netbox/extras/forms/filtersets.py:616
#: netbox/extras/forms/model_forms.py:794 netbox/extras/tables/tables.py:743
#: netbox/templates/extras/journalentry.html:30
msgid "Kind"
msgstr ""
@@ -5745,7 +5744,7 @@ msgid ""
"hyphen."
msgstr ""
#: netbox/dcim/forms/model_forms.py:402 netbox/extras/forms/model_forms.py:594
#: netbox/dcim/forms/model_forms.py:402 netbox/extras/forms/model_forms.py:590
msgid "Enter a valid JSON schema to define supported attributes."
msgstr ""
@@ -7610,7 +7609,7 @@ msgid "VMs"
msgstr ""
#: netbox/dcim/tables/devices.py:103 netbox/dcim/tables/devices.py:223
#: netbox/extras/forms/model_forms.py:745
#: netbox/extras/forms/model_forms.py:741
#: netbox/templates/dcim/devicerole.html:48
#: netbox/templates/dcim/platform.html:45
#: netbox/templates/extras/configtemplate.html:10
@@ -7842,7 +7841,7 @@ msgid "Module Types"
msgstr ""
#: netbox/dcim/tables/devicetypes.py:57 netbox/extras/forms/filtersets.py:485
#: netbox/extras/forms/model_forms.py:652 netbox/extras/tables/tables.py:703
#: netbox/extras/forms/model_forms.py:648 netbox/extras/tables/tables.py:703
#: netbox/netbox/navigation/menu.py:78
msgid "Platforms"
msgstr ""
@@ -8000,7 +7999,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:632
#: netbox/extras/forms/filtersets.py:465 netbox/extras/forms/model_forms.py:628
#: 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
@@ -8083,7 +8082,7 @@ msgid "Application Services"
msgstr ""
#: netbox/dcim/views.py:2677 netbox/extras/forms/filtersets.py:427
#: netbox/extras/forms/model_forms.py:692
#: netbox/extras/forms/model_forms.py:688
#: netbox/templates/extras/configcontext.html:10
#: netbox/virtualization/forms/model_forms.py:225
#: netbox/virtualization/views.py:399
@@ -8505,7 +8504,7 @@ msgstr ""
msgid "Tenant group (slug)"
msgstr ""
#: netbox/extras/filtersets.py:779 netbox/extras/forms/model_forms.py:580
#: netbox/extras/filtersets.py:779 netbox/extras/forms/model_forms.py:576
#: netbox/templates/extras/tag.html:11
msgid "Tag"
msgstr ""
@@ -8644,7 +8643,7 @@ msgstr ""
#: netbox/extras/forms/bulk_import.py:140
#: netbox/extras/forms/bulk_import.py:201
#: netbox/extras/forms/bulk_import.py:225
#: netbox/extras/forms/bulk_import.py:279 netbox/extras/forms/filtersets.py:54
#: netbox/extras/forms/bulk_import.py:275 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:53
#: netbox/extras/forms/model_forms.py:225
@@ -8659,7 +8658,7 @@ msgstr ""
#: netbox/extras/forms/bulk_import.py:142
#: netbox/extras/forms/bulk_import.py:203
#: netbox/extras/forms/bulk_import.py:227
#: netbox/extras/forms/bulk_import.py:281
#: netbox/extras/forms/bulk_import.py:277
#: netbox/tenancy/forms/bulk_import.py:103
msgid "One or more assigned object types"
msgstr ""
@@ -8738,7 +8737,7 @@ msgstr ""
#: netbox/extras/forms/bulk_import.py:195
#: netbox/extras/forms/model_forms.py:292
#: netbox/extras/forms/model_forms.py:773
#: netbox/extras/forms/model_forms.py:769
msgid "Must specify either local content or a data file"
msgstr ""
@@ -8764,15 +8763,15 @@ msgstr ""
msgid "Script {name} not found"
msgstr ""
#: netbox/extras/forms/bulk_import.py:295
#: netbox/extras/forms/bulk_import.py:291
msgid "Assigned object type"
msgstr ""
#: netbox/extras/forms/bulk_import.py:300
#: netbox/extras/forms/bulk_import.py:296
msgid "The classification of entry"
msgstr ""
#: netbox/extras/forms/bulk_import.py:303 netbox/extras/tables/tables.py:746
#: netbox/extras/forms/bulk_import.py:299 netbox/extras/tables/tables.py:746
#: netbox/netbox/tables/tables.py:279 netbox/netbox/tables/tables.py:289
#: netbox/netbox/tables/tables.py:307 netbox/netbox/ui/panels.py:215
#: netbox/templates/dcim/htmx/cable_edit.html:98
@@ -8782,7 +8781,7 @@ msgstr ""
msgid "Comments"
msgstr ""
#: netbox/extras/forms/bulk_import.py:316
#: netbox/extras/forms/bulk_import.py:312
#: 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
@@ -8793,11 +8792,11 @@ msgstr ""
msgid "Users"
msgstr ""
#: netbox/extras/forms/bulk_import.py:320
#: netbox/extras/forms/bulk_import.py:316
msgid "User names separated by commas, encased with double quotes"
msgstr ""
#: netbox/extras/forms/bulk_import.py:323
#: netbox/extras/forms/bulk_import.py:319
#: 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
@@ -8812,7 +8811,7 @@ msgstr ""
msgid "Groups"
msgstr ""
#: netbox/extras/forms/bulk_import.py:327
#: netbox/extras/forms/bulk_import.py:323
msgid "Group names separated by commas, encased with double quotes"
msgstr ""
@@ -8836,14 +8835,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:687 netbox/templates/core/job.html:69
#: netbox/extras/forms/model_forms.py:683 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:270
#: netbox/extras/forms/model_forms.py:748
#: netbox/extras/forms/model_forms.py:744
msgid "Rendering"
msgstr ""
@@ -8871,37 +8870,37 @@ msgstr ""
msgid "Allowed object type"
msgstr ""
#: netbox/extras/forms/filtersets.py:455 netbox/extras/forms/model_forms.py:622
#: netbox/extras/forms/filtersets.py:455 netbox/extras/forms/model_forms.py:618
#: netbox/netbox/navigation/menu.py:17
msgid "Regions"
msgstr ""
#: netbox/extras/forms/filtersets.py:460 netbox/extras/forms/model_forms.py:627
#: netbox/extras/forms/filtersets.py:460 netbox/extras/forms/model_forms.py:623
msgid "Site groups"
msgstr ""
#: netbox/extras/forms/filtersets.py:470 netbox/extras/forms/model_forms.py:637
#: netbox/extras/forms/filtersets.py:470 netbox/extras/forms/model_forms.py:633
#: netbox/netbox/navigation/menu.py:20
msgid "Locations"
msgstr ""
#: netbox/extras/forms/filtersets.py:475 netbox/extras/forms/model_forms.py:642
#: netbox/extras/forms/filtersets.py:475 netbox/extras/forms/model_forms.py:638
msgid "Device types"
msgstr ""
#: netbox/extras/forms/filtersets.py:480 netbox/extras/forms/model_forms.py:647
#: netbox/extras/forms/filtersets.py:480 netbox/extras/forms/model_forms.py:643
msgid "Roles"
msgstr ""
#: netbox/extras/forms/filtersets.py:490 netbox/extras/forms/model_forms.py:657
#: netbox/extras/forms/filtersets.py:490 netbox/extras/forms/model_forms.py:653
msgid "Cluster types"
msgstr ""
#: netbox/extras/forms/filtersets.py:495 netbox/extras/forms/model_forms.py:662
#: netbox/extras/forms/filtersets.py:495 netbox/extras/forms/model_forms.py:658
msgid "Cluster groups"
msgstr ""
#: netbox/extras/forms/filtersets.py:500 netbox/extras/forms/model_forms.py:667
#: netbox/extras/forms/filtersets.py:500 netbox/extras/forms/model_forms.py:663
#: netbox/netbox/navigation/menu.py:264 netbox/netbox/navigation/menu.py:266
#: netbox/templates/virtualization/clustertype.html:30
#: netbox/virtualization/tables/clusters.py:23
@@ -8909,7 +8908,7 @@ msgstr ""
msgid "Clusters"
msgstr ""
#: netbox/extras/forms/filtersets.py:505 netbox/extras/forms/model_forms.py:672
#: netbox/extras/forms/filtersets.py:505 netbox/extras/forms/model_forms.py:668
msgid "Tenant groups"
msgstr ""
@@ -8984,7 +8983,7 @@ msgid ""
msgstr ""
#: netbox/extras/forms/model_forms.py:261
#: netbox/extras/forms/model_forms.py:739
#: netbox/extras/forms/model_forms.py:735
msgid "Template code"
msgstr ""
@@ -8994,7 +8993,7 @@ msgid "Export Template"
msgstr ""
#: netbox/extras/forms/model_forms.py:285
#: netbox/extras/forms/model_forms.py:766
#: netbox/extras/forms/model_forms.py:762
msgid "Template content is populated from the remote source selected below."
msgstr ""
@@ -9064,21 +9063,21 @@ msgstr ""
msgid "Notification group"
msgstr ""
#: netbox/extras/forms/model_forms.py:603
#: netbox/extras/forms/model_forms.py:599
#: netbox/templates/extras/configcontextprofile.html:10
msgid "Config Context Profile"
msgstr ""
#: netbox/extras/forms/model_forms.py:677 netbox/netbox/navigation/menu.py:26
#: netbox/extras/forms/model_forms.py:673 netbox/netbox/navigation/menu.py:26
#: netbox/tenancy/tables/tenants.py:18
msgid "Tenants"
msgstr ""
#: netbox/extras/forms/model_forms.py:721
#: netbox/extras/forms/model_forms.py:717
msgid "Data is populated from the remote source selected below."
msgstr ""
#: netbox/extras/forms/model_forms.py:727
#: netbox/extras/forms/model_forms.py:723
msgid "Must specify either local data or a data file"
msgstr ""
@@ -12038,7 +12037,7 @@ msgstr ""
msgid "date synced"
msgstr ""
#: netbox/netbox/models/features.py:623
#: netbox/netbox/models/features.py:621
#, python-brace-format
msgid "{class_name} must implement a sync_data() method."
msgstr ""
@@ -13936,8 +13935,8 @@ msgid "No VLANs Assigned"
msgstr ""
#: netbox/templates/dcim/inc/interface_vlans_table.html:44
#: netbox/templates/ipam/prefix_list.html:16
#: netbox/templates/ipam/prefix_list.html:33
#: netbox/templates/ipam/inc/max_depth.html:11
#: netbox/templates/ipam/inc/max_length.html:11
msgid "Clear"
msgstr ""
@@ -15054,8 +15053,8 @@ msgstr ""
msgid "Date Added"
msgstr ""
#: netbox/templates/ipam/aggregate/prefixes.html:8
#: netbox/templates/ipam/prefix/prefixes.html:8
#: netbox/templates/ipam/aggregate/prefixes.html:10
#: netbox/templates/ipam/prefix/prefixes.html:10
#: netbox/templates/ipam/role.html:10
msgid "Add Prefix"
msgstr ""
@@ -15084,6 +15083,14 @@ msgstr ""
msgid "Bulk Create"
msgstr ""
#: netbox/templates/ipam/inc/max_depth.html:6
msgid "Max Depth"
msgstr ""
#: netbox/templates/ipam/inc/max_length.html:6
msgid "Max Length"
msgstr ""
#: netbox/templates/ipam/inc/panels/fhrp_groups.html:10
msgid "Create Group"
msgstr ""
@@ -15185,14 +15192,6 @@ msgstr ""
msgid "Hide Depth Indicators"
msgstr ""
#: netbox/templates/ipam/prefix_list.html:11
msgid "Max Depth"
msgstr ""
#: netbox/templates/ipam/prefix_list.html:28
msgid "Max Length"
msgstr ""
#: netbox/templates/ipam/rir.html:10
msgid "Add Aggregate"
msgstr ""
@@ -16587,7 +16586,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:148
msgid "(automatically set)"
msgstr ""
@@ -16727,17 +16726,17 @@ msgstr ""
msgid "{value} is not a valid regular expression."
msgstr ""
#: netbox/utilities/views.py:76
#: netbox/utilities/views.py:80
#, python-brace-format
msgid "{self.__class__.__name__} must implement get_required_permission()"
msgstr ""
#: netbox/utilities/views.py:112
#: netbox/utilities/views.py:116
#, python-brace-format
msgid "{class_name} must implement get_required_permission()"
msgstr ""
#: netbox/utilities/views.py:136
#: netbox/utilities/views.py:140
#, python-brace-format
msgid ""
"{class_name} has no queryset defined. ObjectPermissionRequiredMixin may only "

View File

@@ -5,6 +5,7 @@ from ..utils import add_blank_choice
__all__ = (
'BulkEditNullBooleanSelect',
'ClearableSelect',
'ColorSelect',
'HTMXSelect',
'SelectWithPK',
@@ -28,6 +29,21 @@ class BulkEditNullBooleanSelect(forms.NullBooleanSelect):
)
class ClearableSelect(forms.Select):
"""
A Select widget that will be automatically cleared when one or more required fields are cleared.
Args:
requires_fields: A list of field names that this field depends on. When any of these fields
are cleared, this field will also be cleared automatically via JavaScript.
"""
def __init__(self, *args, requires_fields=None, **kwargs):
super().__init__(*args, **kwargs)
if requires_fields:
self.attrs['data-requires-fields'] = ','.join(requires_fields)
class ColorSelect(forms.Select):
"""
Extends the built-in Select widget to colorize each <option>.

View File

@@ -252,3 +252,16 @@ def isodatetime(value, spec='seconds'):
else:
return ''
return mark_safe(f'<span title="{naturaltime(value)}">{text}</span>')
@register.filter
def truncate_middle(value, length):
if len(value) <= length:
return value
# Calculate split points for the two parts
half_len = (length - 1) // 2 # 1 for the ellipsis
first_part = value[:half_len]
second_part = value[len(value) - (length - 1 - half_len):]
return mark_safe(f"{first_part}&hellip;{second_part}")