diff --git a/CHANGELOG.md b/CHANGELOG.md index f5a8285..6b32de7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Changelog with backdated changes (#51) * List filtering (#54) * Show installations on native objects (#49) +* SPDX license expressions (#47) ### Changed diff --git a/netbox_slm/api/serializers.py b/netbox_slm/api/serializers.py index 044e202..4998d1f 100644 --- a/netbox_slm/api/serializers.py +++ b/netbox_slm/api/serializers.py @@ -1,12 +1,12 @@ -from rest_framework import serializers +from rest_framework.serializers import SerializerMethodField, HyperlinkedIdentityField from netbox.api.serializers import NetBoxModelSerializer from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense class SoftwareLicenseSerializer(NetBoxModelSerializer): - display = serializers.SerializerMethodField() - url = serializers.HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwarelicense-detail") + display = SerializerMethodField() + url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwarelicense-detail") class Meta: model = SoftwareLicense @@ -17,6 +17,7 @@ class SoftwareLicenseSerializer(NetBoxModelSerializer): "name", "description", "type", + "spdx_expression", "stored_location", "stored_location_url", "start_date", @@ -39,8 +40,8 @@ class SoftwareLicenseSerializer(NetBoxModelSerializer): class SoftwareProductSerializer(NetBoxModelSerializer): - display = serializers.SerializerMethodField() - url = serializers.HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproduct-detail") + display = SerializerMethodField() + url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproduct-detail") class Meta: model = SoftwareProduct @@ -65,10 +66,8 @@ class SoftwareProductSerializer(NetBoxModelSerializer): class SoftwareProductInstallationSerializer(NetBoxModelSerializer): - display = serializers.SerializerMethodField() - url = serializers.HyperlinkedIdentityField( - view_name="plugins-api:netbox_slm-api:softwareproductinstallation-detail" - ) + display = SerializerMethodField() + url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproductinstallation-detail") class Meta: model = SoftwareProductInstallation @@ -94,8 +93,8 @@ class SoftwareProductInstallationSerializer(NetBoxModelSerializer): class SoftwareProductVersionSerializer(NetBoxModelSerializer): - display = serializers.SerializerMethodField() - url = serializers.HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproductversion-detail") + display = SerializerMethodField() + url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproductversion-detail") class Meta: model = SoftwareProductVersion diff --git a/netbox_slm/api/urls.py b/netbox_slm/api/urls.py index 75cc757..270e5d3 100644 --- a/netbox_slm/api/urls.py +++ b/netbox_slm/api/urls.py @@ -14,4 +14,5 @@ router.register("softwareproducts", SoftwareProductViewSet) router.register("softwareproductversions", SoftwareProductVersionViewSet) router.register("softwareproductinstallations", SoftwareProductInstallationViewSet) router.register("softwarelicenses", SoftwareLicenseViewSet) + urlpatterns = router.urls diff --git a/netbox_slm/api/views.py b/netbox_slm/api/views.py index 521cfc9..897c90f 100644 --- a/netbox_slm/api/views.py +++ b/netbox_slm/api/views.py @@ -13,14 +13,15 @@ from netbox_slm.filtersets import ( SoftwareProductInstallationFilterSet, SoftwareLicenseFilterSet, ) -from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense +from netbox_slm.models import ( + SoftwareProduct, + SoftwareProductVersion, + SoftwareProductInstallation, + SoftwareLicense, +) class NetboxSLMRootView(APIRootView): - """ - NetboxSLM API root view - """ - def get_view_name(self): return "NetboxSLM" diff --git a/netbox_slm/filtersets.py b/netbox_slm/filtersets.py index bbeeb49..9e82080 100644 --- a/netbox_slm/filtersets.py +++ b/netbox_slm/filtersets.py @@ -113,6 +113,7 @@ class SoftwareLicenseFilterSet(NetBoxModelFilterSet): name = CharFilter(lookup_expr="icontains") description = CharFilter(lookup_expr="icontains") type = CharFilter(lookup_expr="icontains") + spdx_expression = CharFilter(lookup_expr="icontains", label="SPDX expression") stored_location = CharFilter(lookup_expr="icontains") software_product = ModelMultipleChoiceFilter( diff --git a/netbox_slm/forms/software_license.py b/netbox_slm/forms/software_license.py index 79ebdea..e3d52b2 100644 --- a/netbox_slm/forms/software_license.py +++ b/netbox_slm/forms/software_license.py @@ -2,7 +2,13 @@ from django.forms import CharField, DateField, ChoiceField, IntegerField, NullBo from django.urls import reverse_lazy from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm -from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense +from netbox_slm.models import ( + SoftwareProduct, + SoftwareProductVersion, + SoftwareProductInstallation, + SoftwareLicense, + spdx_license_names, +) from utilities.forms.constants import BOOLEAN_WITH_BLANK_CHOICES from utilities.forms.fields import ( CommentField, @@ -18,6 +24,7 @@ from utilities.forms.widgets import APISelect, DatePicker class SoftwareLicenseForm(NetBoxModelForm): comments = CommentField() + spdx_expression = ChoiceField(required=False, choices=spdx_license_names(), label="SPDX expression") stored_location_url = LaxURLField(required=False) start_date = DateField(required=False, widget=DatePicker()) expiration_date = DateField(required=False, widget=DatePicker()) @@ -49,6 +56,7 @@ class SoftwareLicenseForm(NetBoxModelForm): "description", "software_product", "type", + "spdx_expression", "stored_location", "stored_location_url", "start_date", @@ -70,6 +78,7 @@ class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm): "name", "description", "type", + "spdx_expression", "stored_location", "support", "software_product_id", @@ -84,6 +93,7 @@ class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm): name = CharField(required=False) description = CharField(required=False) type = CharField(required=False) + spdx_expression = CharField(required=False, label="SPDX expression") stored_location = CharField(required=False) support = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES) @@ -114,6 +124,7 @@ class SoftwareLicenseBulkImportForm(NetBoxModelImportForm): "description", "software_product", "type", + "spdx_expression", "stored_location", "start_date", "expiration_date", @@ -130,6 +141,7 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm): fieldsets = ( FieldSet( "type", + "spdx_expression", "stored_location", "stored_location_url", "start_date", @@ -143,6 +155,7 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm): ) nullable_fields = ( "type", + "spdx_expression", "stored_location", "stored_location_url", "start_date", @@ -157,6 +170,7 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm): comments = CommentField() type = CharField(required=False) + spdx_expression = ChoiceField(required=False, choices=spdx_license_names(), label="SPDX expression") stored_location = CharField(required=False) stored_location_url = LaxURLField(required=False) start_date = DateField(required=False, widget=DatePicker()) diff --git a/netbox_slm/migrations/0010_softwarelicense_spdx_expression.py b/netbox_slm/migrations/0010_softwarelicense_spdx_expression.py new file mode 100644 index 0000000..5198b3e --- /dev/null +++ b/netbox_slm/migrations/0010_softwarelicense_spdx_expression.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.5 on 2025-02-24 18:03 + +import netbox_slm.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("netbox_slm", "0009_softwareproductversion_description"), + ] + + operations = [ + migrations.AddField( + model_name="softwarelicense", + name="spdx_expression", + field=models.CharField( + blank=True, + max_length=64, + null=True, + validators=[netbox_slm.models.validate_spdx_expression], + ), + ), + ] diff --git a/netbox_slm/models.py b/netbox_slm/models.py index 7051012..1780980 100644 --- a/netbox_slm/models.py +++ b/netbox_slm/models.py @@ -1,11 +1,15 @@ +from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse from django.utils.html import format_html, urlencode +from license_expression import Licensing, get_spdx_licensing from netbox.models import NetBoxModel from utilities.querysets import RestrictedQuerySet from utilities.validators import EnhancedURLValidator +spdx_licensing: Licensing = get_spdx_licensing() + class LaxURLField(models.URLField): """ @@ -129,12 +133,26 @@ class SoftwareProductInstallation(NetBoxModel): return "cluster" +def spdx_license_names(): + names = [(item[0], item[0]) for item in spdx_licensing.known_symbols.items() if not item[1].is_exception] + names.sort() + names.insert(0, ("", "---------")) # set default value + return names + + +def validate_spdx_expression(value): + expression_info = spdx_licensing.validate(value) + if expression_info.errors: + raise ValidationError(f"{value} is not a known SPDX license expression") + + class SoftwareLicense(NetBoxModel): name = models.CharField(max_length=128) comments = models.TextField(blank=True) description = models.CharField(max_length=255, null=True, blank=True) type = models.CharField(max_length=128) + spdx_expression = models.CharField(max_length=64, null=True, blank=True, validators=[validate_spdx_expression]) stored_location = models.CharField(max_length=255, null=True, blank=True) stored_location_url = LaxURLField(max_length=1024, null=True, blank=True) start_date = models.DateField(null=True, blank=True) diff --git a/netbox_slm/tables.py b/netbox_slm/tables.py index b263689..95dbcb6 100644 --- a/netbox_slm/tables.py +++ b/netbox_slm/tables.py @@ -45,7 +45,7 @@ class SoftwareProductVersionTable(NetBoxTable): pk = ToggleColumn() name = tables.LinkColumn() - software_product = tables.Column(accessor="software_product", linkify=True) + software_product = tables.Column(accessor="software_product", verbose_name="Software Product", linkify=True) manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True) installations = tables.Column(accessor="get_installation_count") @@ -89,7 +89,7 @@ class SoftwareProductInstallationTable(NetBoxTable): platform = tables.Column(accessor="platform", linkify=True) type = tables.Column(accessor="render_type") manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True) - software_product = tables.Column(accessor="software_product", linkify=True) + software_product = tables.Column(accessor="software_product", verbose_name="Software Product", linkify=True) version = tables.Column(accessor="version", linkify=True) tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductinstallation_list") @@ -137,10 +137,11 @@ class SoftwareLicenseTable(NetBoxTable): name = tables.LinkColumn() type = tables.Column() + spdx_expression = tables.Column(verbose_name="SPDX expression") stored_location = tables.Column(accessor="stored_location_txt", linkify=lambda record: record.stored_location_url) manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True) - software_product = tables.Column(accessor="software_product", linkify=True) + software_product = tables.Column(accessor="software_product", verbose_name="Software Product", linkify=True) version = tables.Column(accessor="version", linkify=True) installation = tables.Column(accessor="installation", linkify=True) @@ -153,6 +154,7 @@ class SoftwareLicenseTable(NetBoxTable): "name", "description", "type", + "spdx_expression", "stored_location", "start_date", "expiration_date", diff --git a/netbox_slm/templates/netbox_slm/softwarelicense.html b/netbox_slm/templates/netbox_slm/softwarelicense.html index 61af371..73f2c78 100644 --- a/netbox_slm/templates/netbox_slm/softwarelicense.html +++ b/netbox_slm/templates/netbox_slm/softwarelicense.html @@ -24,6 +24,10 @@ Type {{ object.type }} + + SPDX expression + {{ object.spdx_expression }} + Software Product {{ object.software_product|linkify }} diff --git a/pyproject.toml b/pyproject.toml index f60e9e0..f02dd9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,9 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", ] -dependencies = [] +dependencies = [ + "license-expression == 30.4.1", +] [project.optional-dependencies] build = [