Fix unencoded search link of installations table cells (#53)

Closes #48

Signed-off-by: wkoot <3715211+wkoot@users.noreply.github.com>
This commit is contained in:
wkoot
2025-02-13 22:58:22 +01:00
committed by GitHub
parent 193f04a208
commit 306d853a16
11 changed files with 75 additions and 52 deletions
+1 -1
View File
@@ -32,4 +32,4 @@ jobs:
run: ruff check netbox_slm run: ruff check netbox_slm
- name: Run ruff format - name: Run ruff format
run: ruff format netbox_slm run: ruff format --check netbox_slm
+1
View File
@@ -16,6 +16,7 @@
### Fixed ### Fixed
* Model detail view listing of linked objects (#55) * Model detail view listing of linked objects (#55)
* Search link of installations table cells (#48)
## [1.7.0](https://github.com/ICTU/netbox_slm/releases/tag/1.7.0) - 2024-07-09 ## [1.7.0](https://github.com/ICTU/netbox_slm/releases/tag/1.7.0) - 2024-07-09
+3
View File
@@ -21,6 +21,9 @@ services:
- netbox - netbox
healthcheck: healthcheck:
disable: true disable: true
volumes:
- ./config:/etc/netbox/config:ro
- ../netbox_slm:/opt/netbox/netbox/netbox_slm:ro
netbox-housekeeping: netbox-housekeeping:
image: netbox:slm image: netbox:slm
depends_on: depends_on:
+14 -20
View File
@@ -1,9 +1,15 @@
from django_filters import CharFilter, ModelMultipleChoiceFilter, MultipleChoiceFilter
from django.db.models import Q from django.db.models import Q
from django_filters import CharFilter, ModelMultipleChoiceFilter, MultipleChoiceFilter
from dcim.models import Device, Manufacturer from dcim.models import Device, Manufacturer
from netbox.filtersets import NetBoxModelFilterSet from netbox.filtersets import NetBoxModelFilterSet
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense, SoftwareReleaseTypes from netbox_slm.models import (
SoftwareProduct,
SoftwareProductVersion,
SoftwareProductInstallation,
SoftwareLicense,
SoftwareReleaseTypes,
)
from virtualization.models import VirtualMachine, Cluster from virtualization.models import VirtualMachine, Cluster
@@ -13,9 +19,7 @@ class SoftwareProductFilterSet(NetBoxModelFilterSet):
name = CharFilter(lookup_expr="icontains") name = CharFilter(lookup_expr="icontains")
description = CharFilter(lookup_expr="icontains") description = CharFilter(lookup_expr="icontains")
manufacturer = ModelMultipleChoiceFilter( manufacturer = ModelMultipleChoiceFilter(queryset=Manufacturer.objects.all())
queryset=Manufacturer.objects.all()
)
class Meta: class Meta:
model = SoftwareProduct model = SoftwareProduct
@@ -72,23 +76,17 @@ class SoftwareProductVersionFilterSet(NetBoxModelFilterSet):
class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet): class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet):
"""Filter capabilities for SoftwareProductInstallation instances.""" """Filter capabilities for SoftwareProductInstallation instances."""
device = ModelMultipleChoiceFilter( device = ModelMultipleChoiceFilter(queryset=Device.objects.all())
queryset=Device.objects.all()
)
virtualmachine = ModelMultipleChoiceFilter( virtualmachine = ModelMultipleChoiceFilter(
queryset=VirtualMachine.objects.all(), queryset=VirtualMachine.objects.all(),
label="Virtual Machine", label="Virtual Machine",
) )
cluster = ModelMultipleChoiceFilter( cluster = ModelMultipleChoiceFilter(queryset=Cluster.objects.all())
queryset=Cluster.objects.all()
)
software_product = ModelMultipleChoiceFilter( software_product = ModelMultipleChoiceFilter(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
label="Software Product", label="Software Product",
) )
version = ModelMultipleChoiceFilter( version = ModelMultipleChoiceFilter(queryset=SoftwareProductVersion.objects.all())
queryset=SoftwareProductVersion.objects.all()
)
class Meta: class Meta:
model = SoftwareProductInstallation model = SoftwareProductInstallation
@@ -119,12 +117,8 @@ class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
label="Software Product", label="Software Product",
) )
version = ModelMultipleChoiceFilter( version = ModelMultipleChoiceFilter(queryset=SoftwareProductVersion.objects.all())
queryset=SoftwareProductVersion.objects.all() installation = ModelMultipleChoiceFilter(queryset=SoftwareProductInstallation.objects.all())
)
installation = ModelMultipleChoiceFilter(
queryset=SoftwareProductInstallation.objects.all()
)
class Meta: class Meta:
model = SoftwareLicense model = SoftwareLicense
+10 -2
View File
@@ -4,7 +4,13 @@ from django.urls import reverse_lazy
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
from utilities.forms.constants import BOOLEAN_WITH_BLANK_CHOICES from utilities.forms.constants import BOOLEAN_WITH_BLANK_CHOICES
from utilities.forms.fields import CommentField, DynamicModelChoiceField, TagFilterField, LaxURLField, DynamicModelMultipleChoiceField from utilities.forms.fields import (
CommentField,
DynamicModelChoiceField,
TagFilterField,
LaxURLField,
DynamicModelMultipleChoiceField,
)
from utilities.forms.rendering import FieldSet from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import APISelect, DatePicker from utilities.forms.widgets import APISelect, DatePicker
@@ -67,7 +73,9 @@ class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
model = SoftwareLicense model = SoftwareLicense
fieldsets = ( fieldsets = (
FieldSet("q", "filter_id", "tag"), FieldSet("q", "filter_id", "tag"),
FieldSet("name", "description", "type", "stored_location", "support", "software_product", "version", "installation"), FieldSet(
"name", "description", "type", "stored_location", "support", "software_product", "version", "installation"
),
) )
selector_fields = ("q", "filter_id", "name") selector_fields = ("q", "filter_id", "name")
+6 -1
View File
@@ -3,7 +3,12 @@ from django.forms import CharField
from dcim.models import Manufacturer from dcim.models import Manufacturer
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm
from netbox_slm.models import SoftwareProduct from netbox_slm.models import SoftwareProduct
from utilities.forms.fields import CommentField, DynamicModelChoiceField, TagFilterField, DynamicModelMultipleChoiceField from utilities.forms.fields import (
CommentField,
DynamicModelChoiceField,
TagFilterField,
DynamicModelMultipleChoiceField,
)
from utilities.forms.rendering import FieldSet from utilities.forms.rendering import FieldSet
@@ -4,7 +4,12 @@ from django.urls import reverse_lazy
from dcim.models import Device from dcim.models import Device
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm
from netbox_slm.models import SoftwareProductInstallation, SoftwareProduct, SoftwareProductVersion from netbox_slm.models import SoftwareProductInstallation, SoftwareProduct, SoftwareProductVersion
from utilities.forms.fields import CommentField, DynamicModelChoiceField, TagFilterField, DynamicModelMultipleChoiceField from utilities.forms.fields import (
CommentField,
DynamicModelChoiceField,
TagFilterField,
DynamicModelMultipleChoiceField,
)
from utilities.forms.rendering import FieldSet from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import APISelect from utilities.forms.widgets import APISelect
from virtualization.models import VirtualMachine, Cluster from virtualization.models import VirtualMachine, Cluster
+6 -1
View File
@@ -4,7 +4,12 @@ from django.urls import reverse_lazy
from dcim.models import Manufacturer from dcim.models import Manufacturer
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareReleaseTypes from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareReleaseTypes
from utilities.forms.fields import CommentField, DynamicModelChoiceField, TagFilterField, DynamicModelMultipleChoiceField from utilities.forms.fields import (
CommentField,
DynamicModelChoiceField,
TagFilterField,
DynamicModelMultipleChoiceField,
)
from utilities.forms.rendering import FieldSet from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import APISelect, DatePicker from utilities.forms.widgets import APISelect, DatePicker
+10 -22
View File
@@ -1,6 +1,6 @@
from django.db import models from django.db import models
from django.urls import reverse, reverse_lazy from django.urls import reverse
from django.utils import safestring from django.utils.html import format_html, urlencode
from netbox.models import NetBoxModel from netbox.models import NetBoxModel
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
@@ -33,16 +33,10 @@ class SoftwareProduct(NetBoxModel):
def get_installation_count(self): def get_installation_count(self):
count = SoftwareProductInstallation.objects.filter(software_product_id=self.pk).count() count = SoftwareProductInstallation.objects.filter(software_product_id=self.pk).count()
return ( query_string = urlencode(dict(software_product=self.pk))
safestring.mark_safe( search_target = reverse("plugins:netbox_slm:softwareproductinstallation_list")
'<a href="{url}">{count}</a>'.format( # Can be composed directly with reverse(query=) in Django 5.2, see https://code.djangoproject.com/ticket/25582
url=reverse_lazy("plugins:netbox_slm:softwareproductinstallation_list") + f"?q={self.name}", return format_html(f"<a href='{search_target}?{query_string}'>{count}</a>") if count else "0"
count=count,
)
)
if count
else "0"
)
class SoftwareReleaseTypes(models.TextChoices): class SoftwareReleaseTypes(models.TextChoices):
@@ -84,16 +78,10 @@ class SoftwareProductVersion(NetBoxModel):
def get_installation_count(self): def get_installation_count(self):
count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count() count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count()
return ( query_string = urlencode(dict(version=self.pk))
safestring.mark_safe( search_target = reverse("plugins:netbox_slm:softwareproductinstallation_list")
'<a href="{url}">{count}</a>'.format( # Can be composed directly with reverse(query=) in Django 5.2, see https://code.djangoproject.com/ticket/25582
url=reverse_lazy("plugins:netbox_slm:softwareproductinstallation_list") + f"?q={self.name}", return format_html(f"<a href='{search_target}?{query_string}'>{count}</a>") if count else "0"
count=count,
)
)
if count
else "0"
)
class SoftwareProductInstallation(NetBoxModel): class SoftwareProductInstallation(NetBoxModel):
+9 -4
View File
@@ -1,8 +1,8 @@
from django.test import TestCase from django.test import TestCase
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from virtualization.models import Cluster, ClusterType, VirtualMachine
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
from virtualization.models import Cluster, ClusterType, VirtualMachine
class ModelTestCase(TestCase): class ModelTestCase(TestCase):
@@ -49,9 +49,14 @@ class ModelTestCase(TestCase):
self.assertEqual("/plugins/slm/licenses/1/", self.software_license.get_absolute_url()) self.assertEqual("/plugins/slm/licenses/1/", self.software_license.get_absolute_url())
def test_get_installation_count(self): def test_get_installation_count(self):
installation_ss = '<a href="/plugins/slm/installations/?q={}">1</a>' self.assertEqual(
self.assertEqual(installation_ss.format(self.p_name), self.software_product.get_installation_count()) f"<a href='/plugins/slm/installations/?software_product={self.software_product.pk}'>1</a>",
self.assertEqual(installation_ss.format(self.v_name), self.software_product_version.get_installation_count()) self.software_product.get_installation_count(),
)
self.assertEqual(
f"<a href='/plugins/slm/installations/?version={self.software_product_version.pk}'>1</a>",
self.software_product_version.get_installation_count(),
)
def test_product_installation_methods(self): def test_product_installation_methods(self):
self.assertEqual("virtualmachine", self.software_product_installation.render_type()) self.assertEqual("virtualmachine", self.software_product_installation.render_type())
+9
View File
@@ -50,6 +50,15 @@ line-length = 120
target-version = "py312" target-version = "py312"
src = ["netbox_slm"] src = ["netbox_slm"]
[tool.ruff.lint]
select = [
"E", # pycodestyle
"F", # Pyflakes
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
]
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"__init__.py" = [ "__init__.py" = [
"D104", # https://beta.ruff.rs/docs/rules/#pydocstyle-d - don't require doc strings in __init__.py files "D104", # https://beta.ruff.rs/docs/rules/#pydocstyle-d - don't require doc strings in __init__.py files