Compare commits

..

28 Commits

Author SHA1 Message Date
Arthur
de19447317 Merge branch 'main' into 20911-dropdown
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
2026-01-23 15:59:08 -08:00
Arthur
f195af206b fix csv import 2026-01-23 15:46:26 -08:00
Arthur Hanson
a9a300197a Clear Rack Face when clear Rack (#21182)
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
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
* #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)
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
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2026-01-22 09:40:48 -08:00
github-actions
077d9b1129 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-22 05:07:49 +00:00
Arthur
b0ac55ed6a cleanup
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
2026-01-21 16:44:48 -08:00
Arthur
91ab818411 use bulk_update and rebuild 2026-01-21 16:23:24 -08:00
Arthur
62b9367ae3 use bulk_update and rebuild 2026-01-21 16:14:14 -08:00
Aditya Sharma
e81ccb9be6 Fixes #21214: Clean up AutoSyncRecord when detaching from DataSource (#21219)
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
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
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
Arthur
0c091aa80e cleanup 2026-01-21 13:18:34 -08:00
Arthur
94836e5a37 fix migration 2026-01-21 12:55:34 -08:00
Arthur
c92912ff03 fix migration 2026-01-21 12:52:41 -08:00
Arthur
ef0bc18095 fix migration 2026-01-21 12:47:16 -08:00
Arthur
99f727e685 fix migration 2026-01-21 12:41:59 -08:00
Arthur
6a5aced4bc fix migration 2026-01-21 12:28:01 -08:00
Arthur
46f9a12a87 add migration 2026-01-21 11:59:03 -08:00
Matthew Papaleo
339ad455e4 Support for max_length and max_depth standardised for prefix_list, aggreate/prefixes and prefix/prefixes
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
2026-01-21 10:02:06 -05:00
github-actions
f24376cfab 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-21 05:07:22 +00:00
Arthur
be1a008216 rebuild tree after rename
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
2026-01-20 15:28:49 -08:00
Arthur
c4c3518bb4 change ordering field, remove front-end changes 2026-01-20 13:45:17 -08:00
Arthur
5a1282e326 Merge branch 'main' into 20911-dropdown
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
2026-01-14 13:39:45 -08:00
Arthur
cb13eb277f use correct node version 2026-01-14 13:36:33 -08:00
Arthur
24642be351 cleanup 2026-01-08 17:08:10 -08:00
Arthur
89af9efd85 cleanup 2026-01-08 17:04:00 -08:00
Arthur
99d678502f cleanup 2026-01-08 16:23:47 -08:00
Arthur
e6300ee06d Fix TomSelect dropdown ordering 2026-01-08 16:17:40 -08:00
27 changed files with 392 additions and 145 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

@@ -20,7 +20,9 @@ from utilities.forms.fields import (
DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField,
) )
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups 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 utilities.jsonschema import JSONSchemaProperty
from virtualization.models import Cluster, VMInterface from virtualization.models import Cluster, VMInterface
from wireless.models import WirelessLAN, WirelessLANGroup 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( device_type = DynamicModelChoiceField(
label=_('Device type'), label=_('Device type'),
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),

View File

@@ -0,0 +1,32 @@
from django.db import migrations
import mptt.managers
import mptt.models
def rebuild_mptt(apps, schema_editor):
"""
Rebuild the MPTT tree for ModuleBay to apply new ordering.
"""
ModuleBay = apps.get_model('dcim', 'ModuleBay')
# Set MPTTMeta with the correct order_insertion_by
class MPTTMeta:
order_insertion_by = ('module', 'name',)
ModuleBay.MPTTMeta = MPTTMeta
ModuleBay._mptt_meta = mptt.models.MPTTOptions(MPTTMeta)
manager = mptt.managers.TreeManager()
manager.model = ModuleBay
manager.contribute_to_class(ModuleBay, 'objects')
manager.rebuild()
class Migration(migrations.Migration):
dependencies = [
('dcim', '0225_gfk_indexes'),
]
operations = [
migrations.RunPython(code=rebuild_mptt, reverse_code=migrations.RunPython.noop),
]

View File

@@ -1273,7 +1273,7 @@ class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel):
verbose_name_plural = _('module bays') verbose_name_plural = _('module bays')
class MPTTMeta: class MPTTMeta:
order_insertion_by = ('module',) order_insertion_by = ('module', 'name',)
def clean(self): def clean(self):
super().clean() super().clean()

View File

@@ -5,6 +5,7 @@ from django.db import models
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from jsonschema.exceptions import ValidationError as JSONValidationError from jsonschema.exceptions import ValidationError as JSONValidationError
from mptt.models import MPTTModel
from dcim.choices import * from dcim.choices import *
from dcim.utils import update_interface_bridges from dcim.utils import update_interface_bridges
@@ -329,7 +330,7 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
component._location = self.device.location component._location = self.device.location
component._rack = self.device.rack component._rack = self.device.rack
if component_model is not ModuleBay: if not issubclass(component_model, MPTTModel):
component_model.objects.bulk_create(create_instances) component_model.objects.bulk_create(create_instances)
# Emit the post_save signal for each newly created object # Emit the post_save signal for each newly created object
for component in create_instances: for component in create_instances:
@@ -342,11 +343,12 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
update_fields=None update_fields=None
) )
else: else:
# ModuleBays must be saved individually for MPTT # MPTT models must be saved individually to maintain tree structure
for instance in create_instances: for instance in create_instances:
instance.save() instance.save()
update_fields = ['module'] update_fields = ['module']
component_model.objects.bulk_update(update_instances, update_fields) component_model.objects.bulk_update(update_instances, update_fields)
# Emit the post_save signal for each updated object # Emit the post_save signal for each updated object
for component in update_instances: for component in update_instances:
@@ -359,5 +361,9 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
update_fields=update_fields update_fields=update_fields
) )
# Rebuild MPTT tree if needed (bulk_update bypasses model save)
if issubclass(component_model, MPTTModel) and update_instances:
component_model.objects.rebuild()
# Interface bridges have to be set after interface instantiation # Interface bridges have to be set after interface instantiation
update_interface_bridges(self.device, self.module_type.interfacetemplates, self) update_interface_bridges(self.device, self.module_type.interfacetemplates, self)

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): 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: 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 # Resolve the script from action parameters
script = event_rule.action_object.python_class() 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 # Enqueue a Job to record the script's execution
from extras.jobs import ScriptJob from extras.jobs import ScriptJob
params = { params = {

View File

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

View File

@@ -6,7 +6,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile
from django.forms import ValidationError from django.forms import ValidationError
from django.test import tag, TestCase 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 dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup
from extras.models import ConfigContext, ConfigContextProfile, ConfigTemplate, ImageAttachment, Tag, TaggedItem from extras.models import ConfigContext, ConfigContextProfile, ConfigTemplate, ImageAttachment, Tag, TaggedItem
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
@@ -754,3 +754,53 @@ class ConfigTemplateTest(TestCase):
@tag('regression') @tag('regression')
def test_config_template_with_data_source_nested_templates(self): def test_config_template_with_data_source_nested_templates(self):
self.assertEqual(self.BASE_TEMPLATE, self.main_config_template.render({})) 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

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

View File

@@ -438,30 +438,12 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
""" """
return object_form.save() return object_form.save()
def create_and_update_objects(self, form, request): def _process_import_records(self, form, request, records, prefetched_objects):
"""
Process CSV import records and save objects.
"""
saved_objects = [] saved_objects = []
records = list(form.cleaned_data['data'])
# Prefetch objects to be updated, if any
prefetch_ids = [int(record['id']) for record in records if record.get('id')]
# check for duplicate IDs
duplicate_pks = [pk for pk, count in Counter(prefetch_ids).items() if count > 1]
if duplicate_pks:
error_msg = _(
"Duplicate objects found: {model} with ID(s) {ids} appears multiple times"
).format(
model=title(self.queryset.model._meta.verbose_name),
ids=', '.join(str(pk) for pk in sorted(duplicate_pks))
)
raise ValidationError(error_msg)
prefetched_objects = {
obj.pk: obj
for obj in self.queryset.model.objects.filter(id__in=prefetch_ids)
} if prefetch_ids else {}
for i, record in enumerate(records, start=1): for i, record in enumerate(records, start=1):
object_id = int(record.pop('id')) if record.get('id') else None object_id = int(record.pop('id')) if record.get('id') else None
@@ -526,6 +508,38 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
return saved_objects return saved_objects
def create_and_update_objects(self, form, request):
records = list(form.cleaned_data['data'])
# Prefetch objects to be updated, if any
prefetch_ids = [int(record['id']) for record in records if record.get('id')]
# check for duplicate IDs
duplicate_pks = [pk for pk, count in Counter(prefetch_ids).items() if count > 1]
if duplicate_pks:
error_msg = _(
"Duplicate objects found: {model} with ID(s) {ids} appears multiple times"
).format(
model=title(self.queryset.model._meta.verbose_name),
ids=', '.join(str(pk) for pk in sorted(duplicate_pks))
)
raise ValidationError(error_msg)
prefetched_objects = {
obj.pk: obj
for obj in self.queryset.model.objects.filter(id__in=prefetch_ids)
} if prefetch_ids else {}
# For MPTT models, delay tree updates until all saves are complete
if issubclass(self.queryset.model, MPTTModel):
with self.queryset.model.objects.delay_mptt_updates():
saved_objects = self._process_import_records(form, request, records, prefetched_objects)
self.queryset.model.objects.rebuild()
else:
saved_objects = self._process_import_records(form, request, records, prefetched_objects)
return saved_objects
# #
# Request handlers # Request handlers
# #
@@ -895,9 +909,18 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
renamed_pks = self._rename_objects(form, selected_objects) renamed_pks = self._rename_objects(form, selected_objects)
if '_apply' in request.POST: if '_apply' in request.POST:
for obj in selected_objects: # For MPTT models, delay tree updates until all saves are complete
setattr(obj, self.field_name, obj.new_name) if issubclass(self.queryset.model, MPTTModel):
obj.save() with self.queryset.model.objects.delay_mptt_updates():
for obj in selected_objects:
setattr(obj, self.field_name, obj.new_name)
obj.save()
self.queryset.model.objects.rebuild()
else:
for obj in selected_objects:
setattr(obj, self.field_name, obj.new_name)
obj.save()
# Enforce constrained permissions # Enforce constrained permissions
if self.queryset.filter(pk__in=renamed_pks).count() != len(selected_objects): if self.queryset.filter(pk__in=renamed_pks).count() != len(selected_objects):

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 { initFormElements } from './elements';
import { initFilterModifiers } from './filterModifiers'; import { initFilterModifiers } from './filterModifiers';
import { initSpeedSelector } from './speedSelector'; import { initSpeedSelector } from './speedSelector';
export function initForms(): void { export function initForms(): void {
for (const func of [initFormElements, initSpeedSelector, initFilterModifiers]) { for (const func of [initFormElements, initSpeedSelector, initFilterModifiers, initClearField]) {
func(); func();
} }
} }

View File

@@ -3,6 +3,8 @@
{% block extra_controls %} {% block extra_controls %}
{% include 'ipam/inc/toggle_available.html' %} {% 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 %} {% if perms.ipam.add_prefix and first_available_prefix %}
<a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}" class="btn btn-primary"> <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" %} <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 %} {% block extra_controls %}
{% include 'ipam/inc/toggle_available.html' %} {% 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 %} {% 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"> <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" %} <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"> <button class="btn btn-outline-secondary toggle-depth" type="button">
{% trans "Hide Depth Indicators" %} {% trans "Hide Depth Indicators" %}
</button> </button>
<div class="dropdown"> {% include 'ipam/inc/max_depth.html' %}
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="max_depth" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> {% include 'ipam/inc/max_length.html' %}
{% 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>
{% endblock %} {% endblock %}

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -1279,7 +1279,7 @@ msgid "Term Side"
msgstr "" msgstr ""
#: netbox/circuits/forms/filtersets.py:287 netbox/dcim/forms/bulk_edit.py:1537 #: 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/ipam/forms/filtersets.py:627 netbox/ipam/forms/model_forms.py:326
#: netbox/templates/dcim/macaddress.html:25 #: netbox/templates/dcim/macaddress.html:25
#: netbox/templates/extras/configcontext.html:36 #: netbox/templates/extras/configcontext.html:36
@@ -1901,7 +1901,7 @@ msgstr ""
msgid "Device" msgid "Device"
msgstr "" 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." msgid "This user does not have permission to synchronize this data source."
msgstr "" msgstr ""
@@ -2188,9 +2188,9 @@ msgstr ""
#: netbox/core/forms/filtersets.py:30 netbox/core/forms/model_forms.py:100 #: 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:268
#: netbox/extras/forms/model_forms.py:604 #: netbox/extras/forms/model_forms.py:600
#: netbox/extras/forms/model_forms.py:693 #: netbox/extras/forms/model_forms.py:689
#: netbox/extras/forms/model_forms.py:746 netbox/extras/tables/tables.py:218 #: 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:588 netbox/extras/tables/tables.py:618
#: netbox/extras/tables/tables.py:660 netbox/templates/core/datasource.html:31 #: netbox/extras/tables/tables.py:660 netbox/templates/core/datasource.html:31
#: netbox/templates/core/inc/datafile_panel.html:7 #: 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." "enqueue() cannot be called with values for both schedule_at and immediate."
msgstr "" msgstr ""
#: netbox/core/models/object_types.py:188 #: netbox/core/models/object_types.py:194
msgid "object type" msgid "object type"
msgstr "" 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" msgid "object types"
msgstr "" msgstr ""
@@ -4176,9 +4176,9 @@ msgid "Power panel (ID)"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_create.py:40 netbox/extras/forms/filtersets.py:515 #: 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:593
#: netbox/extras/forms/model_forms.py:682 #: netbox/extras/forms/model_forms.py:678
#: netbox/extras/forms/model_forms.py:734 netbox/extras/ui/panels.py:69 #: 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/forms/bulk_import.py:26 netbox/netbox/forms/mixins.py:113
#: netbox/netbox/tables/columns.py:490 #: netbox/netbox/tables/columns.py:490
#: netbox/templates/circuits/inc/circuit_termination.html:29 #: 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: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: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_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:71 netbox/extras/forms/filtersets.py:175
#: netbox/extras/forms/filtersets.py:175 netbox/extras/forms/filtersets.py:279 #: netbox/extras/forms/filtersets.py:279 netbox/extras/forms/filtersets.py:315
#: 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/ipam/forms/bulk_edit.py:159 netbox/templates/dcim/moduletype.html:51
#: netbox/templates/extras/configcontext.html:17 #: netbox/templates/extras/configcontext.html:17
#: netbox/templates/extras/customlink.html:25 #: netbox/templates/extras/customlink.html:25
@@ -4455,7 +4454,7 @@ msgid "Device Type"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_edit.py:540 netbox/dcim/forms/model_forms.py:400 #: 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" msgid "Schema"
msgstr "" msgstr ""
@@ -4464,7 +4463,7 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:1452 netbox/dcim/forms/filtersets.py:679 #: 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/filtersets.py:1197 netbox/dcim/forms/model_forms.py:406
#: netbox/dcim/forms/model_forms.py:419 netbox/dcim/tables/modules.py:42 #: 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/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/dcim/cable.html:23 netbox/templates/dcim/moduletype.html:27
#: netbox/templates/extras/configcontext.html:21 #: netbox/templates/extras/configcontext.html:21
@@ -5599,8 +5598,8 @@ msgid "Connection"
msgstr "" msgstr ""
#: netbox/dcim/forms/filtersets.py:1572 netbox/extras/forms/bulk_edit.py:421 #: 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/bulk_import.py:294 netbox/extras/forms/filtersets.py:616
#: netbox/extras/forms/model_forms.py:798 netbox/extras/tables/tables.py:743 #: netbox/extras/forms/model_forms.py:794 netbox/extras/tables/tables.py:743
#: netbox/templates/extras/journalentry.html:30 #: netbox/templates/extras/journalentry.html:30
msgid "Kind" msgid "Kind"
msgstr "" msgstr ""
@@ -5745,7 +5744,7 @@ msgid ""
"hyphen." "hyphen."
msgstr "" 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." msgid "Enter a valid JSON schema to define supported attributes."
msgstr "" msgstr ""
@@ -7610,7 +7609,7 @@ msgid "VMs"
msgstr "" msgstr ""
#: netbox/dcim/tables/devices.py:103 netbox/dcim/tables/devices.py:223 #: 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/devicerole.html:48
#: netbox/templates/dcim/platform.html:45 #: netbox/templates/dcim/platform.html:45
#: netbox/templates/extras/configtemplate.html:10 #: netbox/templates/extras/configtemplate.html:10
@@ -7842,7 +7841,7 @@ msgid "Module Types"
msgstr "" msgstr ""
#: netbox/dcim/tables/devicetypes.py:57 netbox/extras/forms/filtersets.py:485 #: 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 #: netbox/netbox/navigation/menu.py:78
msgid "Platforms" msgid "Platforms"
msgstr "" msgstr ""
@@ -8000,7 +7999,7 @@ msgid "Space"
msgstr "" msgstr ""
#: netbox/dcim/tables/sites.py:21 netbox/dcim/tables/sites.py:40 #: 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/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/ipam/tables/asn.py:76 netbox/netbox/navigation/menu.py:15
#: netbox/netbox/navigation/menu.py:19 #: netbox/netbox/navigation/menu.py:19
@@ -8083,7 +8082,7 @@ msgid "Application Services"
msgstr "" msgstr ""
#: netbox/dcim/views.py:2677 netbox/extras/forms/filtersets.py:427 #: 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/templates/extras/configcontext.html:10
#: netbox/virtualization/forms/model_forms.py:225 #: netbox/virtualization/forms/model_forms.py:225
#: netbox/virtualization/views.py:399 #: netbox/virtualization/views.py:399
@@ -8505,7 +8504,7 @@ msgstr ""
msgid "Tenant group (slug)" msgid "Tenant group (slug)"
msgstr "" 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 #: netbox/templates/extras/tag.html:11
msgid "Tag" msgid "Tag"
msgstr "" msgstr ""
@@ -8644,7 +8643,7 @@ msgstr ""
#: netbox/extras/forms/bulk_import.py:140 #: netbox/extras/forms/bulk_import.py:140
#: netbox/extras/forms/bulk_import.py:201 #: netbox/extras/forms/bulk_import.py:201
#: netbox/extras/forms/bulk_import.py:225 #: 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:156 netbox/extras/forms/filtersets.py:260
#: netbox/extras/forms/filtersets.py:296 netbox/extras/forms/model_forms.py:53 #: 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:225
@@ -8659,7 +8658,7 @@ msgstr ""
#: netbox/extras/forms/bulk_import.py:142 #: netbox/extras/forms/bulk_import.py:142
#: netbox/extras/forms/bulk_import.py:203 #: netbox/extras/forms/bulk_import.py:203
#: netbox/extras/forms/bulk_import.py:227 #: 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 #: netbox/tenancy/forms/bulk_import.py:103
msgid "One or more assigned object types" msgid "One or more assigned object types"
msgstr "" msgstr ""
@@ -8738,7 +8737,7 @@ msgstr ""
#: netbox/extras/forms/bulk_import.py:195 #: netbox/extras/forms/bulk_import.py:195
#: netbox/extras/forms/model_forms.py:292 #: 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" msgid "Must specify either local content or a data file"
msgstr "" msgstr ""
@@ -8764,15 +8763,15 @@ msgstr ""
msgid "Script {name} not found" msgid "Script {name} not found"
msgstr "" msgstr ""
#: netbox/extras/forms/bulk_import.py:295 #: netbox/extras/forms/bulk_import.py:291
msgid "Assigned object type" msgid "Assigned object type"
msgstr "" msgstr ""
#: netbox/extras/forms/bulk_import.py:300 #: netbox/extras/forms/bulk_import.py:296
msgid "The classification of entry" msgid "The classification of entry"
msgstr "" 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:279 netbox/netbox/tables/tables.py:289
#: netbox/netbox/tables/tables.py:307 netbox/netbox/ui/panels.py:215 #: netbox/netbox/tables/tables.py:307 netbox/netbox/ui/panels.py:215
#: netbox/templates/dcim/htmx/cable_edit.html:98 #: netbox/templates/dcim/htmx/cable_edit.html:98
@@ -8782,7 +8781,7 @@ msgstr ""
msgid "Comments" msgid "Comments"
msgstr "" 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/extras/forms/model_forms.py:401 netbox/netbox/navigation/menu.py:414
#: netbox/templates/extras/notificationgroup.html:41 #: netbox/templates/extras/notificationgroup.html:41
#: netbox/templates/users/group.html:29 netbox/templates/users/owner.html:46 #: netbox/templates/users/group.html:29 netbox/templates/users/owner.html:46
@@ -8793,11 +8792,11 @@ msgstr ""
msgid "Users" msgid "Users"
msgstr "" 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" msgid "User names separated by commas, encased with double quotes"
msgstr "" 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/extras/forms/model_forms.py:396 netbox/netbox/navigation/menu.py:295
#: netbox/netbox/navigation/menu.py:434 #: netbox/netbox/navigation/menu.py:434
#: netbox/templates/extras/notificationgroup.html:31 #: netbox/templates/extras/notificationgroup.html:31
@@ -8812,7 +8811,7 @@ msgstr ""
msgid "Groups" msgid "Groups"
msgstr "" 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" msgid "Group names separated by commas, encased with double quotes"
msgstr "" msgstr ""
@@ -8836,14 +8835,14 @@ msgstr ""
#: netbox/extras/forms/filtersets.py:189 netbox/extras/forms/filtersets.py:406 #: 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/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 #: netbox/templates/extras/eventrule.html:84
msgid "Data" msgid "Data"
msgstr "" msgstr ""
#: netbox/extras/forms/filtersets.py:190 netbox/extras/forms/filtersets.py:529 #: 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:270
#: netbox/extras/forms/model_forms.py:748 #: netbox/extras/forms/model_forms.py:744
msgid "Rendering" msgid "Rendering"
msgstr "" msgstr ""
@@ -8871,37 +8870,37 @@ msgstr ""
msgid "Allowed object type" msgid "Allowed object type"
msgstr "" 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 #: netbox/netbox/navigation/menu.py:17
msgid "Regions" msgid "Regions"
msgstr "" 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" msgid "Site groups"
msgstr "" 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 #: netbox/netbox/navigation/menu.py:20
msgid "Locations" msgid "Locations"
msgstr "" 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" msgid "Device types"
msgstr "" 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" msgid "Roles"
msgstr "" 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" msgid "Cluster types"
msgstr "" 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" msgid "Cluster groups"
msgstr "" 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/netbox/navigation/menu.py:264 netbox/netbox/navigation/menu.py:266
#: netbox/templates/virtualization/clustertype.html:30 #: netbox/templates/virtualization/clustertype.html:30
#: netbox/virtualization/tables/clusters.py:23 #: netbox/virtualization/tables/clusters.py:23
@@ -8909,7 +8908,7 @@ msgstr ""
msgid "Clusters" msgid "Clusters"
msgstr "" 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" msgid "Tenant groups"
msgstr "" msgstr ""
@@ -8984,7 +8983,7 @@ msgid ""
msgstr "" msgstr ""
#: netbox/extras/forms/model_forms.py:261 #: netbox/extras/forms/model_forms.py:261
#: netbox/extras/forms/model_forms.py:739 #: netbox/extras/forms/model_forms.py:735
msgid "Template code" msgid "Template code"
msgstr "" msgstr ""
@@ -8994,7 +8993,7 @@ msgid "Export Template"
msgstr "" msgstr ""
#: netbox/extras/forms/model_forms.py:285 #: 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." msgid "Template content is populated from the remote source selected below."
msgstr "" msgstr ""
@@ -9064,21 +9063,21 @@ msgstr ""
msgid "Notification group" msgid "Notification group"
msgstr "" msgstr ""
#: netbox/extras/forms/model_forms.py:603 #: netbox/extras/forms/model_forms.py:599
#: netbox/templates/extras/configcontextprofile.html:10 #: netbox/templates/extras/configcontextprofile.html:10
msgid "Config Context Profile" msgid "Config Context Profile"
msgstr "" 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 #: netbox/tenancy/tables/tenants.py:18
msgid "Tenants" msgid "Tenants"
msgstr "" 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." msgid "Data is populated from the remote source selected below."
msgstr "" 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" msgid "Must specify either local data or a data file"
msgstr "" msgstr ""
@@ -12038,7 +12037,7 @@ msgstr ""
msgid "date synced" msgid "date synced"
msgstr "" msgstr ""
#: netbox/netbox/models/features.py:623 #: netbox/netbox/models/features.py:621
#, python-brace-format #, python-brace-format
msgid "{class_name} must implement a sync_data() method." msgid "{class_name} must implement a sync_data() method."
msgstr "" msgstr ""
@@ -13936,8 +13935,8 @@ msgid "No VLANs Assigned"
msgstr "" msgstr ""
#: netbox/templates/dcim/inc/interface_vlans_table.html:44 #: netbox/templates/dcim/inc/interface_vlans_table.html:44
#: netbox/templates/ipam/prefix_list.html:16 #: netbox/templates/ipam/inc/max_depth.html:11
#: netbox/templates/ipam/prefix_list.html:33 #: netbox/templates/ipam/inc/max_length.html:11
msgid "Clear" msgid "Clear"
msgstr "" msgstr ""
@@ -15054,8 +15053,8 @@ msgstr ""
msgid "Date Added" msgid "Date Added"
msgstr "" msgstr ""
#: netbox/templates/ipam/aggregate/prefixes.html:8 #: netbox/templates/ipam/aggregate/prefixes.html:10
#: netbox/templates/ipam/prefix/prefixes.html:8 #: netbox/templates/ipam/prefix/prefixes.html:10
#: netbox/templates/ipam/role.html:10 #: netbox/templates/ipam/role.html:10
msgid "Add Prefix" msgid "Add Prefix"
msgstr "" msgstr ""
@@ -15084,6 +15083,14 @@ msgstr ""
msgid "Bulk Create" msgid "Bulk Create"
msgstr "" 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 #: netbox/templates/ipam/inc/panels/fhrp_groups.html:10
msgid "Create Group" msgid "Create Group"
msgstr "" msgstr ""
@@ -15185,14 +15192,6 @@ msgstr ""
msgid "Hide Depth Indicators" msgid "Hide Depth Indicators"
msgstr "" 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 #: netbox/templates/ipam/rir.html:10
msgid "Add Aggregate" msgid "Add Aggregate"
msgstr "" msgstr ""
@@ -16587,7 +16586,7 @@ msgstr ""
msgid "Missing required value for static query param: '{static_params}'" msgid "Missing required value for static query param: '{static_params}'"
msgstr "" msgstr ""
#: netbox/utilities/forms/widgets/modifiers.py:141 #: netbox/utilities/forms/widgets/modifiers.py:148
msgid "(automatically set)" msgid "(automatically set)"
msgstr "" msgstr ""
@@ -16727,17 +16726,17 @@ msgstr ""
msgid "{value} is not a valid regular expression." msgid "{value} is not a valid regular expression."
msgstr "" msgstr ""
#: netbox/utilities/views.py:76 #: netbox/utilities/views.py:80
#, python-brace-format #, python-brace-format
msgid "{self.__class__.__name__} must implement get_required_permission()" msgid "{self.__class__.__name__} must implement get_required_permission()"
msgstr "" msgstr ""
#: netbox/utilities/views.py:112 #: netbox/utilities/views.py:116
#, python-brace-format #, python-brace-format
msgid "{class_name} must implement get_required_permission()" msgid "{class_name} must implement get_required_permission()"
msgstr "" msgstr ""
#: netbox/utilities/views.py:136 #: netbox/utilities/views.py:140
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"{class_name} has no queryset defined. ObjectPermissionRequiredMixin may only " "{class_name} has no queryset defined. ObjectPermissionRequiredMixin may only "

View File

@@ -5,6 +5,7 @@ from ..utils import add_blank_choice
__all__ = ( __all__ = (
'BulkEditNullBooleanSelect', 'BulkEditNullBooleanSelect',
'ClearableSelect',
'ColorSelect', 'ColorSelect',
'HTMXSelect', 'HTMXSelect',
'SelectWithPK', '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): class ColorSelect(forms.Select):
""" """
Extends the built-in Select widget to colorize each <option>. Extends the built-in Select widget to colorize each <option>.

View File

@@ -252,3 +252,16 @@ def isodatetime(value, spec='seconds'):
else: else:
return '' return ''
return mark_safe(f'<span title="{naturaltime(value)}">{text}</span>') 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}")