From 5d20a7ad1507ab0124655f6a906df6813e3c27a0 Mon Sep 17 00:00:00 2001 From: Mario Date: Thu, 23 Mar 2023 09:57:51 +0100 Subject: [PATCH] Closes #252,#78,#177,#265: Move options from config file to GUI (#262) * Added some individual options * Added general options * Added show_single_cable_logical_conns and draw_default_layout * Added ignore_cable_type to IndividualOptions * Corrected permission, tiny improvements * Add preselected_device_roles to IndividualOptions * Add preselected_tags * Updated documentation * Update README.md * Corrected queryset for preselected_tags * Update README.md * move supported_termination_types to inside the function to prevent migration issues * removed general options * added new migration script --------- Co-authored-by: Mattijs Vanhaverbeke --- README.md | 51 ++--- netbox_topology_views/__init__.py | 17 +- netbox_topology_views/api/serializers.py | 8 +- netbox_topology_views/forms.py | 149 +++++++++++++-- .../migrations/0002_individualoptions.py | 42 ++++ netbox_topology_views/models.py | 61 ++++++ netbox_topology_views/navigation.py | 9 +- .../individual_options.html | 5 + netbox_topology_views/urls.py | 1 + netbox_topology_views/views.py | 180 +++++++++++------- 10 files changed, 396 insertions(+), 127 deletions(-) create mode 100644 netbox_topology_views/migrations/0002_individualoptions.py create mode 100644 netbox_topology_views/templates/netbox_topology_views/individual_options.html diff --git a/README.md b/README.md index 4adea7f..48258d4 100644 --- a/README.md +++ b/README.md @@ -67,14 +67,17 @@ You might also set the `always_save_coordinates` option to True. ## Configure -If you want to override the default values configure the `PLUGINS_CONFIG` in your `netbox configuration.py`. +All individual options can be assigned a default value per user directly in the plugin. The default value can be overridden on the filter page. + +The remaining options must be configured in the `PLUGINS_CONFIG` section of your `netbox/configuration.py`. Example: ``` PLUGINS_CONFIG = { 'netbox_topology_views': { - 'preselected_device_roles': ['Router', 'Firewall'], - 'hide_single_cable_logical_conns': True + 'static_image_directory': 'netbox_topology_views/img', + 'allow_coordinates_saving': True, + 'always_save_coordinates': True } } ``` @@ -82,15 +85,8 @@ PLUGINS_CONFIG = { | Setting | Default value | Description | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | static_image_directory | netbox_topology_views/img | (str or pathlib.Path) Specifies the location that images will be loaded from by default. Must be within `STATIC_ROOT` | -| preselected_device_roles | ['Firewall', 'Router', 'Distribution Switch', 'Core Switch', 'Internal Switch', 'Access Switch', 'Server', 'Storage', 'Backup', 'Wireless AP'] | The full name of the device roles you want to pre select in the global view. Note that this is case sensitive | | allow_coordinates_saving | False | (bool) Set to true if you use the custom coordinates fields and want to save the coordinates | | always_save_coordinates | False | (bool) Set if you want to enable the option to save coordinates by default | -| ignore_cable_type | [] | The cable types that you want to ignore in the views | -| preselected_tags | [] | The name of tags you want to preload | -| draw_default_layout | False | (bool) Set to True if you want to load draw the topology on the initial load (when you go to the topology plugin page) | -| hide_single_cable_logical_conns | False | (bool) Set to True if you want to hide duplicate cables & logical connections | - - ### Custom Images @@ -102,27 +98,27 @@ Go to the plugins tab in the navbar and click topology or go to `$NETBOX_URL/plu Select your options for the topology view: -![preview image](doc/img/selection_options.png?raw=true "preview") +![selection_options](https://user-images.githubusercontent.com/20901110/220410144-5633d24c-6383-4cba-8d6d-ba77ef3d3cdd.png)
-
Hide Unconnected
-
Hide devices which have no connections.
Save Coordinates
Save the coordinates of devices in the topology view.
-
Please read the "Configure" chapter to set the allow_coordinates_saving option to True.
+
Show Unconnected
+
Show devices that have no connections or for which no connection is displayed. This option depends on other parameters like 'Show Cables' and 'Show Logical Connections'.
+
Show Circuit Terminations
+
Show connections which end at a circuit termination in the topology view. These connections are displayed as blue dashed lines.
Show Cables
-
Show cable connections between devices, including cables connected to interfaces, front / rear ports, etc., in the topology view
+
Show connections between interfaces, front / rear ports, etc., that are connected with one or more cables. These connections are displayed as solid lines in the color of the cable.
Show Logical Connections
Show logical connections between interfaces (referred to as Interface Connections in NetBox) in the topology view. Where the path between interfaces includes multiple cables (e.g., via patch panels), only the end interface connections are shown, not the - intermediate front / rear port connections, etc. This is similar to what was referred to as 'end-to-end' connections in previous versions.
-
Selecting both 'Show Cables' and 'Show Logical Interfaces' will sometimes result in a topology view with 2 cables per connection as these devices are diretly connected. Select only one of the options or set `hide_single_cable_logical_conns` to True.
-
Show Circuit Terminations
-
Show connections which end at a circuit termination in the topology view.
-
Show Wireless Links
-
Show wireless connections in the topology view.
+ intermediate front / rear port connections, etc. This is similar to what was referred to as 'end-to-end' connections in previous versions. These connections are displayed as yellow dotted lines. +
Show redundant Cable and Locigal Connection
+
Shows a logical connection (in addition to a cable), even if a cable is directly connected. Leaving this option disabled prevents that redundant display. This option only has an effect if 'Show Logical Connections' is activated.
Show Power Feeds
-
Show power connections from power feeds in the topology view.
+
Displays connections between power outlets and power ports. These connections are displayed as solid lines in the color of the cable. This option depends on 'Show Cables'.
+
Show Wireless Links
+
Displays wireless connections. These connections are displayed as blue dotted lines.
### Update @@ -139,12 +135,21 @@ Clear you browser cache. ### Permissions -To view `/plugins/topology-views/` you need the following permissions: +To view `/plugins/netbox_topology-views/topology` you need the following permissions: + dcim | device | can view device + dcim | site | can view site + extras | tag | can view tag + dcim | device role | can view device role +To view `/plugins/netbox_topology-views/image`: + + dcim | site | view + + dcim | device role | view + + dcim | device role | add + + dcim | device role | change + + To view `/plugins/netbox_topology-views/individualoptions`: + + netbox topology views | individualoptions | change + ## Icons info Power icons created by [Freepik - Flaticon](https://www.flaticon.com/free-icons/power). diff --git a/netbox_topology_views/__init__.py b/netbox_topology_views/__init__.py index 4307ba0..fea519d 100644 --- a/netbox_topology_views/__init__.py +++ b/netbox_topology_views/__init__.py @@ -11,25 +11,10 @@ class TopologyViewsConfig(PluginConfig): base_url = "netbox_topology_views" required_settings = [] default_settings = { - "preselected_device_roles": [ - "Firewall", - "Router", - "Distribution Switch", - "Core Switch", - "Internal Switch", - "Access Switch", - "Server", - "Storage", - "Backup", - "Wireless AP", - ], - "ignore_cable_type": [], "static_image_directory": "netbox_topology_views/img", "allow_coordinates_saving": False, "always_save_coordinates": False, - "preselected_tags": [], - "draw_default_layout": False, - "hide_single_cable_logical_conns": False, + } diff --git a/netbox_topology_views/api/serializers.py b/netbox_topology_views/api/serializers.py index 56dd400..b8608aa 100644 --- a/netbox_topology_views/api/serializers.py +++ b/netbox_topology_views/api/serializers.py @@ -1,7 +1,8 @@ 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 +from netbox_topology_views.models import RoleImage, IndividualOptions class TopologyDummySerializer(ModelSerializer): @@ -20,3 +21,8 @@ class DeviceRoleSerializer(ModelSerializer): class Meta: model = DeviceRole fields = ("name", "slug", "color", "vm_role", "description") + +class IndividualOptionsSerializer(NetBoxModelSerializer): + class Meta: + model = IndividualOptions + fields = ("ignore_cable_type", "show_unconnected", "show_cables", "show_logical_connections", "show_single_cable_logical_conns", "show_circuit", "show_power", "show_wireless", "draw_default_layout") \ No newline at end of file diff --git a/netbox_topology_views/forms.py b/netbox_topology_views/forms.py index 1b971cc..3025f62 100644 --- a/netbox_topology_views/forms.py +++ b/netbox_topology_views/forms.py @@ -12,17 +12,14 @@ from dcim.choices import DeviceStatusChoices from tenancy.models import TenantGroup, Tenant from tenancy.forms import TenancyFilterForm from django.conf import settings -from netbox.forms import NetBoxModelFilterSetForm +from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm from utilities.forms import ( TagFilterField, DynamicModelMultipleChoiceField, MultipleChoiceField, + widgets, ) - -allow_coordinates_saving = bool( - settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"] -) - +from .models import IndividualOptions class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Device @@ -32,11 +29,12 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): ( "q", "filter_id", - "hide_unconnected", "save_coords", + "show_unconnected", "show_cables", "show_circuit", "show_logical_connections", + "show_single_cable_logical_conns", "show_power", "show_wireless", ), @@ -104,12 +102,27 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): }, label=_("Rack"), ) - hide_unconnected = forms.BooleanField( - label=_("Hide Unconnected"), required=False, initial=False + status = MultipleChoiceField( + choices=DeviceStatusChoices, required=False, label=_("Device Status") + ) + tag = TagFilterField(model) + + # options + save_coords = forms.BooleanField( + label=_("Save Coordinates"), + required=False, + disabled=(not settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"] or settings.PLUGINS_CONFIG["netbox_topology_views"]["always_save_coordinates"]), + initial=(settings.PLUGINS_CONFIG["netbox_topology_views"]["always_save_coordinates"]) + ) + show_unconnected = forms.BooleanField( + label=_("Show Unconnected"), required=False, initial=False ) show_logical_connections = forms.BooleanField( label =_("Show Logical Connections"), required=False, initial=False ) + show_single_cable_logical_conns = forms.BooleanField( + label =_("Show redundant Cable and Locigal Connection"), required=False, initial=False + ) show_cables = forms.BooleanField( label =_("Show Cables"), required=False, initial=False ) @@ -122,12 +135,118 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): show_power = forms.BooleanField( label=_("Show Power Feeds"), required=False, initial=False ) - save_coords = forms.BooleanField( - label=_("Save Coordinates"), + +class IndividualOptionsForm(NetBoxModelForm): + fieldsets = ( + ( + None, + ( + "user_id", + "ignore_cable_type", + "preselected_device_roles", + "preselected_tags", + "show_unconnected", + "show_cables", + "show_circuit", + "show_logical_connections", + "show_single_cable_logical_conns", + "show_power", + "show_wireless", + "draw_default_layout", + ), + ), + ) + + user_id = forms.CharField(widget=forms.HiddenInput()) + + ignore_cable_type = MultipleChoiceField( + label=_("Ignore Termination Types"), + required=False, + choices=IndividualOptions.CHOICES, + help_text=_("Choose Termination Types that you want to be ignored. " + "If any ignored Termination Type is part of a connection, the " + "cable is not displayed.") + ) + preselected_device_roles = DynamicModelMultipleChoiceField( + label=_("Preselected Device Role"), + queryset=DeviceRole.objects.all(), required=False, - disabled=(not allow_coordinates_saving), + help_text=_("Select Device Roles that you want to have " + "preselected in the filter tab.") ) - status = MultipleChoiceField( - choices=DeviceStatusChoices, required=False, label=_("Device Status") + preselected_tags = forms.ModelMultipleChoiceField( + label=_("Preselected Tags"), + queryset=Device.tags.all(), + required=False, + widget=widgets.StaticSelectMultiple, + help_text=_("Select Tags that you want to have " + "preselected in the filter tab.") ) - tag = TagFilterField(model) + show_unconnected = forms.BooleanField( + label=_("Show Unconnected"), + required=False, + initial=False, + help_text=_("Draws devices that have no connections or for which no " + "connection is displayed. This option depends on other parameters " + "like 'Show Cables' and 'Show Logical Connections'.") + ) + show_cables = forms.BooleanField( + label =_("Show Cables"), + required=False, + initial=False, + help_text=_("Displays connections between interfaces that are connected " + "with one or more cables. These connections are displayed as solid " + "lines in the color of the cable.") + ) + show_logical_connections = forms.BooleanField( + label =_("Show Logical Connections"), + required=False, + initial=False, + help_text=_("Displays connections between devices that are not " + "directly connected (e.g. via patch panels). These connections " + "are displayed as yellow dotted lines.") + ) + show_single_cable_logical_conns = forms.BooleanField( + label = ("Show redundant Cable and Locigal Connection"), + required = False, + initial=False, + help_text=_("Shows a logical connection (in addition to a cable), " + "even if a cable is directly connected. Leaving this option " + "disabled prevents that redundant display. This option only " + "has an effect if 'Show Logical Connections' is activated.") + ) + show_circuit = forms.BooleanField( + label=_("Show Circuit Terminations"), + required=False, + initial=False, + help_text=_("Displays connections between circuit terminations. " + "These connections are displayed as blue dashed lines.") + ) + show_power = forms.BooleanField( + label=_("Show Power Feeds"), + required=False, + initial=False, + help_text=_("Displays connections between power outlets and power " + "ports. These connections are displayed as solid lines in the " + "color of the cable. This option depends on 'Show Cables'.") + ) + show_wireless = forms.BooleanField( + label =_("Show Wireless Links"), + required=False, + initial=False, + help_text=_("Displays wireless connections. These connections are " + "displayed as blue dotted lines.") + ) + draw_default_layout = forms.BooleanField( + label = ("Draw Default Layout"), + required=False, + initial=False, + help_text=_("Enable this option if you want to draw the topology on " + "the initial load (when you go to the topology plugin page).") + ) + + class Meta: + model = IndividualOptions + fields = [ + 'user_id', 'ignore_cable_type', 'preselected_device_roles', 'preselected_tags', 'show_unconnected', 'show_cables', 'show_logical_connections', 'show_single_cable_logical_conns', 'show_circuit', 'show_power', 'show_wireless', 'draw_default_layout' + ] \ No newline at end of file diff --git a/netbox_topology_views/migrations/0002_individualoptions.py b/netbox_topology_views/migrations/0002_individualoptions.py new file mode 100644 index 0000000..3424a52 --- /dev/null +++ b/netbox_topology_views/migrations/0002_individualoptions.py @@ -0,0 +1,42 @@ +# Generated by Django 4.1.4 on 2023-03-19 14:13 + +from django.db import migrations, models +import taggit.managers +import utilities.json + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0167_module_status'), + ('extras', '0084_staging'), + ('netbox_topology_views', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='IndividualOptions', + 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)), + ('user_id', models.IntegerField(null=True, unique=True)), + ('ignore_cable_type', models.CharField(blank=True, max_length=255)), + ('show_unconnected', models.BooleanField(default=False)), + ('show_cables', models.BooleanField(default=False)), + ('show_logical_connections', models.BooleanField(default=False)), + ('show_single_cable_logical_conns', models.BooleanField(default=False)), + ('show_circuit', models.BooleanField(default=False)), + ('show_power', models.BooleanField(default=False)), + ('show_wireless', models.BooleanField(default=False)), + ('draw_default_layout', models.BooleanField(default=False)), + ('preselected_device_roles', models.ManyToManyField(blank=True, db_table='netbox_topology_views_individualoptions_preselected_device', related_name='+', to='dcim.devicerole')), + ('preselected_tags', models.ManyToManyField(blank=True, db_table='netbox_topology_views_individualoptions_preselected_tag', related_name='+', to='extras.tag')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/netbox_topology_views/models.py b/netbox_topology_views/models.py index c4002cc..e95d020 100644 --- a/netbox_topology_views/models.py +++ b/netbox_topology_views/models.py @@ -2,10 +2,12 @@ from pathlib import Path from typing import Optional from dcim.models import 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 netbox.models import NetBoxModel from netbox.models.features import ( ChangeLoggingMixin, ExportTemplatesMixin, @@ -91,3 +93,62 @@ class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, WebhooksMixin): except ValueError: return self.get_default_image(dir) return static(f"/{self.image}") + +class IndividualOptions(NetBoxModel): + CHOICES = ( + ('interface', 'interface'), + ('front port', 'front port'), + ('rear port', 'rear port'), + ('power outlet', 'power outlet'), + ('power port', 'power port'), + ('console port', 'console port'), + ('console server port', 'console server port'), + ) + + user_id = models.IntegerField( + null=True, + unique=True + ) + ignore_cable_type = models.CharField( + max_length = 255, + blank = True, + ) + preselected_device_roles = models.ManyToManyField( + to='dcim.DeviceRole', + related_name='+', + blank=True, + db_table='netbox_topology_views_individualoptions_preselected_device', + ) + preselected_tags = models.ManyToManyField( + to='extras.Tag', + related_name='+', + blank=True, + db_table='netbox_topology_views_individualoptions_preselected_tag', + ) + show_unconnected = models.BooleanField( + default=False + ) + show_cables = models.BooleanField( + default=False + ) + show_logical_connections = models.BooleanField( + default=False + ) + show_single_cable_logical_conns = models.BooleanField( + default=False + ) + show_circuit = models.BooleanField( + default=False + ) + show_power = models.BooleanField( + default=False + ) + show_wireless = models.BooleanField( + default=False + ) + draw_default_layout = models.BooleanField( + default=False + ) + + def __str___(self): + return f"{self.user_id}" \ No newline at end of file diff --git a/netbox_topology_views/navigation.py b/netbox_topology_views/navigation.py index 56d731b..32a29af 100644 --- a/netbox_topology_views/navigation.py +++ b/netbox_topology_views/navigation.py @@ -4,7 +4,12 @@ menu = PluginMenu( label='Topology Views', icon_class="mdi mdi-sitemap", groups=( - ('Topology', (PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),),), - ('CUSTOMIZATION', (PluginMenuItem(link="plugins:netbox_topology_views:images", link_text="Topology Images"),),), + ('TOPOLOGY', (PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),),), + ('PREFERENCES', + ( + PluginMenuItem(link="plugins:netbox_topology_views:images", link_text="Images"), + PluginMenuItem(link="plugins:netbox_topology_views:individualoptions", link_text="Individual Options"), + ), + ), ), ) diff --git a/netbox_topology_views/templates/netbox_topology_views/individual_options.html b/netbox_topology_views/templates/netbox_topology_views/individual_options.html new file mode 100644 index 0000000..27b4300 --- /dev/null +++ b/netbox_topology_views/templates/netbox_topology_views/individual_options.html @@ -0,0 +1,5 @@ +{% extends 'generic/object_edit.html' %} + +{% block title %}Topology Views Individual Options{% endblock %} + +{% block tabs %} {% endblock tabs %} \ No newline at end of file diff --git a/netbox_topology_views/urls.py b/netbox_topology_views/urls.py index 95d7057..8581297 100644 --- a/netbox_topology_views/urls.py +++ b/netbox_topology_views/urls.py @@ -9,4 +9,5 @@ urlpatterns = ( path("", RedirectView.as_view(url="topology/", permanent=True)), path("topology/", views.TopologyHomeView.as_view(), name="home"), path("images/", views.TopologyImagesView.as_view(), name="images"), + path("individualoptions/", views.TopologyIndividualOptionsView.as_view(), name="individualoptions"), ) diff --git a/netbox_topology_views/views.py b/netbox_topology_views/views.py index 4b0faa3..eff9446 100644 --- a/netbox_topology_views/views.py +++ b/netbox_topology_views/views.py @@ -18,9 +18,11 @@ from dcim.models import ( RearPort, ) from django.conf import settings +from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.contenttypes.models import ContentType from django.db.models import Q, QuerySet +from django.db.models.functions import Lower from django.http import HttpRequest, HttpResponseRedirect, QueryDict from django.shortcuts import render from django.views.generic import View @@ -28,8 +30,8 @@ from extras.models import Tag from wireless.models import WirelessLink from netbox_topology_views.filters import DeviceFilterSet -from netbox_topology_views.forms import DeviceFilterForm -from netbox_topology_views.models import RoleImage +from netbox_topology_views.forms import DeviceFilterForm, IndividualOptionsForm +from netbox_topology_views.models import RoleImage, IndividualOptions from netbox_topology_views.utils import ( CONF_IMAGE_DIR, find_image_url, @@ -38,15 +40,6 @@ from netbox_topology_views.utils import ( image_static_url, ) -supported_termination_types = [ - "interface", - "front port", - "rear port", - "power outlet", - "power port", - "console port", - "console server port", -] def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]): @@ -261,14 +254,21 @@ def create_circuit_termination(termination): def get_topology_data( queryset: QuerySet, - hide_unconnected: bool, + individualOptions: IndividualOptions, + show_unconnected: bool, save_coords: bool, show_cables: bool, show_circuit: bool, show_logical_connections: bool, + show_single_cable_logical_conns: bool, show_power: bool, show_wireless: bool, ): + + supported_termination_types = [] + for t in IndividualOptions.CHOICES: + supported_termination_types.append(t[1]) + if not queryset: return None @@ -282,14 +282,8 @@ def get_topology_data( nodes_provider_networks = {} cable_ids = DefaultDict(dict) interface_ids = DefaultDict(dict) - ignore_cable_type = settings.PLUGINS_CONFIG["netbox_topology_views"][ - "ignore_cable_type" - ] - hide_single_cable_logical_conns = bool( - settings.PLUGINS_CONFIG["netbox_topology_views"][ - "hide_single_cable_logical_conns" - ] - ) + + ignore_cable_type = individualOptions.ignore_cable_type device_ids = [d.pk for d in queryset] site_ids = [d.site_id for d in queryset] @@ -301,7 +295,7 @@ def get_topology_data( for circuit_termination in circuit_terminations: circuit_termination: CircuitTermination if ( - not hide_unconnected + show_unconnected and circuit_termination.circuit_id not in nodes_circuits ): nodes_circuits[ @@ -358,7 +352,7 @@ def get_topology_data( if termination.device_id in device_ids: circuit_has_connections = True - if circuit_has_connections and hide_unconnected: + if circuit_has_connections and not show_unconnected: if circuit_termination.circuit_id not in nodes_circuits: nodes_circuits[ circuit_termination.circuit.pk @@ -376,15 +370,15 @@ def get_topology_data( ) for power_feed in power_feeds: - if not hide_unconnected or ( - hide_unconnected and power_feed.cable_id is not None + if show_unconnected or ( + not show_unconnected and power_feed.cable_id is not None ): if power_feed.power_panel_id not in nodes_powerpanel: nodes_powerpanel[power_feed.power_panel.pk] = power_feed.power_panel power_link_name = "" if power_feed.pk not in nodes_powerfeed: - if hide_unconnected: + if not show_unconnected: if power_feed.link_peers[0].device_id in device_ids: nodes_powerfeed[power_feed.pk] = power_feed power_link_name = power_feed.link_peers[0].name @@ -438,7 +432,7 @@ def get_topology_data( # print('Destination interface already exists, ignoring') continue - if hide_single_cable_logical_conns and interface.cable_id==destination.cable_id and show_cables: + if not show_single_cable_logical_conns and interface.cable_id==destination.cable_id and show_cables: # interface connection is the same as the cable connection, ignore this connection continue @@ -569,7 +563,7 @@ def get_topology_data( ) for qs_device in queryset: - if qs_device.pk not in nodes_devices and not hide_unconnected: + if qs_device.pk not in nodes_devices and show_unconnected: nodes_devices[qs_device.pk] = qs_device results = {} @@ -598,16 +592,27 @@ class TopologyHomeView(PermissionRequiredMixin, View): self.model = self.queryset.model topo_data = None + individualOptions, created = IndividualOptions.objects.get_or_create( + user_id=request.user.id, + ) + if request.GET: save_coords = False if "save_coords" in request.GET: if request.GET["save_coords"] == "on": save_coords = True + # General options overrides + if save_coords == True and settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"] == False: + save_coords = False + messages.warning(request, "Coordinate saving not allowed. Setting has been overridden") + elif settings.PLUGINS_CONFIG["netbox_topology_views"]["always_save_coordinates"] == True: + save_coords = True - hide_unconnected = False - if "hide_unconnected" in request.GET: - if request.GET["hide_unconnected"] == "on": - hide_unconnected = True + # Individual options + show_unconnected = False + if "show_unconnected" in request.GET: + if request.GET["show_unconnected"] == "on": + show_unconnected = True show_power = False if "show_power" in request.GET: @@ -624,6 +629,11 @@ class TopologyHomeView(PermissionRequiredMixin, View): if request.GET["show_logical_connections"] == "on" : show_logical_connections = True + show_single_cable_logical_conns = False + if "show_single_cable_logical_conns" in request.GET: + if request.GET["show_single_cable_logical_conns"] == "on" : + show_single_cable_logical_conns = True + show_cables = False if "show_cables" in request.GET: if request.GET["show_cables"] == "on" : @@ -634,60 +644,45 @@ class TopologyHomeView(PermissionRequiredMixin, View): if request.GET["show_wireless"] == "on" : show_wireless = True - if "draw_init" in request.GET: - if request.GET["draw_init"].lower() == "true": - topo_data = get_topology_data( - self.queryset, - hide_unconnected, - save_coords, - show_cables, - show_circuit, - show_logical_connections, - show_power, - show_wireless, - ) - else: + if not "draw_init" in request.GET or "draw_init" in request.GET and request.GET["draw_init"].lower() == "true": topo_data = get_topology_data( self.queryset, - hide_unconnected, + individualOptions, + show_unconnected, save_coords, show_cables, show_circuit, show_logical_connections, + show_single_cable_logical_conns, show_power, show_wireless, ) + else: - preselected_device_roles = settings.PLUGINS_CONFIG["netbox_topology_views"][ - "preselected_device_roles" - ] - preselected_tags = settings.PLUGINS_CONFIG["netbox_topology_views"][ - "preselected_tags" - ] - always_save_coordinates = bool( - settings.PLUGINS_CONFIG["netbox_topology_views"][ - "always_save_coordinates" - ] - ) - - q_device_role_id = DeviceRole.objects.filter( - name__in=preselected_device_roles - ).values_list("id", flat=True) - q_tags = Tag.objects.filter(name__in=preselected_tags).values_list( - "name", flat=True - ) + # No GET-Request in URL. We most likely came here from the navigation menu. + preselected_device_roles = IndividualOptions.objects.get(id=individualOptions.id).preselected_device_roles.all().values_list('id', flat=True) + preselected_tags = IndividualOptions.objects.get(id=individualOptions.id).preselected_tags.all().values_list(Lower('name'), flat=True) q = QueryDict(mutable=True) - q.setlist("device_role_id", list(q_device_role_id)) - q.setlist("tag", list(q_tags)) - q["draw_init"] = settings.PLUGINS_CONFIG["netbox_topology_views"][ - "draw_default_layout" - ] - if always_save_coordinates: - q["save_coords"] = "on" + q.setlist("device_role_id", list(preselected_device_roles)) + q.setlist("tag", list(preselected_tags)) + + if individualOptions.show_unconnected: q['show_unconnected'] = "on" + if individualOptions.show_cables: q['show_cables'] = "on" + if individualOptions.show_logical_connections: q['show_logical_connections'] = "on" + if individualOptions.show_single_cable_logical_conns: q['show_single_cable_logical_conns'] = "on" + if individualOptions.show_circuit: q['show_circuit'] = "on" + if individualOptions.show_power: q['show_power'] = "on" + if individualOptions.show_wireless: q['show_wireless'] = "on" + if individualOptions.draw_default_layout: + q['draw_init'] = "true" + else: + q['draw_init'] = "false" + query_string = q.urlencode() return HttpResponseRedirect(f"{request.path}?{query_string}") + if is_htmx(request): return render( request, @@ -768,3 +763,48 @@ class TopologyImagesView(PermissionRequiredMixin, View): "images": images, }, ) + +class TopologyIndividualOptionsView(PermissionRequiredMixin, View): + permission_required = 'netbox_topology_views.change_individualoptions' + + def post(self, request): + instance = IndividualOptions.objects.get(user_id=request.user.id) + form = IndividualOptionsForm(request.POST, instance=instance) + if form.is_valid(): + form.save() + messages.success(request, "Options have been sucessfully saved") + else: + messages.error(request, form.errors) + + return HttpResponseRedirect("./") + + def get(self, request): + queryset, created = IndividualOptions.objects.get_or_create( + user_id=request.user.id, + ) + + form = IndividualOptionsForm( + initial={ + 'user_id': request.user.id, + 'ignore_cable_type': tuple(queryset.ignore_cable_type.translate({ord(i): None for i in '[]\''}).split(', ')), + 'preselected_device_roles': IndividualOptions.objects.get(id=queryset.id).preselected_device_roles.all(), + 'preselected_tags': IndividualOptions.objects.get(id=queryset.id).preselected_tags.all(), + 'show_unconnected': queryset.show_unconnected, + 'show_cables': queryset.show_cables, + 'show_logical_connections': queryset.show_logical_connections, + 'show_single_cable_logical_conns': queryset.show_single_cable_logical_conns, + 'show_circuit': queryset.show_circuit, + 'show_power': queryset.show_power, + 'show_wireless': queryset.show_wireless, + 'draw_default_layout': queryset.draw_default_layout, + }, + ) + + return render( + request, + "netbox_topology_views/individual_options.html", + { + "form": form, + "object": queryset, + }, + ) \ No newline at end of file