Closes #8511: Enable custom fields and tags for circuit terminations

This commit is contained in:
jeremystretch 2022-07-01 15:10:31 -04:00
parent a57398b0d6
commit a5124ab9c8
10 changed files with 155 additions and 79 deletions

View File

@ -28,6 +28,7 @@
* [#8222](https://github.com/netbox-community/netbox/issues/8222) - Enable the assignment of a VM to a specific host device within a cluster
* [#8471](https://github.com/netbox-community/netbox/issues/8471) - Add `status` field to Cluster
* [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping
* [#8511](https://github.com/netbox-community/netbox/issues/8511) - Enable custom fields and tags for circuit terminations
* [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results
* [#9070](https://github.com/netbox-community/netbox/issues/9070) - Hide navigation menu items based on user permissions
* [#9166](https://github.com/netbox-community/netbox/issues/9166) - Add UI visibility toggle for custom fields
@ -51,6 +52,8 @@
* circuits.Circuit
* Added optional `termination_date` field
* circuits.CircuitTermination
* Added 'custom_fields' and 'tags' fields
* dcim.Device
* The `position` field has been changed from an integer to a decimal
* dcim.DeviceType

View File

@ -98,7 +98,7 @@ class CircuitSerializer(NetBoxModelSerializer):
]
class CircuitTerminationSerializer(ValidatedModelSerializer, LinkTerminationSerializer):
class CircuitTerminationSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
circuit = NestedCircuitSerializer()
site = NestedSiteSerializer(required=False, allow_null=True)
@ -110,5 +110,5 @@ class CircuitTerminationSerializer(ValidatedModelSerializer, LinkTerminationSeri
fields = [
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type',
'_occupied', 'created', 'last_updated',
'_occupied', 'tags', 'custom_fields', 'created', 'last_updated',
]

View File

@ -198,7 +198,7 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
).distinct()
class CircuitTerminationFilterSet(ChangeLoggedModelFilterSet, CableTerminationFilterSet):
class CircuitTerminationFilterSet(NetBoxModelFilterSet, CableTerminationFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',

View File

@ -116,7 +116,7 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
}
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
class CircuitTerminationForm(NetBoxModelForm):
provider = DynamicModelChoiceField(
queryset=Provider.objects.all(),
required=False,
@ -161,7 +161,7 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
model = CircuitTermination
fields = [
'provider', 'circuit', 'term_side', 'region', 'site_group', 'site', 'provider_network', 'mark_connected',
'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description',
'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'tags',
]
help_texts = {
'port_speed': "Physical circuit speed",

View File

@ -1,4 +1,5 @@
from circuits import filtersets, models
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
__all__ = (
@ -10,7 +11,7 @@ __all__ = (
)
class CircuitTerminationType(ObjectType):
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
class Meta:
model = models.CircuitTermination

View File

@ -0,0 +1,24 @@
import django.core.serializers.json
from django.db import migrations, models
import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('extras', '0076_configcontext_locations'),
('circuits', '0036_circuit_termination_date'),
]
operations = [
migrations.AddField(
model_name='circuittermination',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='circuittermination',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
]

View File

@ -5,7 +5,9 @@ from django.urls import reverse
from circuits.choices import *
from dcim.models import LinkTermination
from netbox.models import ChangeLoggedModel, OrganizationalModel, NetBoxModel
from netbox.models import (
ChangeLoggedModel, CustomFieldsMixin, CustomLinksMixin, OrganizationalModel, NetBoxModel, TagsMixin,
)
from netbox.models.features import WebhooksMixin
__all__ = (
@ -141,7 +143,14 @@ class Circuit(NetBoxModel):
return CircuitStatusChoices.colors.get(self.status)
class CircuitTermination(WebhooksMixin, ChangeLoggedModel, LinkTermination):
class CircuitTermination(
CustomFieldsMixin,
CustomLinksMixin,
TagsMixin,
WebhooksMixin,
ChangeLoggedModel,
LinkTermination
):
circuit = models.ForeignKey(
to='circuits.Circuit',
on_delete=models.CASCADE,

View File

@ -8,74 +8,78 @@
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Circuit
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Provider</th>
<td>{{ object.provider|linkify }}</td>
</tr>
<tr>
<th scope="row">Circuit ID</th>
<td>{{ object.cid }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>{{ object.type|linkify }}</td>
</tr>
<tr>
<th scope="row">Status</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr>
<tr>
<th scope="row">Tenant</th>
<td>
{% if object.tenant.group %}
{{ object.tenant.group|linkify }} /
{% endif %}
{{ object.tenant|linkify|placeholder }}
</td>
</tr>
<tr>
<th scope="row">Install Date</th>
<td>{{ object.install_date|annotated_date|placeholder }}</td>
</tr>
<tr>
<th scope="row">Termination Date</th>
<td>{{ object.termination_date|annotated_date|placeholder }}</td>
</tr>
<tr>
<th scope="row">Commit Rate</th>
<td>{{ object.commit_rate|humanize_speed|placeholder }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">Circuit</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Provider</th>
<td>{{ object.provider|linkify }}</td>
</tr>
<tr>
<th scope="row">Circuit ID</th>
<td>{{ object.cid }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>{{ object.type|linkify }}</td>
</tr>
<tr>
<th scope="row">Status</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr>
<tr>
<th scope="row">Tenant</th>
<td>
{% if object.tenant.group %}
{{ object.tenant.group|linkify }} /
{% endif %}
{{ object.tenant|linkify|placeholder }}
</td>
</tr>
<tr>
<th scope="row">Install Date</th>
<td>{{ object.install_date|annotated_date|placeholder }}</td>
</tr>
<tr>
<th scope="row">Termination Date</th>
<td>{{ object.termination_date|annotated_date|placeholder }}</td>
</tr>
<tr>
<th scope="row">Commit Rate</th>
<td>{{ object.commit_rate|humanize_speed|placeholder }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
{% include 'inc/panels/contacts.html' %}
{% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
</div>
<div class="col col-md-6">
{% include 'inc/panels/comments.html' %}
{% include 'inc/panels/contacts.html' %}
{% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-6">
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
</div>
<div class="col col-md-6">
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -10,6 +10,7 @@
{% render_field form.provider %}
{% render_field form.circuit %}
{% render_field form.term_side %}
{% render_field form.tags %}
{% render_field form.mark_connected %}
{% with providernetwork_tab_active=form.initial.provider_network %}
<div class="row mb-2">
@ -47,6 +48,13 @@
{% render_field form.pp_info %}
{% render_field form.description %}
</div>
<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>
{% endblock %}
{# Override buttons block, 'Create & Add Another'/'_addanother' is not needed on a circuit. #}

View File

@ -2,7 +2,6 @@
<div class="card">
<div class="card-header">
<strong class="d-block d-md-inline mb-3 mb-md-0">Termination - {{ side }} Side</strong>
<div class="float-md-end">
{% if not termination and perms.circuits.add_circuittermination %}
<a href="{% url 'circuits:circuittermination_add' %}?circuit={{ object.pk }}&term_side={{ side }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-success lh-1">
@ -10,10 +9,10 @@
</a>
{% endif %}
{% if termination and perms.circuits.change_circuittermination %}
<a href="{% url 'circuits:circuittermination_edit' pk=termination.pk %}" class="btn btn-sm btn-warning lh-1">
<a href="{% url 'circuits:circuittermination_edit' pk=termination.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-warning lh-1">
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
</a>
<a href="{% url 'circuits:circuit_terminations_swap' pk=object.pk %}" class="btn btn-sm btn-primary lh-1">
<a href="{% url 'circuits:circuit_terminations_swap' pk=object.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-primary lh-1">
<span class="mdi mdi-swap-vertical" aria-hidden="true"></span> Swap
</a>
{% endif %}
@ -23,6 +22,7 @@
</a>
{% endif %}
</div>
<h5>Termination {{ side }}</h5>
</div>
<div class="card-body">
{% if termination %}
@ -110,6 +110,33 @@
<td>Description</td>
<td>{{ termination.description|placeholder }}</td>
</tr>
<tr>
<td>Tags</td>
<td>
{% for tag in termination.tags.all %}
{% tag tag %}
{% empty %}
{{ ''|placeholder }}
{% endfor %}
</td>
</tr>
{% for group_name, fields in termination.get_custom_fields_by_group.items %}
<tr>
<td colspan="2">
<strong>{{ group_name|default:"Custom Fields" }}</strong>
</td>
</tr>
{% for field, value in fields.items %}
<tr>
<td>
<span title="{{ field.description|escape }}">{{ field }}</span>
</td>
<td>
{% customfield_value field value %}
</td>
</tr>
{% endfor %}
{% endfor %}
</table>
{% else %}
<span class="text-muted">None</span>