mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-06 01:17:16 +02:00
Compare commits
2 Commits
20924-plug
...
21455-sql-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b1f4ab51a | ||
|
|
84502e80d0 |
@@ -3,7 +3,10 @@
|
||||
!!! note "New in NetBox v4.5"
|
||||
All UI components described here were introduced in NetBox v4.5. Be sure to set the minimum NetBox version to 4.5.0 for your plugin before incorporating any of these resources.
|
||||
|
||||
To simplify the process of designing your plugin's user interface, and to encourage a consistent look and feel throughout the entire application, NetBox provides a set of components that enable programmatic UI design. These make it possible to declare complex page layouts with little or no custom HTML.
|
||||
!!! danger "Beta Feature"
|
||||
UI components are considered a beta feature, and are still under active development. Please be aware that the API for resources on this page is subject to change in future releases.
|
||||
|
||||
To simply the process of designing your plugin's user interface, and to encourage a consistent look and feel throughout the entire application, NetBox provides a set of components that enable programmatic UI design. These make it possible to declare complex page layouts with little or no custom HTML.
|
||||
|
||||
## Page Layout
|
||||
|
||||
@@ -74,7 +77,7 @@ class RecentChangesPanel(Panel):
|
||||
}
|
||||
```
|
||||
|
||||
NetBox also includes a set of panels suited for specific uses, such as display object details or embedding a table of related objects. These are listed below.
|
||||
NetBox also includes a set of panels suite for specific uses, such as display object details or embedding a table of related objects. These are listed below.
|
||||
|
||||
::: netbox.ui.panels.Panel
|
||||
|
||||
@@ -82,6 +85,26 @@ NetBox also includes a set of panels suited for specific uses, such as display o
|
||||
|
||||
::: netbox.ui.panels.ObjectAttributesPanel
|
||||
|
||||
#### Object Attributes
|
||||
|
||||
The following classes are available to represent object attributes within an ObjectAttributesPanel. Additionally, plugins can subclass `netbox.ui.attrs.ObjectAttribute` to create custom classes.
|
||||
|
||||
| Class | Description |
|
||||
|--------------------------------------|--------------------------------------------------|
|
||||
| `netbox.ui.attrs.AddressAttr` | A physical or mailing address. |
|
||||
| `netbox.ui.attrs.BooleanAttr` | A boolean value |
|
||||
| `netbox.ui.attrs.ColorAttr` | A color expressed in RGB |
|
||||
| `netbox.ui.attrs.ChoiceAttr` | A selection from a set of choices |
|
||||
| `netbox.ui.attrs.GPSCoordinatesAttr` | GPS coordinates (latitude and longitude) |
|
||||
| `netbox.ui.attrs.ImageAttr` | An attached image (displays the image) |
|
||||
| `netbox.ui.attrs.NestedObjectAttr` | A related nested object |
|
||||
| `netbox.ui.attrs.NumericAttr` | An integer or float value |
|
||||
| `netbox.ui.attrs.RelatedObjectAttr` | A related object |
|
||||
| `netbox.ui.attrs.TemplatedAttr` | Renders an attribute using a custom template |
|
||||
| `netbox.ui.attrs.TextAttr` | A string (text) value |
|
||||
| `netbox.ui.attrs.TimezoneAttr` | A timezone with annotated offset |
|
||||
| `netbox.ui.attrs.UtilizationAttr` | A numeric value expressed as a utilization graph |
|
||||
|
||||
::: netbox.ui.panels.OrganizationalObjectPanel
|
||||
|
||||
::: netbox.ui.panels.NestedGroupObjectPanel
|
||||
@@ -96,13 +119,9 @@ NetBox also includes a set of panels suited for specific uses, such as display o
|
||||
|
||||
::: netbox.ui.panels.TemplatePanel
|
||||
|
||||
::: netbox.ui.panels.TextCodePanel
|
||||
|
||||
::: netbox.ui.panels.ContextTablePanel
|
||||
|
||||
::: netbox.ui.panels.PluginContentPanel
|
||||
|
||||
### Panel Actions
|
||||
## Panel Actions
|
||||
|
||||
Each panel may have actions associated with it. These render as links or buttons within the panel header, opposite the panel's title. For example, a common use case is to include an "Add" action on a panel which displays a list of objects. Below is an example of this.
|
||||
|
||||
@@ -127,60 +146,3 @@ panels.ObjectsTablePanel(
|
||||
::: netbox.ui.actions.AddObject
|
||||
|
||||
::: netbox.ui.actions.CopyContent
|
||||
|
||||
## Object Attributes
|
||||
|
||||
The following classes are available to represent object attributes within an ObjectAttributesPanel. Additionally, plugins can subclass `netbox.ui.attrs.ObjectAttribute` to create custom classes.
|
||||
|
||||
| Class | Description |
|
||||
|------------------------------------------|--------------------------------------------------|
|
||||
| `netbox.ui.attrs.AddressAttr` | A physical or mailing address. |
|
||||
| `netbox.ui.attrs.BooleanAttr` | A boolean value |
|
||||
| `netbox.ui.attrs.ChoiceAttr` | A selection from a set of choices |
|
||||
| `netbox.ui.attrs.ColorAttr` | A color expressed in RGB |
|
||||
| `netbox.ui.attrs.DateTimeAttr` | A date or datetime value |
|
||||
| `netbox.ui.attrs.GenericForeignKeyAttr` | A related object via a generic foreign key |
|
||||
| `netbox.ui.attrs.GPSCoordinatesAttr` | GPS coordinates (latitude and longitude) |
|
||||
| `netbox.ui.attrs.ImageAttr` | An attached image (displays the image) |
|
||||
| `netbox.ui.attrs.NestedObjectAttr` | A related nested object (includes ancestors) |
|
||||
| `netbox.ui.attrs.NumericAttr` | An integer or float value |
|
||||
| `netbox.ui.attrs.RelatedObjectAttr` | A related object |
|
||||
| `netbox.ui.attrs.RelatedObjectListAttr` | A list of related objects |
|
||||
| `netbox.ui.attrs.TemplatedAttr` | Renders an attribute using a custom template |
|
||||
| `netbox.ui.attrs.TextAttr` | A string (text) value |
|
||||
| `netbox.ui.attrs.TimezoneAttr` | A timezone with annotated offset |
|
||||
| `netbox.ui.attrs.UtilizationAttr` | A numeric value expressed as a utilization graph |
|
||||
|
||||
::: netbox.ui.attrs.ObjectAttribute
|
||||
|
||||
::: netbox.ui.attrs.AddressAttr
|
||||
|
||||
::: netbox.ui.attrs.BooleanAttr
|
||||
|
||||
::: netbox.ui.attrs.ChoiceAttr
|
||||
|
||||
::: netbox.ui.attrs.ColorAttr
|
||||
|
||||
::: netbox.ui.attrs.DateTimeAttr
|
||||
|
||||
::: netbox.ui.attrs.GenericForeignKeyAttr
|
||||
|
||||
::: netbox.ui.attrs.GPSCoordinatesAttr
|
||||
|
||||
::: netbox.ui.attrs.ImageAttr
|
||||
|
||||
::: netbox.ui.attrs.NestedObjectAttr
|
||||
|
||||
::: netbox.ui.attrs.NumericAttr
|
||||
|
||||
::: netbox.ui.attrs.RelatedObjectAttr
|
||||
|
||||
::: netbox.ui.attrs.RelatedObjectListAttr
|
||||
|
||||
::: netbox.ui.attrs.TemplatedAttr
|
||||
|
||||
::: netbox.ui.attrs.TextAttr
|
||||
|
||||
::: netbox.ui.attrs.TimezoneAttr
|
||||
|
||||
::: netbox.ui.attrs.UtilizationAttr
|
||||
|
||||
35
netbox/circuits/migrations/0057_default_ordering_indexes.py
Normal file
35
netbox/circuits/migrations/0057_default_ordering_indexes.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0056_gfk_indexes'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('dcim', '0230_interface_rf_channel_frequency_precision'),
|
||||
('extras', '0136_customfield_validation_schema'),
|
||||
('tenancy', '0023_add_mptt_tree_indexes'),
|
||||
('users', '0015_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='circuit',
|
||||
index=models.Index(fields=['provider', 'provider_account', 'cid'], name='circuits_ci_provide_a0c42c_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='circuitgroupassignment',
|
||||
index=models.Index(
|
||||
fields=['group', 'member_type', 'member_id', 'priority', 'id'], name='circuits_ci_group_i_2f8327_idx'
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='virtualcircuit',
|
||||
index=models.Index(
|
||||
fields=['provider_network', 'provider_account', 'cid'], name='circuits_vi_provide_989efa_idx'
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='virtualcircuittermination',
|
||||
index=models.Index(fields=['virtual_circuit', 'role', 'id'], name='circuits_vi_virtual_4b5c0c_idx'),
|
||||
),
|
||||
]
|
||||
@@ -144,6 +144,9 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, DistanceMixin, PrimaryModel)
|
||||
name='%(app_label)s_%(class)s_unique_provideraccount_cid'
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('provider', 'provider_account', 'cid')), # Default ordering
|
||||
)
|
||||
verbose_name = _('circuit')
|
||||
verbose_name_plural = _('circuits')
|
||||
|
||||
@@ -221,6 +224,9 @@ class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin,
|
||||
name='%(app_label)s_%(class)s_unique_member_group'
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('group', 'member_type', 'member_id', 'priority', 'id')), # Default ordering
|
||||
)
|
||||
verbose_name = _('Circuit group assignment')
|
||||
verbose_name_plural = _('Circuit group assignments')
|
||||
|
||||
|
||||
@@ -97,6 +97,9 @@ class VirtualCircuit(ContactsMixin, PrimaryModel):
|
||||
name='%(app_label)s_%(class)s_unique_provideraccount_cid'
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('provider_network', 'provider_account', 'cid')), # Default ordering
|
||||
)
|
||||
verbose_name = _('virtual circuit')
|
||||
verbose_name_plural = _('virtual circuits')
|
||||
|
||||
@@ -150,6 +153,9 @@ class VirtualCircuitTermination(
|
||||
|
||||
class Meta:
|
||||
ordering = ['virtual_circuit', 'role', 'pk']
|
||||
indexes = (
|
||||
models.Index(fields=('virtual_circuit', 'role', 'id')), # Default ordering
|
||||
)
|
||||
verbose_name = _('virtual circuit termination')
|
||||
verbose_name_plural = _('virtual circuit terminations')
|
||||
|
||||
|
||||
@@ -13,9 +13,13 @@ class CircuitCircuitTerminationPanel(panels.ObjectPanel):
|
||||
template_name = 'circuits/panels/circuit_circuit_termination.html'
|
||||
title = _('Termination')
|
||||
|
||||
def __init__(self, side, accessor=None, **kwargs):
|
||||
super().__init__(accessor=accessor, **kwargs)
|
||||
self.side = side
|
||||
def __init__(self, accessor=None, side=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if accessor is not None:
|
||||
self.accessor = accessor
|
||||
if side is not None:
|
||||
self.side = side
|
||||
|
||||
def get_context(self, context):
|
||||
return {
|
||||
@@ -54,26 +58,6 @@ class CircuitGroupAssignmentsPanel(panels.ObjectsTablePanel):
|
||||
)
|
||||
|
||||
|
||||
class CircuitTerminationPanel(panels.ObjectAttributesPanel):
|
||||
title = _('Circuit Termination')
|
||||
circuit = attrs.RelatedObjectAttr('circuit', linkify=True)
|
||||
provider = attrs.RelatedObjectAttr('circuit.provider', linkify=True)
|
||||
termination = attrs.GenericForeignKeyAttr('termination', linkify=True, label=_('Termination point'))
|
||||
connection = attrs.TemplatedAttr(
|
||||
'pk',
|
||||
template_name='circuits/circuit_termination/attrs/connection.html',
|
||||
label=_('Connection'),
|
||||
)
|
||||
speed = attrs.TemplatedAttr(
|
||||
'port_speed',
|
||||
template_name='circuits/circuit_termination/attrs/speed.html',
|
||||
label=_('Speed'),
|
||||
)
|
||||
xconnect_id = attrs.TextAttr('xconnect_id', label=_('Cross-Connect'), style='font-monospace')
|
||||
pp_info = attrs.TextAttr('pp_info', label=_('Patch Panel/Port'))
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class CircuitGroupPanel(panels.OrganizationalObjectPanel):
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from netbox.ui import actions, layout
|
||||
from netbox.ui.panels import (
|
||||
CommentsPanel,
|
||||
ObjectsTablePanel,
|
||||
Panel,
|
||||
RelatedObjectsPanel,
|
||||
)
|
||||
from netbox.views import generic
|
||||
@@ -507,7 +508,10 @@ class CircuitTerminationView(generic.ObjectView):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.CircuitTerminationPanel(),
|
||||
Panel(
|
||||
template_name='circuits/panels/circuit_termination.html',
|
||||
title=_('Circuit Termination'),
|
||||
)
|
||||
],
|
||||
right_panels=[
|
||||
CustomFieldsPanel(),
|
||||
|
||||
21
netbox/core/migrations/0022_default_ordering_indexes.py
Normal file
21
netbox/core/migrations/0022_default_ordering_indexes.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('core', '0021_job_queue_name'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='configrevision',
|
||||
index=models.Index(fields=['-created'], name='core_config_created_ef9552_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='job',
|
||||
index=models.Index(fields=['-created'], name='core_job_created_efa7cb_idx'),
|
||||
),
|
||||
]
|
||||
@@ -37,6 +37,9 @@ class ConfigRevision(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created']
|
||||
indexes = (
|
||||
models.Index(fields=('-created',)), # Default ordering
|
||||
)
|
||||
verbose_name = _('config revision')
|
||||
verbose_name_plural = _('config revisions')
|
||||
constraints = [
|
||||
|
||||
@@ -133,6 +133,7 @@ class Job(models.Model):
|
||||
class Meta:
|
||||
ordering = ['-created']
|
||||
indexes = (
|
||||
models.Index(fields=('-created',)), # Default ordering
|
||||
models.Index(fields=('object_type', 'object_id')),
|
||||
)
|
||||
verbose_name = _('job')
|
||||
|
||||
@@ -192,14 +192,8 @@ class DataFileView(generic.ObjectView):
|
||||
layout.Column(
|
||||
panels.DataFilePanel(),
|
||||
panels.DataFileContentPanel(),
|
||||
PluginContentPanel('left_page'),
|
||||
),
|
||||
),
|
||||
layout.Row(
|
||||
layout.Column(
|
||||
PluginContentPanel('full_width_page'),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
78
netbox/dcim/migrations/0231_default_ordering_indexes.py
Normal file
78
netbox/dcim/migrations/0231_default_ordering_indexes.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('dcim', '0230_interface_rf_channel_frequency_precision'),
|
||||
('extras', '0136_customfield_validation_schema'),
|
||||
('ipam', '0088_rename_vlangroup_total_vlan_ids'),
|
||||
('tenancy', '0023_add_mptt_tree_indexes'),
|
||||
('users', '0015_owner'),
|
||||
('virtualization', '0054_virtualmachinetype'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='consoleporttemplate',
|
||||
index=models.Index(fields=['device_type', 'module_type', 'name'], name='dcim_consol_device__101ed5_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='consoleserverporttemplate',
|
||||
index=models.Index(fields=['device_type', 'module_type', 'name'], name='dcim_consol_device__a901e6_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='device',
|
||||
index=models.Index(fields=['name', 'id'], name='dcim_device_name_c27913_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='frontporttemplate',
|
||||
index=models.Index(fields=['device_type', 'module_type', 'name'], name='dcim_frontp_device__ec2ffb_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='interfacetemplate',
|
||||
index=models.Index(fields=['device_type', 'module_type', 'name'], name='dcim_interf_device__601012_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='macaddress',
|
||||
index=models.Index(fields=['mac_address', 'id'], name='dcim_macadd_mac_add_f2662a_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='modulebaytemplate',
|
||||
index=models.Index(fields=['device_type', 'module_type', 'name'], name='dcim_module_device__9eabad_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='moduletype',
|
||||
index=models.Index(fields=['profile', 'manufacturer', 'model'], name='dcim_module_profile_868277_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='poweroutlettemplate',
|
||||
index=models.Index(fields=['device_type', 'module_type', 'name'], name='dcim_powero_device__b83a8f_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='powerporttemplate',
|
||||
index=models.Index(fields=['device_type', 'module_type', 'name'], name='dcim_powerp_device__6c25da_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='rack',
|
||||
index=models.Index(fields=['site', 'location', 'name', 'id'], name='dcim_rack_site_id_715040_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='rackreservation',
|
||||
index=models.Index(fields=['created', 'id'], name='dcim_rackre_created_84f02e_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='rearporttemplate',
|
||||
index=models.Index(fields=['device_type', 'module_type', 'name'], name='dcim_rearpo_device__27f194_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='virtualchassis',
|
||||
index=models.Index(fields=['name'], name='dcim_virtua_name_2dc5cd_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='virtualdevicecontext',
|
||||
index=models.Index(fields=['name'], name='dcim_virtua_name_079d4d_idx'),
|
||||
),
|
||||
]
|
||||
@@ -143,6 +143,9 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
||||
name='%(app_label)s_%(class)s_unique_module_type_name'
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('device_type', 'module_type', 'name')), # Default ordering
|
||||
)
|
||||
|
||||
def to_objectchange(self, action):
|
||||
objectchange = super().to_objectchange(action)
|
||||
|
||||
@@ -737,6 +737,9 @@ class Device(
|
||||
|
||||
class Meta:
|
||||
ordering = ('name', 'pk') # Name may be null
|
||||
indexes = (
|
||||
models.Index(fields=('name', 'id')), # Default ordering
|
||||
)
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
Lower('name'), 'site', 'tenant',
|
||||
@@ -1184,6 +1187,9 @@ class VirtualChassis(PrimaryModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
indexes = (
|
||||
models.Index(fields=('name',)), # Default ordering
|
||||
)
|
||||
verbose_name = _('virtual chassis')
|
||||
verbose_name_plural = _('virtual chassis')
|
||||
|
||||
@@ -1290,6 +1296,9 @@ class VirtualDeviceContext(PrimaryModel):
|
||||
name='%(app_label)s_%(class)s_device_name'
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('name',)), # Default ordering
|
||||
)
|
||||
verbose_name = _('virtual device context')
|
||||
verbose_name_plural = _('virtual device contexts')
|
||||
|
||||
@@ -1356,6 +1365,7 @@ class MACAddress(PrimaryModel):
|
||||
class Meta:
|
||||
ordering = ('mac_address', 'pk')
|
||||
indexes = (
|
||||
models.Index(fields=('mac_address', 'id')), # Default ordering
|
||||
models.Index(fields=('assigned_object_type', 'assigned_object_id')),
|
||||
)
|
||||
verbose_name = _('MAC address')
|
||||
|
||||
@@ -113,6 +113,9 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
||||
name='%(app_label)s_%(class)s_unique_manufacturer_model'
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('profile', 'manufacturer', 'model')), # Default ordering
|
||||
)
|
||||
verbose_name = _('module type')
|
||||
verbose_name_plural = _('module types')
|
||||
|
||||
|
||||
@@ -390,6 +390,9 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, TrackingModelMixin, RackBase):
|
||||
name='%(app_label)s_%(class)s_unique_location_facility_id'
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('site', 'location', 'name', 'id')), # Default ordering
|
||||
)
|
||||
verbose_name = _('rack')
|
||||
verbose_name_plural = _('racks')
|
||||
|
||||
@@ -738,6 +741,9 @@ class RackReservation(PrimaryModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ['created', 'pk']
|
||||
indexes = (
|
||||
models.Index(fields=('created', 'id')), # Default ordering
|
||||
)
|
||||
verbose_name = _('rack reservation')
|
||||
verbose_name_plural = _('rack reservations')
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.ui import actions, attrs, panels
|
||||
@@ -74,7 +75,7 @@ class RackReservationPanel(panels.ObjectAttributesPanel):
|
||||
unit_count = attrs.TextAttr('unit_count', label=_("Total U's"))
|
||||
status = attrs.ChoiceAttr('status')
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
user = attrs.RelatedObjectAttr('user', linkify=True)
|
||||
user = attrs.RelatedObjectAttr('user')
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
@@ -219,8 +220,8 @@ class PowerPortPanel(panels.ObjectAttributesPanel):
|
||||
label = attrs.TextAttr('label')
|
||||
type = attrs.ChoiceAttr('type')
|
||||
description = attrs.TextAttr('description')
|
||||
maximum_draw = attrs.TextAttr('maximum_draw', format_string='{}W')
|
||||
allocated_draw = attrs.TextAttr('allocated_draw', format_string='{}W')
|
||||
maximum_draw = attrs.TextAttr('maximum_draw')
|
||||
allocated_draw = attrs.TextAttr('allocated_draw')
|
||||
|
||||
|
||||
class PowerOutletPanel(panels.ObjectAttributesPanel):
|
||||
@@ -243,7 +244,7 @@ class FrontPortPanel(panels.ObjectAttributesPanel):
|
||||
label = attrs.TextAttr('label')
|
||||
type = attrs.ChoiceAttr('type')
|
||||
color = attrs.ColorAttr('color')
|
||||
positions = attrs.NumericAttr('positions')
|
||||
positions = attrs.TextAttr('positions')
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
@@ -254,7 +255,7 @@ class RearPortPanel(panels.ObjectAttributesPanel):
|
||||
label = attrs.TextAttr('label')
|
||||
type = attrs.ChoiceAttr('type')
|
||||
color = attrs.ColorAttr('color')
|
||||
positions = attrs.NumericAttr('positions')
|
||||
positions = attrs.TextAttr('positions')
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
@@ -267,15 +268,6 @@ class ModuleBayPanel(panels.ObjectAttributesPanel):
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class InstalledModulePanel(panels.ObjectAttributesPanel):
|
||||
title = _('Installed Module')
|
||||
module = attrs.RelatedObjectAttr('installed_module', linkify=True)
|
||||
manufacturer = attrs.RelatedObjectAttr('installed_module.module_type.manufacturer', linkify=True)
|
||||
module_type = attrs.RelatedObjectAttr('installed_module.module_type', linkify=True)
|
||||
serial = attrs.TextAttr('installed_module.serial', label=_('Serial number'), style='font-monospace')
|
||||
asset_tag = attrs.TextAttr('installed_module.asset_tag', style='font-monospace')
|
||||
|
||||
|
||||
class DeviceBayPanel(panels.ObjectAttributesPanel):
|
||||
device = attrs.RelatedObjectAttr('device', linkify=True)
|
||||
name = attrs.TextAttr('name')
|
||||
@@ -283,12 +275,6 @@ class DeviceBayPanel(panels.ObjectAttributesPanel):
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class InstalledDevicePanel(panels.ObjectAttributesPanel):
|
||||
title = _('Installed Device')
|
||||
device = attrs.RelatedObjectAttr('installed_device', linkify=True)
|
||||
device_type = attrs.RelatedObjectAttr('installed_device.device_type')
|
||||
|
||||
|
||||
class InventoryItemPanel(panels.ObjectAttributesPanel):
|
||||
device = attrs.RelatedObjectAttr('device', linkify=True)
|
||||
parent = attrs.RelatedObjectAttr('parent', linkify=True, label=_('Parent item'))
|
||||
@@ -407,6 +393,10 @@ class ConnectionPanel(panels.ObjectPanel):
|
||||
'show_endpoints': self.show_endpoints,
|
||||
}
|
||||
|
||||
def render(self, context):
|
||||
ctx = self.get_context(context)
|
||||
return render_to_string(self.template_name, ctx, request=ctx.get('request'))
|
||||
|
||||
|
||||
class InventoryItemsPanel(panels.ObjectPanel):
|
||||
"""
|
||||
@@ -424,6 +414,10 @@ class InventoryItemsPanel(panels.ObjectPanel):
|
||||
),
|
||||
]
|
||||
|
||||
def render(self, context):
|
||||
ctx = self.get_context(context)
|
||||
return render_to_string(self.template_name, ctx, request=ctx.get('request'))
|
||||
|
||||
|
||||
class VirtualChassisMembersPanel(panels.ObjectPanel):
|
||||
"""
|
||||
@@ -457,8 +451,10 @@ class VirtualChassisMembersPanel(panels.ObjectPanel):
|
||||
'vc_members': context.get('vc_members'),
|
||||
}
|
||||
|
||||
def should_render(self, context):
|
||||
return bool(context.get('vc_members'))
|
||||
def render(self, context):
|
||||
if not context.get('vc_members'):
|
||||
return ''
|
||||
return super().render(context)
|
||||
|
||||
|
||||
class PowerUtilizationPanel(panels.ObjectPanel):
|
||||
@@ -474,9 +470,11 @@ class PowerUtilizationPanel(panels.ObjectPanel):
|
||||
'vc_members': context.get('vc_members'),
|
||||
}
|
||||
|
||||
def should_render(self, context):
|
||||
def render(self, context):
|
||||
obj = context['object']
|
||||
return obj.powerports.exists() and obj.poweroutlets.exists()
|
||||
if not obj.powerports.exists() or not obj.poweroutlets.exists():
|
||||
return ''
|
||||
return super().render(context)
|
||||
|
||||
|
||||
class InterfacePanel(panels.ObjectAttributesPanel):
|
||||
@@ -487,7 +485,7 @@ class InterfacePanel(panels.ObjectAttributesPanel):
|
||||
type = attrs.ChoiceAttr('type')
|
||||
speed = attrs.TemplatedAttr('speed', template_name='dcim/interface/attrs/speed.html', label=_('Speed'))
|
||||
duplex = attrs.ChoiceAttr('duplex')
|
||||
mtu = attrs.NumericAttr('mtu', label=_('MTU'))
|
||||
mtu = attrs.TextAttr('mtu', label=_('MTU'))
|
||||
enabled = attrs.BooleanAttr('enabled')
|
||||
mgmt_only = attrs.BooleanAttr('mgmt_only', label=_('Management only'))
|
||||
description = attrs.TextAttr('description')
|
||||
@@ -496,7 +494,7 @@ class InterfacePanel(panels.ObjectAttributesPanel):
|
||||
mode = attrs.ChoiceAttr('mode', label=_('802.1Q mode'))
|
||||
qinq_svlan = attrs.RelatedObjectAttr('qinq_svlan', linkify=True, label=_('Q-in-Q SVLAN'))
|
||||
untagged_vlan = attrs.RelatedObjectAttr('untagged_vlan', linkify=True, label=_('Untagged VLAN'))
|
||||
tx_power = attrs.TextAttr('tx_power', label=_('Transmit power'), format_string='{} dBm')
|
||||
tx_power = attrs.TextAttr('tx_power', label=_('Transmit power (dBm)'))
|
||||
tunnel = attrs.RelatedObjectAttr('tunnel_termination.tunnel', linkify=True, label=_('Tunnel'))
|
||||
l2vpn = attrs.RelatedObjectAttr('l2vpn_termination.l2vpn', linkify=True, label=_('L2VPN'))
|
||||
|
||||
@@ -529,9 +527,12 @@ class InterfaceConnectionPanel(panels.ObjectPanel):
|
||||
template_name = 'dcim/panels/interface_connection.html'
|
||||
title = _('Connection')
|
||||
|
||||
def should_render(self, context):
|
||||
def render(self, context):
|
||||
obj = context.get('object')
|
||||
return False if (obj is None or obj.is_virtual) else True
|
||||
if obj and obj.is_virtual:
|
||||
return ''
|
||||
ctx = self.get_context(context)
|
||||
return render_to_string(self.template_name, ctx, request=ctx.get('request'))
|
||||
|
||||
|
||||
class VirtualCircuitPanel(panels.ObjectPanel):
|
||||
@@ -541,11 +542,12 @@ class VirtualCircuitPanel(panels.ObjectPanel):
|
||||
template_name = 'dcim/panels/interface_virtual_circuit.html'
|
||||
title = _('Virtual Circuit')
|
||||
|
||||
def should_render(self, context):
|
||||
def render(self, context):
|
||||
obj = context.get('object')
|
||||
if not obj or not obj.is_virtual or not hasattr(obj, 'virtual_circuit_termination'):
|
||||
return False
|
||||
return True
|
||||
return ''
|
||||
ctx = self.get_context(context)
|
||||
return render_to_string(self.template_name, ctx, request=ctx.get('request'))
|
||||
|
||||
|
||||
class InterfaceWirelessPanel(panels.ObjectPanel):
|
||||
@@ -555,9 +557,12 @@ class InterfaceWirelessPanel(panels.ObjectPanel):
|
||||
template_name = 'dcim/panels/interface_wireless.html'
|
||||
title = _('Wireless')
|
||||
|
||||
def should_render(self, context):
|
||||
def render(self, context):
|
||||
obj = context.get('object')
|
||||
return False if (obj is None or not obj.is_wireless) else True
|
||||
if not obj or not obj.is_wireless:
|
||||
return ''
|
||||
ctx = self.get_context(context)
|
||||
return render_to_string(self.template_name, ctx, request=ctx.get('request'))
|
||||
|
||||
|
||||
class WirelessLANsPanel(panels.ObjectPanel):
|
||||
@@ -567,6 +572,9 @@ class WirelessLANsPanel(panels.ObjectPanel):
|
||||
template_name = 'dcim/panels/interface_wireless_lans.html'
|
||||
title = _('Wireless LANs')
|
||||
|
||||
def should_render(self, context):
|
||||
def render(self, context):
|
||||
obj = context.get('object')
|
||||
return False if (obj is None or not obj.is_wireless) else True
|
||||
if not obj or not obj.is_wireless:
|
||||
return ''
|
||||
ctx = self.get_context(context)
|
||||
return render_to_string(self.template_name, ctx, request=ctx.get('request'))
|
||||
|
||||
@@ -27,6 +27,7 @@ from netbox.ui.panels import (
|
||||
NestedGroupObjectPanel,
|
||||
ObjectsTablePanel,
|
||||
OrganizationalObjectPanel,
|
||||
Panel,
|
||||
RelatedObjectsPanel,
|
||||
TemplatePanel,
|
||||
)
|
||||
@@ -1763,7 +1764,7 @@ class ModuleTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
TemplatePanel(
|
||||
Panel(
|
||||
title=_('Attributes'),
|
||||
template_name='dcim/panels/module_type_attributes.html',
|
||||
),
|
||||
@@ -2933,7 +2934,7 @@ class ModuleView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
TemplatePanel(
|
||||
Panel(
|
||||
title=_('Module Type'),
|
||||
template_name='dcim/panels/module_type.html',
|
||||
),
|
||||
@@ -3739,7 +3740,10 @@ class ModuleBayView(generic.ObjectView):
|
||||
],
|
||||
right_panels=[
|
||||
CustomFieldsPanel(),
|
||||
panels.InstalledModulePanel(),
|
||||
Panel(
|
||||
title=_('Installed Module'),
|
||||
template_name='dcim/panels/installed_module.html',
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -3811,7 +3815,10 @@ class DeviceBayView(generic.ObjectView):
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
panels.InstalledDevicePanel(),
|
||||
Panel(
|
||||
title=_('Installed Device'),
|
||||
template_name='dcim/panels/installed_device.html',
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -4303,11 +4310,11 @@ class CableView(generic.ObjectView):
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
TemplatePanel(
|
||||
Panel(
|
||||
title=_('Termination A'),
|
||||
template_name='dcim/panels/cable_termination_a.html',
|
||||
),
|
||||
TemplatePanel(
|
||||
Panel(
|
||||
title=_('Termination B'),
|
||||
template_name='dcim/panels/cable_termination_b.html',
|
||||
),
|
||||
|
||||
74
netbox/extras/migrations/0137_default_ordering_indexes.py
Normal file
74
netbox/extras/migrations/0137_default_ordering_indexes.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('core', '0022_default_ordering_indexes'),
|
||||
('dcim', '0231_default_ordering_indexes'),
|
||||
('extras', '0136_customfield_validation_schema'),
|
||||
('tenancy', '0023_add_mptt_tree_indexes'),
|
||||
('users', '0015_owner'),
|
||||
('virtualization', '0054_virtualmachinetype'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='bookmark',
|
||||
index=models.Index(fields=['created', 'id'], name='extras_book_created_1cb4a5_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='configcontext',
|
||||
index=models.Index(fields=['weight', 'name'], name='extras_conf_weight_ef9a81_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='configtemplate',
|
||||
index=models.Index(fields=['name'], name='extras_conf_name_e276bf_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='customfield',
|
||||
index=models.Index(fields=['group_name', 'weight', 'name'], name='extras_cust_group_n_40cb93_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='customlink',
|
||||
index=models.Index(fields=['group_name', 'weight', 'name'], name='extras_cust_group_n_5a8be0_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='exporttemplate',
|
||||
index=models.Index(fields=['name'], name='extras_expo_name_55a9af_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='imageattachment',
|
||||
index=models.Index(fields=['name', 'id'], name='extras_imag_name_23cd9f_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='journalentry',
|
||||
index=models.Index(fields=['-created'], name='extras_jour_created_ec0fac_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='notification',
|
||||
index=models.Index(fields=['-created', 'id'], name='extras_noti_created_1d5146_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='savedfilter',
|
||||
index=models.Index(fields=['weight', 'name'], name='extras_save_weight_c070c4_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='script',
|
||||
index=models.Index(fields=['module', 'name'], name='extras_scri_module__8bd99c_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='subscription',
|
||||
index=models.Index(fields=['-created', 'user'], name='extras_subs_created_034618_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='tableconfig',
|
||||
index=models.Index(fields=['weight', 'name'], name='extras_tabl_weight_7c4bb6_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='tag',
|
||||
index=models.Index(fields=['weight', 'name'], name='extras_tag_weight_d99f50_idx'),
|
||||
),
|
||||
]
|
||||
@@ -176,6 +176,9 @@ class ConfigContext(SyncedDataMixin, CloningMixin, CustomLinksMixin, OwnerMixin,
|
||||
|
||||
class Meta:
|
||||
ordering = ['weight', 'name']
|
||||
indexes = (
|
||||
models.Index(fields=('weight', 'name')), # Default ordering
|
||||
)
|
||||
verbose_name = _('config context')
|
||||
verbose_name_plural = _('config contexts')
|
||||
|
||||
@@ -294,6 +297,9 @@ class ConfigTemplate(
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
indexes = (
|
||||
models.Index(fields=('name',)), # Default ordering
|
||||
)
|
||||
verbose_name = _('config template')
|
||||
verbose_name_plural = _('config templates')
|
||||
|
||||
|
||||
@@ -274,6 +274,9 @@ class CustomField(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedMo
|
||||
|
||||
class Meta:
|
||||
ordering = ['group_name', 'weight', 'name']
|
||||
indexes = (
|
||||
models.Index(fields=('group_name', 'weight', 'name')), # Default ordering
|
||||
)
|
||||
verbose_name = _('custom field')
|
||||
verbose_name_plural = _('custom fields')
|
||||
|
||||
|
||||
@@ -356,6 +356,9 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedMod
|
||||
|
||||
class Meta:
|
||||
ordering = ['group_name', 'weight', 'name']
|
||||
indexes = (
|
||||
models.Index(fields=('group_name', 'weight', 'name')), # Default ordering
|
||||
)
|
||||
verbose_name = _('custom link')
|
||||
verbose_name_plural = _('custom links')
|
||||
|
||||
@@ -429,6 +432,9 @@ class ExportTemplate(
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
indexes = (
|
||||
models.Index(fields=('name',)), # Default ordering
|
||||
)
|
||||
verbose_name = _('export template')
|
||||
verbose_name_plural = _('export templates')
|
||||
|
||||
@@ -515,6 +521,9 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedMo
|
||||
|
||||
class Meta:
|
||||
ordering = ('weight', 'name')
|
||||
indexes = (
|
||||
models.Index(fields=('weight', 'name')), # Default ordering
|
||||
)
|
||||
verbose_name = _('saved filter')
|
||||
verbose_name_plural = _('saved filters')
|
||||
|
||||
@@ -597,6 +606,9 @@ class TableConfig(CloningMixin, ChangeLoggedModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ('weight', 'name')
|
||||
indexes = (
|
||||
models.Index(fields=('weight', 'name')), # Default ordering
|
||||
)
|
||||
verbose_name = _('table config')
|
||||
verbose_name_plural = _('table configs')
|
||||
|
||||
@@ -700,6 +712,7 @@ class ImageAttachment(ChangeLoggedModel):
|
||||
class Meta:
|
||||
ordering = ('name', 'pk') # name may be non-unique
|
||||
indexes = (
|
||||
models.Index(fields=('name', 'id')), # Default ordering
|
||||
models.Index(fields=('object_type', 'object_id')),
|
||||
)
|
||||
verbose_name = _('image attachment')
|
||||
@@ -810,6 +823,7 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
|
||||
class Meta:
|
||||
ordering = ('-created',)
|
||||
indexes = (
|
||||
models.Index(fields=('-created',)), # Default ordering
|
||||
models.Index(fields=('assigned_object_type', 'assigned_object_id')),
|
||||
)
|
||||
verbose_name = _('journal entry')
|
||||
@@ -865,6 +879,7 @@ class Bookmark(models.Model):
|
||||
class Meta:
|
||||
ordering = ('created', 'pk')
|
||||
indexes = (
|
||||
models.Index(fields=('created', 'id')), # Default ordering
|
||||
models.Index(fields=('object_type', 'object_id')),
|
||||
)
|
||||
constraints = (
|
||||
|
||||
@@ -73,6 +73,7 @@ class Notification(models.Model):
|
||||
class Meta:
|
||||
ordering = ('-created', 'pk')
|
||||
indexes = (
|
||||
models.Index(fields=('-created', 'id')), # Default ordering
|
||||
models.Index(fields=('object_type', 'object_id')),
|
||||
)
|
||||
constraints = (
|
||||
@@ -215,6 +216,7 @@ class Subscription(models.Model):
|
||||
class Meta:
|
||||
ordering = ('-created', 'user')
|
||||
indexes = (
|
||||
models.Index(fields=('-created', 'user')), # Default ordering
|
||||
models.Index(fields=('object_type', 'object_id')),
|
||||
)
|
||||
constraints = (
|
||||
|
||||
@@ -62,6 +62,9 @@ class Script(EventRulesMixin, JobsMixin):
|
||||
name='extras_script_unique_name_module'
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('module', 'name')), # Default ordering
|
||||
)
|
||||
verbose_name = _('script')
|
||||
verbose_name_plural = _('scripts')
|
||||
|
||||
|
||||
@@ -52,6 +52,9 @@ class Tag(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedModel, Tag
|
||||
|
||||
class Meta:
|
||||
ordering = ('weight', 'name')
|
||||
indexes = (
|
||||
models.Index(fields=('weight', 'name')), # Default ordering
|
||||
)
|
||||
verbose_name = _('tag')
|
||||
verbose_name_plural = _('tags')
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.ui import actions, attrs, panels
|
||||
@@ -64,8 +65,12 @@ class CustomFieldsPanel(panels.ObjectPanel):
|
||||
'custom_fields': obj.get_custom_fields_by_group(),
|
||||
}
|
||||
|
||||
def should_render(self, context):
|
||||
return bool(context['custom_fields'])
|
||||
def render(self, context):
|
||||
ctx = self.get_context(context)
|
||||
# Hide the panel if no custom fields exist
|
||||
if not ctx['custom_fields']:
|
||||
return ''
|
||||
return render_to_string(self.template_name, self.get_context(context))
|
||||
|
||||
|
||||
class ImageAttachmentsPanel(panels.ObjectsTablePanel):
|
||||
|
||||
47
netbox/ipam/migrations/0089_default_ordering_indexes.py
Normal file
47
netbox/ipam/migrations/0089_default_ordering_indexes.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('dcim', '0231_default_ordering_indexes'),
|
||||
('extras', '0137_default_ordering_indexes'),
|
||||
('ipam', '0088_rename_vlangroup_total_vlan_ids'),
|
||||
('tenancy', '0023_add_mptt_tree_indexes'),
|
||||
('users', '0015_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='aggregate',
|
||||
index=models.Index(fields=['prefix', 'id'], name='ipam_aggreg_prefix_89dd71_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='fhrpgroup',
|
||||
index=models.Index(fields=['protocol', 'group_id', 'id'], name='ipam_fhrpgr_protoco_0445ae_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='fhrpgroupassignment',
|
||||
index=models.Index(fields=['-priority', 'id'], name='ipam_fhrpgr_priorit_b76335_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='ipaddress',
|
||||
index=models.Index(fields=['address', 'id'], name='ipam_ipaddr_address_3ddeea_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='role',
|
||||
index=models.Index(fields=['weight', 'name'], name='ipam_role_weight_01396b_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='service',
|
||||
index=models.Index(fields=['protocol', 'ports', 'id'], name='ipam_servic_protoco_687d13_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='vlan',
|
||||
index=models.Index(fields=['site', 'group', 'vid', 'id'], name='ipam_vlan_site_id_985573_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='vrf',
|
||||
index=models.Index(fields=['name', 'rd', 'id'], name='ipam_vrf_name_ec911d_idx'),
|
||||
),
|
||||
]
|
||||
@@ -59,6 +59,9 @@ class FHRPGroup(PrimaryModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ['protocol', 'group_id', 'pk']
|
||||
indexes = (
|
||||
models.Index(fields=('protocol', 'group_id', 'id')), # Default ordering
|
||||
)
|
||||
verbose_name = _('FHRP group')
|
||||
verbose_name_plural = _('FHRP groups')
|
||||
|
||||
@@ -105,6 +108,7 @@ class FHRPGroupAssignment(ChangeLoggedModel):
|
||||
class Meta:
|
||||
ordering = ('-priority', 'pk')
|
||||
indexes = (
|
||||
models.Index(fields=('-priority', 'id')), # Default ordering
|
||||
models.Index(fields=('interface_type', 'interface_id')),
|
||||
)
|
||||
constraints = (
|
||||
|
||||
@@ -110,6 +110,9 @@ class Aggregate(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ('prefix', 'pk') # prefix may be non-unique
|
||||
indexes = (
|
||||
models.Index(fields=('prefix', 'id')), # Default ordering
|
||||
)
|
||||
verbose_name = _('aggregate')
|
||||
verbose_name_plural = _('aggregates')
|
||||
|
||||
@@ -200,6 +203,9 @@ class Role(OrganizationalModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ('weight', 'name')
|
||||
indexes = (
|
||||
models.Index(fields=('weight', 'name')), # Default ordering
|
||||
)
|
||||
verbose_name = _('role')
|
||||
verbose_name_plural = _('roles')
|
||||
|
||||
@@ -833,6 +839,7 @@ class IPAddress(ContactsMixin, PrimaryModel):
|
||||
class Meta:
|
||||
ordering = ('address', 'pk') # address may be non-unique
|
||||
indexes = (
|
||||
models.Index(fields=('address', 'id')), # Default ordering
|
||||
models.Index(Cast(Host('address'), output_field=IPAddressField()), name='ipam_ipaddress_host'),
|
||||
models.Index(fields=('assigned_object_type', 'assigned_object_id')),
|
||||
)
|
||||
|
||||
@@ -93,6 +93,7 @@ class Service(ContactsMixin, ServiceBase, PrimaryModel):
|
||||
|
||||
class Meta:
|
||||
indexes = (
|
||||
models.Index(fields=('protocol', 'ports', 'id')), # Default ordering
|
||||
models.Index(fields=('parent_object_type', 'parent_object_id')),
|
||||
)
|
||||
ordering = ('protocol', 'ports', 'pk') # (protocol, port) may be non-unique
|
||||
|
||||
@@ -267,6 +267,9 @@ class VLAN(PrimaryModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ('site', 'group', 'vid', 'pk') # (site, group, vid) may be non-unique
|
||||
indexes = (
|
||||
models.Index(fields=('site', 'group', 'vid', 'id')), # Default ordering
|
||||
)
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('group', 'vid'),
|
||||
|
||||
@@ -58,6 +58,9 @@ class VRF(PrimaryModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ('name', 'rd', 'pk') # (name, rd) may be non-unique
|
||||
indexes = (
|
||||
models.Index(fields=('name', 'rd', 'id')), # Default ordering
|
||||
)
|
||||
verbose_name = _('VRF')
|
||||
verbose_name_plural = _('VRFs')
|
||||
|
||||
|
||||
@@ -7,9 +7,6 @@ class VRFDisplayAttr(attrs.ObjectAttribute):
|
||||
"""
|
||||
Renders a VRF reference, displaying 'Global' when no VRF is assigned. Optionally includes
|
||||
the route distinguisher (RD).
|
||||
|
||||
Parameters:
|
||||
show_rd (bool): If true, the VRF's RD will be included. (Default: False)
|
||||
"""
|
||||
template_name = 'ipam/attrs/vrf.html'
|
||||
|
||||
@@ -17,17 +14,11 @@ class VRFDisplayAttr(attrs.ObjectAttribute):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.show_rd = show_rd
|
||||
|
||||
def get_context(self, obj, attr, value, context):
|
||||
return {
|
||||
'show_rd': self.show_rd,
|
||||
}
|
||||
|
||||
def render(self, obj, context):
|
||||
name = context['name']
|
||||
value = self.get_value(obj)
|
||||
|
||||
return render_to_string(self.template_name, {
|
||||
**self.get_context(obj, name, value, context),
|
||||
'name': name,
|
||||
**self.get_context(obj, context),
|
||||
'name': context['name'],
|
||||
'value': value,
|
||||
'show_rd': self.show_rd,
|
||||
})
|
||||
|
||||
@@ -229,9 +229,11 @@ class VLANCustomerVLANsPanel(panels.ObjectsTablePanel):
|
||||
],
|
||||
)
|
||||
|
||||
def should_render(self, context):
|
||||
def render(self, context):
|
||||
obj = context.get('object')
|
||||
return False if (obj is None or obj.qinq_role != 'svlan') else True
|
||||
if not obj or obj.qinq_role != 'svlan':
|
||||
return ''
|
||||
return super().render(context)
|
||||
|
||||
|
||||
class ServiceTemplatePanel(panels.ObjectAttributesPanel):
|
||||
|
||||
@@ -16,7 +16,6 @@ from netbox.ui.panels import (
|
||||
CommentsPanel,
|
||||
ContextTablePanel,
|
||||
ObjectsTablePanel,
|
||||
PluginContentPanel,
|
||||
RelatedObjectsPanel,
|
||||
TemplatePanel,
|
||||
)
|
||||
@@ -56,13 +55,11 @@ class VRFView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
layout.Column(
|
||||
panels.VRFPanel(),
|
||||
TagsPanel(),
|
||||
PluginContentPanel('left_page'),
|
||||
),
|
||||
layout.Column(
|
||||
RelatedObjectsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
PluginContentPanel('right_page'),
|
||||
),
|
||||
),
|
||||
layout.Row(
|
||||
@@ -73,11 +70,6 @@ class VRFView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
ContextTablePanel('export_targets_table', title=_('Export route targets')),
|
||||
),
|
||||
),
|
||||
layout.Row(
|
||||
layout.Column(
|
||||
PluginContentPanel('full_width_page'),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
@@ -177,12 +169,10 @@ class RouteTargetView(generic.ObjectView):
|
||||
layout.Column(
|
||||
panels.RouteTargetPanel(),
|
||||
TagsPanel(),
|
||||
PluginContentPanel('left_page'),
|
||||
),
|
||||
layout.Column(
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
PluginContentPanel('right_page'),
|
||||
),
|
||||
),
|
||||
layout.Row(
|
||||
@@ -217,11 +207,6 @@ class RouteTargetView(generic.ObjectView):
|
||||
),
|
||||
),
|
||||
),
|
||||
layout.Row(
|
||||
layout.Column(
|
||||
PluginContentPanel('full_width_page'),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from circuits.choices import CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
|
||||
@@ -78,7 +76,7 @@ class ChoiceAttrTest(TestCase):
|
||||
self.termination.get_role_display(),
|
||||
)
|
||||
self.assertEqual(
|
||||
attr.get_context(self.termination, 'role', attr.get_value(self.termination), {}),
|
||||
attr.get_context(self.termination, {}),
|
||||
{'bg_color': self.termination.get_role_color()},
|
||||
)
|
||||
|
||||
@@ -90,7 +88,7 @@ class ChoiceAttrTest(TestCase):
|
||||
self.termination.interface.get_type_display(),
|
||||
)
|
||||
self.assertEqual(
|
||||
attr.get_context(self.termination, 'interface.type', attr.get_value(self.termination), {}),
|
||||
attr.get_context(self.termination, {}),
|
||||
{'bg_color': None},
|
||||
)
|
||||
|
||||
@@ -102,9 +100,7 @@ class ChoiceAttrTest(TestCase):
|
||||
self.termination.virtual_circuit.get_status_display(),
|
||||
)
|
||||
self.assertEqual(
|
||||
attr.get_context(
|
||||
self.termination, 'virtual_circuit.status', attr.get_value(self.termination), {}
|
||||
),
|
||||
attr.get_context(self.termination, {}),
|
||||
{'bg_color': self.termination.virtual_circuit.get_status_color()},
|
||||
)
|
||||
|
||||
@@ -217,176 +213,3 @@ class RelatedObjectListAttrTest(TestCase):
|
||||
self.assertInHTML('<li>IKE Proposal 2</li>', rendered)
|
||||
self.assertNotIn('IKE Proposal 3', rendered)
|
||||
self.assertIn('…', rendered)
|
||||
|
||||
|
||||
class TextAttrTest(TestCase):
|
||||
|
||||
def test_get_value_with_format_string(self):
|
||||
attr = attrs.TextAttr('asn', format_string='AS{}')
|
||||
obj = SimpleNamespace(asn=65000)
|
||||
self.assertEqual(attr.get_value(obj), 'AS65000')
|
||||
|
||||
def test_get_value_without_format_string(self):
|
||||
attr = attrs.TextAttr('name')
|
||||
obj = SimpleNamespace(name='foo')
|
||||
self.assertEqual(attr.get_value(obj), 'foo')
|
||||
|
||||
def test_get_value_none_skips_format_string(self):
|
||||
attr = attrs.TextAttr('name', format_string='prefix-{}')
|
||||
obj = SimpleNamespace(name=None)
|
||||
self.assertIsNone(attr.get_value(obj))
|
||||
|
||||
def test_get_context(self):
|
||||
attr = attrs.TextAttr('name', style='text-monospace', copy_button=True)
|
||||
obj = SimpleNamespace(name='bar')
|
||||
context = attr.get_context(obj, 'name', 'bar', {})
|
||||
self.assertEqual(context['style'], 'text-monospace')
|
||||
self.assertTrue(context['copy_button'])
|
||||
|
||||
|
||||
class NumericAttrTest(TestCase):
|
||||
|
||||
def test_get_context_with_unit_accessor(self):
|
||||
attr = attrs.NumericAttr('speed', unit_accessor='speed_unit')
|
||||
obj = SimpleNamespace(speed=1000, speed_unit='Mbps')
|
||||
context = attr.get_context(obj, 'speed', 1000, {})
|
||||
self.assertEqual(context['unit'], 'Mbps')
|
||||
|
||||
def test_get_context_without_unit_accessor(self):
|
||||
attr = attrs.NumericAttr('speed')
|
||||
obj = SimpleNamespace(speed=1000)
|
||||
context = attr.get_context(obj, 'speed', 1000, {})
|
||||
self.assertIsNone(context['unit'])
|
||||
|
||||
def test_get_context_copy_button(self):
|
||||
attr = attrs.NumericAttr('speed', copy_button=True)
|
||||
obj = SimpleNamespace(speed=1000)
|
||||
context = attr.get_context(obj, 'speed', 1000, {})
|
||||
self.assertTrue(context['copy_button'])
|
||||
|
||||
|
||||
class BooleanAttrTest(TestCase):
|
||||
|
||||
def test_false_value_shown_by_default(self):
|
||||
attr = attrs.BooleanAttr('enabled')
|
||||
obj = SimpleNamespace(enabled=False)
|
||||
self.assertIs(attr.get_value(obj), False)
|
||||
|
||||
def test_false_value_hidden_when_display_false_disabled(self):
|
||||
attr = attrs.BooleanAttr('enabled', display_false=False)
|
||||
obj = SimpleNamespace(enabled=False)
|
||||
self.assertIsNone(attr.get_value(obj))
|
||||
|
||||
def test_true_value_always_shown(self):
|
||||
attr = attrs.BooleanAttr('enabled', display_false=False)
|
||||
obj = SimpleNamespace(enabled=True)
|
||||
self.assertIs(attr.get_value(obj), True)
|
||||
|
||||
|
||||
class ImageAttrTest(TestCase):
|
||||
|
||||
def test_invalid_decoding_raises_value_error(self):
|
||||
with self.assertRaises(ValueError):
|
||||
attrs.ImageAttr('image', decoding='invalid')
|
||||
|
||||
def test_default_decoding_for_lazy_image(self):
|
||||
attr = attrs.ImageAttr('image')
|
||||
self.assertTrue(attr.load_lazy)
|
||||
self.assertEqual(attr.decoding, 'async')
|
||||
|
||||
def test_default_decoding_for_non_lazy_image(self):
|
||||
attr = attrs.ImageAttr('image', load_lazy=False)
|
||||
self.assertFalse(attr.load_lazy)
|
||||
self.assertIsNone(attr.decoding)
|
||||
|
||||
def test_explicit_decoding_value(self):
|
||||
attr = attrs.ImageAttr('image', load_lazy=False, decoding='sync')
|
||||
self.assertEqual(attr.decoding, 'sync')
|
||||
|
||||
def test_get_context(self):
|
||||
attr = attrs.ImageAttr('image', load_lazy=False, decoding='async')
|
||||
obj = SimpleNamespace(image='test.png')
|
||||
context = attr.get_context(obj, 'image', 'test.png', {})
|
||||
self.assertEqual(context['decoding'], 'async')
|
||||
self.assertFalse(context['load_lazy'])
|
||||
|
||||
|
||||
class RelatedObjectAttrTest(TestCase):
|
||||
|
||||
def test_get_context_with_grouped_by(self):
|
||||
region = SimpleNamespace(name='Region 1')
|
||||
site = SimpleNamespace(name='Site 1', region=region)
|
||||
obj = SimpleNamespace(site=site)
|
||||
attr = attrs.RelatedObjectAttr('site', grouped_by='region')
|
||||
context = attr.get_context(obj, 'site', site, {})
|
||||
self.assertEqual(context['group'], region)
|
||||
|
||||
def test_get_context_without_grouped_by(self):
|
||||
site = SimpleNamespace(name='Site 1')
|
||||
obj = SimpleNamespace(site=site)
|
||||
attr = attrs.RelatedObjectAttr('site')
|
||||
context = attr.get_context(obj, 'site', site, {})
|
||||
self.assertIsNone(context['group'])
|
||||
|
||||
def test_get_context_linkify(self):
|
||||
site = SimpleNamespace(name='Site 1')
|
||||
obj = SimpleNamespace(site=site)
|
||||
attr = attrs.RelatedObjectAttr('site', linkify=True)
|
||||
context = attr.get_context(obj, 'site', site, {})
|
||||
self.assertTrue(context['linkify'])
|
||||
|
||||
|
||||
class GenericForeignKeyAttrTest(TestCase):
|
||||
|
||||
def test_get_context_content_type(self):
|
||||
value = SimpleNamespace(_meta=SimpleNamespace(verbose_name='provider'))
|
||||
obj = SimpleNamespace()
|
||||
attr = attrs.GenericForeignKeyAttr('assigned_object')
|
||||
context = attr.get_context(obj, 'assigned_object', value, {})
|
||||
self.assertEqual(context['content_type'], 'provider')
|
||||
|
||||
def test_get_context_linkify(self):
|
||||
value = SimpleNamespace(_meta=SimpleNamespace(verbose_name='provider'))
|
||||
obj = SimpleNamespace()
|
||||
attr = attrs.GenericForeignKeyAttr('assigned_object', linkify=True)
|
||||
context = attr.get_context(obj, 'assigned_object', value, {})
|
||||
self.assertTrue(context['linkify'])
|
||||
|
||||
|
||||
class GPSCoordinatesAttrTest(TestCase):
|
||||
|
||||
def test_missing_latitude_returns_placeholder(self):
|
||||
attr = attrs.GPSCoordinatesAttr()
|
||||
obj = SimpleNamespace(latitude=None, longitude=-74.006)
|
||||
self.assertEqual(attr.render(obj, {'name': 'coordinates'}), attr.placeholder)
|
||||
|
||||
def test_missing_longitude_returns_placeholder(self):
|
||||
attr = attrs.GPSCoordinatesAttr()
|
||||
obj = SimpleNamespace(latitude=40.712, longitude=None)
|
||||
self.assertEqual(attr.render(obj, {'name': 'coordinates'}), attr.placeholder)
|
||||
|
||||
def test_both_missing_returns_placeholder(self):
|
||||
attr = attrs.GPSCoordinatesAttr()
|
||||
obj = SimpleNamespace(latitude=None, longitude=None)
|
||||
self.assertEqual(attr.render(obj, {'name': 'coordinates'}), attr.placeholder)
|
||||
|
||||
|
||||
class DateTimeAttrTest(TestCase):
|
||||
|
||||
def test_default_spec(self):
|
||||
attr = attrs.DateTimeAttr('created')
|
||||
obj = SimpleNamespace(created='2024-01-01')
|
||||
context = attr.get_context(obj, 'created', '2024-01-01', {})
|
||||
self.assertEqual(context['spec'], 'seconds')
|
||||
|
||||
def test_date_spec(self):
|
||||
attr = attrs.DateTimeAttr('created', spec='date')
|
||||
obj = SimpleNamespace(created='2024-01-01')
|
||||
context = attr.get_context(obj, 'created', '2024-01-01', {})
|
||||
self.assertEqual(context['spec'], 'date')
|
||||
|
||||
def test_minutes_spec(self):
|
||||
attr = attrs.DateTimeAttr('created', spec='minutes')
|
||||
obj = SimpleNamespace(created='2024-01-01')
|
||||
context = attr.get_context(obj, 'created', '2024-01-01', {})
|
||||
self.assertEqual(context['spec'], 'minutes')
|
||||
|
||||
@@ -59,7 +59,7 @@ class PanelAction:
|
||||
"""
|
||||
# Enforce permissions
|
||||
user = context['request'].user
|
||||
if self.permissions and not user.has_perms(self.permissions):
|
||||
if not user.has_perms(self.permissions):
|
||||
return ''
|
||||
|
||||
return render_to_string(self.template_name, self.get_context(context))
|
||||
@@ -118,19 +118,19 @@ class AddObject(LinkAction):
|
||||
url_params (dict): A dictionary of arbitrary URL parameters to append to the resolved URL
|
||||
"""
|
||||
def __init__(self, model, url_params=None, **kwargs):
|
||||
# Resolve the model from its label
|
||||
if '.' not in model:
|
||||
raise ValueError(f"Invalid model label: {model}")
|
||||
# Resolve the model class from its app.name label
|
||||
try:
|
||||
self.model = apps.get_model(model)
|
||||
except LookupError:
|
||||
app_label, model_name = model.split('.')
|
||||
model = apps.get_model(app_label, model_name)
|
||||
except (ValueError, LookupError):
|
||||
raise ValueError(f"Invalid model label: {model}")
|
||||
view_name = get_viewname(model, 'add')
|
||||
|
||||
kwargs.setdefault('label', _('Add'))
|
||||
kwargs.setdefault('button_icon', 'plus-thick')
|
||||
kwargs.setdefault('permissions', [get_permission_for_model(self.model, 'add')])
|
||||
kwargs.setdefault('permissions', [get_permission_for_model(model, 'add')])
|
||||
|
||||
super().__init__(view_name=get_viewname(self.model, 'add'), url_params=url_params, **kwargs)
|
||||
super().__init__(view_name=view_name, url_params=url_params, **kwargs)
|
||||
|
||||
|
||||
class CopyContent(PanelAction):
|
||||
@@ -148,8 +148,10 @@ class CopyContent(PanelAction):
|
||||
super().__init__(**kwargs)
|
||||
self.target_id = target_id
|
||||
|
||||
def get_context(self, context):
|
||||
return {
|
||||
**super().get_context(context),
|
||||
def render(self, context):
|
||||
return render_to_string(self.template_name, {
|
||||
'target_id': self.target_id,
|
||||
}
|
||||
'label': self.label,
|
||||
'button_class': self.button_class,
|
||||
'button_icon': self.button_icon,
|
||||
})
|
||||
|
||||
@@ -29,27 +29,11 @@ PLACEHOLDER_HTML = '<span class="text-muted">—</span>'
|
||||
|
||||
IMAGE_DECODING_CHOICES = ('auto', 'async', 'sync')
|
||||
|
||||
|
||||
#
|
||||
# Mixins
|
||||
#
|
||||
|
||||
class MapURLMixin:
|
||||
_map_url = None
|
||||
|
||||
@property
|
||||
def map_url(self):
|
||||
if self._map_url is True:
|
||||
return get_config().MAPS_URL
|
||||
if self._map_url:
|
||||
return self._map_url
|
||||
return None
|
||||
|
||||
|
||||
#
|
||||
# Attributes
|
||||
#
|
||||
|
||||
|
||||
class ObjectAttribute:
|
||||
"""
|
||||
Base class for representing an attribute of an object.
|
||||
@@ -80,20 +64,17 @@ class ObjectAttribute:
|
||||
"""
|
||||
return resolve_attr_path(obj, self.accessor)
|
||||
|
||||
def get_context(self, obj, attr, value, context):
|
||||
def get_context(self, obj, context):
|
||||
"""
|
||||
Return any additional template context used to render the attribute value.
|
||||
|
||||
Parameters:
|
||||
obj (object): The object for which the attribute is being rendered
|
||||
attr (str): The name of the attribute being rendered
|
||||
value: The value of the attribute on the object
|
||||
context (dict): The root template context
|
||||
"""
|
||||
return {}
|
||||
|
||||
def render(self, obj, context):
|
||||
name = context['name']
|
||||
value = self.get_value(obj)
|
||||
|
||||
# If the value is empty, render a placeholder
|
||||
@@ -101,8 +82,8 @@ class ObjectAttribute:
|
||||
return self.placeholder
|
||||
|
||||
return render_to_string(self.template_name, {
|
||||
**self.get_context(obj, name, value, context),
|
||||
'name': name,
|
||||
**self.get_context(obj, context),
|
||||
'name': context['name'],
|
||||
'value': value,
|
||||
})
|
||||
|
||||
@@ -131,7 +112,7 @@ class TextAttr(ObjectAttribute):
|
||||
return self.format_string.format(value)
|
||||
return value
|
||||
|
||||
def get_context(self, obj, attr, value, context):
|
||||
def get_context(self, obj, context):
|
||||
return {
|
||||
'style': self.style,
|
||||
'copy_button': self.copy_button,
|
||||
@@ -153,7 +134,7 @@ class NumericAttr(ObjectAttribute):
|
||||
self.unit_accessor = unit_accessor
|
||||
self.copy_button = copy_button
|
||||
|
||||
def get_context(self, obj, attr, value, context):
|
||||
def get_context(self, obj, context):
|
||||
unit = resolve_attr_path(obj, self.unit_accessor) if self.unit_accessor else None
|
||||
return {
|
||||
'unit': unit,
|
||||
@@ -191,7 +172,7 @@ class ChoiceAttr(ObjectAttribute):
|
||||
|
||||
return resolve_attr_path(target, field_name)
|
||||
|
||||
def get_context(self, obj, attr, value, context):
|
||||
def get_context(self, obj, context):
|
||||
target, field_name = self._resolve_target(obj)
|
||||
if target is None:
|
||||
return {'bg_color': None}
|
||||
@@ -260,7 +241,7 @@ class ImageAttr(ObjectAttribute):
|
||||
decoding = 'async'
|
||||
self.decoding = decoding
|
||||
|
||||
def get_context(self, obj, attr, value, context):
|
||||
def get_context(self, obj, context):
|
||||
return {
|
||||
'decoding': self.decoding,
|
||||
'load_lazy': self.load_lazy,
|
||||
@@ -283,7 +264,8 @@ class RelatedObjectAttr(ObjectAttribute):
|
||||
self.linkify = linkify
|
||||
self.grouped_by = grouped_by
|
||||
|
||||
def get_context(self, obj, attr, value, context):
|
||||
def get_context(self, obj, context):
|
||||
value = self.get_value(obj)
|
||||
group = getattr(value, self.grouped_by, None) if self.grouped_by else None
|
||||
return {
|
||||
'linkify': self.linkify,
|
||||
@@ -318,13 +300,14 @@ class RelatedObjectListAttr(RelatedObjectAttr):
|
||||
self.max_items = max_items
|
||||
self.overflow_indicator = overflow_indicator
|
||||
|
||||
def _get_items(self, items):
|
||||
def _get_items(self, obj):
|
||||
"""
|
||||
Retrieve items from the given object using the accessor path.
|
||||
|
||||
Returns a tuple of (items, has_more) where items is a list of resolved objects
|
||||
and has_more indicates whether additional items exist beyond the max_items limit.
|
||||
"""
|
||||
items = resolve_attr_path(obj, self.accessor)
|
||||
if items is None:
|
||||
return [], False
|
||||
|
||||
@@ -339,8 +322,8 @@ class RelatedObjectListAttr(RelatedObjectAttr):
|
||||
|
||||
return items[:self.max_items], has_more
|
||||
|
||||
def get_context(self, obj, attr, value, context):
|
||||
items, has_more = self._get_items(value)
|
||||
def get_context(self, obj, context):
|
||||
items, has_more = self._get_items(obj)
|
||||
|
||||
return {
|
||||
'linkify': self.linkify,
|
||||
@@ -355,15 +338,14 @@ class RelatedObjectListAttr(RelatedObjectAttr):
|
||||
}
|
||||
|
||||
def render(self, obj, context):
|
||||
name = context['name']
|
||||
value = self.get_value(obj)
|
||||
context_data = self.get_context(obj, name, value, context)
|
||||
context = context or {}
|
||||
context_data = self.get_context(obj, context)
|
||||
|
||||
if not context_data['items']:
|
||||
return self.placeholder
|
||||
|
||||
return render_to_string(self.template_name, {
|
||||
'name': name,
|
||||
'name': context.get('name'),
|
||||
**context_data,
|
||||
})
|
||||
|
||||
@@ -384,13 +366,11 @@ class NestedObjectAttr(ObjectAttribute):
|
||||
self.linkify = linkify
|
||||
self.max_depth = max_depth
|
||||
|
||||
def get_context(self, obj, attr, value, context):
|
||||
if value is not None:
|
||||
nodes = value.get_ancestors(include_self=True)
|
||||
if self.max_depth:
|
||||
nodes = list(nodes)[-self.max_depth:]
|
||||
else:
|
||||
nodes = []
|
||||
def get_context(self, obj, context):
|
||||
value = self.get_value(obj)
|
||||
nodes = value.get_ancestors(include_self=True)
|
||||
if self.max_depth:
|
||||
nodes = list(nodes)[-self.max_depth:]
|
||||
return {
|
||||
'nodes': nodes,
|
||||
'linkify': self.linkify,
|
||||
@@ -414,35 +394,40 @@ class GenericForeignKeyAttr(ObjectAttribute):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.linkify = linkify
|
||||
|
||||
def get_context(self, obj, attr, value, context):
|
||||
content_type = value._meta.verbose_name if value is not None else None
|
||||
def get_context(self, obj, context):
|
||||
value = self.get_value(obj)
|
||||
content_type = value._meta.verbose_name
|
||||
return {
|
||||
'content_type': content_type,
|
||||
'linkify': self.linkify,
|
||||
}
|
||||
|
||||
|
||||
class AddressAttr(MapURLMixin, ObjectAttribute):
|
||||
class AddressAttr(ObjectAttribute):
|
||||
"""
|
||||
A physical or mailing address.
|
||||
|
||||
Parameters:
|
||||
map_url (bool/str): The URL to use when rendering the address. If True, the address will render as a
|
||||
hyperlink using settings.MAPS_URL.
|
||||
map_url (bool): If true, the address will render as a hyperlink using settings.MAPS_URL
|
||||
"""
|
||||
template_name = 'ui/attrs/address.html'
|
||||
|
||||
def __init__(self, *args, map_url=True, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._map_url = map_url
|
||||
if map_url is True:
|
||||
self.map_url = get_config().MAPS_URL
|
||||
elif map_url:
|
||||
self.map_url = map_url
|
||||
else:
|
||||
self.map_url = None
|
||||
|
||||
def get_context(self, obj, attr, value, context):
|
||||
def get_context(self, obj, context):
|
||||
return {
|
||||
'map_url': self.map_url,
|
||||
}
|
||||
|
||||
|
||||
class GPSCoordinatesAttr(MapURLMixin, ObjectAttribute):
|
||||
class GPSCoordinatesAttr(ObjectAttribute):
|
||||
"""
|
||||
A GPS coordinates pair comprising latitude and longitude values.
|
||||
|
||||
@@ -455,18 +440,24 @@ class GPSCoordinatesAttr(MapURLMixin, ObjectAttribute):
|
||||
label = _('GPS coordinates')
|
||||
|
||||
def __init__(self, latitude_attr='latitude', longitude_attr='longitude', map_url=True, **kwargs):
|
||||
super().__init__(accessor=latitude_attr, **kwargs)
|
||||
super().__init__(accessor=None, **kwargs)
|
||||
self.latitude_attr = latitude_attr
|
||||
self.longitude_attr = longitude_attr
|
||||
self._map_url = map_url
|
||||
if map_url is True:
|
||||
self.map_url = get_config().MAPS_URL
|
||||
elif map_url:
|
||||
self.map_url = map_url
|
||||
else:
|
||||
self.map_url = None
|
||||
|
||||
def render(self, obj, context):
|
||||
def render(self, obj, context=None):
|
||||
context = context or {}
|
||||
latitude = resolve_attr_path(obj, self.latitude_attr)
|
||||
longitude = resolve_attr_path(obj, self.longitude_attr)
|
||||
if latitude is None or longitude is None:
|
||||
return self.placeholder
|
||||
return render_to_string(self.template_name, {
|
||||
'name': context['name'],
|
||||
**context,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'map_url': self.map_url,
|
||||
@@ -487,7 +478,7 @@ class DateTimeAttr(ObjectAttribute):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.spec = spec
|
||||
|
||||
def get_context(self, obj, attr, value, context):
|
||||
def get_context(self, obj, context):
|
||||
return {
|
||||
'spec': self.spec,
|
||||
}
|
||||
@@ -513,9 +504,8 @@ class TemplatedAttr(ObjectAttribute):
|
||||
self.template_name = template_name
|
||||
self.context = context or {}
|
||||
|
||||
def get_context(self, obj, attr, value, context):
|
||||
def get_context(self, obj, context):
|
||||
return {
|
||||
**context,
|
||||
**self.context,
|
||||
'object': obj,
|
||||
}
|
||||
|
||||
@@ -21,16 +21,10 @@ class Layout:
|
||||
"""
|
||||
def __init__(self, *rows):
|
||||
for i, row in enumerate(rows):
|
||||
if not isinstance(row, Row):
|
||||
if type(row) is not Row:
|
||||
raise TypeError(f"Row {i} must be a Row instance, not {type(row)}.")
|
||||
self.rows = rows
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.rows)
|
||||
|
||||
def __repr__(self):
|
||||
return f"Layout({len(self.rows)} rows)"
|
||||
|
||||
|
||||
class Row:
|
||||
"""
|
||||
@@ -41,16 +35,10 @@ class Row:
|
||||
"""
|
||||
def __init__(self, *columns):
|
||||
for i, column in enumerate(columns):
|
||||
if not isinstance(column, Column):
|
||||
if type(column) is not Column:
|
||||
raise TypeError(f"Column {i} must be a Column instance, not {type(column)}.")
|
||||
self.columns = columns
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.columns)
|
||||
|
||||
def __repr__(self):
|
||||
return f"Row({len(self.columns)} columns)"
|
||||
|
||||
|
||||
class Column:
|
||||
"""
|
||||
@@ -58,25 +46,12 @@ class Column:
|
||||
|
||||
Parameters:
|
||||
*panels: One or more Panel instances
|
||||
width: Bootstrap grid column width (1-12). If unset, the column will expand to fill available space.
|
||||
"""
|
||||
def __init__(self, *panels, width=None):
|
||||
def __init__(self, *panels):
|
||||
for i, panel in enumerate(panels):
|
||||
if not isinstance(panel, Panel):
|
||||
raise TypeError(f"Panel {i} must be an instance of a Panel, not {type(panel)}.")
|
||||
if width is not None:
|
||||
if type(width) is not int:
|
||||
raise ValueError(f"Column width must be an integer, not {type(width)}")
|
||||
if width not in range(1, 13):
|
||||
raise ValueError(f"Column width must be an integer between 1 and 12 (got {width}).")
|
||||
self.panels = panels
|
||||
self.width = width
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.panels)
|
||||
|
||||
def __repr__(self):
|
||||
return f"Column({len(self.panels)} panels)"
|
||||
|
||||
|
||||
#
|
||||
@@ -87,7 +62,7 @@ class SimpleLayout(Layout):
|
||||
"""
|
||||
A layout with one row of two columns and a second row with one column.
|
||||
|
||||
Plugin content registered for `left_page`, `right_page`, or `full_width_page` is included automatically. Most object
|
||||
Plugin content registered for `left_page`, `right_page`, or `full_width_path` is included automatically. Most object
|
||||
views in NetBox utilize this layout.
|
||||
|
||||
```
|
||||
|
||||
@@ -45,17 +45,18 @@ class Panel:
|
||||
Parameters:
|
||||
title (str): The human-friendly title of the panel
|
||||
actions (list): An iterable of PanelActions to include in the panel header
|
||||
template_name (str): Overrides the default template name, if defined
|
||||
"""
|
||||
template_name = None
|
||||
title = None
|
||||
actions = None
|
||||
|
||||
def __init__(self, title=None, actions=None):
|
||||
def __init__(self, title=None, actions=None, template_name=None):
|
||||
if title is not None:
|
||||
self.title = title
|
||||
if actions is not None:
|
||||
self.actions = actions
|
||||
self.actions = list(self.actions) if self.actions else []
|
||||
self.actions = actions or self.actions or []
|
||||
if template_name is not None:
|
||||
self.template_name = template_name
|
||||
|
||||
def get_context(self, context):
|
||||
"""
|
||||
@@ -73,15 +74,6 @@ class Panel:
|
||||
'panel_class': self.__class__.__name__,
|
||||
}
|
||||
|
||||
def should_render(self, context):
|
||||
"""
|
||||
Determines whether the panel should render on the page. (Default: True)
|
||||
|
||||
Parameters:
|
||||
context (dict): The panel's prepared context (the return value of get_context())
|
||||
"""
|
||||
return True
|
||||
|
||||
def render(self, context):
|
||||
"""
|
||||
Render the panel as HTML.
|
||||
@@ -89,10 +81,7 @@ class Panel:
|
||||
Parameters:
|
||||
context (dict): The template context
|
||||
"""
|
||||
ctx = self.get_context(context)
|
||||
if not self.should_render(ctx):
|
||||
return ''
|
||||
return render_to_string(self.template_name, ctx, request=ctx.get('request'))
|
||||
return render_to_string(self.template_name, self.get_context(context))
|
||||
|
||||
|
||||
#
|
||||
@@ -116,15 +105,9 @@ class ObjectPanel(Panel):
|
||||
|
||||
def get_context(self, context):
|
||||
obj = resolve_attr_path(context, self.accessor)
|
||||
if self.title is not None:
|
||||
title_ = self.title
|
||||
elif obj is not None:
|
||||
title_ = title(obj._meta.verbose_name)
|
||||
else:
|
||||
title_ = None
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'title': title_,
|
||||
'title': self.title or title(obj._meta.verbose_name),
|
||||
'object': obj,
|
||||
}
|
||||
|
||||
@@ -204,7 +187,7 @@ class ObjectAttributesPanel(ObjectPanel, metaclass=ObjectAttributesPanelMeta):
|
||||
'attrs': [
|
||||
{
|
||||
'label': attr.label or self._name_to_label(name),
|
||||
'value': attr.render(ctx['object'], {'name': name, 'perms': ctx['perms']}),
|
||||
'value': attr.render(ctx['object'], {'name': name}),
|
||||
} for name, attr in self._attrs.items() if name in attr_names
|
||||
],
|
||||
}
|
||||
@@ -242,10 +225,9 @@ class CommentsPanel(ObjectPanel):
|
||||
self.field_name = field_name
|
||||
|
||||
def get_context(self, context):
|
||||
ctx = super().get_context(context)
|
||||
return {
|
||||
**ctx,
|
||||
'comments': getattr(ctx['object'], self.field_name, None),
|
||||
**super().get_context(context),
|
||||
'comments': getattr(context['object'], self.field_name),
|
||||
}
|
||||
|
||||
|
||||
@@ -267,10 +249,9 @@ class JSONPanel(ObjectPanel):
|
||||
self.actions.append(CopyContent(f'panel_{field_name}'))
|
||||
|
||||
def get_context(self, context):
|
||||
ctx = super().get_context(context)
|
||||
return {
|
||||
**ctx,
|
||||
'data': getattr(ctx['object'], self.field_name, None),
|
||||
**super().get_context(context),
|
||||
'data': getattr(context['object'], self.field_name),
|
||||
'field_name': self.field_name,
|
||||
}
|
||||
|
||||
@@ -308,25 +289,20 @@ class ObjectsTablePanel(Panel):
|
||||
def __init__(self, model, filters=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Validate the model label format
|
||||
if '.' not in model:
|
||||
# Resolve the model class from its app.name label
|
||||
try:
|
||||
app_label, model_name = model.split('.')
|
||||
self.model = apps.get_model(app_label, model_name)
|
||||
except (ValueError, LookupError):
|
||||
raise ValueError(f"Invalid model label: {model}")
|
||||
self.model_label = model
|
||||
|
||||
self.filters = filters or {}
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
try:
|
||||
return apps.get_model(self.model_label)
|
||||
except LookupError:
|
||||
raise ValueError(f"Invalid model label: {self.model_label}")
|
||||
# If no title is specified, derive one from the model name
|
||||
if self.title is None:
|
||||
self.title = title(self.model._meta.verbose_name_plural)
|
||||
|
||||
def get_context(self, context):
|
||||
model = self.model
|
||||
|
||||
# If no title is specified, derive one from the model name
|
||||
panel_title = self.title or title(model._meta.verbose_name_plural)
|
||||
|
||||
url_params = {
|
||||
k: v(context) if callable(v) else v for k, v in self.filters.items()
|
||||
}
|
||||
@@ -334,8 +310,7 @@ class ObjectsTablePanel(Panel):
|
||||
url_params['return_url'] = context['object'].get_absolute_url()
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'title': panel_title,
|
||||
'viewname': get_viewname(model, 'list'),
|
||||
'viewname': get_viewname(self.model, 'list'),
|
||||
'url_params': dict_to_querydict(url_params),
|
||||
}
|
||||
|
||||
@@ -347,17 +322,12 @@ class TemplatePanel(Panel):
|
||||
Parameters:
|
||||
template_name (str): The name of the template to render
|
||||
"""
|
||||
def __init__(self, template_name, **kwargs):
|
||||
self.template_name = template_name
|
||||
super().__init__(**kwargs)
|
||||
def __init__(self, template_name):
|
||||
super().__init__(template_name=template_name)
|
||||
|
||||
def get_context(self, context):
|
||||
# Pass the entire context to the template, but let the panel's own context take precedence
|
||||
# for panel-specific variables (title, actions, panel_class)
|
||||
return {
|
||||
**context.flatten(),
|
||||
**super().get_context(context)
|
||||
}
|
||||
def render(self, context):
|
||||
# Pass the entire context to the template
|
||||
return render_to_string(self.template_name, context.flatten())
|
||||
|
||||
|
||||
class TextCodePanel(ObjectPanel):
|
||||
@@ -372,11 +342,10 @@ class TextCodePanel(ObjectPanel):
|
||||
self.show_sync_warning = show_sync_warning
|
||||
|
||||
def get_context(self, context):
|
||||
ctx = super().get_context(context)
|
||||
return {
|
||||
**ctx,
|
||||
**super().get_context(context),
|
||||
'show_sync_warning': self.show_sync_warning,
|
||||
'value': getattr(ctx['object'], self.field_name, None),
|
||||
'value': getattr(context.get('object'), self.field_name, None),
|
||||
}
|
||||
|
||||
|
||||
@@ -392,7 +361,6 @@ class PluginContentPanel(Panel):
|
||||
self.method = method
|
||||
|
||||
def render(self, context):
|
||||
# Override the default render() method to simply embed rendered plugin content
|
||||
obj = context.get('object')
|
||||
return _get_registered_content(obj, self.method, context)
|
||||
|
||||
@@ -423,10 +391,14 @@ class ContextTablePanel(ObjectPanel):
|
||||
return context.get(self.table)
|
||||
|
||||
def get_context(self, context):
|
||||
table = self._resolve_table(context)
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'table': self._resolve_table(context),
|
||||
'table': table,
|
||||
}
|
||||
|
||||
def should_render(self, context):
|
||||
return context.get('table') is not None
|
||||
def render(self, context):
|
||||
table = self._resolve_table(context)
|
||||
if table is None:
|
||||
return ''
|
||||
return super().render(context)
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
{% load helpers i18n %}
|
||||
{% if object.mark_connected %}
|
||||
<div>
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
|
||||
<span class="text-muted">{% trans "Marked as connected" %}</span>
|
||||
</div>
|
||||
{% elif object.cable %}
|
||||
<div>
|
||||
<a class="d-block d-md-inline" href="{{ object.cable.get_absolute_url }}">{{ object.cable }}</a> {% trans "to" %}
|
||||
{% for peer in object.link_peers %}
|
||||
{% if peer.device %}
|
||||
{{ peer.device|linkify }}<br/>
|
||||
{% elif peer.circuit %}
|
||||
{{ peer.circuit|linkify }}<br/>
|
||||
{% endif %}
|
||||
{{ peer|linkify }}{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<a href="{% url 'circuits:circuittermination_trace' pk=object.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
|
||||
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i> {% trans "Trace" %}
|
||||
</a>
|
||||
{% if perms.dcim.change_cable %}
|
||||
<a href="{% url 'dcim:cable_edit' pk=object.cable.pk %}?return_url={{ object.circuit.get_absolute_url }}" title="{% trans "Edit cable" %}" class="btn btn-sm btn-warning">
|
||||
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.delete_cable %}
|
||||
<a href="{% url 'dcim:cable_delete' pk=object.cable.pk %}?return_url={{ object.circuit.get_absolute_url }}" title="{% trans "Remove cable" %}" class="btn btn-sm btn-danger">
|
||||
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i> {% trans "Disconnect" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<div class="dropdown">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&return_url={{ object.get_absolute_url }}">{% trans "Interface" %}</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}">{% trans "Front Port" %}</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}">{% trans "Rear Port" %}</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}">{% trans "Circuit Termination" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
@@ -1,8 +0,0 @@
|
||||
{% load helpers i18n %}
|
||||
{% if object.upstream_speed %}
|
||||
<i class="mdi mdi-arrow-down-bold" title="{% trans "Downstream" %}"></i> {{ value|humanize_speed }}
|
||||
<i class="mdi mdi-slash-forward"></i>
|
||||
<i class="mdi mdi-arrow-up-bold" title="{% trans "Upstream" %}"></i> {{ object.upstream_speed|humanize_speed }}
|
||||
{% else %}
|
||||
{{ value|humanize_speed }}
|
||||
{% endif %}
|
||||
16
netbox/templates/circuits/panels/circuit_termination.html
Normal file
16
netbox/templates/circuits/panels/circuit_termination.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends "ui/panels/_base.html" %}
|
||||
{% load helpers i18n %}
|
||||
|
||||
{% block panel_content %}
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Circuit" %}</th>
|
||||
<td>{{ object.circuit|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider" %}</th>
|
||||
<td>{{ object.circuit.provider|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
{% include 'circuits/inc/circuit_termination_fields.html' with termination=object %}
|
||||
</table>
|
||||
{% endblock panel_content %}
|
||||
21
netbox/templates/dcim/panels/installed_device.html
Normal file
21
netbox/templates/dcim/panels/installed_device.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends "ui/panels/_base.html" %}
|
||||
{% load helpers i18n %}
|
||||
|
||||
{% block panel_content %}
|
||||
{% if object.installed_device %}
|
||||
{% with device=object.installed_device %}
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Device" %}</th>
|
||||
<td>{{ device|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Device type" %}</th>
|
||||
<td>{{ device.device_type }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<div class="card-body text-muted">{% trans "None" %}</div>
|
||||
{% endif %}
|
||||
{% endblock panel_content %}
|
||||
33
netbox/templates/dcim/panels/installed_module.html
Normal file
33
netbox/templates/dcim/panels/installed_module.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends "ui/panels/_base.html" %}
|
||||
{% load helpers i18n %}
|
||||
|
||||
{% block panel_content %}
|
||||
{% if object.installed_module %}
|
||||
{% with module=object.installed_module %}
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Module" %}</th>
|
||||
<td>{{ module|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Manufacturer" %}</th>
|
||||
<td>{{ module.module_type.manufacturer|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Module type" %}</th>
|
||||
<td>{{ module.module_type|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Serial number" %}</th>
|
||||
<td class="font-monospace">{{ module.serial|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Asset tag" %}</th>
|
||||
<td class="font-monospace">{{ module.asset_tag|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<div class="card-body text-muted">{% trans "None" %}</div>
|
||||
{% endif %}
|
||||
{% endblock panel_content %}
|
||||
@@ -124,11 +124,11 @@ Context:
|
||||
|
||||
{% block content %}
|
||||
{# Render panel layout declared on view class #}
|
||||
{% for row in layout %}
|
||||
{% for row in layout.rows %}
|
||||
<div class="row">
|
||||
{% for column in row %}
|
||||
<div class="col{% if column.width %}-{{ column.width }}{% endif %}">
|
||||
{% for panel in column %}
|
||||
{% for column in row.columns %}
|
||||
<div class="col">
|
||||
{% for panel in column.panels %}
|
||||
{% render panel %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% load i18n %}
|
||||
<span>
|
||||
<span{% if style %} class="{{ style }}"{% endif %}>
|
||||
<span{% if name %} id="attr_{{ name }}"{% endif %}>{{ value }}</span>
|
||||
{% if unit %}
|
||||
{{ unit|lower }}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{% load i18n %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<div class="alert-icon">
|
||||
<i class="mdi mdi-alert"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
{% blocktrans %}An error occurred when loading content from plugin {{ plugin }}:{% endblocktrans %}
|
||||
</p>
|
||||
<pre class="p-0">{{ exception }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,17 +1,15 @@
|
||||
<!-- begin {{ panel_class|default:"panel" }} -->
|
||||
<div class="card">
|
||||
{% if title or actions %}
|
||||
<h2 class="card-header">
|
||||
{{ title|default:"" }}
|
||||
{% if actions %}
|
||||
<div class="card-actions">
|
||||
{% for action in actions %}
|
||||
{% render action %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% endif %}
|
||||
<h2 class="card-header">
|
||||
{{ title }}
|
||||
{% if actions %}
|
||||
<div class="card-actions">
|
||||
{% for action in actions %}
|
||||
{% render action %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% block panel_content %}{% endblock %}
|
||||
</div>
|
||||
<!-- end {{ panel_class|default:"panel" }} -->
|
||||
|
||||
34
netbox/templates/vpn/panels/ipsecprofile_ike_policy.html
Normal file
34
netbox/templates/vpn/panels/ipsecprofile_ike_policy.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "IKE Policy" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.ike_policy|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.ike_policy.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Version" %}</th>
|
||||
<td>{{ object.ike_policy.get_version_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Mode" %}</th>
|
||||
<td>{{ object.ike_policy.get_mode_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Proposals" %}</th>
|
||||
<td>
|
||||
<ul class="list-unstyled mb-0">
|
||||
{% for proposal in object.ike_policy.proposals.all %}
|
||||
<li><a href="{{ proposal.get_absolute_url }}">{{ proposal }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
30
netbox/templates/vpn/panels/ipsecprofile_ipsec_policy.html
Normal file
30
netbox/templates/vpn/panels/ipsecprofile_ipsec_policy.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "IPSec Policy" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.ipsec_policy|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.ipsec_policy.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Proposals" %}</th>
|
||||
<td>
|
||||
<ul class="list-unstyled mb-0">
|
||||
{% for proposal in object.ipsec_policy.proposals.all %}
|
||||
<li><a href="{{ proposal.get_absolute_url }}">{{ proposal }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "PFS Group" %}</th>
|
||||
<td>{{ object.ipsec_policy.get_pfs_group_display }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
21
netbox/tenancy/migrations/0024_default_ordering_indexes.py
Normal file
21
netbox/tenancy/migrations/0024_default_ordering_indexes.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('extras', '0137_default_ordering_indexes'),
|
||||
('tenancy', '0023_add_mptt_tree_indexes'),
|
||||
('users', '0015_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='contact',
|
||||
index=models.Index(fields=['name'], name='tenancy_con_name_c26153_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='contactassignment',
|
||||
index=models.Index(fields=['contact', 'priority', 'role', 'id'], name='tenancy_con_contact_23011f_idx'),
|
||||
),
|
||||
]
|
||||
@@ -90,6 +90,9 @@ class Contact(PrimaryModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
indexes = (
|
||||
models.Index(fields=('name',)), # Default ordering
|
||||
)
|
||||
verbose_name = _('contact')
|
||||
verbose_name_plural = _('contacts')
|
||||
|
||||
@@ -130,6 +133,7 @@ class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, Chan
|
||||
class Meta:
|
||||
ordering = ('contact', 'priority', 'role', 'pk')
|
||||
indexes = (
|
||||
models.Index(fields=('contact', 'priority', 'role', 'id')), # Default ordering
|
||||
models.Index(fields=('object_type', 'object_id')),
|
||||
)
|
||||
constraints = (
|
||||
|
||||
19
netbox/users/migrations/0016_default_ordering_indexes.py
Normal file
19
netbox/users/migrations/0016_default_ordering_indexes.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('users', '0015_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='objectpermission',
|
||||
index=models.Index(fields=['name'], name='users_objec_name_ca707b_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='token',
|
||||
index=models.Index(fields=['-created'], name='users_token_created_1467b4_idx'),
|
||||
),
|
||||
]
|
||||
@@ -52,6 +52,9 @@ class ObjectPermission(CloningMixin, models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
indexes = (
|
||||
models.Index(fields=('name',)), # Default ordering
|
||||
)
|
||||
verbose_name = _('permission')
|
||||
verbose_name_plural = _('permissions')
|
||||
|
||||
|
||||
@@ -117,6 +117,9 @@ class Token(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ('-created',)
|
||||
indexes = (
|
||||
models.Index(fields=('-created',)), # Default ordering
|
||||
)
|
||||
verbose_name = _('token')
|
||||
verbose_name_plural = _('tokens')
|
||||
constraints = [
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from django import template as template_
|
||||
from django.conf import settings
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from netbox.plugins import PluginTemplateExtension
|
||||
@@ -39,11 +38,8 @@ def _get_registered_content(obj, method, template_context):
|
||||
context['config'] = settings.PLUGINS_CONFIG.get(plugin_name, {})
|
||||
|
||||
# Call the method to render content
|
||||
try:
|
||||
instance = template_extension(context)
|
||||
content = getattr(instance, method)()
|
||||
except Exception as e:
|
||||
content = render_to_string('ui/exception.html', {'plugin': plugin_name, 'exception': repr(e)})
|
||||
instance = template_extension(context)
|
||||
content = getattr(instance, method)()
|
||||
html += content
|
||||
|
||||
return mark_safe(html)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('dcim', '0231_default_ordering_indexes'),
|
||||
('extras', '0137_default_ordering_indexes'),
|
||||
('ipam', '0089_default_ordering_indexes'),
|
||||
('tenancy', '0024_default_ordering_indexes'),
|
||||
('users', '0016_default_ordering_indexes'),
|
||||
('virtualization', '0054_virtualmachinetype'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='virtualmachine',
|
||||
index=models.Index(fields=['name', 'id'], name='virtualizat_name_16033e_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='virtualmachinetype',
|
||||
index=models.Index(fields=['name'], name='virtualizat_name_6cff11_idx'),
|
||||
),
|
||||
]
|
||||
@@ -97,6 +97,9 @@ class Cluster(ContactsMixin, CachedScopeMixin, PrimaryModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
indexes = (
|
||||
models.Index(fields=('name',)), # Default ordering
|
||||
)
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('group', 'name'),
|
||||
|
||||
@@ -92,6 +92,9 @@ class VirtualMachineType(ImageAttachmentsMixin, PrimaryModel):
|
||||
violation_error_message=_('Virtual machine type slug must be unique.'),
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('name',)), # Default ordering
|
||||
)
|
||||
verbose_name = _('virtual machine type')
|
||||
verbose_name_plural = _('virtual machine types')
|
||||
|
||||
@@ -249,6 +252,9 @@ class VirtualMachine(
|
||||
|
||||
class Meta:
|
||||
ordering = ('name', 'pk') # Name may be non-unique
|
||||
indexes = (
|
||||
models.Index(fields=('name', 'id')), # Default ordering
|
||||
)
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
Lower('name'), 'cluster', 'tenant',
|
||||
|
||||
17
netbox/vpn/migrations/0012_default_ordering_indexes.py
Normal file
17
netbox/vpn/migrations/0012_default_ordering_indexes.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('extras', '0137_default_ordering_indexes'),
|
||||
('ipam', '0089_default_ordering_indexes'),
|
||||
('vpn', '0011_add_comments_to_organizationalmodel'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='tunneltermination',
|
||||
index=models.Index(fields=['tunnel', 'role', 'id'], name='vpn_tunnelt_tunnel__f542d3_idx'),
|
||||
),
|
||||
]
|
||||
@@ -145,6 +145,9 @@ class TunnelTermination(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ChangeLo
|
||||
violation_error_message=_("An object may be terminated to only one tunnel at a time.")
|
||||
),
|
||||
)
|
||||
indexes = (
|
||||
models.Index(fields=('tunnel', 'role', 'id')), # Default ordering
|
||||
)
|
||||
verbose_name = _('tunnel termination')
|
||||
verbose_name_plural = _('tunnel terminations')
|
||||
|
||||
|
||||
@@ -71,23 +71,6 @@ class IPSecProfilePanel(panels.ObjectAttributesPanel):
|
||||
mode = attrs.ChoiceAttr('mode')
|
||||
|
||||
|
||||
class IPSecProfileIKEPolicyPanel(panels.ObjectAttributesPanel):
|
||||
title = _('IKE Policy')
|
||||
name = attrs.RelatedObjectAttr('ike_policy', linkify=True)
|
||||
description = attrs.TextAttr('ike_policy.description')
|
||||
version = attrs.ChoiceAttr('ike_policy.version', label=_('IKE version'))
|
||||
mode = attrs.ChoiceAttr('ike_policy.mode')
|
||||
proposals = attrs.RelatedObjectListAttr('ike_policy.proposals', linkify=True)
|
||||
|
||||
|
||||
class IPSecProfileIPSecPolicyPanel(panels.ObjectAttributesPanel):
|
||||
title = _('IPSec Policy')
|
||||
name = attrs.RelatedObjectAttr('ipsec_policy', linkify=True)
|
||||
description = attrs.TextAttr('ipsec_policy.description')
|
||||
proposals = attrs.RelatedObjectListAttr('ipsec_policy.proposals', linkify=True)
|
||||
pfs_group = attrs.ChoiceAttr('ipsec_policy.pfs_group', label=_('PFS group'))
|
||||
|
||||
|
||||
class L2VPNPanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
identifier = attrs.TextAttr('identifier')
|
||||
|
||||
@@ -10,6 +10,7 @@ from netbox.ui.panels import (
|
||||
ObjectsTablePanel,
|
||||
PluginContentPanel,
|
||||
RelatedObjectsPanel,
|
||||
TemplatePanel,
|
||||
)
|
||||
from netbox.views import generic
|
||||
from utilities.query import count_related
|
||||
@@ -588,8 +589,8 @@ class IPSecProfileView(generic.ObjectView):
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
panels.IPSecProfileIKEPolicyPanel(),
|
||||
panels.IPSecProfileIPSecPolicyPanel(),
|
||||
TemplatePanel('vpn/panels/ipsecprofile_ike_policy.html'),
|
||||
TemplatePanel('vpn/panels/ipsecprofile_ipsec_policy.html'),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
20
netbox/wireless/migrations/0019_default_ordering_indexes.py
Normal file
20
netbox/wireless/migrations/0019_default_ordering_indexes.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('dcim', '0231_default_ordering_indexes'),
|
||||
('extras', '0137_default_ordering_indexes'),
|
||||
('ipam', '0089_default_ordering_indexes'),
|
||||
('tenancy', '0024_default_ordering_indexes'),
|
||||
('users', '0016_default_ordering_indexes'),
|
||||
('wireless', '0018_add_mptt_tree_indexes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='wirelesslan',
|
||||
index=models.Index(fields=['ssid', 'id'], name='wireless_wi_ssid_64a9ce_idx'),
|
||||
),
|
||||
]
|
||||
@@ -118,6 +118,7 @@ class WirelessLAN(WirelessAuthenticationBase, CachedScopeMixin, PrimaryModel):
|
||||
class Meta:
|
||||
ordering = ('ssid', 'pk')
|
||||
indexes = (
|
||||
models.Index(fields=('ssid', 'id')), # Default ordering
|
||||
models.Index(fields=('scope_type', 'scope_id')),
|
||||
)
|
||||
verbose_name = _('wireless LAN')
|
||||
|
||||
@@ -34,10 +34,10 @@ class WirelessLinkInterfacePanel(panels.ObjectPanel):
|
||||
self.title = title
|
||||
|
||||
def get_context(self, context):
|
||||
ctx = super().get_context(context)
|
||||
obj = context['object']
|
||||
return {
|
||||
**ctx,
|
||||
'interface': getattr(ctx['object'], self.interface_attr),
|
||||
**super().get_context(context),
|
||||
'interface': getattr(obj, self.interface_attr),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user