Add Coordinate models, forms, views, templates,...

This commit is contained in:
dreng
2023-05-06 22:57:12 +02:00
parent fe83a62615
commit 60c272acb7
18 changed files with 452 additions and 9 deletions
+11 -1
View File
@@ -2,7 +2,7 @@ from dcim.models import Device, DeviceRole
from rest_framework.serializers import ModelSerializer
from netbox.api.serializers import NetBoxModelSerializer
from netbox_topology_views.models import RoleImage, IndividualOptions
from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroups, Coordinates
class TopologyDummySerializer(ModelSerializer):
@@ -22,6 +22,16 @@ class DeviceRoleSerializer(ModelSerializer):
model = DeviceRole
fields = ("name", "slug", "color", "vm_role", "description")
class CoordinateGroupsSerializer(NetBoxModelSerializer):
class Meta:
model = CoordinateGroups
fields = ("name", "description")
class CoordinatesSerializer(NetBoxModelSerializer):
class Meta:
model = Coordinates
fields = ("x", "y")
class IndividualOptionsSerializer(NetBoxModelSerializer):
class Meta:
model = IndividualOptions
+13
View File
@@ -1,6 +1,7 @@
import django_filters
from dcim.choices import DeviceStatusChoices
from dcim.models import Device, DeviceRole, Location, Rack, Region, Site
from .models import Coordinates
from django.db.models import Q
from netbox.filtersets import NetBoxModelFilterSet
from tenancy.filtersets import TenancyFilterSet
@@ -53,3 +54,15 @@ class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
return queryset
qs_filter = Q(name__icontains=value)
return queryset.filter(qs_filter)
class CoordinateFilterSet(NetBoxModelFilterSet):
class Meta:
model = Coordinates
fields = ('id', 'group', 'device', 'x', 'y')
def search(self, queryset, name, value):
"""Perform the filtered search."""
if not value.strip():
return queryset
qs_filter = Q(group__icontains=value | Q(device__icontains=value))
return queryset.filter(qs_filter)
+51 -1
View File
@@ -15,12 +15,13 @@ from django.conf import settings
from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm
from utilities.forms.fields import (
TagFilterField,
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
MultipleChoiceField
)
from .models import IndividualOptions
from .models import IndividualOptions, CoordinateGroups, Coordinates
class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Device
@@ -141,6 +142,55 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
label =_("Show Wireless Links"), required=False, initial=False
)
class CoordinateGroupsForm(NetBoxModelForm):
fieldsets = (
('Group Details', ('name', 'description')),
)
class Meta:
model = CoordinateGroups
fields = ('name', 'description')
class CoordinatesForm(NetBoxModelForm):
fieldsets = (
('Coordinates', ('group', 'device', 'x', 'y')),
)
class Meta:
model = Coordinates
fields = ('group', 'device', 'x', 'y')
class CoordinatesFilterForm(NetBoxModelFilterSetForm):
model = Coordinates
group = forms.ModelMultipleChoiceField(
queryset=CoordinateGroups.objects.all(),
required=False
)
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
required=False
)
id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False,
label=_("Device"),
query_params={
"location_id": "$location_id",
"region_id": "$region_id",
"site_id": "$site_id",
"role_id": "$device_role_id",
},
)
x = forms.IntegerField(
required=False
)
y = forms.IntegerField(
required=False
)
class IndividualOptionsForm(NetBoxModelForm):
fieldsets = (
(
@@ -0,0 +1,51 @@
# Generated by Django 4.1.8 on 2023-05-01 08:21
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
import utilities.json
class Migration(migrations.Migration):
dependencies = [
('dcim', '0171_cabletermination_change_logging'),
('extras', '0092_delete_jobresult'),
('netbox_topology_views', '0003_individualoptions_show_neighbors'),
]
operations = [
migrations.CreateModel(
name='CoordinateGroups',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
('name', models.CharField(max_length=100, unique=True)),
('description', models.CharField(blank=True, max_length=255)),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Coordinates',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
('x', models.IntegerField()),
('y', models.IntegerField()),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dcim.device')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='netbox_topology_views.coordinategroups')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'ordering': ['group', 'device'],
'unique_together': {('device', 'group')},
},
),
]
+59 -1
View File
@@ -1,12 +1,13 @@
from pathlib import Path
from typing import Optional
from dcim.models import DeviceRole
from dcim.models import Device, DeviceRole
from extras.models import Tag
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.templatetags.static import static
from django.urls import reverse
from netbox.models import NetBoxModel
from netbox.models.features import (
ChangeLoggingMixin,
@@ -94,6 +95,63 @@ class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, WebhooksMixin):
return self.get_default_image(dir)
return static(f"/{self.image}")
class CoordinateGroups(NetBoxModel):
"""
A coordinate group is used to display the topology for a particular group.
This allows different visualizations with the same devices.
"""
name = models.CharField(
max_length=100,
unique=True,
)
description = models.CharField(
max_length=255,
blank = True,
)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('plugins:netbox_topology_views:coordinategroups', args=[self.pk])
class Coordinates(NetBoxModel):
"""
Coordinates are being used to place devices in a topology view onto a certian
position. Devices belong to one or more coordinate groups. They have to
be unique together.
"""
device = models.ForeignKey(Device, on_delete=models.CASCADE)
group = models.ForeignKey(CoordinateGroups, on_delete=models.CASCADE)
x = models.IntegerField(
help_text='X-coordinate of the device (horizontal) on the canvas. '
'Smaller values correspond to a position further up on the monitor.',
)
y = models.IntegerField(
help_text='Y-coordinate of the device (vertical) on the canvas. '
'Smaller values correspond to a position further to the left on the monitor.',
)
class Meta:
ordering = ['group', 'device']
unique_together = ('device', 'group')
def __str__(self):
return f'{self.x};{self.y}'
def get_absolute_url(self):
return reverse('plugins:netbox_topology_views:coordinates', args=[self.pk])
def set_xy_from_text_coords(self, coords: str):
xy = coords.split(';')
self.x = xy[0]
self.y = xy[1]
class IndividualOptions(NetBoxModel):
CHOICES = (
('interface', 'interface'),
+27 -2
View File
@@ -1,10 +1,35 @@
from extras.plugins import PluginMenu, PluginMenuItem
from extras.plugins import PluginMenu, PluginMenuItem, PluginMenuButton
from utilities.choices import ButtonColorChoices
coordinategroups_buttons = [
PluginMenuButton(
link='plugins:netbox_topology_views:coordinategroups_add',
title='Add',
icon_class='mdi mdi-plus-thick',
color=ButtonColorChoices.GREEN,
)
]
coordinates_buttons = [
PluginMenuButton(
link='plugins:netbox_topology_views:coordinates_add',
title='Add',
icon_class='mdi mdi-plus-thick',
color=ButtonColorChoices.GREEN,
)
]
menu = PluginMenu(
label='Topology Views',
icon_class="mdi mdi-sitemap",
groups=(
('TOPOLOGY', (PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),),),
('TOPOLOGY',
(
PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),
PluginMenuItem(link="plugins:netbox_topology_views:coordinategroups_list", link_text="Coordinate Groups", buttons=coordinategroups_buttons),
PluginMenuItem(link="plugins:netbox_topology_views:coordinates_list", link_text="Coordinates", buttons=coordinates_buttons),
),
),
('PREFERENCES',
(
PluginMenuItem(link="plugins:netbox_topology_views:images", link_text="Images"),
+18
View File
@@ -0,0 +1,18 @@
from netbox.search import SearchIndex, register_search
from .models import CoordinateGroups, Coordinates
@register_search
class CoordinateGroupsIndex(SearchIndex):
model = CoordinateGroups
fields = (
('name', 100),
('description', 2000),
)
@register_search
class CoordinatesIndex(SearchIndex):
model = Coordinates
fields = (
('group', 100),
('device', 200),
)
+29
View File
@@ -0,0 +1,29 @@
import django_tables2 as tables
from netbox.tables import NetBoxTable, ChoiceFieldColumn
from .models import CoordinateGroups, Coordinates
class CoordinateGroupListTable(NetBoxTable):
name = tables.Column(
linkify=True
)
class Meta(NetBoxTable.Meta):
model = CoordinateGroups
fields = ('pk', 'name', 'description')
default_columns = ('name', 'description')
class CoordinateListTable(NetBoxTable):
group = tables.Column(
linkify=True
)
device = tables.Column(
linkify=True
)
class Meta(NetBoxTable.Meta):
model = Coordinates
fields = ('pk', 'group', 'device', 'x', 'y')
default_columns = ('id', 'group', 'device', 'x', 'y')
@@ -0,0 +1,35 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block title %}Topology Views Coordinate Group{% endblock title %}
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Coordinate Group
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>
</div>
{% endblock content %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_edit.html' %}
{% block title %}Add a new Coordinate Group{% endblock title %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_edit.html' %}
{% block title %}Edit Coordinate Group{% endblock title %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_list.html' %}
{% block title %}Coordinate Groups{% endblock title %}
@@ -0,0 +1,43 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block title %}Topology Views Coordinates{% endblock title %}
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Coordinates
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Group</th>
<td>{{ object.group }}</td>
</tr>
<tr>
<th scope="row">Device</th>
<td>{{ object.device }}</td>
</tr>
<tr>
<th scope="row">X-Coordinate</th>
<td>{{ object.x }}</td>
</tr>
<tr>
<th scope="row">Y-Coordinate</th>
<td>{{ object.y }}</td>
</tr>
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>
</div>
{% endblock content %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_edit.html' %}
{% block title %}Add new Device Coordinates{% endblock title %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_edit.html' %}
{% block title %}Edit Device Coordinates{% endblock title %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_list.html' %}
{% block title %}Device Coordinates{% endblock title %}
+17 -1
View File
@@ -1,7 +1,7 @@
from django.urls import path
from django.views.generic.base import RedirectView
from . import views
from . import models, views
# Define a list of URL patterns to be imported by NetBox. Each pattern maps a URL to
# a specific view so that it can be accessed by users.
@@ -10,4 +10,20 @@ urlpatterns = (
path("topology/", views.TopologyHomeView.as_view(), name="home"),
path("images/", views.TopologyImagesView.as_view(), name="images"),
path("individualoptions/", views.TopologyIndividualOptionsView.as_view(), name="individualoptions"),
# Coordinate Group
path("coordinate-groups/", views.CoordinateGroupListView.as_view(), name="coordinategroups_list"),
path("coordinate-groups/add/", views.CoordinateGroupAddView.as_view(), name="coordinategroups_add"),
path("coordinate-groups/<int:pk>/", views.CoordinateGroupView.as_view(), name="coordinategroups"),
path("coordinate-groups/<int:pk>/edit/", views.CoordinateGroupEditView.as_view(), name="coordinategroups_edit"),
path("coordinate-groups/<int:pk>/delete/", views.CoordinateGroupDeleteView.as_view(), name="coordinategroups_delete"),
path("coordinate-groups/<int:pk>/changelog/", views.CoordinateGroupChangeLogView.as_view(), name="coordinategroups_changelog", kwargs={'model': models.CoordinateGroups}),
# Coordinate
path("coordinates/", views.CoordinateListView.as_view(), name="coordinates_list"),
path("coordinates/add/", views.CoordinateAddView.as_view(), name="coordinates_add"),
path("coordinates/<int:pk>/", views.CoordinateView.as_view(), name="coordinates"),
path("coordinates/<int:pk>/edit/", views.CoordinateEditView.as_view(), name="coordinates_edit"),
path("coordinates/<int:pk>/delete/", views.CoordinateDeleteView.as_view(), name="coordinates_delete"),
path("coordinates/<int:pk>/changelog/", views.CoordinateChangeLogView.as_view(), name="coordinates_changelog", kwargs={'model': models.Coordinates}),
)
+80 -3
View File
@@ -29,10 +29,13 @@ from django.shortcuts import render
from django.views.generic import View
from extras.models import Tag
from wireless.models import WirelessLink
from netbox.views.generic import ObjectView, ObjectListView, ObjectEditView, ObjectDeleteView, ObjectChangeLogView
from netbox_topology_views.filters import DeviceFilterSet
from netbox_topology_views.forms import DeviceFilterForm, IndividualOptionsForm
from netbox_topology_views.models import RoleImage, IndividualOptions
from netbox_topology_views.filters import DeviceFilterSet, CoordinateFilterSet
from netbox_topology_views.forms import DeviceFilterForm, IndividualOptionsForm, CoordinateGroupsForm, CoordinatesForm, CoordinatesFilterForm
from netbox_topology_views.models import RoleImage, CoordinateGroups, Coordinates, IndividualOptions
from netbox_topology_views.tables import CoordinateGroupListTable, CoordinateListTable
from netbox_topology_views.utils import (
CONF_IMAGE_DIR,
find_image_url,
@@ -751,6 +754,80 @@ class TopologyImagesView(PermissionRequiredMixin, View):
},
)
class CoordinateView(PermissionRequiredMixin, ObjectView):
permission_required = 'netbox_topology_views.view_coordinates'
queryset = Coordinates.objects.all()
class CoordinateAddView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'netbox_topology_views.add_coordinates'
queryset = Coordinates.objects.all()
form = CoordinatesForm
template_name = 'netbox_topology_views/coordinates_add.html'
class CoordinateListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'netbox_topology_views.view_coordinates'
queryset = Coordinates.objects.all()
table = CoordinateListTable
template_name = 'netbox_topology_views/coordinates_list.html'
filterset = CoordinateFilterSet
filterset_form = CoordinatesFilterForm
class CoordinateEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'netbox_topology_views.change_coordinates'
queryset = Coordinates.objects.all()
form = CoordinatesForm
template_name = 'netbox_topology_views/coordinates_edit.html'
class CoordinateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'netbox_topology_views.delete_coordinates'
queryset = Coordinates.objects.all()
class CoordinateChangeLogView(PermissionRequiredMixin, ObjectChangeLogView):
permission_required = 'netbox_topology_views.view_coordinates'
queryset = Coordinates.objects.all()
class CoordinateGroupView(PermissionRequiredMixin, ObjectView):
permission_required = 'netbox_topology_views.view_coordinategroups'
queryset = CoordinateGroups.objects.all()
class CoordinateGroupAddView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'netbox_topology_views.add_coordinategroups'
queryset = CoordinateGroups.objects.all()
form = CoordinateGroupsForm
template_name = 'netbox_topology_views/coordinategroups_add.html'
class CoordinateGroupListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'netbox_topology_views.view_coordinategroups'
queryset = CoordinateGroups.objects.all()
table = CoordinateGroupListTable
template_name = 'netbox_topology_views/coordinategroups_list.html'
class CoordinateGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'netbox_topology_views.change_coordinategroups'
queryset = CoordinateGroups.objects.all()
form = CoordinateGroupsForm
template_name = 'netbox_topology_views/coordinategroups_edit.html'
class CoordinateGroupDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'netbox_topology_views.delete_coordinategroups'
queryset = CoordinateGroups.objects.all()
class CoordinateGroupChangeLogView(PermissionRequiredMixin, ObjectChangeLogView):
permission_required = 'netbox_topology_views.view_coordinategroups'
queryset = CoordinateGroups.objects.all()
class TopologyIndividualOptionsView(PermissionRequiredMixin, View):
permission_required = 'netbox_topology_views.change_individualoptions'