Compare commits

...

10 Commits

Author SHA1 Message Date
Jeremy Stretch
e69fc9a4b4 Pass user object to EventContext 2026-01-23 15:22:52 -05:00
Jeremy Stretch
a6c6a58fb9 Initial work on #21260 2026-01-23 14:55:58 -05: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
* #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
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
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
24 changed files with 359 additions and 169 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

@@ -1,5 +1,5 @@
import logging import logging
from collections import defaultdict from collections import UserDict, defaultdict
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
@@ -12,7 +12,6 @@ from core.models import ObjectType
from netbox.config import get_config from netbox.config import get_config
from netbox.constants import RQ_QUEUE_DEFAULT from netbox.constants import RQ_QUEUE_DEFAULT
from netbox.models.features import has_feature from netbox.models.features import has_feature
from users.models import User
from utilities.api import get_serializer_for_model from utilities.api import get_serializer_for_model
from utilities.request import copy_safe_request from utilities.request import copy_safe_request
from utilities.rqworker import get_rq_retry from utilities.rqworker import get_rq_retry
@@ -23,6 +22,19 @@ from .models import EventRule
logger = logging.getLogger('netbox.events_processor') logger = logging.getLogger('netbox.events_processor')
class EventContext(UserDict):
"""
A custom dictionary that automatically serializes its associated object on demand.
"""
def __getitem__(self, item):
if item == 'data' and 'data' not in self:
data = serialize_for_event(self['object'])
self.__setitem__('data', data)
return data
return super().__getitem__(item)
def serialize_for_event(instance): def serialize_for_event(instance):
""" """
Return a serialized representation of the given instance suitable for use in a queued event. Return a serialized representation of the given instance suitable for use in a queued event.
@@ -66,37 +78,42 @@ def enqueue_event(queue, instance, request, event_type):
assert instance.pk is not None assert instance.pk is not None
key = f'{app_label}.{model_name}:{instance.pk}' key = f'{app_label}.{model_name}:{instance.pk}'
if key in queue: if key in queue:
queue[key]['data'] = serialize_for_event(instance)
queue[key]['snapshots']['postchange'] = get_snapshots(instance, event_type)['postchange'] queue[key]['snapshots']['postchange'] = get_snapshots(instance, event_type)['postchange']
# If the object is being deleted, update any prior "update" event to "delete" # If the object is being deleted, update any prior "update" event to "delete"
if event_type == OBJECT_DELETED: if event_type == OBJECT_DELETED:
queue[key]['event_type'] = event_type queue[key]['event_type'] = event_type
else: else:
queue[key] = { queue[key] = EventContext(
'object_type': ObjectType.objects.get_for_model(instance), object_type=ObjectType.objects.get_for_model(instance),
'object_id': instance.pk, object_id=instance.pk,
'event_type': event_type, object=instance,
'data': serialize_for_event(instance), event_type=event_type,
'snapshots': get_snapshots(instance, event_type), snapshots=get_snapshots(instance, event_type),
'request': request, request=request,
user=request.user,
# Legacy request attributes for backward compatibility # Legacy request attributes for backward compatibility
'username': request.user.username, username=request.user.username,
'request_id': request.id, request_id=request.id,
} )
# Force serialization of objects prior to them actually being deleted
if event_type == OBJECT_DELETED:
queue[key]['data'] = serialize_for_event(instance)
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):
user = User.objects.get(username=username) if username else None """
Process a list of EventRules against an event.
"""
for event_rule in event_rules: for event_rule in event_rules:
# Evaluate event rule conditions (if any) # Evaluate event rule conditions (if any)
if not event_rule.eval_conditions(data): if not event_rule.eval_conditions(event['data']):
continue continue
# Compile event data # Compile event data
event_data = event_rule.action_data or {} event_data = event_rule.action_data or {}
event_data.update(data) event_data.update(event['data'])
# Webhooks # Webhooks
if event_rule.action_type == EventRuleActionChoices.WEBHOOK: if event_rule.action_type == EventRuleActionChoices.WEBHOOK:
@@ -109,25 +126,22 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
params = { params = {
"event_rule": event_rule, "event_rule": event_rule,
"object_type": object_type, "object_type": object_type,
"event_type": event_type, "event_type": event['event_type'],
"data": event_data, "data": event_data,
"snapshots": snapshots, "snapshots": event['snapshots'],
"timestamp": timezone.now().isoformat(), "timestamp": timezone.now().isoformat(),
"username": username, "username": event['username'],
"retry": get_rq_retry() "retry": get_rq_retry()
} }
if snapshots: if 'snapshots' in event:
params["snapshots"] = snapshots params['snapshots'] = event['snapshots']
if request: if 'request' in event:
# Exclude FILES - webhooks don't need uploaded files, # Exclude FILES - webhooks don't need uploaded files,
# which can cause pickle errors with Pillow. # which can cause pickle errors with Pillow.
params["request"] = copy_safe_request(request, include_files=False) params['request'] = copy_safe_request(event['request'], include_files=False)
# Enqueue the task # Enqueue the task
rq_queue.enqueue( rq_queue.enqueue('extras.webhooks.send_webhook', **params)
"extras.webhooks.send_webhook",
**params
)
# Scripts # Scripts
elif event_rule.action_type == EventRuleActionChoices.SCRIPT: elif event_rule.action_type == EventRuleActionChoices.SCRIPT:
@@ -139,16 +153,16 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
params = { params = {
"instance": event_rule.action_object, "instance": event_rule.action_object,
"name": script.name, "name": script.name,
"user": user, "user": event['user'],
"data": event_data "data": event_data
} }
if snapshots: if 'snapshots' in event:
params["snapshots"] = snapshots params['snapshots'] = event['snapshots']
if request: if 'request' in event:
params["request"] = copy_safe_request(request) params['request'] = copy_safe_request(event['request'])
ScriptJob.enqueue(
**params # Enqueue the job
) ScriptJob.enqueue(**params)
# Notification groups # Notification groups
elif event_rule.action_type == EventRuleActionChoices.NOTIFICATION: elif event_rule.action_type == EventRuleActionChoices.NOTIFICATION:
@@ -157,7 +171,7 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
object_type=object_type, object_type=object_type,
object_id=event_data['id'], object_id=event_data['id'],
object_repr=event_data.get('display'), object_repr=event_data.get('display'),
event_type=event_type event_type=event['event_type']
) )
else: else:
@@ -169,6 +183,8 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
def process_event_queue(events): def process_event_queue(events):
""" """
Flush a list of object representation to RQ for EventRule processing. Flush a list of object representation to RQ for EventRule processing.
This is the default processor listed in EVENTS_PIPELINE.
""" """
events_cache = defaultdict(dict) events_cache = defaultdict(dict)
@@ -188,11 +204,7 @@ def process_event_queue(events):
process_event_rules( process_event_rules(
event_rules=event_rules, event_rules=event_rules,
object_type=object_type, object_type=object_type,
event_type=event['event_type'], event=event,
data=event['data'],
username=event['username'],
snapshots=event['snapshots'],
request=event['request'],
) )

View File

@@ -4,7 +4,7 @@ from django.dispatch import receiver
from core.events import * from core.events import *
from core.signals import job_end, job_start from core.signals import job_end, job_start
from extras.events import process_event_rules from extras.events import EventContext, process_event_rules
from extras.models import EventRule, Notification, Subscription from extras.models import EventRule, Notification, Subscription
from netbox.config import get_config from netbox.config import get_config
from netbox.models.features import has_feature from netbox.models.features import has_feature
@@ -102,14 +102,12 @@ def process_job_start_event_rules(sender, **kwargs):
enabled=True, enabled=True,
object_types=sender.object_type object_types=sender.object_type
) )
username = sender.user.username if sender.user else None event = EventContext(
process_event_rules(
event_rules=event_rules,
object_type=sender.object_type,
event_type=JOB_STARTED, event_type=JOB_STARTED,
data=sender.data, data=sender.data,
username=username user=sender.user,
) )
process_event_rules(event_rules, sender.object_type, event)
@receiver(job_end) @receiver(job_end)
@@ -122,14 +120,12 @@ def process_job_end_event_rules(sender, **kwargs):
enabled=True, enabled=True,
object_types=sender.object_type object_types=sender.object_type
) )
username = sender.user.username if sender.user else None event = EventContext(
process_event_rules(
event_rules=event_rules,
object_type=sender.object_type,
event_type=JOB_COMPLETED, event_type=JOB_COMPLETED,
data=sender.data, data=sender.data,
username=username user=sender.user,
) )
process_event_rules(event_rules, sender.object_type, event)
# #

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

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}")