mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-24 04:22:41 -06:00
Compare commits
10 Commits
v4.5.1
...
21260-even
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e69fc9a4b4 | ||
|
|
a6c6a58fb9 | ||
|
|
a9a300197a | ||
|
|
3dcca73ecc | ||
|
|
4b4c542dce | ||
|
|
077d9b1129 | ||
|
|
e81ccb9be6 | ||
|
|
bc83d04c8f | ||
|
|
339ad455e4 | ||
|
|
f24376cfab |
43
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
Normal file
43
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
Normal 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
|
||||
@@ -20,7 +20,9 @@ from utilities.forms.fields import (
|
||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
|
||||
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
|
||||
from utilities.forms.widgets import (
|
||||
APISelect, ClearableFileInput, ClearableSelect, HTMXSelect, NumberWithOptions, SelectWithPK,
|
||||
)
|
||||
from utilities.jsonschema import JSONSchemaProperty
|
||||
from virtualization.models import Cluster, VMInterface
|
||||
from wireless.models import WirelessLAN, WirelessLANGroup
|
||||
@@ -592,6 +594,14 @@ class DeviceForm(TenancyForm, PrimaryModelForm):
|
||||
},
|
||||
)
|
||||
)
|
||||
face = forms.ChoiceField(
|
||||
label=_('Face'),
|
||||
choices=add_blank_choice(DeviceFaceChoices),
|
||||
required=False,
|
||||
widget=ClearableSelect(
|
||||
requires_fields=['rack']
|
||||
)
|
||||
)
|
||||
device_type = DynamicModelChoiceField(
|
||||
label=_('Device type'),
|
||||
queryset=DeviceType.objects.all(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from collections import UserDict, defaultdict
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
@@ -12,7 +12,6 @@ from core.models import ObjectType
|
||||
from netbox.config import get_config
|
||||
from netbox.constants import RQ_QUEUE_DEFAULT
|
||||
from netbox.models.features import has_feature
|
||||
from users.models import User
|
||||
from utilities.api import get_serializer_for_model
|
||||
from utilities.request import copy_safe_request
|
||||
from utilities.rqworker import get_rq_retry
|
||||
@@ -23,6 +22,19 @@ from .models import EventRule
|
||||
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):
|
||||
"""
|
||||
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
|
||||
key = f'{app_label}.{model_name}:{instance.pk}'
|
||||
if key in queue:
|
||||
queue[key]['data'] = serialize_for_event(instance)
|
||||
queue[key]['snapshots']['postchange'] = get_snapshots(instance, event_type)['postchange']
|
||||
# If the object is being deleted, update any prior "update" event to "delete"
|
||||
if event_type == OBJECT_DELETED:
|
||||
queue[key]['event_type'] = event_type
|
||||
else:
|
||||
queue[key] = {
|
||||
'object_type': ObjectType.objects.get_for_model(instance),
|
||||
'object_id': instance.pk,
|
||||
'event_type': event_type,
|
||||
'data': serialize_for_event(instance),
|
||||
'snapshots': get_snapshots(instance, event_type),
|
||||
'request': request,
|
||||
queue[key] = EventContext(
|
||||
object_type=ObjectType.objects.get_for_model(instance),
|
||||
object_id=instance.pk,
|
||||
object=instance,
|
||||
event_type=event_type,
|
||||
snapshots=get_snapshots(instance, event_type),
|
||||
request=request,
|
||||
user=request.user,
|
||||
# Legacy request attributes for backward compatibility
|
||||
'username': request.user.username,
|
||||
'request_id': request.id,
|
||||
}
|
||||
username=request.user.username,
|
||||
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):
|
||||
user = User.objects.get(username=username) if username else None
|
||||
def process_event_rules(event_rules, object_type, event):
|
||||
"""
|
||||
Process a list of EventRules against an event.
|
||||
"""
|
||||
|
||||
for event_rule in event_rules:
|
||||
|
||||
# Evaluate event rule conditions (if any)
|
||||
if not event_rule.eval_conditions(data):
|
||||
if not event_rule.eval_conditions(event['data']):
|
||||
continue
|
||||
|
||||
# Compile event data
|
||||
event_data = event_rule.action_data or {}
|
||||
event_data.update(data)
|
||||
event_data.update(event['data'])
|
||||
|
||||
# Webhooks
|
||||
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 = {
|
||||
"event_rule": event_rule,
|
||||
"object_type": object_type,
|
||||
"event_type": event_type,
|
||||
"event_type": event['event_type'],
|
||||
"data": event_data,
|
||||
"snapshots": snapshots,
|
||||
"snapshots": event['snapshots'],
|
||||
"timestamp": timezone.now().isoformat(),
|
||||
"username": username,
|
||||
"username": event['username'],
|
||||
"retry": get_rq_retry()
|
||||
}
|
||||
if snapshots:
|
||||
params["snapshots"] = snapshots
|
||||
if request:
|
||||
if 'snapshots' in event:
|
||||
params['snapshots'] = event['snapshots']
|
||||
if 'request' in event:
|
||||
# Exclude FILES - webhooks don't need uploaded files,
|
||||
# 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
|
||||
rq_queue.enqueue(
|
||||
"extras.webhooks.send_webhook",
|
||||
**params
|
||||
)
|
||||
rq_queue.enqueue('extras.webhooks.send_webhook', **params)
|
||||
|
||||
# Scripts
|
||||
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 = {
|
||||
"instance": event_rule.action_object,
|
||||
"name": script.name,
|
||||
"user": user,
|
||||
"user": event['user'],
|
||||
"data": event_data
|
||||
}
|
||||
if snapshots:
|
||||
params["snapshots"] = snapshots
|
||||
if request:
|
||||
params["request"] = copy_safe_request(request)
|
||||
ScriptJob.enqueue(
|
||||
**params
|
||||
)
|
||||
if 'snapshots' in event:
|
||||
params['snapshots'] = event['snapshots']
|
||||
if 'request' in event:
|
||||
params['request'] = copy_safe_request(event['request'])
|
||||
|
||||
# Enqueue the job
|
||||
ScriptJob.enqueue(**params)
|
||||
|
||||
# Notification groups
|
||||
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_id=event_data['id'],
|
||||
object_repr=event_data.get('display'),
|
||||
event_type=event_type
|
||||
event_type=event['event_type']
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -169,6 +183,8 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non
|
||||
def process_event_queue(events):
|
||||
"""
|
||||
Flush a list of object representation to RQ for EventRule processing.
|
||||
|
||||
This is the default processor listed in EVENTS_PIPELINE.
|
||||
"""
|
||||
events_cache = defaultdict(dict)
|
||||
|
||||
@@ -188,11 +204,7 @@ def process_event_queue(events):
|
||||
process_event_rules(
|
||||
event_rules=event_rules,
|
||||
object_type=object_type,
|
||||
event_type=event['event_type'],
|
||||
data=event['data'],
|
||||
username=event['username'],
|
||||
snapshots=event['snapshots'],
|
||||
request=event['request'],
|
||||
event=event,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.dispatch import receiver
|
||||
|
||||
from core.events import *
|
||||
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 netbox.config import get_config
|
||||
from netbox.models.features import has_feature
|
||||
@@ -102,14 +102,12 @@ def process_job_start_event_rules(sender, **kwargs):
|
||||
enabled=True,
|
||||
object_types=sender.object_type
|
||||
)
|
||||
username = sender.user.username if sender.user else None
|
||||
process_event_rules(
|
||||
event_rules=event_rules,
|
||||
object_type=sender.object_type,
|
||||
event = EventContext(
|
||||
event_type=JOB_STARTED,
|
||||
data=sender.data,
|
||||
username=username
|
||||
user=sender.user,
|
||||
)
|
||||
process_event_rules(event_rules, sender.object_type, event)
|
||||
|
||||
|
||||
@receiver(job_end)
|
||||
@@ -122,14 +120,12 @@ def process_job_end_event_rules(sender, **kwargs):
|
||||
enabled=True,
|
||||
object_types=sender.object_type
|
||||
)
|
||||
username = sender.user.username if sender.user else None
|
||||
process_event_rules(
|
||||
event_rules=event_rules,
|
||||
object_type=sender.object_type,
|
||||
event = EventContext(
|
||||
event_type=JOB_COMPLETED,
|
||||
data=sender.data,
|
||||
username=username
|
||||
user=sender.user,
|
||||
)
|
||||
process_event_rules(event_rules, sender.object_type, event)
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -43,7 +43,7 @@ IMAGEATTACHMENT_IMAGE = """
|
||||
<a href="{{ record.image.url }}" target="_blank" class="image-preview" data-bs-placement="top">
|
||||
<i class="mdi mdi-image"></i></a>
|
||||
{% endif %}
|
||||
<a href="{{ record.get_absolute_url }}">{{ record }}</a>
|
||||
<a href="{{ record.get_absolute_url }}">{{ record.filename|truncate_middle:16 }}</a>
|
||||
"""
|
||||
|
||||
NOTIFICATION_ICON = """
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.forms import ValidationError
|
||||
from django.test import tag, TestCase
|
||||
|
||||
from core.models import DataSource, ObjectType
|
||||
from core.models import AutoSyncRecord, DataSource, ObjectType
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup
|
||||
from extras.models import ConfigContext, ConfigContextProfile, ConfigTemplate, ImageAttachment, Tag, TaggedItem
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
@@ -754,3 +754,53 @@ class ConfigTemplateTest(TestCase):
|
||||
@tag('regression')
|
||||
def test_config_template_with_data_source_nested_templates(self):
|
||||
self.assertEqual(self.BASE_TEMPLATE, self.main_config_template.render({}))
|
||||
|
||||
@tag('regression')
|
||||
def test_autosyncrecord_cleanup_on_detach(self):
|
||||
"""Test that AutoSyncRecord is deleted when detaching from DataSource."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
templates_dir = Path(temp_dir) / "templates"
|
||||
templates_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self._create_template_file(templates_dir, 'test.j2', 'Test content')
|
||||
|
||||
data_source = DataSource(
|
||||
name="Test DataSource for Detach",
|
||||
type="local",
|
||||
source_url=str(templates_dir),
|
||||
)
|
||||
data_source.save()
|
||||
data_source.sync()
|
||||
|
||||
data_file = data_source.datafiles.filter(path__endswith='test.j2').first()
|
||||
|
||||
# Create a ConfigTemplate with data_file and auto_sync_enabled
|
||||
config_template = ConfigTemplate(
|
||||
name="TestTemplateForDetach",
|
||||
data_file=data_file,
|
||||
auto_sync_enabled=True
|
||||
)
|
||||
config_template.clean()
|
||||
config_template.save()
|
||||
|
||||
# Verify AutoSyncRecord was created
|
||||
object_type = ObjectType.objects.get_for_model(ConfigTemplate)
|
||||
autosync_records = AutoSyncRecord.objects.filter(
|
||||
object_type=object_type,
|
||||
object_id=config_template.pk
|
||||
)
|
||||
self.assertEqual(autosync_records.count(), 1, "AutoSyncRecord should be created")
|
||||
|
||||
# Detach from DataSource
|
||||
config_template.data_file = None
|
||||
config_template.data_source = None
|
||||
config_template.auto_sync_enabled = False
|
||||
config_template.clean()
|
||||
config_template.save()
|
||||
|
||||
# Verify AutoSyncRecord was deleted
|
||||
autosync_records = AutoSyncRecord.objects.filter(
|
||||
object_type=object_type,
|
||||
object_id=config_template.pk
|
||||
)
|
||||
self.assertEqual(autosync_records.count(), 0, "AutoSyncRecord should be deleted after detaching")
|
||||
|
||||
@@ -569,7 +569,6 @@ class SyncedDataMixin(models.Model):
|
||||
)
|
||||
else:
|
||||
AutoSyncRecord.objects.filter(
|
||||
datafile=self.data_file,
|
||||
object_type=object_type,
|
||||
object_id=self.pk
|
||||
).delete()
|
||||
@@ -582,7 +581,6 @@ class SyncedDataMixin(models.Model):
|
||||
# Delete AutoSyncRecord
|
||||
object_type = ObjectType.objects.get_for_model(self)
|
||||
AutoSyncRecord.objects.filter(
|
||||
datafile=self.data_file,
|
||||
object_type=object_type,
|
||||
object_id=self.pk
|
||||
).delete()
|
||||
|
||||
8
netbox/project-static/dist/netbox.js
vendored
8
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
8
netbox/project-static/dist/netbox.js.map
vendored
8
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
40
netbox/project-static/src/forms/clearField.ts
Normal file
40
netbox/project-static/src/forms/clearField.ts
Normal 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 = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { initClearField } from './clearField';
|
||||
import { initFormElements } from './elements';
|
||||
import { initFilterModifiers } from './filterModifiers';
|
||||
import { initSpeedSelector } from './speedSelector';
|
||||
|
||||
export function initForms(): void {
|
||||
for (const func of [initFormElements, initSpeedSelector, initFilterModifiers]) {
|
||||
for (const func of [initFormElements, initSpeedSelector, initFilterModifiers, initClearField]) {
|
||||
func();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
{% block extra_controls %}
|
||||
{% include 'ipam/inc/toggle_available.html' %}
|
||||
{% include 'ipam/inc/max_depth.html' %}
|
||||
{% include 'ipam/inc/max_length.html' %}
|
||||
{% if perms.ipam.add_prefix and first_available_prefix %}
|
||||
<a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}" class="btn btn-primary">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Prefix" %}
|
||||
|
||||
20
netbox/templates/ipam/inc/max_depth.html
Normal file
20
netbox/templates/ipam/inc/max_depth.html
Normal 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>
|
||||
20
netbox/templates/ipam/inc/max_length.html
Normal file
20
netbox/templates/ipam/inc/max_length.html
Normal 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>
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
{% block extra_controls %}
|
||||
{% include 'ipam/inc/toggle_available.html' %}
|
||||
{% include 'ipam/inc/max_depth.html' %}
|
||||
{% include 'ipam/inc/max_length.html' %}
|
||||
{% if perms.ipam.add_prefix and first_available_prefix %}
|
||||
<a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}&vrf={{ object.vrf.pk }}&site={{ object.site.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-primary">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Prefix" %}
|
||||
|
||||
@@ -6,38 +6,6 @@
|
||||
<button class="btn btn-outline-secondary toggle-depth" type="button">
|
||||
{% trans "Hide Depth Indicators" %}
|
||||
</button>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="max_depth" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
{% trans "Max Depth" %}{% if "depth__lte" in request.GET %}: {{ request.GET.depth__lte }}{% endif %}
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="max_depth">
|
||||
{% if request.GET.depth__lte %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'ipam:prefix_list' %}{% querystring request depth__lte=None page=1 %}">{% trans "Clear" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for i in 16|as_range %}
|
||||
<li><a class="dropdown-item" href="{% url 'ipam:prefix_list' %}{% querystring request depth__lte=i page=1 %}">
|
||||
{{ i }} {% if request.GET.depth__lte == i %}<i class="mdi mdi-check-bold"></i>{% endif %}
|
||||
</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="max_length" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
{% trans "Max Length" %}{% if "mask_length__lte" in request.GET %}: {{ request.GET.mask_length__lte }}{% endif %}
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="max_length">
|
||||
{% if request.GET.mask_length__lte %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'ipam:prefix_list' %}{% querystring request mask_length__lte=None page=1 %}">{% trans "Clear" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for i in "4,8,12,16,20,24,28,32,40,48,56,64"|split %}
|
||||
<li><a class="dropdown-item" href="{% url 'ipam:prefix_list' %}{% querystring request mask_length__lte=i page=1 %}">
|
||||
{{ i }} {% if request.GET.mask_length__lte == i %}<i class="mdi mdi-check-bold"></i>{% endif %}
|
||||
</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% include 'ipam/inc/max_depth.html' %}
|
||||
{% include 'ipam/inc/max_length.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-20 05:07+0000\n"
|
||||
"POT-Creation-Date: 2026-01-22 05:07+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -1279,7 +1279,7 @@ msgid "Term Side"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/circuits/forms/filtersets.py:287 netbox/dcim/forms/bulk_edit.py:1537
|
||||
#: netbox/extras/forms/model_forms.py:697 netbox/ipam/forms/filtersets.py:149
|
||||
#: netbox/extras/forms/model_forms.py:693 netbox/ipam/forms/filtersets.py:149
|
||||
#: netbox/ipam/forms/filtersets.py:627 netbox/ipam/forms/model_forms.py:326
|
||||
#: netbox/templates/dcim/macaddress.html:25
|
||||
#: netbox/templates/extras/configcontext.html:36
|
||||
@@ -1901,7 +1901,7 @@ msgstr ""
|
||||
msgid "Device"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/api/views.py:51
|
||||
#: netbox/core/api/views.py:50
|
||||
msgid "This user does not have permission to synchronize this data source."
|
||||
msgstr ""
|
||||
|
||||
@@ -2188,9 +2188,9 @@ msgstr ""
|
||||
|
||||
#: netbox/core/forms/filtersets.py:30 netbox/core/forms/model_forms.py:100
|
||||
#: netbox/extras/forms/model_forms.py:268
|
||||
#: netbox/extras/forms/model_forms.py:604
|
||||
#: netbox/extras/forms/model_forms.py:693
|
||||
#: netbox/extras/forms/model_forms.py:746 netbox/extras/tables/tables.py:218
|
||||
#: netbox/extras/forms/model_forms.py:600
|
||||
#: netbox/extras/forms/model_forms.py:689
|
||||
#: netbox/extras/forms/model_forms.py:742 netbox/extras/tables/tables.py:218
|
||||
#: netbox/extras/tables/tables.py:588 netbox/extras/tables/tables.py:618
|
||||
#: netbox/extras/tables/tables.py:660 netbox/templates/core/datasource.html:31
|
||||
#: netbox/templates/core/inc/datafile_panel.html:7
|
||||
@@ -2685,11 +2685,11 @@ msgid ""
|
||||
"enqueue() cannot be called with values for both schedule_at and immediate."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/object_types.py:188
|
||||
#: netbox/core/models/object_types.py:194
|
||||
msgid "object type"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/object_types.py:189 netbox/extras/models/models.py:57
|
||||
#: netbox/core/models/object_types.py:195 netbox/extras/models/models.py:57
|
||||
msgid "object types"
|
||||
msgstr ""
|
||||
|
||||
@@ -4176,9 +4176,9 @@ msgid "Power panel (ID)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_create.py:40 netbox/extras/forms/filtersets.py:515
|
||||
#: netbox/extras/forms/model_forms.py:597
|
||||
#: netbox/extras/forms/model_forms.py:682
|
||||
#: netbox/extras/forms/model_forms.py:734 netbox/extras/ui/panels.py:69
|
||||
#: netbox/extras/forms/model_forms.py:593
|
||||
#: netbox/extras/forms/model_forms.py:678
|
||||
#: netbox/extras/forms/model_forms.py:730 netbox/extras/ui/panels.py:69
|
||||
#: netbox/netbox/forms/bulk_import.py:26 netbox/netbox/forms/mixins.py:113
|
||||
#: netbox/netbox/tables/columns.py:490
|
||||
#: netbox/templates/circuits/inc/circuit_termination.html:29
|
||||
@@ -4317,9 +4317,8 @@ msgstr ""
|
||||
#: netbox/extras/forms/bulk_edit.py:57 netbox/extras/forms/bulk_edit.py:137
|
||||
#: netbox/extras/forms/bulk_edit.py:191 netbox/extras/forms/bulk_edit.py:219
|
||||
#: netbox/extras/forms/bulk_edit.py:315 netbox/extras/forms/bulk_edit.py:341
|
||||
#: netbox/extras/forms/bulk_import.py:275 netbox/extras/forms/filtersets.py:71
|
||||
#: netbox/extras/forms/filtersets.py:175 netbox/extras/forms/filtersets.py:279
|
||||
#: netbox/extras/forms/filtersets.py:315 netbox/extras/forms/model_forms.py:575
|
||||
#: netbox/extras/forms/filtersets.py:71 netbox/extras/forms/filtersets.py:175
|
||||
#: netbox/extras/forms/filtersets.py:279 netbox/extras/forms/filtersets.py:315
|
||||
#: netbox/ipam/forms/bulk_edit.py:159 netbox/templates/dcim/moduletype.html:51
|
||||
#: netbox/templates/extras/configcontext.html:17
|
||||
#: netbox/templates/extras/customlink.html:25
|
||||
@@ -4455,7 +4454,7 @@ msgid "Device Type"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_edit.py:540 netbox/dcim/forms/model_forms.py:400
|
||||
#: netbox/dcim/views.py:1578 netbox/extras/forms/model_forms.py:592
|
||||
#: netbox/dcim/views.py:1578 netbox/extras/forms/model_forms.py:588
|
||||
msgid "Schema"
|
||||
msgstr ""
|
||||
|
||||
@@ -4464,7 +4463,7 @@ msgstr ""
|
||||
#: netbox/dcim/forms/bulk_import.py:1452 netbox/dcim/forms/filtersets.py:679
|
||||
#: netbox/dcim/forms/filtersets.py:1197 netbox/dcim/forms/model_forms.py:406
|
||||
#: netbox/dcim/forms/model_forms.py:419 netbox/dcim/tables/modules.py:42
|
||||
#: netbox/extras/forms/filtersets.py:437 netbox/extras/forms/model_forms.py:617
|
||||
#: netbox/extras/forms/filtersets.py:437 netbox/extras/forms/model_forms.py:613
|
||||
#: netbox/extras/tables/tables.py:615 netbox/templates/account/base.html:7
|
||||
#: netbox/templates/dcim/cable.html:23 netbox/templates/dcim/moduletype.html:27
|
||||
#: netbox/templates/extras/configcontext.html:21
|
||||
@@ -5599,8 +5598,8 @@ msgid "Connection"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/filtersets.py:1572 netbox/extras/forms/bulk_edit.py:421
|
||||
#: netbox/extras/forms/bulk_import.py:298 netbox/extras/forms/filtersets.py:616
|
||||
#: netbox/extras/forms/model_forms.py:798 netbox/extras/tables/tables.py:743
|
||||
#: netbox/extras/forms/bulk_import.py:294 netbox/extras/forms/filtersets.py:616
|
||||
#: netbox/extras/forms/model_forms.py:794 netbox/extras/tables/tables.py:743
|
||||
#: netbox/templates/extras/journalentry.html:30
|
||||
msgid "Kind"
|
||||
msgstr ""
|
||||
@@ -5745,7 +5744,7 @@ msgid ""
|
||||
"hyphen."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/model_forms.py:402 netbox/extras/forms/model_forms.py:594
|
||||
#: netbox/dcim/forms/model_forms.py:402 netbox/extras/forms/model_forms.py:590
|
||||
msgid "Enter a valid JSON schema to define supported attributes."
|
||||
msgstr ""
|
||||
|
||||
@@ -7610,7 +7609,7 @@ msgid "VMs"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/devices.py:103 netbox/dcim/tables/devices.py:223
|
||||
#: netbox/extras/forms/model_forms.py:745
|
||||
#: netbox/extras/forms/model_forms.py:741
|
||||
#: netbox/templates/dcim/devicerole.html:48
|
||||
#: netbox/templates/dcim/platform.html:45
|
||||
#: netbox/templates/extras/configtemplate.html:10
|
||||
@@ -7842,7 +7841,7 @@ msgid "Module Types"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/devicetypes.py:57 netbox/extras/forms/filtersets.py:485
|
||||
#: netbox/extras/forms/model_forms.py:652 netbox/extras/tables/tables.py:703
|
||||
#: netbox/extras/forms/model_forms.py:648 netbox/extras/tables/tables.py:703
|
||||
#: netbox/netbox/navigation/menu.py:78
|
||||
msgid "Platforms"
|
||||
msgstr ""
|
||||
@@ -8000,7 +7999,7 @@ msgid "Space"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/tables/sites.py:21 netbox/dcim/tables/sites.py:40
|
||||
#: netbox/extras/forms/filtersets.py:465 netbox/extras/forms/model_forms.py:632
|
||||
#: netbox/extras/forms/filtersets.py:465 netbox/extras/forms/model_forms.py:628
|
||||
#: netbox/ipam/forms/bulk_edit.py:112 netbox/ipam/forms/model_forms.py:154
|
||||
#: netbox/ipam/tables/asn.py:76 netbox/netbox/navigation/menu.py:15
|
||||
#: netbox/netbox/navigation/menu.py:19
|
||||
@@ -8083,7 +8082,7 @@ msgid "Application Services"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/views.py:2677 netbox/extras/forms/filtersets.py:427
|
||||
#: netbox/extras/forms/model_forms.py:692
|
||||
#: netbox/extras/forms/model_forms.py:688
|
||||
#: netbox/templates/extras/configcontext.html:10
|
||||
#: netbox/virtualization/forms/model_forms.py:225
|
||||
#: netbox/virtualization/views.py:399
|
||||
@@ -8505,7 +8504,7 @@ msgstr ""
|
||||
msgid "Tenant group (slug)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/filtersets.py:779 netbox/extras/forms/model_forms.py:580
|
||||
#: netbox/extras/filtersets.py:779 netbox/extras/forms/model_forms.py:576
|
||||
#: netbox/templates/extras/tag.html:11
|
||||
msgid "Tag"
|
||||
msgstr ""
|
||||
@@ -8644,7 +8643,7 @@ msgstr ""
|
||||
#: netbox/extras/forms/bulk_import.py:140
|
||||
#: netbox/extras/forms/bulk_import.py:201
|
||||
#: netbox/extras/forms/bulk_import.py:225
|
||||
#: netbox/extras/forms/bulk_import.py:279 netbox/extras/forms/filtersets.py:54
|
||||
#: netbox/extras/forms/bulk_import.py:275 netbox/extras/forms/filtersets.py:54
|
||||
#: netbox/extras/forms/filtersets.py:156 netbox/extras/forms/filtersets.py:260
|
||||
#: netbox/extras/forms/filtersets.py:296 netbox/extras/forms/model_forms.py:53
|
||||
#: netbox/extras/forms/model_forms.py:225
|
||||
@@ -8659,7 +8658,7 @@ msgstr ""
|
||||
#: netbox/extras/forms/bulk_import.py:142
|
||||
#: netbox/extras/forms/bulk_import.py:203
|
||||
#: netbox/extras/forms/bulk_import.py:227
|
||||
#: netbox/extras/forms/bulk_import.py:281
|
||||
#: netbox/extras/forms/bulk_import.py:277
|
||||
#: netbox/tenancy/forms/bulk_import.py:103
|
||||
msgid "One or more assigned object types"
|
||||
msgstr ""
|
||||
@@ -8738,7 +8737,7 @@ msgstr ""
|
||||
|
||||
#: netbox/extras/forms/bulk_import.py:195
|
||||
#: netbox/extras/forms/model_forms.py:292
|
||||
#: netbox/extras/forms/model_forms.py:773
|
||||
#: netbox/extras/forms/model_forms.py:769
|
||||
msgid "Must specify either local content or a data file"
|
||||
msgstr ""
|
||||
|
||||
@@ -8764,15 +8763,15 @@ msgstr ""
|
||||
msgid "Script {name} not found"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/bulk_import.py:295
|
||||
#: netbox/extras/forms/bulk_import.py:291
|
||||
msgid "Assigned object type"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/bulk_import.py:300
|
||||
#: netbox/extras/forms/bulk_import.py:296
|
||||
msgid "The classification of entry"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/bulk_import.py:303 netbox/extras/tables/tables.py:746
|
||||
#: netbox/extras/forms/bulk_import.py:299 netbox/extras/tables/tables.py:746
|
||||
#: netbox/netbox/tables/tables.py:279 netbox/netbox/tables/tables.py:289
|
||||
#: netbox/netbox/tables/tables.py:307 netbox/netbox/ui/panels.py:215
|
||||
#: netbox/templates/dcim/htmx/cable_edit.html:98
|
||||
@@ -8782,7 +8781,7 @@ msgstr ""
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/bulk_import.py:316
|
||||
#: netbox/extras/forms/bulk_import.py:312
|
||||
#: netbox/extras/forms/model_forms.py:401 netbox/netbox/navigation/menu.py:414
|
||||
#: netbox/templates/extras/notificationgroup.html:41
|
||||
#: netbox/templates/users/group.html:29 netbox/templates/users/owner.html:46
|
||||
@@ -8793,11 +8792,11 @@ msgstr ""
|
||||
msgid "Users"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/bulk_import.py:320
|
||||
#: netbox/extras/forms/bulk_import.py:316
|
||||
msgid "User names separated by commas, encased with double quotes"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/bulk_import.py:323
|
||||
#: netbox/extras/forms/bulk_import.py:319
|
||||
#: netbox/extras/forms/model_forms.py:396 netbox/netbox/navigation/menu.py:295
|
||||
#: netbox/netbox/navigation/menu.py:434
|
||||
#: netbox/templates/extras/notificationgroup.html:31
|
||||
@@ -8812,7 +8811,7 @@ msgstr ""
|
||||
msgid "Groups"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/bulk_import.py:327
|
||||
#: netbox/extras/forms/bulk_import.py:323
|
||||
msgid "Group names separated by commas, encased with double quotes"
|
||||
msgstr ""
|
||||
|
||||
@@ -8836,14 +8835,14 @@ msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:189 netbox/extras/forms/filtersets.py:406
|
||||
#: netbox/extras/forms/filtersets.py:428 netbox/extras/forms/filtersets.py:528
|
||||
#: netbox/extras/forms/model_forms.py:687 netbox/templates/core/job.html:69
|
||||
#: netbox/extras/forms/model_forms.py:683 netbox/templates/core/job.html:69
|
||||
#: netbox/templates/extras/eventrule.html:84
|
||||
msgid "Data"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:190 netbox/extras/forms/filtersets.py:529
|
||||
#: netbox/extras/forms/model_forms.py:270
|
||||
#: netbox/extras/forms/model_forms.py:748
|
||||
#: netbox/extras/forms/model_forms.py:744
|
||||
msgid "Rendering"
|
||||
msgstr ""
|
||||
|
||||
@@ -8871,37 +8870,37 @@ msgstr ""
|
||||
msgid "Allowed object type"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:455 netbox/extras/forms/model_forms.py:622
|
||||
#: netbox/extras/forms/filtersets.py:455 netbox/extras/forms/model_forms.py:618
|
||||
#: netbox/netbox/navigation/menu.py:17
|
||||
msgid "Regions"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:460 netbox/extras/forms/model_forms.py:627
|
||||
#: netbox/extras/forms/filtersets.py:460 netbox/extras/forms/model_forms.py:623
|
||||
msgid "Site groups"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:470 netbox/extras/forms/model_forms.py:637
|
||||
#: netbox/extras/forms/filtersets.py:470 netbox/extras/forms/model_forms.py:633
|
||||
#: netbox/netbox/navigation/menu.py:20
|
||||
msgid "Locations"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:475 netbox/extras/forms/model_forms.py:642
|
||||
#: netbox/extras/forms/filtersets.py:475 netbox/extras/forms/model_forms.py:638
|
||||
msgid "Device types"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:480 netbox/extras/forms/model_forms.py:647
|
||||
#: netbox/extras/forms/filtersets.py:480 netbox/extras/forms/model_forms.py:643
|
||||
msgid "Roles"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:490 netbox/extras/forms/model_forms.py:657
|
||||
#: netbox/extras/forms/filtersets.py:490 netbox/extras/forms/model_forms.py:653
|
||||
msgid "Cluster types"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:495 netbox/extras/forms/model_forms.py:662
|
||||
#: netbox/extras/forms/filtersets.py:495 netbox/extras/forms/model_forms.py:658
|
||||
msgid "Cluster groups"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:500 netbox/extras/forms/model_forms.py:667
|
||||
#: netbox/extras/forms/filtersets.py:500 netbox/extras/forms/model_forms.py:663
|
||||
#: netbox/netbox/navigation/menu.py:264 netbox/netbox/navigation/menu.py:266
|
||||
#: netbox/templates/virtualization/clustertype.html:30
|
||||
#: netbox/virtualization/tables/clusters.py:23
|
||||
@@ -8909,7 +8908,7 @@ msgstr ""
|
||||
msgid "Clusters"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/filtersets.py:505 netbox/extras/forms/model_forms.py:672
|
||||
#: netbox/extras/forms/filtersets.py:505 netbox/extras/forms/model_forms.py:668
|
||||
msgid "Tenant groups"
|
||||
msgstr ""
|
||||
|
||||
@@ -8984,7 +8983,7 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:261
|
||||
#: netbox/extras/forms/model_forms.py:739
|
||||
#: netbox/extras/forms/model_forms.py:735
|
||||
msgid "Template code"
|
||||
msgstr ""
|
||||
|
||||
@@ -8994,7 +8993,7 @@ msgid "Export Template"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:285
|
||||
#: netbox/extras/forms/model_forms.py:766
|
||||
#: netbox/extras/forms/model_forms.py:762
|
||||
msgid "Template content is populated from the remote source selected below."
|
||||
msgstr ""
|
||||
|
||||
@@ -9064,21 +9063,21 @@ msgstr ""
|
||||
msgid "Notification group"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:603
|
||||
#: netbox/extras/forms/model_forms.py:599
|
||||
#: netbox/templates/extras/configcontextprofile.html:10
|
||||
msgid "Config Context Profile"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:677 netbox/netbox/navigation/menu.py:26
|
||||
#: netbox/extras/forms/model_forms.py:673 netbox/netbox/navigation/menu.py:26
|
||||
#: netbox/tenancy/tables/tenants.py:18
|
||||
msgid "Tenants"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:721
|
||||
#: netbox/extras/forms/model_forms.py:717
|
||||
msgid "Data is populated from the remote source selected below."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/forms/model_forms.py:727
|
||||
#: netbox/extras/forms/model_forms.py:723
|
||||
msgid "Must specify either local data or a data file"
|
||||
msgstr ""
|
||||
|
||||
@@ -12038,7 +12037,7 @@ msgstr ""
|
||||
msgid "date synced"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/models/features.py:623
|
||||
#: netbox/netbox/models/features.py:621
|
||||
#, python-brace-format
|
||||
msgid "{class_name} must implement a sync_data() method."
|
||||
msgstr ""
|
||||
@@ -13936,8 +13935,8 @@ msgid "No VLANs Assigned"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/dcim/inc/interface_vlans_table.html:44
|
||||
#: netbox/templates/ipam/prefix_list.html:16
|
||||
#: netbox/templates/ipam/prefix_list.html:33
|
||||
#: netbox/templates/ipam/inc/max_depth.html:11
|
||||
#: netbox/templates/ipam/inc/max_length.html:11
|
||||
msgid "Clear"
|
||||
msgstr ""
|
||||
|
||||
@@ -15054,8 +15053,8 @@ msgstr ""
|
||||
msgid "Date Added"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/ipam/aggregate/prefixes.html:8
|
||||
#: netbox/templates/ipam/prefix/prefixes.html:8
|
||||
#: netbox/templates/ipam/aggregate/prefixes.html:10
|
||||
#: netbox/templates/ipam/prefix/prefixes.html:10
|
||||
#: netbox/templates/ipam/role.html:10
|
||||
msgid "Add Prefix"
|
||||
msgstr ""
|
||||
@@ -15084,6 +15083,14 @@ msgstr ""
|
||||
msgid "Bulk Create"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/ipam/inc/max_depth.html:6
|
||||
msgid "Max Depth"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/ipam/inc/max_length.html:6
|
||||
msgid "Max Length"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/ipam/inc/panels/fhrp_groups.html:10
|
||||
msgid "Create Group"
|
||||
msgstr ""
|
||||
@@ -15185,14 +15192,6 @@ msgstr ""
|
||||
msgid "Hide Depth Indicators"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/ipam/prefix_list.html:11
|
||||
msgid "Max Depth"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/ipam/prefix_list.html:28
|
||||
msgid "Max Length"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/ipam/rir.html:10
|
||||
msgid "Add Aggregate"
|
||||
msgstr ""
|
||||
@@ -16587,7 +16586,7 @@ msgstr ""
|
||||
msgid "Missing required value for static query param: '{static_params}'"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/utilities/forms/widgets/modifiers.py:141
|
||||
#: netbox/utilities/forms/widgets/modifiers.py:148
|
||||
msgid "(automatically set)"
|
||||
msgstr ""
|
||||
|
||||
@@ -16727,17 +16726,17 @@ msgstr ""
|
||||
msgid "{value} is not a valid regular expression."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/utilities/views.py:76
|
||||
#: netbox/utilities/views.py:80
|
||||
#, python-brace-format
|
||||
msgid "{self.__class__.__name__} must implement get_required_permission()"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/utilities/views.py:112
|
||||
#: netbox/utilities/views.py:116
|
||||
#, python-brace-format
|
||||
msgid "{class_name} must implement get_required_permission()"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/utilities/views.py:136
|
||||
#: netbox/utilities/views.py:140
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"{class_name} has no queryset defined. ObjectPermissionRequiredMixin may only "
|
||||
|
||||
@@ -5,6 +5,7 @@ from ..utils import add_blank_choice
|
||||
|
||||
__all__ = (
|
||||
'BulkEditNullBooleanSelect',
|
||||
'ClearableSelect',
|
||||
'ColorSelect',
|
||||
'HTMXSelect',
|
||||
'SelectWithPK',
|
||||
@@ -28,6 +29,21 @@ class BulkEditNullBooleanSelect(forms.NullBooleanSelect):
|
||||
)
|
||||
|
||||
|
||||
class ClearableSelect(forms.Select):
|
||||
"""
|
||||
A Select widget that will be automatically cleared when one or more required fields are cleared.
|
||||
|
||||
Args:
|
||||
requires_fields: A list of field names that this field depends on. When any of these fields
|
||||
are cleared, this field will also be cleared automatically via JavaScript.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, requires_fields=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if requires_fields:
|
||||
self.attrs['data-requires-fields'] = ','.join(requires_fields)
|
||||
|
||||
|
||||
class ColorSelect(forms.Select):
|
||||
"""
|
||||
Extends the built-in Select widget to colorize each <option>.
|
||||
|
||||
@@ -252,3 +252,16 @@ def isodatetime(value, spec='seconds'):
|
||||
else:
|
||||
return ''
|
||||
return mark_safe(f'<span title="{naturaltime(value)}">{text}</span>')
|
||||
|
||||
|
||||
@register.filter
|
||||
def truncate_middle(value, length):
|
||||
if len(value) <= length:
|
||||
return value
|
||||
|
||||
# Calculate split points for the two parts
|
||||
half_len = (length - 1) // 2 # 1 for the ellipsis
|
||||
first_part = value[:half_len]
|
||||
second_part = value[len(value) - (length - 1 - half_len):]
|
||||
|
||||
return mark_safe(f"{first_part}…{second_part}")
|
||||
|
||||
Reference in New Issue
Block a user