feat: extend software license lifecycle data

Make license type optional and add lifetime or recurring renewal terms, license file and key storage, and provider portal URLs across the UI, API, filters, imports, translations, and migrations.
This commit is contained in:
2026-07-27 12:16:14 +02:00
parent 5887129546
commit adfc2ae72c
13 changed files with 298 additions and 6 deletions
+3
View File
@@ -6,6 +6,8 @@
* Optional tenant group and tenant assignments for software licenses * Optional tenant group and tenant assignments for software licenses
* German translation for navigation, forms, tables, detail views, choices, and validation messages * German translation for navigation, forms, tables, detail views, choices, and validation messages
* Optional lifetime or recurring renewal interval for licenses
* Optional license file, license key, and provider portal URL
### Changed ### Changed
@@ -13,6 +15,7 @@
* Replace the Docker-based setup with Git/Pip installation documentation * Replace the Docker-based setup with Git/Pip installation documentation
* Render tenant group and tenant in a dedicated license form section * Render tenant group and tenant in a dedicated license form section
* Bump the package version so Git/Pip upgrades replace older installations * Bump the package version so Git/Pip upgrades replace older installations
* Make the license type field optional
## [1.9.0](https://github.com/ICTU/netbox_slm/releases/tag/1.9.0) - 2026-06-25 ## [1.9.0](https://github.com/ICTU/netbox_slm/releases/tag/1.9.0) - 2026-06-25
+7 -1
View File
@@ -14,10 +14,16 @@ Benutzer ausgewählten Sprache.
- Versionen, Release-Typen und Supportzeiträume erfassen - Versionen, Release-Typen und Supportzeiträume erfassen
- Installationen Geräten, virtuellen Maschinen oder Clustern zuordnen - Installationen Geräten, virtuellen Maschinen oder Clustern zuordnen
- Softwarelizenzen einschließlich Laufzeit, Umfang und Ablageort verwalten - Softwarelizenzen einschließlich Laufzeit, Umfang und Ablageort verwalten
- Lifetime-Lizenzen oder Erneuerungsintervalle in Tagen, Monaten oder Jahren erfassen
- Lizenzdateien, Lizenzschlüssel und Links zum Anbieterportal hinterlegen
- Lizenzen Mandantengruppen und Mandanten zuordnen - Lizenzen Mandantengruppen und Mandanten zuordnen
- Bedienung über die NetBox-Oberfläche und REST-API - Bedienung über die NetBox-Oberfläche und REST-API
- Filter, Bulk-Import und Bulk-Bearbeitung - Filter, Bulk-Import und Bulk-Bearbeitung
> **Hinweis:** Lizenzschlüssel werden in der NetBox-Datenbank und Lizenzdateien im konfigurierten NetBox-
> Medienverzeichnis gespeichert. Der Zugriff auf Datenbank, REST-API und Medienverzeichnis sollte entsprechend
> geschützt und gesichert werden.
## Installation ## Installation
NetBox installiert lokale Erweiterungen aus `/opt/netbox/local_requirements.txt`. Die Git-URL muss dort dauerhaft NetBox installiert lokale Erweiterungen aus `/opt/netbox/local_requirements.txt`. Die Git-URL muss dort dauerhaft
@@ -57,7 +63,7 @@ Die installierte Version kann anschließend geprüft werden:
/opt/netbox/venv/bin/pip show netbox-slm /opt/netbox/venv/bin/pip show netbox-slm
``` ```
Für diese Variante muss dort mindestens Version `1.10.0` stehen. Für diese Variante muss dort mindestens Version `1.11.0` stehen.
### 3. Plugin in NetBox aktivieren ### 3. Plugin in NetBox aktivieren
+1 -1
View File
@@ -17,7 +17,7 @@ limitations under the License.
from netbox.plugins import PluginConfig from netbox.plugins import PluginConfig
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
__version__ = "1.10.0" __version__ = "1.11.0"
class SLMConfig(PluginConfig): class SLMConfig(PluginConfig):
+20
View File
@@ -24,8 +24,14 @@ class SoftwareLicenseSerializer(NetBoxModelSerializer):
"stored_location_url", "stored_location_url",
"start_date", "start_date",
"expiration_date", "expiration_date",
"lifetime",
"renewal_interval",
"renewal_interval_unit",
"support", "support",
"license_amount", "license_amount",
"license_file",
"license_key",
"provider_portal_url",
"software_product", "software_product",
"version", "version",
"installation", "installation",
@@ -49,6 +55,20 @@ class SoftwareLicenseSerializer(NetBoxModelSerializer):
raise serializers.ValidationError( raise serializers.ValidationError(
{"tenant": _("The selected tenant does not belong to the selected tenant group.")} {"tenant": _("The selected tenant does not belong to the selected tenant group.")}
) )
lifetime = attrs.get("lifetime", getattr(self.instance, "lifetime", False))
renewal_interval = attrs.get("renewal_interval", getattr(self.instance, "renewal_interval", None))
renewal_interval_unit = attrs.get(
"renewal_interval_unit", getattr(self.instance, "renewal_interval_unit", "")
)
expiration_date = attrs.get("expiration_date", getattr(self.instance, "expiration_date", None))
if bool(renewal_interval) != bool(renewal_interval_unit):
raise serializers.ValidationError(
{"renewal_interval": _("Renewal interval and unit must be specified together.")}
)
if lifetime and (renewal_interval or renewal_interval_unit or expiration_date):
raise serializers.ValidationError(
{"lifetime": _("A lifetime license cannot have a renewal interval or expiration date.")}
)
return attrs return attrs
+5 -2
View File
@@ -1,6 +1,6 @@
from django.db.models import Q from django.db.models import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_filters import CharFilter, ModelMultipleChoiceFilter, MultipleChoiceFilter from django_filters import CharFilter, ModelMultipleChoiceFilter, MultipleChoiceFilter, NumberFilter
from dcim.models import Device, Manufacturer from dcim.models import Device, Manufacturer
from netbox.filtersets import NetBoxModelFilterSet from netbox.filtersets import NetBoxModelFilterSet
@@ -10,6 +10,7 @@ from netbox_slm.models import (
SoftwareProductInstallation, SoftwareProductInstallation,
SoftwareLicense, SoftwareLicense,
SoftwareReleaseTypes, SoftwareReleaseTypes,
RenewalIntervalUnits,
) )
from virtualization.models import Cluster, VirtualMachine from virtualization.models import Cluster, VirtualMachine
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
@@ -117,6 +118,8 @@ class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
type = CharFilter(lookup_expr="icontains") type = CharFilter(lookup_expr="icontains")
spdx_expression = CharFilter(lookup_expr="icontains", label=_("SPDX expression")) spdx_expression = CharFilter(lookup_expr="icontains", label=_("SPDX expression"))
stored_location = CharFilter(lookup_expr="icontains") stored_location = CharFilter(lookup_expr="icontains")
renewal_interval = NumberFilter()
renewal_interval_unit = MultipleChoiceFilter(choices=RenewalIntervalUnits.choices)
software_product_id = ModelMultipleChoiceFilter( software_product_id = ModelMultipleChoiceFilter(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
@@ -129,7 +132,7 @@ class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = SoftwareLicense model = SoftwareLicense
fields = ("support",) fields = ("support", "lifetime")
def search(self, queryset, name, value): def search(self, queryset, name, value):
"""Perform the filtered search.""" """Perform the filtered search."""
+40 -1
View File
@@ -1,4 +1,4 @@
from django.forms import CharField, DateField, ChoiceField, IntegerField, NullBooleanField from django.forms import CharField, DateField, ChoiceField, IntegerField, NullBooleanField, Textarea
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -8,6 +8,7 @@ from netbox_slm.models import (
SoftwareProductVersion, SoftwareProductVersion,
SoftwareProductInstallation, SoftwareProductInstallation,
SoftwareLicense, SoftwareLicense,
RenewalIntervalUnits,
spdx_license_names, spdx_license_names,
) )
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
@@ -42,10 +43,14 @@ class SoftwareLicenseForm(NetBoxModelForm):
"stored_location_url", "stored_location_url",
"start_date", "start_date",
"expiration_date", "expiration_date",
"lifetime",
"renewal_interval",
"renewal_interval_unit",
"support", "support",
"license_amount", "license_amount",
name=_("License Details"), name=_("License Details"),
), ),
FieldSet("license_file", "license_key", "provider_portal_url", name=_("License Credentials")),
FieldSet("tags", name=_("Tags")), FieldSet("tags", name=_("Tags")),
) )
@@ -53,6 +58,9 @@ class SoftwareLicenseForm(NetBoxModelForm):
stored_location_url = LaxURLField(required=False) stored_location_url = LaxURLField(required=False)
start_date = DateField(required=False, widget=DatePicker()) start_date = DateField(required=False, widget=DatePicker())
expiration_date = DateField(required=False, widget=DatePicker()) expiration_date = DateField(required=False, widget=DatePicker())
renewal_interval = IntegerField(required=False, min_value=1)
license_key = CharField(required=False, widget=Textarea(attrs={"rows": 3}))
provider_portal_url = LaxURLField(required=False)
software_product = DynamicModelChoiceField( software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
@@ -97,8 +105,14 @@ class SoftwareLicenseForm(NetBoxModelForm):
"stored_location_url", "stored_location_url",
"start_date", "start_date",
"expiration_date", "expiration_date",
"lifetime",
"renewal_interval",
"renewal_interval_unit",
"support", "support",
"license_amount", "license_amount",
"license_file",
"license_key",
"provider_portal_url",
"version", "version",
"installation", "installation",
"tenant_group", "tenant_group",
@@ -119,6 +133,9 @@ class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
"spdx_expression", "spdx_expression",
"stored_location", "stored_location",
"support", "support",
"lifetime",
"renewal_interval",
"renewal_interval_unit",
"software_product_id", "software_product_id",
"version_id", "version_id",
"installation_id", "installation_id",
@@ -136,6 +153,9 @@ class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
spdx_expression = CharField(required=False, label=_("SPDX expression")) spdx_expression = CharField(required=False, label=_("SPDX expression"))
stored_location = CharField(required=False) stored_location = CharField(required=False)
support = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES) support = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES)
lifetime = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES)
renewal_interval = IntegerField(required=False, min_value=1)
renewal_interval_unit = ChoiceField(required=False, choices=RenewalIntervalUnits.choices)
software_product_id = DynamicModelMultipleChoiceField( software_product_id = DynamicModelMultipleChoiceField(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
@@ -181,8 +201,13 @@ class SoftwareLicenseBulkImportForm(NetBoxModelImportForm):
"stored_location", "stored_location",
"start_date", "start_date",
"expiration_date", "expiration_date",
"lifetime",
"renewal_interval",
"renewal_interval_unit",
"support", "support",
"license_amount", "license_amount",
"license_key",
"provider_portal_url",
"version", "version",
"installation", "installation",
"tenant_group", "tenant_group",
@@ -201,8 +226,13 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
"stored_location_url", "stored_location_url",
"start_date", "start_date",
"expiration_date", "expiration_date",
"lifetime",
"renewal_interval",
"renewal_interval_unit",
"support", "support",
"license_amount", "license_amount",
"license_key",
"provider_portal_url",
"software_product", "software_product",
"version", "version",
"installation", "installation",
@@ -217,8 +247,12 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
"stored_location_url", "stored_location_url",
"start_date", "start_date",
"expiration_date", "expiration_date",
"renewal_interval",
"renewal_interval_unit",
"support", "support",
"license_amount", "license_amount",
"license_key",
"provider_portal_url",
"version", "version",
"installation", "installation",
"tenant_group", "tenant_group",
@@ -234,8 +268,13 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
stored_location_url = LaxURLField(required=False) stored_location_url = LaxURLField(required=False)
start_date = DateField(required=False, widget=DatePicker()) start_date = DateField(required=False, widget=DatePicker())
expiration_date = DateField(required=False, widget=DatePicker()) expiration_date = DateField(required=False, widget=DatePicker())
lifetime = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES)
renewal_interval = IntegerField(required=False, min_value=1)
renewal_interval_unit = ChoiceField(required=False, choices=RenewalIntervalUnits.choices)
support = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES) support = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES)
license_amount = IntegerField(required=False, min_value=0) license_amount = IntegerField(required=False, min_value=0)
license_key = CharField(required=False, widget=Textarea(attrs={"rows": 3}))
provider_portal_url = LaxURLField(required=False)
software_product = DynamicModelChoiceField( software_product = DynamicModelChoiceField(
queryset=SoftwareProduct.objects.all(), queryset=SoftwareProduct.objects.all(),
Binary file not shown.
@@ -261,3 +261,63 @@ msgstr "Softwarelizenz"
msgid "software licenses" msgid "software licenses"
msgstr "Softwarelizenzen" msgstr "Softwarelizenzen"
msgid "Days"
msgstr "Tage"
msgid "Months"
msgstr "Monate"
msgid "Years"
msgstr "Jahre"
msgid "lifetime license"
msgstr "Lifetime-Lizenz"
msgid "renewal interval"
msgstr "Erneuerungsintervall"
msgid "renewal interval unit"
msgstr "Einheit des Erneuerungsintervalls"
msgid "license file"
msgstr "Lizenzdatei"
msgid "license key"
msgstr "Lizenzschlüssel"
msgid "provider portal URL"
msgstr "Anbieterportal-URL"
msgid "License Credentials"
msgstr "Lizenzdaten"
msgid "Lifetime license"
msgstr "Lifetime-Lizenz"
msgid "Renewal interval"
msgstr "Erneuerungsintervall"
msgid "License file"
msgstr "Lizenzdatei"
msgid "Download license file"
msgstr "Lizenzdatei herunterladen"
msgid "License key"
msgstr "Lizenzschlüssel"
msgid "Provider Portal"
msgstr "Anbieterportal"
msgid "Yes"
msgstr "Ja"
msgid "No"
msgstr "Nein"
msgid "Renewal interval and unit must be specified together."
msgstr "Erneuerungsintervall und Einheit müssen gemeinsam angegeben werden."
msgid "A lifetime license cannot have a renewal interval or expiration date."
msgstr "Eine Lifetime-Lizenz darf kein Erneuerungsintervall und kein Ablaufdatum haben."
@@ -0,0 +1,51 @@
from django.db import migrations, models
import django.core.validators
import netbox_slm.models
class Migration(migrations.Migration):
dependencies = [
("netbox_slm", "0011_softwarelicense_tenancy"),
]
operations = [
migrations.AlterField(
model_name="softwarelicense",
name="type",
field=models.CharField(blank=True, max_length=128),
),
migrations.AddField(
model_name="softwarelicense",
name="lifetime",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="softwarelicense",
name="renewal_interval",
field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]),
),
migrations.AddField(
model_name="softwarelicense",
name="renewal_interval_unit",
field=models.CharField(
blank=True,
choices=[("days", "Days"), ("months", "Months"), ("years", "Years")],
max_length=8,
),
),
migrations.AddField(
model_name="softwarelicense",
name="license_file",
field=models.FileField(blank=True, null=True, upload_to="netbox_slm/licenses/"),
),
migrations.AddField(
model_name="softwarelicense",
name="license_key",
field=models.TextField(blank=True),
),
migrations.AddField(
model_name="softwarelicense",
name="provider_portal_url",
field=netbox_slm.models.LaxURLField(blank=True, max_length=1024, null=True),
),
]
+26 -1
View File
@@ -1,4 +1,5 @@
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.html import format_html from django.utils.html import format_html
@@ -57,6 +58,12 @@ class SoftwareReleaseTypes(models.TextChoices):
STABLE = "S", _("Stable release") STABLE = "S", _("Stable release")
class RenewalIntervalUnits(models.TextChoices):
DAYS = "days", _("Days")
MONTHS = "months", _("Months")
YEARS = "years", _("Years")
class SoftwareProductVersion(NetBoxModel): class SoftwareProductVersion(NetBoxModel):
name = models.CharField(_("name"), max_length=64) name = models.CharField(_("name"), max_length=64)
comments = models.TextField(_("comments"), blank=True) comments = models.TextField(_("comments"), blank=True)
@@ -176,7 +183,7 @@ class SoftwareLicense(NetBoxModel):
comments = models.TextField(_("comments"), blank=True) comments = models.TextField(_("comments"), blank=True)
description = models.CharField(_("description"), max_length=255, null=True, blank=True) description = models.CharField(_("description"), max_length=255, null=True, blank=True)
type = models.CharField(_("type"), max_length=128) type = models.CharField(_("type"), max_length=128, blank=True)
spdx_expression = models.CharField( spdx_expression = models.CharField(
_("SPDX expression"), max_length=64, null=True, blank=True, validators=[validate_spdx_expression] _("SPDX expression"), max_length=64, null=True, blank=True, validators=[validate_spdx_expression]
) )
@@ -186,6 +193,16 @@ class SoftwareLicense(NetBoxModel):
expiration_date = models.DateField(_("expiration date"), null=True, blank=True) expiration_date = models.DateField(_("expiration date"), null=True, blank=True)
support = models.BooleanField(_("support"), default=None, null=True, blank=True) support = models.BooleanField(_("support"), default=None, null=True, blank=True)
license_amount = models.PositiveIntegerField(_("license amount"), default=None, null=True, blank=True) license_amount = models.PositiveIntegerField(_("license amount"), default=None, null=True, blank=True)
lifetime = models.BooleanField(_("lifetime license"), default=False)
renewal_interval = models.PositiveIntegerField(
_("renewal interval"), null=True, blank=True, validators=[MinValueValidator(1)]
)
renewal_interval_unit = models.CharField(
_("renewal interval unit"), max_length=8, choices=RenewalIntervalUnits.choices, blank=True
)
license_file = models.FileField(_("license file"), upload_to="netbox_slm/licenses/", null=True, blank=True)
license_key = models.TextField(_("license key"), blank=True)
provider_portal_url = LaxURLField(_("provider portal URL"), max_length=1024, null=True, blank=True)
software_product = models.ForeignKey( software_product = models.ForeignKey(
to="netbox_slm.SoftwareProduct", verbose_name=_("software product"), on_delete=models.PROTECT to="netbox_slm.SoftwareProduct", verbose_name=_("software product"), on_delete=models.PROTECT
@@ -239,6 +256,14 @@ class SoftwareLicense(NetBoxModel):
raise ValidationError( raise ValidationError(
{"tenant": _("The selected tenant does not belong to the selected tenant group.")} {"tenant": _("The selected tenant does not belong to the selected tenant group.")}
) )
if bool(self.renewal_interval) != bool(self.renewal_interval_unit):
raise ValidationError(
{"renewal_interval": _("Renewal interval and unit must be specified together.")}
)
if self.lifetime and (self.renewal_interval or self.renewal_interval_unit or self.expiration_date):
raise ValidationError(
{"lifetime": _("A lifetime license cannot have a renewal interval or expiration date.")}
)
@property @property
def stored_location_txt(self): def stored_location_txt(self):
+6
View File
@@ -147,6 +147,7 @@ class SoftwareLicenseTable(NetBoxTable):
installation = tables.Column(accessor="installation", linkify=True) installation = tables.Column(accessor="installation", linkify=True)
tenant_group = tables.Column(accessor="tenant_group", verbose_name=_("Tenant Group"), linkify=True) tenant_group = tables.Column(accessor="tenant_group", verbose_name=_("Tenant Group"), linkify=True)
tenant = tables.Column(accessor="tenant", linkify=True) tenant = tables.Column(accessor="tenant", linkify=True)
provider_portal_url = tables.Column(verbose_name=_("Provider Portal"), linkify=True)
tags = columns.TagColumn(url_name="plugins:netbox_slm:softwarelicense_list") tags = columns.TagColumn(url_name="plugins:netbox_slm:softwarelicense_list")
@@ -161,6 +162,9 @@ class SoftwareLicenseTable(NetBoxTable):
"stored_location", "stored_location",
"start_date", "start_date",
"expiration_date", "expiration_date",
"lifetime",
"renewal_interval",
"renewal_interval_unit",
"manufacturer", "manufacturer",
"software_product", "software_product",
"version", "version",
@@ -169,6 +173,7 @@ class SoftwareLicenseTable(NetBoxTable):
"tenant", "tenant",
"support", "support",
"license_amount", "license_amount",
"provider_portal_url",
"tags", "tags",
) )
default_columns = ( default_columns = (
@@ -181,6 +186,7 @@ class SoftwareLicenseTable(NetBoxTable):
"tenant_group", "tenant_group",
"tenant", "tenant",
"expiration_date", "expiration_date",
"lifetime",
"tags", "tags",
) )
@@ -65,6 +65,20 @@
<th scope="row">{% trans "Expiration date" %}</th> <th scope="row">{% trans "Expiration date" %}</th>
<td>{{ object.expiration_date }}</td> <td>{{ object.expiration_date }}</td>
</tr> </tr>
<tr>
<th scope="row">{% trans "Lifetime license" %}</th>
<td>{% if object.lifetime %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}</td>
</tr>
<tr>
<th scope="row">{% trans "Renewal interval" %}</th>
<td>
{% if object.renewal_interval %}
{{ object.renewal_interval }} {{ object.get_renewal_interval_unit_display }}
{% else %}
{{ None|placeholder }}
{% endif %}
</td>
</tr>
<tr> <tr>
<th scope="row">{% trans "Support" %}</th> <th scope="row">{% trans "Support" %}</th>
<td>{{ object.support }}</td> <td>{{ object.support }}</td>
@@ -73,6 +87,30 @@
<th scope="row">{% trans "License amount" %}</th> <th scope="row">{% trans "License amount" %}</th>
<td>{{ object.license_amount }}</td> <td>{{ object.license_amount }}</td>
</tr> </tr>
<tr>
<th scope="row">{% trans "License file" %}</th>
<td>
{% if object.license_file %}
<a href="{{ object.license_file.url }}">{% trans "Download license file" %}</a>
{% else %}
{{ None|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "License key" %}</th>
<td>{% if object.license_key %}<code>{{ object.license_key }}</code>{% else %}{{ None|placeholder }}{% endif %}</td>
</tr>
<tr>
<th scope="row">{% trans "Provider Portal" %}</th>
<td>
{% if object.provider_portal_url %}
<a href="{{ object.provider_portal_url }}">{{ object.provider_portal_url }}</a>
{% else %}
{{ None|placeholder }}
{% endif %}
</td>
</tr>
</table> </table>
</div> </div>
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/custom_fields.html' %}
+41
View File
@@ -1,6 +1,7 @@
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import gettext, override from django.utils.translation import gettext, override
from netbox_slm.forms import SoftwareLicenseForm from netbox_slm.forms import SoftwareLicenseForm
from netbox_slm.models import RenewalIntervalUnits, SoftwareLicense
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from .base import SlmBaseTestCase from .base import SlmBaseTestCase
@@ -74,6 +75,46 @@ class ModelTestCase(SlmBaseTestCase):
any("tenant_group" in fieldset.items and "tenant" in fieldset.items for fieldset in form.fieldsets) any("tenant_group" in fieldset.items and "tenant" in fieldset.items for fieldset in form.fieldsets)
) )
def test_software_license_form_contains_renewal_and_credentials(self):
form = SoftwareLicenseForm(instance=self.software_license)
for field_name in (
"lifetime",
"renewal_interval",
"renewal_interval_unit",
"license_file",
"license_key",
"provider_portal_url",
):
self.assertIn(field_name, form.fields)
def test_software_license_accepts_optional_type_and_renewal_interval(self):
license = SoftwareLicense(
name="renewable license",
software_product=self.software_product,
renewal_interval=12,
renewal_interval_unit=RenewalIntervalUnits.MONTHS,
)
license.full_clean()
def test_software_license_requires_renewal_interval_and_unit_together(self):
license = SoftwareLicense(
name="invalid renewable license",
software_product=self.software_product,
renewal_interval=12,
)
with self.assertRaisesMessage(ValidationError, "must be specified together"):
license.full_clean()
def test_lifetime_license_rejects_expiration(self):
license = SoftwareLicense(
name="invalid lifetime license",
software_product=self.software_product,
lifetime=True,
expiration_date="2030-01-01",
)
with self.assertRaisesMessage(ValidationError, "cannot have a renewal interval or expiration date"):
license.full_clean()
def test_german_translation(self): def test_german_translation(self):
with override("de"): with override("de"):
self.assertEqual("Softwarelizenz", gettext("Software License")) self.assertEqual("Softwarelizenz", gettext("Software License"))