mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-22 11:38:45 -06:00
Compare commits
1 Commits
19221-midd
...
15801-vlan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af8e53d8fb |
43
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
43
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
@@ -1,43 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -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.filename|truncate_middle:16 }}</a>
|
||||
<a href="{{ record.get_absolute_url }}">{{ record }}</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 AutoSyncRecord, DataSource, ObjectType
|
||||
from core.models import 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,53 +754,3 @@ 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")
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import Interface
|
||||
from dcim.tables.template_code import INTERFACE_LINKTERMINATION, LINKTERMINATION
|
||||
from ipam.models import *
|
||||
from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
|
||||
from tenancy.tables import TenancyColumnsMixin, TenantColumn
|
||||
@@ -159,11 +160,26 @@ class VLANDevicesTable(VLANMembersTable):
|
||||
actions = columns.ActionsColumn(
|
||||
actions=('edit',)
|
||||
)
|
||||
link_peer = columns.TemplateColumn(
|
||||
accessor='link_peers',
|
||||
template_code=LINKTERMINATION,
|
||||
orderable=False,
|
||||
verbose_name=_('Link Peers'),
|
||||
)
|
||||
|
||||
# Override PathEndpointTable.connection to accommodate virtual circuits
|
||||
connection = columns.TemplateColumn(
|
||||
accessor='_path__destinations',
|
||||
template_code=INTERFACE_LINKTERMINATION,
|
||||
orderable=False,
|
||||
verbose_name=_('Connection'),
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Interface
|
||||
fields = ('device', 'name', 'tagged', 'actions')
|
||||
exclude = ('id', )
|
||||
fields = ('device', 'name', 'link_peer', 'connection', 'tagged', 'actions')
|
||||
default_columns = ('device', 'name', 'connection', 'tagged', 'actions')
|
||||
exclude = ('id',)
|
||||
|
||||
|
||||
class VLANVirtualMachinesTable(VLANMembersTable):
|
||||
|
||||
@@ -569,6 +569,7 @@ class SyncedDataMixin(models.Model):
|
||||
)
|
||||
else:
|
||||
AutoSyncRecord.objects.filter(
|
||||
datafile=self.data_file,
|
||||
object_type=object_type,
|
||||
object_id=self.pk
|
||||
).delete()
|
||||
@@ -581,6 +582,7 @@ 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()
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
{% 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" %}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{% 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>
|
||||
@@ -1,20 +0,0 @@
|
||||
{% 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,8 +3,6 @@
|
||||
|
||||
{% 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,6 +6,38 @@
|
||||
<button class="btn btn-outline-secondary toggle-depth" type="button">
|
||||
{% trans "Hide Depth Indicators" %}
|
||||
</button>
|
||||
{% include 'ipam/inc/max_depth.html' %}
|
||||
{% include 'ipam/inc/max_length.html' %}
|
||||
<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>
|
||||
{% endblock %}
|
||||
|
||||
@@ -252,16 +252,3 @@ 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