Merge branch 'develop' into 10201-cable-terminations2

This commit is contained in:
Arthur 2023-01-03 08:42:22 -08:00
commit 4bee634d7c
18 changed files with 241 additions and 36 deletions

View File

@ -63,6 +63,7 @@ Redis is configured using a configuration setting similar to `DATABASE` and thes
* `HOST` - Name or IP address of the Redis server (use `localhost` if running locally) * `HOST` - Name or IP address of the Redis server (use `localhost` if running locally)
* `PORT` - TCP port of the Redis service; leave blank for default port (6379) * `PORT` - TCP port of the Redis service; leave blank for default port (6379)
* `USERNAME` - Redis username (if set)
* `PASSWORD` - Redis password (if set) * `PASSWORD` - Redis password (if set)
* `DATABASE` - Numeric database ID * `DATABASE` - Numeric database ID
* `SSL` - Use SSL connection to Redis * `SSL` - Use SSL connection to Redis
@ -75,6 +76,7 @@ REDIS = {
'tasks': { 'tasks': {
'HOST': 'redis.example.com', 'HOST': 'redis.example.com',
'PORT': 1234, 'PORT': 1234,
'USERNAME': 'netbox'
'PASSWORD': 'foobar', 'PASSWORD': 'foobar',
'DATABASE': 0, 'DATABASE': 0,
'SSL': False, 'SSL': False,
@ -82,6 +84,7 @@ REDIS = {
'caching': { 'caching': {
'HOST': 'localhost', 'HOST': 'localhost',
'PORT': 6379, 'PORT': 6379,
'USERNAME': ''
'PASSWORD': '', 'PASSWORD': '',
'DATABASE': 1, 'DATABASE': 1,
'SSL': False, 'SSL': False,

View File

@ -7,15 +7,19 @@
* [#9285](https://github.com/netbox-community/netbox/issues/9285) - Enable specifying assigned component during bulk import of inventory items * [#9285](https://github.com/netbox-community/netbox/issues/9285) - Enable specifying assigned component during bulk import of inventory items
* [#10700](https://github.com/netbox-community/netbox/issues/10700) - Match device name when using modules quick search * [#10700](https://github.com/netbox-community/netbox/issues/10700) - Match device name when using modules quick search
* [#11121](https://github.com/netbox-community/netbox/issues/11121) - Add VM resource totals to cluster view * [#11121](https://github.com/netbox-community/netbox/issues/11121) - Add VM resource totals to cluster view
* [#11156](https://github.com/netbox-community/netbox/issues/11156) - Enable selecting assigned component when editing inventory item in UI
* [#11223](https://github.com/netbox-community/netbox/issues/11223) - `reindex` management command should accept app label without model name * [#11223](https://github.com/netbox-community/netbox/issues/11223) - `reindex` management command should accept app label without model name
* [#11244](https://github.com/netbox-community/netbox/issues/11244) - Add controls for saved filters to rack elevations list * [#11244](https://github.com/netbox-community/netbox/issues/11244) - Add controls for saved filters to rack elevations list
* [#11248](https://github.com/netbox-community/netbox/issues/11248) - Fix database migration when plugin with search indexer is enabled * [#11248](https://github.com/netbox-community/netbox/issues/11248) - Fix database migration when plugin with search indexer is enabled
* [#11259](https://github.com/netbox-community/netbox/issues/11259) - Add support for Redis username configuration
### Bug Fixes ### Bug Fixes
* [#11280](https://github.com/netbox-community/netbox/issues/11280) - Fix errant newlines when exporting interfaces with multiple IP addresses assigned * [#11280](https://github.com/netbox-community/netbox/issues/11280) - Fix errant newlines when exporting interfaces with multiple IP addresses assigned
* [#11290](https://github.com/netbox-community/netbox/issues/11290) - Correct reporting of scheduled job duration * [#11290](https://github.com/netbox-community/netbox/issues/11290) - Correct reporting of scheduled job duration
* [#11232](https://github.com/netbox-community/netbox/issues/11232) - Enable partial & regular expression matching for non-string types in global search * [#11232](https://github.com/netbox-community/netbox/issues/11232) - Enable partial & regular expression matching for non-string types in global search
* [#11342](https://github.com/netbox-community/netbox/issues/11342) - Correct cable trace URL under "connection" tab for device components
* [#11345](https://github.com/netbox-community/netbox/issues/11345) - Fix form validation for bulk import of modules
--- ---

View File

@ -56,8 +56,8 @@ class ModuleCommonForm(forms.Form):
def clean(self): def clean(self):
super().clean() super().clean()
replicate_components = self.cleaned_data.get("replicate_components") replicate_components = self.cleaned_data.get('replicate_components')
adopt_components = self.cleaned_data.get("adopt_components") adopt_components = self.cleaned_data.get('adopt_components')
device = self.cleaned_data.get('device') device = self.cleaned_data.get('device')
module_type = self.cleaned_data.get('module_type') module_type = self.cleaned_data.get('module_type')
module_bay = self.cleaned_data.get('module_bay') module_bay = self.cleaned_data.get('module_bay')
@ -65,8 +65,9 @@ class ModuleCommonForm(forms.Form):
if adopt_components: if adopt_components:
self.instance._adopt_components = True self.instance._adopt_components = True
# Bail out if we are not installing a new module or if we are not replicating components # Bail out if we are not installing a new module or if we are not replicating components (or if
if self.instance.pk or not replicate_components: # validation has already failed)
if self.errors or self.instance.pk or not replicate_components:
self.instance._disable_replication = True self.instance._disable_replication = True
return return

View File

@ -1549,15 +1549,63 @@ class InventoryItemForm(DeviceComponentForm):
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False required=False
) )
component_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(), # Assigned component selectors
limit_choices_to=MODULAR_COMPONENT_MODELS, consoleport = DynamicModelChoiceField(
queryset=ConsolePort.objects.all(),
required=False, required=False,
widget=forms.HiddenInput query_params={
'device_id': '$device'
},
label=_('Console port')
) )
component_id = forms.IntegerField( consoleserverport = DynamicModelChoiceField(
queryset=ConsoleServerPort.objects.all(),
required=False, required=False,
widget=forms.HiddenInput query_params={
'device_id': '$device'
},
label=_('Console server port')
)
frontport = DynamicModelChoiceField(
queryset=FrontPort.objects.all(),
required=False,
query_params={
'device_id': '$device'
},
label=_('Front port')
)
interface = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
query_params={
'device_id': '$device'
},
label=_('Interface')
)
poweroutlet = DynamicModelChoiceField(
queryset=PowerOutlet.objects.all(),
required=False,
query_params={
'device_id': '$device'
},
label=_('Power outlet')
)
powerport = DynamicModelChoiceField(
queryset=PowerPort.objects.all(),
required=False,
query_params={
'device_id': '$device'
},
label=_('Power port')
)
rearport = DynamicModelChoiceField(
queryset=RearPort.objects.all(),
required=False,
query_params={
'device_id': '$device'
},
label=_('Rear port')
) )
fieldsets = ( fieldsets = (
@ -1565,22 +1613,61 @@ class InventoryItemForm(DeviceComponentForm):
('Hardware', ('manufacturer', 'part_id', 'serial', 'asset_tag')), ('Hardware', ('manufacturer', 'part_id', 'serial', 'asset_tag')),
) )
class Meta:
model = InventoryItem
fields = [
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
'description', 'tags',
]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
initial = kwargs.get('initial', {}).copy()
component_type = initial.get('component_type')
component_id = initial.get('component_id')
# Used for picking the default active tab for component selection
self.no_component = True
if instance:
# When editing set the initial value for component selectin
for component_model in ContentType.objects.filter(MODULAR_COMPONENT_MODELS):
if type(instance.component) is component_model.model_class():
initial[component_model.model] = instance.component
self.no_component = False
break
elif component_type and component_id:
# When adding the InventoryItem from a component page
if content_type := ContentType.objects.filter(MODULAR_COMPONENT_MODELS).filter(pk=component_type).first():
if component := content_type.model_class().objects.filter(pk=component_id).first():
initial[content_type.model] = component
self.no_component = False
kwargs['initial'] = initial
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Specifically allow editing the device of IntentoryItems # Specifically allow editing the device of IntentoryItems
if self.instance.pk: if self.instance.pk:
self.fields['device'].disabled = False self.fields['device'].disabled = False
class Meta: def clean(self):
model = InventoryItem super().clean()
fields = [
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', # Handle object assignment
'description', 'component_type', 'component_id', 'tags', selected_objects = [
field for field in (
'consoleport', 'consoleserverport', 'frontport', 'interface', 'poweroutlet', 'powerport', 'rearport'
) if self.cleaned_data[field]
] ]
if len(selected_objects) > 1:
raise forms.ValidationError("An InventoryItem can only be assigned to a single component.")
elif selected_objects:
self.instance.component = self.cleaned_data[selected_objects[0]]
else:
self.instance.component = None
#
# Device component roles # Device component roles
# #

View File

@ -1146,3 +1146,8 @@ class InventoryItem(MPTTModel, ComponentModel):
# When moving an InventoryItem to another device, remove any associated component # When moving an InventoryItem to another device, remove any associated component
if self.component and self.component.device != self.device: if self.component and self.component.device != self.device:
self.component = None self.component = None
else:
if self.component and self.component.device != self.device:
raise ValidationError({
"device": "Cannot assign inventory item to component on another device"
})

View File

@ -961,7 +961,7 @@ class Module(PrimaryModel, ConfigContextModel):
def clean(self): def clean(self):
super().clean() super().clean()
if self.module_bay.device != self.device: if hasattr(self, "module_bay") and (self.module_bay.device != self.device):
raise ValidationError( raise ValidationError(
f"Module must be installed within a module bay belonging to the assigned device ({self.device})." f"Module must be installed within a module bay belonging to the assigned device ({self.device})."
) )

View File

@ -2914,23 +2914,14 @@ class InventoryItemView(generic.ObjectView):
class InventoryItemEditView(generic.ObjectEditView): class InventoryItemEditView(generic.ObjectEditView):
queryset = InventoryItem.objects.all() queryset = InventoryItem.objects.all()
form = forms.InventoryItemForm form = forms.InventoryItemForm
template_name = 'dcim/inventoryitem_edit.html'
class InventoryItemCreateView(generic.ComponentCreateView): class InventoryItemCreateView(generic.ComponentCreateView):
queryset = InventoryItem.objects.all() queryset = InventoryItem.objects.all()
form = forms.InventoryItemCreateForm form = forms.InventoryItemCreateForm
model_form = forms.InventoryItemForm model_form = forms.InventoryItemForm
template_name = 'dcim/inventoryitem_edit.html'
def alter_object(self, instance, request):
# Set component (if any)
component_type = request.GET.get('component_type')
component_id = request.GET.get('component_id')
if component_type and component_id:
content_type = get_object_or_404(ContentType, pk=component_type)
instance.component = get_object_or_404(content_type.model_class(), pk=component_id)
return instance
@register_model_view(InventoryItem, 'delete') @register_model_view(InventoryItem, 'delete')

View File

@ -31,6 +31,7 @@ REDIS = {
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel # Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)], # 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
# 'SENTINEL_SERVICE': 'netbox', # 'SENTINEL_SERVICE': 'netbox',
'USERNAME': '',
'PASSWORD': '', 'PASSWORD': '',
'DATABASE': 0, 'DATABASE': 0,
'SSL': False, 'SSL': False,
@ -44,6 +45,7 @@ REDIS = {
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel # Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)], # 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
# 'SENTINEL_SERVICE': 'netbox', # 'SENTINEL_SERVICE': 'netbox',
'USERNAME': '',
'PASSWORD': '', 'PASSWORD': '',
'DATABASE': 1, 'DATABASE': 1,
'SSL': False, 'SSL': False,

View File

@ -22,6 +22,7 @@ REDIS = {
'tasks': { 'tasks': {
'HOST': 'localhost', 'HOST': 'localhost',
'PORT': 6379, 'PORT': 6379,
'USERNAME': '',
'PASSWORD': '', 'PASSWORD': '',
'DATABASE': 0, 'DATABASE': 0,
'SSL': False, 'SSL': False,
@ -29,6 +30,7 @@ REDIS = {
'caching': { 'caching': {
'HOST': 'localhost', 'HOST': 'localhost',
'PORT': 6379, 'PORT': 6379,
'USERNAME': '',
'PASSWORD': '', 'PASSWORD': '',
'DATABASE': 1, 'DATABASE': 1,
'SSL': False, 'SSL': False,

View File

@ -230,6 +230,7 @@ TASKS_REDIS_USING_SENTINEL = all([
]) ])
TASKS_REDIS_SENTINEL_SERVICE = TASKS_REDIS.get('SENTINEL_SERVICE', 'default') TASKS_REDIS_SENTINEL_SERVICE = TASKS_REDIS.get('SENTINEL_SERVICE', 'default')
TASKS_REDIS_SENTINEL_TIMEOUT = TASKS_REDIS.get('SENTINEL_TIMEOUT', 10) TASKS_REDIS_SENTINEL_TIMEOUT = TASKS_REDIS.get('SENTINEL_TIMEOUT', 10)
TASKS_REDIS_USERNAME = TASKS_REDIS.get('USERNAME', '')
TASKS_REDIS_PASSWORD = TASKS_REDIS.get('PASSWORD', '') TASKS_REDIS_PASSWORD = TASKS_REDIS.get('PASSWORD', '')
TASKS_REDIS_DATABASE = TASKS_REDIS.get('DATABASE', 0) TASKS_REDIS_DATABASE = TASKS_REDIS.get('DATABASE', 0)
TASKS_REDIS_SSL = TASKS_REDIS.get('SSL', False) TASKS_REDIS_SSL = TASKS_REDIS.get('SSL', False)
@ -243,6 +244,8 @@ if 'caching' not in REDIS:
CACHING_REDIS_HOST = REDIS['caching'].get('HOST', 'localhost') CACHING_REDIS_HOST = REDIS['caching'].get('HOST', 'localhost')
CACHING_REDIS_PORT = REDIS['caching'].get('PORT', 6379) CACHING_REDIS_PORT = REDIS['caching'].get('PORT', 6379)
CACHING_REDIS_DATABASE = REDIS['caching'].get('DATABASE', 0) CACHING_REDIS_DATABASE = REDIS['caching'].get('DATABASE', 0)
CACHING_REDIS_USERNAME = REDIS['caching'].get('USERNAME', '')
CACHING_REDIS_USERNAME_HOST = '@'.join(filter(None, [CACHING_REDIS_USERNAME, CACHING_REDIS_HOST]))
CACHING_REDIS_PASSWORD = REDIS['caching'].get('PASSWORD', '') CACHING_REDIS_PASSWORD = REDIS['caching'].get('PASSWORD', '')
CACHING_REDIS_SENTINELS = REDIS['caching'].get('SENTINELS', []) CACHING_REDIS_SENTINELS = REDIS['caching'].get('SENTINELS', [])
CACHING_REDIS_SENTINEL_SERVICE = REDIS['caching'].get('SENTINEL_SERVICE', 'default') CACHING_REDIS_SENTINEL_SERVICE = REDIS['caching'].get('SENTINEL_SERVICE', 'default')
@ -252,7 +255,7 @@ CACHING_REDIS_SKIP_TLS_VERIFY = REDIS['caching'].get('INSECURE_SKIP_TLS_VERIFY',
CACHES = { CACHES = {
'default': { 'default': {
'BACKEND': 'django_redis.cache.RedisCache', 'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': f'{CACHING_REDIS_PROTO}://{CACHING_REDIS_HOST}:{CACHING_REDIS_PORT}/{CACHING_REDIS_DATABASE}', 'LOCATION': f'{CACHING_REDIS_PROTO}://{CACHING_REDIS_USERNAME_HOST}:{CACHING_REDIS_PORT}/{CACHING_REDIS_DATABASE}',
'OPTIONS': { 'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient', 'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'PASSWORD': CACHING_REDIS_PASSWORD, 'PASSWORD': CACHING_REDIS_PASSWORD,
@ -640,6 +643,7 @@ else:
} }
RQ_PARAMS.update({ RQ_PARAMS.update({
'DB': TASKS_REDIS_DATABASE, 'DB': TASKS_REDIS_DATABASE,
'USERNAME': TASKS_REDIS_USERNAME,
'PASSWORD': TASKS_REDIS_PASSWORD, 'PASSWORD': TASKS_REDIS_PASSWORD,
'DEFAULT_TIMEOUT': RQ_DEFAULT_TIMEOUT, 'DEFAULT_TIMEOUT': RQ_DEFAULT_TIMEOUT,
}) })

View File

@ -60,7 +60,7 @@
{% if object.mark_connected %} {% if object.mark_connected %}
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected <span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
{% elif object.cable %} {% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' %} {% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:consoleport_trace' %}
{% else %} {% else %}
<div class="text-muted"> <div class="text-muted">
Not Connected Not Connected

View File

@ -60,7 +60,7 @@
{% if object.mark_connected %} {% if object.mark_connected %}
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected <span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
{% elif object.cable %} {% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' %} {% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:consoleserverport_trace' %}
{% else %} {% else %}
<div class="text-muted"> <div class="text-muted">
Not Connected Not Connected

View File

@ -3,7 +3,7 @@
<th scope="row">Cable</th> <th scope="row">Cable</th>
<td> <td>
{{ object.cable|linkify }} {{ object.cable|linkify }}
<a href="{% url 'dcim:interface_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace"> <a href="{% url trace_url pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i> <i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a> </a>
</td> </td>

View File

@ -145,7 +145,7 @@
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected <span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
</div> </div>
{% elif object.cable %} {% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' %} {% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:interface_trace' %}
{% elif object.wireless_link %} {% elif object.wireless_link %}
<table class="table table-hover"> <table class="table table-hover">
<tr> <tr>

View File

@ -0,0 +1,106 @@
{% extends 'generic/object_edit.html' %}
{% load static %}
{% load form_helpers %}
{% load helpers %}
{% block form %}
<div class="field-group my-5">
<div class="row mb-2">
<h5 class="offset-sm-3">InventoryItem</h5>
</div>
{% render_field form.device %}
{% render_field form.parent %}
{% render_field form.name %}
{% render_field form.label %}
{% render_field form.role %}
{% render_field form.description %}
{% render_field form.tags %}
</div>
<div class="field-group my-5">
<div class="row mb-2">
<h5 class="offset-sm-3">Hardware</h5>
</div>
{% render_field form.manufacturer %}
{% render_field form.part_id %}
{% render_field form.serial %}
{% render_field form.asset_tag %}
</div>
<div class="field-group my-5">
<div class="row mb-2">
<h5 class="offset-sm-3">Component Assignment</h5>
</div>
<div class="row mb-2 offset-sm-3">
<ul class="nav nav-pills" role="tablist">
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="consoleport_tab" data-bs-toggle="tab" aria-controls="consoleport" data-bs-target="#consoleport" class="nav-link {% if form.initial.consoleport or form.no_component %}active{% endif %}">
Console Port
</button>
</li>
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="consoleserverport_tab" data-bs-toggle="tab" aria-controls="consoleserverport" data-bs-target="#consoleserverport" class="nav-link {% if form.initial.consoleserverport %}active{% endif %}">
Console Server Port
</button>
</li>
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="frontport_tab" data-bs-toggle="tab" aria-controls="frontport" data-bs-target="#frontport" class="nav-link {% if form.initial.frontport %}active{% endif %}">
Front Port
</button>
</li>
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="interface_tab" data-bs-toggle="tab" aria-controls="interface" data-bs-target="#interface" class="nav-link {% if form.initial.interface %}active{% endif %}">
Interface
</button>
</li>
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="poweroutlet_tab" data-bs-toggle="tab" aria-controls="poweroutlet" data-bs-target="#poweroutlet" class="nav-link {% if form.initial.poweroutlet %}active{% endif %}">
Power Outlet
</button>
</li>
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="powerport_tab" data-bs-toggle="tab" aria-controls="powerport" data-bs-target="#powerport" class="nav-link {% if form.initial.powerport %}active{% endif %}">
Power Port
</button>
</li>
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="rearport_tab" data-bs-toggle="tab" aria-controls="rearport" data-bs-target="#rearport" class="nav-link {% if form.initial.rearport %}active{% endif %}">
Rear Port
</button>
</li>
</ul>
</div>
<div class="tab-content p-0 border-0">
<div class="tab-pane {% if form.initial.consoleport or form.no_component %}active{% endif %}" id="consoleport" role="tabpanel" aria-labeled-by="consoleport_tab">
{% render_field form.consoleport %}
</div>
<div class="tab-pane {% if form.initial.consoleserverport %}active{% endif %}" id="consoleserverport" role="tabpanel" aria-labeled-by="consoleserverport_tab">
{% render_field form.consoleserverport %}
</div>
<div class="tab-pane {% if form.initial.frontport %}active{% endif %}" id="frontport" role="tabpanel" aria-labeled-by="frontport_tab">
{% render_field form.frontport %}
</div>
<div class="tab-pane {% if form.initial.interface %}active{% endif %}" id="interface" role="tabpanel" aria-labeled-by="interface_tab">
{% render_field form.interface %}
</div>
<div class="tab-pane {% if form.initial.poweroutlet %}active{% endif %}" id="poweroutlet" role="tabpanel" aria-labeled-by="poweroutlet_tab">
{% render_field form.poweroutlet %}
</div>
<div class="tab-pane {% if form.initial.powerport %}active{% endif %}" id="powerport" role="tabpanel" aria-labeled-by="powerport_tab">
{% render_field form.powerport %}
</div>
<div class="tab-pane {% if form.initial.rearport %}active{% endif %}" id="rearport" role="tabpanel" aria-labeled-by="rearport_tab">
{% render_field form.rearport %}
</div>
</div>
</div>
{% if form.custom_fields %}
<div class="field-group my-5">
<div class="row mb-2">
<h5 class="offset-sm-3">Custom Fields</h5>
</div>
{% render_custom_fields form %}
</div>
{% endif %}
{% endblock %}

View File

@ -112,7 +112,7 @@
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected <span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
</div> </div>
{% elif object.cable %} {% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' %} {% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:powerfeed_trace' %}
{% else %} {% else %}
<div class="text-muted"> <div class="text-muted">
Not connected Not connected

View File

@ -66,7 +66,7 @@
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected <span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
</div> </div>
{% elif object.cable %} {% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' %} {% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:poweroutlet_trace' %}
{% else %} {% else %}
<div class="text-muted"> <div class="text-muted">
Not Connected Not Connected

View File

@ -66,7 +66,7 @@
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected <span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
</div> </div>
{% elif object.cable %} {% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' %} {% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:powerport_trace' %}
{% else %} {% else %}
<div class="text-muted"> <div class="text-muted">
Not Connected Not Connected