mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 09:16:10 -06:00
Merge branch 'develop' into 12468-custom-field-name
This commit is contained in:
commit
8b2f0cdc1e
@ -153,15 +153,10 @@ New objects can be created by instantiating the desired model, defining values f
|
|||||||
```
|
```
|
||||||
>>> lab1 = Site.objects.get(pk=7)
|
>>> lab1 = Site.objects.get(pk=7)
|
||||||
>>> myvlan = VLAN(vid=123, name='MyNewVLAN', site=lab1)
|
>>> myvlan = VLAN(vid=123, name='MyNewVLAN', site=lab1)
|
||||||
|
>>> myvlan.full_clean()
|
||||||
>>> myvlan.save()
|
>>> myvlan.save()
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, the above can be performed as a single operation. (Note, however, that `save()` does _not_ return the new instance for reuse.)
|
|
||||||
|
|
||||||
```
|
|
||||||
>>> VLAN(vid=123, name='MyNewVLAN', site=Site.objects.get(pk=7)).save()
|
|
||||||
```
|
|
||||||
|
|
||||||
To modify an existing object, we retrieve it, update the desired field(s), and call `save()` again.
|
To modify an existing object, we retrieve it, update the desired field(s), and call `save()` again.
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -169,6 +164,7 @@ To modify an existing object, we retrieve it, update the desired field(s), and c
|
|||||||
>>> vlan.name
|
>>> vlan.name
|
||||||
'MyNewVLAN'
|
'MyNewVLAN'
|
||||||
>>> vlan.name = 'BetterName'
|
>>> vlan.name = 'BetterName'
|
||||||
|
>>> vlan.full_clean()
|
||||||
>>> vlan.save()
|
>>> vlan.save()
|
||||||
>>> VLAN.objects.get(pk=1280).name
|
>>> VLAN.objects.get(pk=1280).name
|
||||||
'BetterName'
|
'BetterName'
|
||||||
|
@ -378,6 +378,7 @@ class NewBranchScript(Script):
|
|||||||
slug=slugify(data['site_name']),
|
slug=slugify(data['site_name']),
|
||||||
status=SiteStatusChoices.STATUS_PLANNED
|
status=SiteStatusChoices.STATUS_PLANNED
|
||||||
)
|
)
|
||||||
|
site.full_clean()
|
||||||
site.save()
|
site.save()
|
||||||
self.log_success(f"Created new site: {site}")
|
self.log_success(f"Created new site: {site}")
|
||||||
|
|
||||||
@ -391,6 +392,7 @@ class NewBranchScript(Script):
|
|||||||
status=DeviceStatusChoices.STATUS_PLANNED,
|
status=DeviceStatusChoices.STATUS_PLANNED,
|
||||||
device_role=switch_role
|
device_role=switch_role
|
||||||
)
|
)
|
||||||
|
switch.full_clean()
|
||||||
switch.save()
|
switch.save()
|
||||||
self.log_success(f"Created new switch: {switch}")
|
self.log_success(f"Created new switch: {switch}")
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ sudo apt install -y libldap2-dev libsasl2-dev libssl-dev
|
|||||||
On CentOS:
|
On CentOS:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
sudo yum install -y openldap-devel
|
sudo yum install -y openldap-devel python3-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install django-auth-ldap
|
### Install django-auth-ldap
|
||||||
|
@ -4,9 +4,19 @@
|
|||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
|
* [#11670](https://github.com/netbox-community/netbox/issues/11670) - Enable setting device type & module type weight via bulk import
|
||||||
|
* [#11900](https://github.com/netbox-community/netbox/issues/11900) - Add an outline to the reservation markers on rack elevations
|
||||||
|
* [#12131](https://github.com/netbox-community/netbox/issues/12131) - Show custom field description as an icon tooltip under object views
|
||||||
* [#12223](https://github.com/netbox-community/netbox/issues/12223) - Add columns for parent device bay and position to devices list
|
* [#12223](https://github.com/netbox-community/netbox/issues/12223) - Add columns for parent device bay and position to devices list
|
||||||
|
* [#12233](https://github.com/netbox-community/netbox/issues/12233) - Move related IP addresses table to a separate tab
|
||||||
|
* [#12286](https://github.com/netbox-community/netbox/issues/12286) - Show height and total weight under device view
|
||||||
|
* [#12323](https://github.com/netbox-community/netbox/issues/12323) - Add 100GE CXP interface type
|
||||||
* [#12498](https://github.com/netbox-community/netbox/issues/12498) - Hide map button if `MAPS_URL` is empty
|
* [#12498](https://github.com/netbox-community/netbox/issues/12498) - Hide map button if `MAPS_URL` is empty
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#12550](https://github.com/netbox-community/netbox/issues/12550) - Fix rear port selection widget under front port creation form
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v3.5.1 (2023-05-05)
|
## v3.5.1 (2023-05-05)
|
||||||
|
@ -807,6 +807,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
|||||||
TYPE_100GE_CFP = '100gbase-x-cfp'
|
TYPE_100GE_CFP = '100gbase-x-cfp'
|
||||||
TYPE_100GE_CFP2 = '100gbase-x-cfp2'
|
TYPE_100GE_CFP2 = '100gbase-x-cfp2'
|
||||||
TYPE_100GE_CFP4 = '100gbase-x-cfp4'
|
TYPE_100GE_CFP4 = '100gbase-x-cfp4'
|
||||||
|
TYPE_100GE_CXP = '100gbase-x-cxp'
|
||||||
TYPE_100GE_CPAK = '100gbase-x-cpak'
|
TYPE_100GE_CPAK = '100gbase-x-cpak'
|
||||||
TYPE_100GE_QSFP28 = '100gbase-x-qsfp28'
|
TYPE_100GE_QSFP28 = '100gbase-x-qsfp28'
|
||||||
TYPE_200GE_CFP2 = '200gbase-x-cfp2'
|
TYPE_200GE_CFP2 = '200gbase-x-cfp2'
|
||||||
@ -952,6 +953,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
|||||||
(TYPE_100GE_CFP2, 'CFP2 (100GE)'),
|
(TYPE_100GE_CFP2, 'CFP2 (100GE)'),
|
||||||
(TYPE_200GE_CFP2, 'CFP2 (200GE)'),
|
(TYPE_200GE_CFP2, 'CFP2 (200GE)'),
|
||||||
(TYPE_100GE_CFP4, 'CFP4 (100GE)'),
|
(TYPE_100GE_CFP4, 'CFP4 (100GE)'),
|
||||||
|
(TYPE_100GE_CXP, 'CXP (100GE)'),
|
||||||
(TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'),
|
(TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'),
|
||||||
(TYPE_100GE_QSFP28, 'QSFP28 (100GE)'),
|
(TYPE_100GE_QSFP28, 'QSFP28 (100GE)'),
|
||||||
(TYPE_200GE_QSFP56, 'QSFP56 (200GE)'),
|
(TYPE_200GE_QSFP56, 'QSFP56 (200GE)'),
|
||||||
|
@ -292,12 +292,21 @@ class DeviceTypeImportForm(NetBoxModelImportForm):
|
|||||||
required=False,
|
required=False,
|
||||||
help_text=_('The default platform for devices of this type (optional)')
|
help_text=_('The default platform for devices of this type (optional)')
|
||||||
)
|
)
|
||||||
|
weight = forms.DecimalField(
|
||||||
|
required=False,
|
||||||
|
help_text=_('Device weight'),
|
||||||
|
)
|
||||||
|
weight_unit = CSVChoiceField(
|
||||||
|
choices=WeightUnitChoices,
|
||||||
|
required=False,
|
||||||
|
help_text=_('Unit for device weight')
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = [
|
fields = [
|
||||||
'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
||||||
'subdevice_role', 'airflow', 'description', 'comments',
|
'subdevice_role', 'airflow', 'description', 'weight', 'weight_unit', 'comments',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -306,10 +315,19 @@ class ModuleTypeImportForm(NetBoxModelImportForm):
|
|||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
to_field_name='name'
|
to_field_name='name'
|
||||||
)
|
)
|
||||||
|
weight = forms.DecimalField(
|
||||||
|
required=False,
|
||||||
|
help_text=_('Module weight'),
|
||||||
|
)
|
||||||
|
weight_unit = CSVChoiceField(
|
||||||
|
choices=WeightUnitChoices,
|
||||||
|
required=False,
|
||||||
|
help_text=_('Unit for module weight')
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fields = ['manufacturer', 'model', 'part_number', 'description', 'comments']
|
fields = ['manufacturer', 'model', 'part_number', 'description', 'weight', 'weight_unit', 'comments']
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleImportForm(NetBoxModelImportForm):
|
class DeviceRoleImportForm(NetBoxModelImportForm):
|
||||||
|
@ -242,6 +242,7 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
|
|||||||
choices=[],
|
choices=[],
|
||||||
label=_('Rear ports'),
|
label=_('Rear ports'),
|
||||||
help_text=_('Select one rear port assignment for each front port being created.'),
|
help_text=_('Select one rear port assignment for each front port being created.'),
|
||||||
|
widget=forms.SelectMultiple(attrs={'size': 6})
|
||||||
)
|
)
|
||||||
|
|
||||||
# Override fieldsets from FrontPortForm to omit rear_port_position
|
# Override fieldsets from FrontPortForm to omit rear_port_position
|
||||||
|
@ -184,6 +184,8 @@ class DeviceType(PrimaryModel, WeightMixin):
|
|||||||
'subdevice_role': self.subdevice_role,
|
'subdevice_role': self.subdevice_role,
|
||||||
'airflow': self.airflow,
|
'airflow': self.airflow,
|
||||||
'comments': self.comments,
|
'comments': self.comments,
|
||||||
|
'weight': float(self.weight) if self.weight is not None else None,
|
||||||
|
'weight_unit': self.weight_unit,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Component templates
|
# Component templates
|
||||||
@ -361,6 +363,8 @@ class ModuleType(PrimaryModel, WeightMixin):
|
|||||||
'model': self.model,
|
'model': self.model,
|
||||||
'part_number': self.part_number,
|
'part_number': self.part_number,
|
||||||
'comments': self.comments,
|
'comments': self.comments,
|
||||||
|
'weight': float(self.weight) if self.weight is not None else None,
|
||||||
|
'weight_unit': self.weight_unit,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Component templates
|
# Component templates
|
||||||
|
@ -22,6 +22,11 @@ __all__ = (
|
|||||||
'RackElevationSVG',
|
'RackElevationSVG',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
GRADIENT_RESERVED = '#b0b0ff'
|
||||||
|
GRADIENT_OCCUPIED = '#d7d7d7'
|
||||||
|
GRADIENT_BLOCKED = '#ffc0c0'
|
||||||
|
STROKE_RESERVED = '#4d4dff'
|
||||||
|
|
||||||
|
|
||||||
def get_device_name(device):
|
def get_device_name(device):
|
||||||
if device.virtual_chassis:
|
if device.virtual_chassis:
|
||||||
@ -132,9 +137,9 @@ class RackElevationSVG:
|
|||||||
drawing.defs.add(drawing.style(css_file.read()))
|
drawing.defs.add(drawing.style(css_file.read()))
|
||||||
|
|
||||||
# Add gradients
|
# Add gradients
|
||||||
RackElevationSVG._add_gradient(drawing, 'reserved', '#b0b0ff')
|
RackElevationSVG._add_gradient(drawing, 'reserved', GRADIENT_RESERVED)
|
||||||
RackElevationSVG._add_gradient(drawing, 'occupied', '#d7d7d7')
|
RackElevationSVG._add_gradient(drawing, 'occupied', GRADIENT_OCCUPIED)
|
||||||
RackElevationSVG._add_gradient(drawing, 'blocked', '#ffc0c0')
|
RackElevationSVG._add_gradient(drawing, 'blocked', GRADIENT_BLOCKED)
|
||||||
|
|
||||||
return drawing
|
return drawing
|
||||||
|
|
||||||
@ -246,13 +251,13 @@ class RackElevationSVG:
|
|||||||
coords = self._get_device_coords(segment[0], u_height)
|
coords = self._get_device_coords(segment[0], u_height)
|
||||||
coords = (coords[0] + self.unit_width + RACK_ELEVATION_BORDER_WIDTH * 2, coords[1])
|
coords = (coords[0] + self.unit_width + RACK_ELEVATION_BORDER_WIDTH * 2, coords[1])
|
||||||
size = (
|
size = (
|
||||||
self.margin_width,
|
self.margin_width - 3,
|
||||||
u_height * self.unit_height
|
u_height * self.unit_height
|
||||||
)
|
)
|
||||||
link = Hyperlink(href=f'{self.base_url}{reservation.get_absolute_url()}', target='_parent')
|
link = Hyperlink(href=f'{self.base_url}{reservation.get_absolute_url()}', target='_parent')
|
||||||
link.set_desc(f'Reservation #{reservation.pk}: {reservation.description}')
|
link.set_desc(f'Reservation #{reservation.pk}: {reservation.description}')
|
||||||
link.add(
|
link.add(
|
||||||
Rect(coords, size, class_='reservation')
|
Rect(coords, size, class_='reservation', stroke=STROKE_RESERVED, stroke_width=2)
|
||||||
)
|
)
|
||||||
self.drawing.add(link)
|
self.drawing.add(link)
|
||||||
|
|
||||||
|
@ -681,11 +681,15 @@ class DeviceTypeTestCase(
|
|||||||
"""
|
"""
|
||||||
IMPORT_DATA = """
|
IMPORT_DATA = """
|
||||||
manufacturer: Generic
|
manufacturer: Generic
|
||||||
default_platform: Platform
|
|
||||||
model: TEST-1000
|
model: TEST-1000
|
||||||
slug: test-1000
|
slug: test-1000
|
||||||
|
default_platform: Platform
|
||||||
u_height: 2
|
u_height: 2
|
||||||
|
is_full_depth: false
|
||||||
|
airflow: front-to-rear
|
||||||
subdevice_role: parent
|
subdevice_role: parent
|
||||||
|
weight: 10
|
||||||
|
weight_unit: kg
|
||||||
comments: Test comment
|
comments: Test comment
|
||||||
console-ports:
|
console-ports:
|
||||||
- name: Console Port 1
|
- name: Console Port 1
|
||||||
@ -794,8 +798,16 @@ inventory-items:
|
|||||||
self.assertHttpStatus(response, 200)
|
self.assertHttpStatus(response, 200)
|
||||||
|
|
||||||
device_type = DeviceType.objects.get(model='TEST-1000')
|
device_type = DeviceType.objects.get(model='TEST-1000')
|
||||||
self.assertEqual(device_type.comments, 'Test comment')
|
self.assertEqual(device_type.manufacturer.pk, manufacturer.pk)
|
||||||
self.assertEqual(device_type.default_platform.pk, platform.pk)
|
self.assertEqual(device_type.default_platform.pk, platform.pk)
|
||||||
|
self.assertEqual(device_type.slug, 'test-1000')
|
||||||
|
self.assertEqual(device_type.u_height, 2)
|
||||||
|
self.assertFalse(device_type.is_full_depth)
|
||||||
|
self.assertEqual(device_type.airflow, DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR)
|
||||||
|
self.assertEqual(device_type.subdevice_role, SubdeviceRoleChoices.ROLE_PARENT)
|
||||||
|
self.assertEqual(device_type.weight, 10)
|
||||||
|
self.assertEqual(device_type.weight_unit, WeightUnitChoices.UNIT_KILOGRAM)
|
||||||
|
self.assertEqual(device_type.comments, 'Test comment')
|
||||||
|
|
||||||
# Verify all of the components were created
|
# Verify all of the components were created
|
||||||
self.assertEqual(device_type.consoleporttemplates.count(), 3)
|
self.assertEqual(device_type.consoleporttemplates.count(), 3)
|
||||||
@ -1019,6 +1031,8 @@ class ModuleTypeTestCase(
|
|||||||
IMPORT_DATA = """
|
IMPORT_DATA = """
|
||||||
manufacturer: Generic
|
manufacturer: Generic
|
||||||
model: TEST-1000
|
model: TEST-1000
|
||||||
|
weight: 10
|
||||||
|
weight_unit: lb
|
||||||
comments: Test comment
|
comments: Test comment
|
||||||
console-ports:
|
console-ports:
|
||||||
- name: Console Port 1
|
- name: Console Port 1
|
||||||
@ -1082,7 +1096,8 @@ front-ports:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Create the manufacturer
|
# Create the manufacturer
|
||||||
Manufacturer(name='Generic', slug='generic').save()
|
manufacturer = Manufacturer(name='Generic', slug='generic')
|
||||||
|
manufacturer.save()
|
||||||
|
|
||||||
# Add all required permissions to the test user
|
# Add all required permissions to the test user
|
||||||
self.add_permissions(
|
self.add_permissions(
|
||||||
@ -1105,6 +1120,9 @@ front-ports:
|
|||||||
self.assertHttpStatus(response, 200)
|
self.assertHttpStatus(response, 200)
|
||||||
|
|
||||||
module_type = ModuleType.objects.get(model='TEST-1000')
|
module_type = ModuleType.objects.get(model='TEST-1000')
|
||||||
|
self.assertEqual(module_type.manufacturer.pk, manufacturer.pk)
|
||||||
|
self.assertEqual(module_type.weight, 10)
|
||||||
|
self.assertEqual(module_type.weight_unit, WeightUnitChoices.UNIT_POUND)
|
||||||
self.assertEqual(module_type.comments, 'Test comment')
|
self.assertEqual(module_type.comments, 'Test comment')
|
||||||
|
|
||||||
# Verify all the components were created
|
# Verify all the components were created
|
||||||
|
@ -783,6 +783,14 @@ class IPAddress(PrimaryModel):
|
|||||||
if available_ips:
|
if available_ips:
|
||||||
return next(iter(available_ips))
|
return next(iter(available_ips))
|
||||||
|
|
||||||
|
def get_related_ips(self):
|
||||||
|
"""
|
||||||
|
Return all IPAddresses belonging to the same VRF.
|
||||||
|
"""
|
||||||
|
return IPAddress.objects.exclude(address=str(self.address)).filter(
|
||||||
|
vrf=self.vrf, address__net_contained_or_equal=str(self.address)
|
||||||
|
)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
|
@ -755,19 +755,9 @@ class IPAddressView(generic.ObjectView):
|
|||||||
# Limit to a maximum of 10 duplicates displayed here
|
# Limit to a maximum of 10 duplicates displayed here
|
||||||
duplicate_ips_table = tables.IPAddressTable(duplicate_ips[:10], orderable=False)
|
duplicate_ips_table = tables.IPAddressTable(duplicate_ips[:10], orderable=False)
|
||||||
|
|
||||||
# Related IP table
|
|
||||||
related_ips = IPAddress.objects.restrict(request.user, 'view').exclude(
|
|
||||||
address=str(instance.address)
|
|
||||||
).filter(
|
|
||||||
vrf=instance.vrf, address__net_contained_or_equal=str(instance.address)
|
|
||||||
)
|
|
||||||
related_ips_table = tables.IPAddressTable(related_ips, orderable=False)
|
|
||||||
related_ips_table.configure(request)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'parent_prefixes_table': parent_prefixes_table,
|
'parent_prefixes_table': parent_prefixes_table,
|
||||||
'duplicate_ips_table': duplicate_ips_table,
|
'duplicate_ips_table': duplicate_ips_table,
|
||||||
'related_ips_table': related_ips_table,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -872,6 +862,24 @@ class IPAddressBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.IPAddressTable
|
table = tables.IPAddressTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(IPAddress, 'related_ips', path='related-ip-addresses')
|
||||||
|
class IPAddressRelatedIPsView(generic.ObjectChildrenView):
|
||||||
|
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
|
||||||
|
child_model = IPAddress
|
||||||
|
table = tables.IPAddressTable
|
||||||
|
filterset = filtersets.IPAddressFilterSet
|
||||||
|
template_name = 'ipam/ipaddress/ip_addresses.html'
|
||||||
|
tab = ViewTab(
|
||||||
|
label=_('Related IPs'),
|
||||||
|
badge=lambda x: x.get_related_ips().count(),
|
||||||
|
weight=500,
|
||||||
|
hide_if_empty=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_children(self, request, parent):
|
||||||
|
return parent.get_related_ips().restrict(request.user, 'view')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VLAN groups
|
# VLAN groups
|
||||||
#
|
#
|
||||||
|
@ -132,9 +132,16 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% for field, value in fields.items %}
|
{% for field, value in fields.items %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<th scope="row">{{ field }}
|
||||||
<span title="{{ field.description|escape }}">{{ field }}</span>
|
{% if field.description %}
|
||||||
</td>
|
<i
|
||||||
|
class="mdi mdi-information text-primary"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="right"
|
||||||
|
title="{{ field.description|escape }}"
|
||||||
|
></i>
|
||||||
|
{% endif %}
|
||||||
|
</th>
|
||||||
<td>
|
<td>
|
||||||
{% customfield_value field value %}
|
{% customfield_value field value %}
|
||||||
</td>
|
</td>
|
||||||
|
@ -313,8 +313,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Weight</th>
|
<th scope="row">Weight</th>
|
||||||
<td>
|
<td>
|
||||||
{% if object.device_type.weight %}
|
{% if object.total_weight %}
|
||||||
{{ object.device_type.weight|floatformat }} {{ object.device_type.get_weight_unit_display }}
|
{{ object.total_weight|floatformat }} Kilograms
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ ''|placeholder }}
|
{{ ''|placeholder }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -12,8 +12,15 @@
|
|||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
{% for field, value in fields.items %}
|
{% for field, value in fields.items %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">
|
<th scope="row">{{ field }}
|
||||||
<span title="{{ field.description|escape }}">{{ field }}</span>
|
{% if field.description %}
|
||||||
|
<i
|
||||||
|
class="mdi mdi-information text-primary"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="right"
|
||||||
|
title="{{ field.description|escape }}"
|
||||||
|
></i>
|
||||||
|
{% endif %}
|
||||||
</th>
|
</th>
|
||||||
<td>
|
<td>
|
||||||
{% customfield_value field value %}
|
{% customfield_value field value %}
|
||||||
|
@ -3,13 +3,6 @@
|
|||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
{% load render_table from django_tables2 %}
|
{% load render_table from django_tables2 %}
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
|
||||||
{{ block.super }}
|
|
||||||
{% if object.vrf %}
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'ipam:ipaddress_list' %}?vrf_id={{ object.vrf.pk }}">{{ object.vrf }}</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-4">
|
<div class="col col-md-4">
|
||||||
@ -116,7 +109,6 @@
|
|||||||
{% if duplicate_ips_table.rows %}
|
{% if duplicate_ips_table.rows %}
|
||||||
{% include 'inc/panel_table.html' with table=duplicate_ips_table heading='Duplicate IPs' panel_class='danger' %}
|
{% include 'inc/panel_table.html' with table=duplicate_ips_table heading='Duplicate IPs' panel_class='danger' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include 'inc/panel_table.html' with table=related_ips_table heading='Related IPs' %}
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">Services</h5>
|
<h5 class="card-header">Services</h5>
|
||||||
<div class="card-body htmx-container table-responsive"
|
<div class="card-body htmx-container table-responsive"
|
||||||
|
8
netbox/templates/ipam/ipaddress/base.html
Normal file
8
netbox/templates/ipam/ipaddress/base.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if object.vrf %}
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'ipam:ipaddress_list' %}?vrf_id={{ object.vrf.pk }}">{{ object.vrf }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
19
netbox/templates/ipam/ipaddress/ip_addresses.html
Normal file
19
netbox/templates/ipam/ipaddress/ip_addresses.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% extends 'ipam/ipaddress/base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body" id="object_list">
|
||||||
|
{% include 'htmx/table.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block modals %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% table_config_form table %}
|
||||||
|
{% endblock modals %}
|
@ -32,11 +32,11 @@ class BootstrapMixin:
|
|||||||
elif isinstance(field.widget, forms.CheckboxInput):
|
elif isinstance(field.widget, forms.CheckboxInput):
|
||||||
field.widget.attrs['class'] = f'{css} form-check-input'
|
field.widget.attrs['class'] = f'{css} form-check-input'
|
||||||
|
|
||||||
elif isinstance(field.widget, forms.SelectMultiple):
|
elif isinstance(field.widget, forms.SelectMultiple) and 'size' in field.widget.attrs:
|
||||||
if 'size' not in field.widget.attrs:
|
# Use native Bootstrap class for multi-line <select> widgets
|
||||||
field.widget.attrs['class'] = f'{css} netbox-static-select'
|
field.widget.attrs['class'] = f'{css} form-select form-select-sm'
|
||||||
|
|
||||||
elif isinstance(field.widget, forms.Select):
|
elif isinstance(field.widget, (forms.Select, forms.SelectMultiple)):
|
||||||
field.widget.attrs['class'] = f'{css} netbox-static-select'
|
field.widget.attrs['class'] = f'{css} netbox-static-select'
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
Loading…
Reference in New Issue
Block a user