diff --git a/netbox_topology_views/api/serializers.py b/netbox_topology_views/api/serializers.py
index 82963eb..5aa3054 100644
--- a/netbox_topology_views/api/serializers.py
+++ b/netbox_topology_views/api/serializers.py
@@ -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
diff --git a/netbox_topology_views/filters.py b/netbox_topology_views/filters.py
index 9a8e70d..324c298 100644
--- a/netbox_topology_views/filters.py
+++ b/netbox_topology_views/filters.py
@@ -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)
\ No newline at end of file
diff --git a/netbox_topology_views/forms.py b/netbox_topology_views/forms.py
index af1ab06..38bf126 100644
--- a/netbox_topology_views/forms.py
+++ b/netbox_topology_views/forms.py
@@ -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 = (
(
diff --git a/netbox_topology_views/migrations/0004_coordinategroups_coordinates.py b/netbox_topology_views/migrations/0004_coordinategroups_coordinates.py
new file mode 100644
index 0000000..8d7c295
--- /dev/null
+++ b/netbox_topology_views/migrations/0004_coordinategroups_coordinates.py
@@ -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')},
+ },
+ ),
+ ]
diff --git a/netbox_topology_views/models.py b/netbox_topology_views/models.py
index 941ef34..7eb239e 100644
--- a/netbox_topology_views/models.py
+++ b/netbox_topology_views/models.py
@@ -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'),
diff --git a/netbox_topology_views/navigation.py b/netbox_topology_views/navigation.py
index 32a29af..25b6c90 100644
--- a/netbox_topology_views/navigation.py
+++ b/netbox_topology_views/navigation.py
@@ -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"),
diff --git a/netbox_topology_views/search.py b/netbox_topology_views/search.py
new file mode 100644
index 0000000..9bae850
--- /dev/null
+++ b/netbox_topology_views/search.py
@@ -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),
+ )
\ No newline at end of file
diff --git a/netbox_topology_views/tables.py b/netbox_topology_views/tables.py
new file mode 100644
index 0000000..abcb507
--- /dev/null
+++ b/netbox_topology_views/tables.py
@@ -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')
+
diff --git a/netbox_topology_views/templates/netbox_topology_views/coordinategroups.html b/netbox_topology_views/templates/netbox_topology_views/coordinategroups.html
new file mode 100644
index 0000000..a8e0db1
--- /dev/null
+++ b/netbox_topology_views/templates/netbox_topology_views/coordinategroups.html
@@ -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 %}
+
+
+
+
+
+
+
+ | Name |
+ {{ object.name }} |
+
+
+ | Description |
+ {{ object.description|placeholder }} |
+
+
+
+
+ {% plugin_left_page object %}
+
+
+ {% include 'inc/panels/custom_fields.html' %}
+ {% plugin_right_page object %}
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/netbox_topology_views/templates/netbox_topology_views/coordinategroups_add.html b/netbox_topology_views/templates/netbox_topology_views/coordinategroups_add.html
new file mode 100644
index 0000000..1fb7b4e
--- /dev/null
+++ b/netbox_topology_views/templates/netbox_topology_views/coordinategroups_add.html
@@ -0,0 +1,3 @@
+{% extends 'generic/object_edit.html' %}
+
+{% block title %}Add a new Coordinate Group{% endblock title %}
diff --git a/netbox_topology_views/templates/netbox_topology_views/coordinategroups_edit.html b/netbox_topology_views/templates/netbox_topology_views/coordinategroups_edit.html
new file mode 100644
index 0000000..4f67ce8
--- /dev/null
+++ b/netbox_topology_views/templates/netbox_topology_views/coordinategroups_edit.html
@@ -0,0 +1,3 @@
+{% extends 'generic/object_edit.html' %}
+
+{% block title %}Edit Coordinate Group{% endblock title %}
\ No newline at end of file
diff --git a/netbox_topology_views/templates/netbox_topology_views/coordinategroups_list.html b/netbox_topology_views/templates/netbox_topology_views/coordinategroups_list.html
new file mode 100644
index 0000000..f43874c
--- /dev/null
+++ b/netbox_topology_views/templates/netbox_topology_views/coordinategroups_list.html
@@ -0,0 +1,3 @@
+{% extends 'generic/object_list.html' %}
+
+{% block title %}Coordinate Groups{% endblock title %}
\ No newline at end of file
diff --git a/netbox_topology_views/templates/netbox_topology_views/coordinates.html b/netbox_topology_views/templates/netbox_topology_views/coordinates.html
new file mode 100644
index 0000000..1ffc230
--- /dev/null
+++ b/netbox_topology_views/templates/netbox_topology_views/coordinates.html
@@ -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 %}
+
+
+
+
+
+
+
+ | Group |
+ {{ object.group }} |
+
+
+ | Device |
+ {{ object.device }} |
+
+
+ | X-Coordinate |
+ {{ object.x }} |
+
+
+ | Y-Coordinate |
+ {{ object.y }} |
+
+
+
+
+ {% plugin_left_page object %}
+
+
+ {% include 'inc/panels/custom_fields.html' %}
+ {% plugin_right_page object %}
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/netbox_topology_views/templates/netbox_topology_views/coordinates_add.html b/netbox_topology_views/templates/netbox_topology_views/coordinates_add.html
new file mode 100644
index 0000000..184c857
--- /dev/null
+++ b/netbox_topology_views/templates/netbox_topology_views/coordinates_add.html
@@ -0,0 +1,3 @@
+{% extends 'generic/object_edit.html' %}
+
+{% block title %}Add new Device Coordinates{% endblock title %}
\ No newline at end of file
diff --git a/netbox_topology_views/templates/netbox_topology_views/coordinates_edit.html b/netbox_topology_views/templates/netbox_topology_views/coordinates_edit.html
new file mode 100644
index 0000000..8debe4d
--- /dev/null
+++ b/netbox_topology_views/templates/netbox_topology_views/coordinates_edit.html
@@ -0,0 +1,3 @@
+{% extends 'generic/object_edit.html' %}
+
+{% block title %}Edit Device Coordinates{% endblock title %}
\ No newline at end of file
diff --git a/netbox_topology_views/templates/netbox_topology_views/coordinates_list.html b/netbox_topology_views/templates/netbox_topology_views/coordinates_list.html
new file mode 100644
index 0000000..6470402
--- /dev/null
+++ b/netbox_topology_views/templates/netbox_topology_views/coordinates_list.html
@@ -0,0 +1,3 @@
+{% extends 'generic/object_list.html' %}
+
+{% block title %}Device Coordinates{% endblock title %}
\ No newline at end of file
diff --git a/netbox_topology_views/urls.py b/netbox_topology_views/urls.py
index 8581297..f344c67 100644
--- a/netbox_topology_views/urls.py
+++ b/netbox_topology_views/urls.py
@@ -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//", views.CoordinateGroupView.as_view(), name="coordinategroups"),
+ path("coordinate-groups//edit/", views.CoordinateGroupEditView.as_view(), name="coordinategroups_edit"),
+ path("coordinate-groups//delete/", views.CoordinateGroupDeleteView.as_view(), name="coordinategroups_delete"),
+ path("coordinate-groups//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//", views.CoordinateView.as_view(), name="coordinates"),
+ path("coordinates//edit/", views.CoordinateEditView.as_view(), name="coordinates_edit"),
+ path("coordinates//delete/", views.CoordinateDeleteView.as_view(), name="coordinates_delete"),
+ path("coordinates//changelog/", views.CoordinateChangeLogView.as_view(), name="coordinates_changelog", kwargs={'model': models.Coordinates}),
)
diff --git a/netbox_topology_views/views.py b/netbox_topology_views/views.py
index 890999e..f775cc7 100644
--- a/netbox_topology_views/views.py
+++ b/netbox_topology_views/views.py
@@ -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'