diff --git a/netbox_lifecycle/choices/__init__.py b/netbox_lifecycle/choices/__init__.py
new file mode 100644
index 0000000..3d4ad1f
--- /dev/null
+++ b/netbox_lifecycle/choices/__init__.py
@@ -0,0 +1,3 @@
+from .contract import ContractStatusChoices
+
+__all__ = ('ContractStatusChoices',)
diff --git a/netbox_lifecycle/choices/contract.py b/netbox_lifecycle/choices/contract.py
new file mode 100644
index 0000000..48967c6
--- /dev/null
+++ b/netbox_lifecycle/choices/contract.py
@@ -0,0 +1,22 @@
+from django.utils.translation import gettext as _
+from utilities.choices import ChoiceSet
+
+from netbox_lifecycle import constants
+
+
+class ContractStatusChoices(ChoiceSet):
+ """
+ Support contract status choices.
+ """
+
+ ACTIVE = constants.CONTRACT_STATUS_ACTIVE
+ EXPIRED = constants.CONTRACT_STATUS_EXPIRED
+ FUTURE = constants.CONTRACT_STATUS_FUTURE
+ UNSPECIFIED = constants.CONTRACT_STATUS_UNSPECIFIED
+
+ CHOICES = (
+ (ACTIVE, _('Active')),
+ (FUTURE, _('Future')),
+ (UNSPECIFIED, _('Unspecified')),
+ (EXPIRED, _('Expired')),
+ )
diff --git a/netbox_lifecycle/filtersets/contract.py b/netbox_lifecycle/filtersets/contract.py
index 6c6f9e8..957bf54 100644
--- a/netbox_lifecycle/filtersets/contract.py
+++ b/netbox_lifecycle/filtersets/contract.py
@@ -1,10 +1,15 @@
import django_filters
-from dcim.models import Device, Manufacturer, Module
+from django.utils import timezone
+
+from dcim.models import Device, Manufacturer, Module, DeviceType
from django.db.models import Q
from django.utils.translation import gettext as _
from netbox.filtersets import NetBoxModelFilterSet
+from utilities.filtersets import register_filterset
from virtualization.models import VirtualMachine
+from netbox_lifecycle import constants
+from netbox_lifecycle.choices import ContractStatusChoices
from netbox_lifecycle.models import (
License,
SupportContract,
@@ -21,6 +26,7 @@
)
+@register_filterset
class VendorFilterSet(NetBoxModelFilterSet):
class Meta:
@@ -38,6 +44,7 @@ def search(self, queryset, name, value):
return queryset.filter(qs_filter).distinct()
+@register_filterset
class SupportSKUFilterSet(NetBoxModelFilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
field_name='manufacturer',
@@ -56,6 +63,7 @@ class Meta:
fields = (
'id',
'q',
+ 'manufacturer_id',
'sku',
)
@@ -66,6 +74,7 @@ def search(self, queryset, name, value):
return queryset.filter(qs_filter).distinct()
+@register_filterset
class SupportContractFilterSet(NetBoxModelFilterSet):
vendor_id = django_filters.ModelMultipleChoiceFilter(
field_name='vendor',
@@ -85,6 +94,7 @@ class Meta:
'id',
'q',
'contract_id',
+ 'vendor_id',
)
def search(self, queryset, name, value):
@@ -94,6 +104,7 @@ def search(self, queryset, name, value):
return queryset.filter(qs_filter).distinct()
+@register_filterset
class SupportContractAssignmentFilterSet(NetBoxModelFilterSet):
contract_id = django_filters.ModelMultipleChoiceFilter(
field_name='contract',
@@ -117,6 +128,17 @@ class SupportContractAssignmentFilterSet(NetBoxModelFilterSet):
to_field_name='sku',
label=_('SKU'),
)
+ device_type_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='device__device_type',
+ queryset=DeviceType.objects.all(),
+ label=_('Device Type (ID)'),
+ )
+ device_type = django_filters.ModelMultipleChoiceFilter(
+ field_name='device__device_type__model',
+ queryset=DeviceType.objects.all(),
+ to_field_name='model',
+ label=_('Device Type (model)'),
+ )
device_id = django_filters.ModelMultipleChoiceFilter(
field_name='device',
queryset=Device.objects.all(),
@@ -167,12 +189,26 @@ class SupportContractAssignmentFilterSet(NetBoxModelFilterSet):
to_field_name='status',
label=_('Device Status'),
)
+ status = django_filters.ChoiceFilter(
+ choices=ContractStatusChoices.CHOICES,
+ method='filter_status',
+ label=_('Status'),
+ )
class Meta:
model = SupportContractAssignment
fields = (
'id',
'q',
+ 'contract_id',
+ 'sku_id',
+ 'device_type_id',
+ 'device_id',
+ 'module_id',
+ 'virtual_machine_id',
+ 'license_id',
+ 'device_status',
+ 'status',
)
def search(self, queryset, name, value):
@@ -191,3 +227,18 @@ def search(self, queryset, name, value):
| Q(license__license__name__icontains=value)
)
return queryset.filter(qs_filter).distinct()
+
+ def filter_status(self, queryset, name, value):
+ today = timezone.now().date()
+ expired = Q(end__lt=today) | Q(end__isnull=True, contract__end__lt=today)
+ future = Q(contract__start__gt=today)
+ unspecified = Q(end__isnull=True, contract__end__isnull=True)
+ if value == constants.CONTRACT_STATUS_ACTIVE:
+ return queryset.exclude(expired).exclude(future)
+ elif value == constants.CONTRACT_STATUS_FUTURE:
+ return queryset.filter(future)
+ elif value == constants.CONTRACT_STATUS_EXPIRED:
+ return queryset.filter(expired)
+ elif value == constants.CONTRACT_STATUS_UNSPECIFIED:
+ return queryset.filter(unspecified).exclude(future)
+ return queryset
diff --git a/netbox_lifecycle/filtersets/hardware.py b/netbox_lifecycle/filtersets/hardware.py
index 169c1da..226893f 100644
--- a/netbox_lifecycle/filtersets/hardware.py
+++ b/netbox_lifecycle/filtersets/hardware.py
@@ -9,7 +9,10 @@
__all__ = ('HardwareLifecycleFilterSet',)
+from utilities.filtersets import register_filterset
+
+@register_filterset
class HardwareLifecycleFilterSet(NetBoxModelFilterSet):
assigned_object_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=ContentType.objects.all()
diff --git a/netbox_lifecycle/filtersets/license.py b/netbox_lifecycle/filtersets/license.py
index 633a876..9f7e36b 100644
--- a/netbox_lifecycle/filtersets/license.py
+++ b/netbox_lifecycle/filtersets/license.py
@@ -3,6 +3,7 @@
from django.db.models import Q
from django.utils.translation import gettext as _
from netbox.filtersets import NetBoxModelFilterSet
+from utilities.filtersets import register_filterset
from virtualization.models import VirtualMachine
from netbox_lifecycle.models import License, LicenseAssignment, Vendor
@@ -13,6 +14,7 @@
)
+@register_filterset
class LicenseFilterSet(NetBoxModelFilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
field_name='manufacturer',
@@ -32,6 +34,7 @@ class Meta:
'id',
'q',
'name',
+ 'manufacturer_id',
)
def search(self, queryset, name, value):
@@ -41,6 +44,7 @@ def search(self, queryset, name, value):
return queryset.filter(qs_filter).distinct()
+@register_filterset
class LicenseAssignmentFilterSet(NetBoxModelFilterSet):
license_id = django_filters.ModelMultipleChoiceFilter(
field_name='license',
@@ -92,6 +96,10 @@ class Meta:
fields = (
'id',
'q',
+ 'license_id',
+ 'vendor_id',
+ 'device_id',
+ 'virtual_machine_id',
)
def search(self, queryset, name, value):
diff --git a/netbox_lifecycle/forms/model_forms.py b/netbox_lifecycle/forms/model_forms.py
index 7e8d50d..88466c4 100644
--- a/netbox_lifecycle/forms/model_forms.py
+++ b/netbox_lifecycle/forms/model_forms.py
@@ -46,6 +46,7 @@ class SupportSKUForm(NetBoxModelForm):
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
selector=False,
+ quick_add=True,
)
class Meta:
@@ -63,6 +64,7 @@ class SupportContractForm(NetBoxModelForm):
vendor = DynamicModelChoiceField(
queryset=Vendor.objects.all(),
selector=True,
+ quick_add=True,
)
class Meta:
@@ -88,11 +90,13 @@ class SupportContractAssignmentForm(NetBoxModelForm):
contract = DynamicModelChoiceField(
queryset=SupportContract.objects.all(),
selector=True,
+ quick_add=True,
)
sku = DynamicModelChoiceField(
queryset=SupportSKU.objects.all(),
required=False,
selector=True,
+ quick_add=True,
label=_('SKU'),
)
device = DynamicModelChoiceField(
@@ -238,6 +242,7 @@ class LicenseForm(NetBoxModelForm):
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
selector=False,
+ quick_add=True,
)
class Meta:
@@ -255,10 +260,12 @@ class LicenseAssignmentForm(NetBoxModelForm):
vendor = DynamicModelChoiceField(
queryset=Vendor.objects.all(),
selector=True,
+ quick_add=True,
)
license = DynamicModelChoiceField(
queryset=License.objects.all(),
selector=True,
+ quick_add=True,
)
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
diff --git a/netbox_lifecycle/models/contract.py b/netbox_lifecycle/models/contract.py
index 1467e87..b90409a 100644
--- a/netbox_lifecycle/models/contract.py
+++ b/netbox_lifecycle/models/contract.py
@@ -51,13 +51,15 @@ class SupportSKU(PrimaryModel):
on_delete=models.CASCADE,
related_name='skus',
)
- sku = models.CharField(max_length=100)
+ sku = models.CharField(verbose_name=_('SKU'), max_length=100)
clone_fields = ('manufacturer',)
prerequisite_models = ('dcim.Manufacturer',)
class Meta:
ordering = ['manufacturer', 'sku']
+ verbose_name = 'Support SKU'
+ verbose_name_plural = 'Support SKUs'
constraints = (
models.UniqueConstraint(
'manufacturer',
diff --git a/netbox_lifecycle/navigation.py b/netbox_lifecycle/navigation.py
index d63565d..7181f3f 100644
--- a/netbox_lifecycle/navigation.py
+++ b/netbox_lifecycle/navigation.py
@@ -1,40 +1,141 @@
-from netbox.plugins import PluginMenu, PluginMenuItem
+from netbox.plugins import PluginMenu, PluginMenuItem, PluginMenuButton
+
+COL_ADD = 'mdi mdi-plus'
+COL_IMPORT = 'mdi mdi-upload'
lifecycle = PluginMenuItem(
link='plugins:netbox_lifecycle:hardwarelifecycle_list',
link_text='Hardware Lifecycle',
permissions=['netbox_lifecycle.view_hardwarelifecycle'],
+ buttons=(
+ PluginMenuButton(
+ link='plugins:netbox_lifecycle:hardwarelifecycle_add',
+ title='Add',
+ icon_class=COL_ADD,
+ permissions=['netbox_lifecycle.add_hardwarelifecycle'],
+ ),
+ PluginMenuButton(
+ link='plugins:netbox_lifecycle:hardwarelifecycle_bulk_import',
+ title='Import',
+ icon_class=COL_IMPORT,
+ permissions=['netbox_lifecycle.add_hardwarelifecycle'],
+ ),
+ ),
)
vendors = PluginMenuItem(
link='plugins:netbox_lifecycle:vendor_list',
link_text='Vendors',
permissions=['netbox_lifecycle.view_vendor'],
+ buttons=(
+ PluginMenuButton(
+ link='plugins:netbox_lifecycle:vendor_add',
+ title='Add',
+ icon_class=COL_ADD,
+ permissions=['netbox_lifecycle.add_vendor'],
+ ),
+ PluginMenuButton(
+ link='plugins:netbox_lifecycle:vendor_bulk_import',
+ title='Import',
+ icon_class=COL_IMPORT,
+ permissions=['netbox_lifecycle.add_vendor'],
+ ),
+ ),
)
skus = PluginMenuItem(
link='plugins:netbox_lifecycle:supportsku_list',
link_text='Support SKUs',
permissions=['netbox_lifecycle.view_supportsku'],
+ buttons=(
+ PluginMenuButton(
+ link='plugins:netbox_lifecycle:supportsku_add',
+ title='Add',
+ icon_class=COL_ADD,
+ permissions=['netbox_lifecycle.add_supportsku'],
+ ),
+ PluginMenuButton(
+ link='plugins:netbox_lifecycle:supportsku_bulk_import',
+ title='Import',
+ icon_class=COL_IMPORT,
+ permissions=['netbox_lifecycle.add_supportsku'],
+ ),
+ ),
)
contracts = PluginMenuItem(
link='plugins:netbox_lifecycle:supportcontract_list',
link_text='Support Contracts',
permissions=['netbox_lifecycle.view_supportcontract'],
+ buttons=(
+ PluginMenuButton(
+ link='plugins:netbox_lifecycle:supportcontract_add',
+ title='Add',
+ icon_class=COL_ADD,
+ permissions=['netbox_lifecycle.add_supportcontract'],
+ ),
+ PluginMenuButton(
+ link='plugins:netbox_lifecycle:supportcontract_bulk_import',
+ title='Import',
+ icon_class=COL_IMPORT,
+ permissions=['netbox_lifecycle.add_supportcontract'],
+ ),
+ ),
)
contract_assignments = PluginMenuItem(
link='plugins:netbox_lifecycle:supportcontractassignment_list',
link_text='Support Assignments',
permissions=['netbox_lifecycle.view_supportcontractassignment'],
+ buttons=(
+ PluginMenuButton(
+ link='plugins:netbox_lifecycle:supportcontractassignment_add',
+ title='Add',
+ icon_class=COL_ADD,
+ permissions=['netbox_lifecycle.add_supportcontractassignment'],
+ ),
+ PluginMenuButton(
+ link='plugins:netbox_lifecycle:supportcontractassignment_bulk_import',
+ title='Import',
+ icon_class=COL_IMPORT,
+ permissions=['netbox_lifecycle.add_supportcontractassignment'],
+ ),
+ ),
)
licenses = PluginMenuItem(
link='plugins:netbox_lifecycle:license_list',
link_text='Licenses',
permissions=['netbox_lifecycle.view_license'],
+ buttons=(
+ PluginMenuButton(
+ link='plugins:netbox_lifecycle:license_add',
+ title='Add',
+ icon_class=COL_ADD,
+ permissions=['netbox_lifecycle.add_license'],
+ ),
+ PluginMenuButton(
+ link='plugins:netbox_lifecycle:license_bulk_import',
+ title='Import',
+ icon_class=COL_IMPORT,
+ permissions=['netbox_lifecycle.add_license'],
+ ),
+ ),
)
license_assignments = PluginMenuItem(
link='plugins:netbox_lifecycle:licenseassignment_list',
link_text='License Assignments',
permissions=['netbox_lifecycle.view_licenseassignment'],
+ buttons=(
+ PluginMenuButton(
+ link='plugins:netbox_lifecycle:licenseassignment_add',
+ title='Add',
+ icon_class=COL_ADD,
+ permissions=['netbox_lifecycle.add_licenseassignment'],
+ ),
+ PluginMenuButton(
+ link='plugins:netbox_lifecycle:licenseassignment_bulk_import',
+ title='Import',
+ icon_class=COL_IMPORT,
+ permissions=['netbox_lifecycle.add_licenseassignment'],
+ ),
+ ),
)
diff --git a/netbox_lifecycle/template_content.py b/netbox_lifecycle/template_content.py
index 052450f..820e220 100644
--- a/netbox_lifecycle/template_content.py
+++ b/netbox_lifecycle/template_content.py
@@ -1,19 +1,92 @@
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
-from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
from netbox.plugins import PluginTemplateExtension
+from netbox.ui import panels, actions
-from .models import hardware
-from .models.license import LicenseAssignment
+from netbox_lifecycle import constants
+from netbox_lifecycle.models import hardware
+from netbox_lifecycle.ui import HardwareLifecyclePanel, HardwareLifecycleDatesPanel
+from netbox_lifecycle.ui.panels.tabbed import TabbedTablePanel
PLUGIN_SETTINGS = settings.PLUGINS_CONFIG.get('netbox_lifecycle', {})
-class BaseLifecycleContent(PluginTemplateExtension):
- """Base class for lifecycle template extensions."""
+class BaseMixin:
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.model_name = None
+ self.field_name = None
+ self.lifecycle_content_type = None
+ self.lifecycle_object_id_attr = None
+
+ def get_context(self, context):
+ """
+ Return the context data to be used when rendering the panel.
+ Borrowed from netbox/ui/panels.py
+
+ Parameters:
+ context (dict): The template context
+ """
+ obj = context.get('object')
+ self.model_name = obj._meta.model_name if obj is not None else None
+ if self.model_name in ['device', 'devicetype']:
+ self.lifecycle_content_type = 'devicetype'
+ self.lifecycle_object_id_attr = 'device_type_id'
+ elif self.model_name in ['module', 'moduletype']:
+ self.lifecycle_content_type = 'moduletype'
+ self.lifecycle_object_id_attr = 'module_type_id'
+ else:
+ self.lifecycle_content_type = None
+
+ if self.model_name == 'device':
+ self.field_name = 'device_id'
+ elif self.model_name == 'module':
+ self.field_name = 'module_id'
+ elif self.model_name == 'virtualmachine':
+ self.field_name = 'virtual_machine_id'
+ else:
+ self.field_name = None
+
+ return {
+ 'request': context.get('request'),
+ 'object': context.get('object'),
+ 'perms': context.get('perms'),
+ 'panel_class': self.__class__.__name__,
+ }
- lifecycle_content_type = None # Override: 'devicetype' or 'moduletype'
- lifecycle_object_id_attr = None # Override: attribute name for object ID
+ def right_page(self):
+ result = ''
+ if hasattr(self, '_render_lifecycle_info'):
+ result += self._render_lifecycle_info('right_page')
+ if hasattr(self, '_render_contract_card'):
+ result += self._render_contract_card('right_page')
+ if hasattr(self, '_render_license_card'):
+ result += self._render_license_card('right_page')
+ return result
+
+ def left_page(self):
+ result = ''
+ if hasattr(self, '_render_lifecycle_info'):
+ result += self._render_lifecycle_info('left_page')
+ if hasattr(self, '_render_contract_card'):
+ result += self._render_contract_card('left_page')
+ if hasattr(self, '_render_license_card'):
+ result += self._render_license_card('left_page')
+ return result
+
+ def full_width_page(self):
+ result = ''
+ if hasattr(self, '_render_lifecycle_info'):
+ result += self._render_lifecycle_info('full_width_page')
+ if hasattr(self, '_render_contract_card'):
+ result += self._render_contract_card('full_width_page')
+ if hasattr(self, '_render_license_card'):
+ result += self._render_license_card('full_width_page')
+ return result
+
+
+class LifecycleMixin:
def get_lifecycle_card_position(self):
return PLUGIN_SETTINGS.get('lifecycle_card_position', 'right_page')
@@ -29,178 +102,176 @@ def _get_lifecycle_info(self):
assigned_object_type_id=content_type.id,
).first()
- def _render_lifecycle_info(self):
- return self.render(
- 'netbox_lifecycle/inc/hardware_lifecycle_info.html',
- extra_context={'lifecycle_info': self._get_lifecycle_info()},
- )
+ def _render_lifecycle_info(self, location=None):
+ if self.get_lifecycle_card_position() != location:
+ return ''
- def right_page(self):
- if self.get_lifecycle_card_position() == 'right_page':
- return self._render_lifecycle_info()
- return ''
+ context = self.get_context(self.context)
+ obj = self._get_lifecycle_info()
+ context['object'] = obj
- def left_page(self):
- if self.get_lifecycle_card_position() == 'left_page':
- return self._render_lifecycle_info()
- return ''
+ content = HardwareLifecyclePanel().render(
+ context
+ ) + HardwareLifecycleDatesPanel().render(context)
+ return content
- def full_width_page(self):
- if self.get_lifecycle_card_position() == 'full_width_page':
- return self._render_lifecycle_info()
- return ''
-
-class DeviceLifecycleContent(BaseLifecycleContent):
- models = ['dcim.device']
- lifecycle_content_type = 'devicetype'
- lifecycle_object_id_attr = 'device_type_id'
+class ContractMixin:
def get_contract_card_position(self):
return PLUGIN_SETTINGS.get('contract_card_position', 'right_page')
- def get_license_card_position(self):
- return PLUGIN_SETTINGS.get('license_card_position', 'right_page')
+ def _render_contract_card(self, location=None):
+ if self.get_contract_card_position() != location:
+ return ''
- def _render_contract_card(self):
- obj = self.context.get('object')
- return self.render(
- 'netbox_lifecycle/inc/contract_card_placeholder.html',
- extra_context={
- 'htmx_url': reverse(
- 'plugins:netbox_lifecycle:device_contracts_htmx',
- kwargs={'pk': obj.pk},
+ title = _('Contracts')
+ action = [
+ actions.AddObject(
+ 'netbox_lifecycle.SupportContractAssignment',
+ url_params={
+ self.model_name: lambda ctx: ctx['object'].pk,
+ },
+ ),
+ ]
+
+ include_columns = [
+ 'contract',
+ 'sku',
+ ]
+ exclude_columns = [
+ 'device_name',
+ 'module_name',
+ 'virtual_machine_name',
+ 'license_name',
+ 'device_model',
+ 'device_serial',
+ 'module_serial',
+ 'device_status',
+ 'virtual_machine_status',
+ 'quantity',
+ 'renewal',
+ 'end',
+ 'description',
+ 'comments',
+ 'actions',
+ ]
+ filter = {
+ self.field_name: lambda ctx: ctx['object'].pk,
+ }
+ context = self.get_context(self.context)
+ panel = TabbedTablePanel(
+ title=title,
+ tabs={
+ 'active': panels.ObjectsTablePanel(
+ title=_('Active'),
+ model='netbox_lifecycle.supportcontractassignment',
+ filters={**filter, 'status': constants.CONTRACT_STATUS_ACTIVE},
+ include_columns=include_columns,
+ exclude_columns=exclude_columns,
+ ),
+ 'expired': panels.ObjectsTablePanel(
+ title=_('Expired'),
+ model='netbox_lifecycle.supportcontractassignment',
+ filters={**filter, 'status': constants.CONTRACT_STATUS_EXPIRED},
+ include_columns=include_columns,
+ exclude_columns=exclude_columns,
+ ),
+ 'future': panels.ObjectsTablePanel(
+ title=_('Future'),
+ model='netbox_lifecycle.supportcontractassignment',
+ filters={**filter, 'status': constants.CONTRACT_STATUS_FUTURE},
+ include_columns=include_columns,
+ exclude_columns=exclude_columns,
+ ),
+ 'unspecified': panels.ObjectsTablePanel(
+ title=_('Unspecified'),
+ model='netbox_lifecycle.supportcontractassignment',
+ filters={
+ **filter,
+ 'status': constants.CONTRACT_STATUS_UNSPECIFIED,
+ },
+ include_columns=include_columns,
+ exclude_columns=exclude_columns,
),
},
+ actions=action,
)
+ return panel.render(context=context)
- def _render_license_card(self):
- obj = self.context.get('object')
- if not LicenseAssignment.objects.filter(device=obj).exists():
+
+class LicenseMixin:
+
+ def get_license_card_position(self):
+ return PLUGIN_SETTINGS.get('license_card_position', 'right_page')
+
+ def _render_license_card(self, location=None, exclude=None, include=None):
+ if self.get_license_card_position() != location:
return ''
- return self.render(
- 'netbox_lifecycle/inc/license_card_placeholder.html',
- extra_context={
- 'htmx_url': reverse(
- 'plugins:netbox_lifecycle:device_licenses_htmx',
- kwargs={'pk': obj.pk},
- ),
- },
- )
- def right_page(self):
- result = ''
- if self.get_lifecycle_card_position() == 'right_page':
- result += self._render_lifecycle_info()
- if self.get_contract_card_position() == 'right_page':
- result += self._render_contract_card()
- if self.get_license_card_position() == 'right_page':
- result += self._render_license_card()
- return result
+ action = [
+ actions.AddObject(
+ 'netbox_lifecycle.LicenseAssignment',
+ url_params={
+ self.model_name: lambda ctx: ctx['object'].pk,
+ },
+ ),
+ ]
+
+ context = self.get_context(self.context)
+ panel = panels.ObjectsTablePanel(
+ title=_('Licenses'),
+ model='netbox_lifecycle.licenseassignment',
+ filters={self.field_name: lambda ctx: ctx['object'].pk},
+ include_columns=[
+ 'vendor',
+ 'license',
+ 'quantity',
+ ],
+ exclude_columns=[
+ 'device',
+ 'virtual_machine',
+ 'description',
+ 'comments',
+ 'actions',
+ ],
+ actions=action,
+ )
+ return panel.render(context=context)
- def left_page(self):
- result = ''
- if self.get_lifecycle_card_position() == 'left_page':
- result += self._render_lifecycle_info()
- if self.get_contract_card_position() == 'left_page':
- result += self._render_contract_card()
- if self.get_license_card_position() == 'left_page':
- result += self._render_license_card()
- return result
- def full_width_page(self):
- result = ''
- if self.get_lifecycle_card_position() == 'full_width_page':
- result += self._render_lifecycle_info()
- if self.get_contract_card_position() == 'full_width_page':
- result += self._render_contract_card()
- if self.get_license_card_position() == 'full_width_page':
- result += self._render_license_card()
- return result
+class DeviceContent(
+ ContractMixin, LicenseMixin, LifecycleMixin, BaseMixin, PluginTemplateExtension
+):
+ models = ['dcim.device']
+ lifecycle_content_type = 'devicetype'
+ lifecycle_object_id_attr = 'device_type_id'
-class ModuleLifecycleContent(BaseLifecycleContent):
+class ModuleLifecycleContent(
+ ContractMixin, LifecycleMixin, BaseMixin, PluginTemplateExtension
+):
models = ['dcim.module']
- lifecycle_content_type = 'moduletype'
- lifecycle_object_id_attr = 'module_type_id'
-class DeviceTypeLifecycleContent(BaseLifecycleContent):
+class DeviceTypeLifecycleContent(LifecycleMixin, BaseMixin, PluginTemplateExtension):
models = ['dcim.devicetype']
- lifecycle_content_type = 'devicetype'
- lifecycle_object_id_attr = 'id'
-class ModuleTypeLifecycleContent(BaseLifecycleContent):
+class ModuleTypeLifecycleContent(LifecycleMixin, BaseMixin, PluginTemplateExtension):
models = ['dcim.moduletype']
- lifecycle_content_type = 'moduletype'
- lifecycle_object_id_attr = 'id'
-class VirtualMachineContractContent(PluginTemplateExtension):
+class VirtualMachineContractContent(
+ ContractMixin, LicenseMixin, BaseMixin, PluginTemplateExtension
+):
"""Template extension for VirtualMachine detail pages showing contracts and licenses."""
models = ['virtualization.virtualmachine']
- def get_contract_card_position(self):
- return PLUGIN_SETTINGS.get('contract_card_position', 'right_page')
-
- def get_license_card_position(self):
- return PLUGIN_SETTINGS.get('license_card_position', 'right_page')
-
- def _render_contract_card(self):
- obj = self.context.get('object')
- return self.render(
- 'netbox_lifecycle/inc/contract_card_placeholder.html',
- extra_context={
- 'htmx_url': reverse(
- 'plugins:netbox_lifecycle:virtualmachine_contracts_htmx',
- kwargs={'pk': obj.pk},
- ),
- },
- )
-
- def _render_license_card(self):
- obj = self.context.get('object')
- if not LicenseAssignment.objects.filter(virtual_machine=obj).exists():
- return ''
- return self.render(
- 'netbox_lifecycle/inc/license_card_placeholder.html',
- extra_context={
- 'htmx_url': reverse(
- 'plugins:netbox_lifecycle:virtualmachine_licenses_htmx',
- kwargs={'pk': obj.pk},
- ),
- },
- )
-
- def right_page(self):
- result = ''
- if self.get_contract_card_position() == 'right_page':
- result += self._render_contract_card()
- if self.get_license_card_position() == 'right_page':
- result += self._render_license_card()
- return result
-
- def left_page(self):
- result = ''
- if self.get_contract_card_position() == 'left_page':
- result += self._render_contract_card()
- if self.get_license_card_position() == 'left_page':
- result += self._render_license_card()
- return result
-
- def full_width_page(self):
- result = ''
- if self.get_contract_card_position() == 'full_width_page':
- result += self._render_contract_card()
- if self.get_license_card_position() == 'full_width_page':
- result += self._render_license_card()
- return result
-
template_extensions = (
- DeviceLifecycleContent,
+ DeviceContent,
ModuleLifecycleContent,
DeviceTypeLifecycleContent,
ModuleTypeLifecycleContent,
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/generic/base.html b/netbox_lifecycle/templates/netbox_lifecycle/generic/base.html
deleted file mode 100644
index f59544b..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/generic/base.html
+++ /dev/null
@@ -1 +0,0 @@
-{% extends 'generic/object.html' %}
\ No newline at end of file
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/hardwarelifecycle.html b/netbox_lifecycle/templates/netbox_lifecycle/hardwarelifecycle.html
deleted file mode 100644
index 0fbad41..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/hardwarelifecycle.html
+++ /dev/null
@@ -1,78 +0,0 @@
-{% extends 'generic/object.html' %}
-{% load buttons %}
-{% load custom_links %}
-{% load helpers %}
-{% load netbox_lifecycle_filters %}
-{% load perms %}
-{% load plugins %}
-{% load tabs %}
-
-{% block content %}
-
-
-
-
-
-
-
- | Manufacturer |
- {{ object.assigned_object.manufacturer|linkify }} |
-
-
- | Object |
- {{ object.assigned_object|linkify }} |
-
-
- | Description |
- {{ object.description }} |
-
-
-
-
-
-
-
-
-
- | End of Sale |
- {{ object.end_of_sale|placeholder }} |
-
-
- | End of Maintenance Updates |
- {{ object.end_of_maintenance|placeholder }} |
-
-
- | End of Security Updates |
- {{ object.end_of_security|placeholder }} |
-
-
- | Last Support Contract Attach |
- {{ object.last_contract_attach|placeholder }} |
-
-
- | Last Support Contract Renewal |
- {{ object.last_contract_renewal|placeholder }} |
-
-
- | End of Support |
- {{ object.end_of_support|placeholder }} |
-
-
-
-
- {% plugin_left_page object %}
- {% include 'inc/panels/tags.html' %}
-
-
- {% include 'inc/panels/related_objects.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/htmx/contract_list.html b/netbox_lifecycle/templates/netbox_lifecycle/htmx/contract_list.html
deleted file mode 100644
index e8a3e4f..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/htmx/contract_list.html
+++ /dev/null
@@ -1,23 +0,0 @@
-{% load i18n %}
-{% load netbox_lifecycle_filters %}
-
-
-
-
- | {% trans "Contract" %} |
- {% trans "SKU" %} |
- {% trans "Vendor" %} |
- {% trans "Ended" %} |
-
-
-
- {% for assignment in assignments %}
-
- | {{ assignment.contract.contract_id }} |
- {% if assignment.sku %}{{ assignment.sku.sku }}{% else %}-{% endif %} |
- {{ assignment.contract.vendor|default:"-" }} |
- {{ assignment.end_date|date:"Y-m-d"|default:"-" }} |
-
- {% endfor %}
-
-
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/htmx/device_contracts.html b/netbox_lifecycle/templates/netbox_lifecycle/htmx/device_contracts.html
deleted file mode 100644
index 6c5ec0e..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/htmx/device_contracts.html
+++ /dev/null
@@ -1,130 +0,0 @@
-{% load i18n %}
-{% load netbox_lifecycle_filters %}
-
-
-
- {% if active or future or unspecified or expired_count %}
-
-
-
- {% if active %}
-
-
-
-
- | {% trans "Contract" %} |
- {% trans "SKU" %} |
- {% trans "Vendor" %} |
- {% trans "End Date" %} |
-
-
-
- {% for assignment in active %}
-
- | {{ assignment.contract.contract_id }} |
- {% if assignment.sku %}{{ assignment.sku.sku }}{% else %}-{% endif %} |
- {{ assignment.contract.vendor|default:"-" }} |
- {{ assignment.end_date|date:"Y-m-d" }} |
-
- {% endfor %}
-
-
-
- {% endif %}
- {% if future %}
-
-
-
-
- | {% trans "Contract" %} |
- {% trans "SKU" %} |
- {% trans "Vendor" %} |
- {% trans "Starts" %} |
- {% trans "Ends" %} |
-
-
-
- {% for assignment in future %}
-
- | {{ assignment.contract.contract_id }} |
- {% if assignment.sku %}{{ assignment.sku.sku }}{% else %}-{% endif %} |
- {{ assignment.contract.vendor|default:"-" }} |
- {{ assignment.contract.start|date:"Y-m-d" }} |
- {{ assignment.end_date|date:"Y-m-d" }} |
-
- {% endfor %}
-
-
-
- {% endif %}
- {% if unspecified %}
-
-
-
-
- | {% trans "Contract" %} |
- {% trans "SKU" %} |
- {% trans "Vendor" %} |
- {% trans "Started" %} |
-
-
-
- {% for assignment in unspecified %}
-
- | {{ assignment.contract.contract_id }} |
- {% if assignment.sku %}{{ assignment.sku.sku }}{% else %}-{% endif %} |
- {{ assignment.contract.vendor|default:"-" }} |
- {{ assignment.contract.start|date:"Y-m-d"|default:"-" }} |
-
- {% endfor %}
-
-
-
- {% endif %}
- {% if expired_count %}
-
- {% endif %}
-
-
- {% else %}
-
- {% trans "None" %}
-
- {% endif %}
-
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/htmx/device_licenses.html b/netbox_lifecycle/templates/netbox_lifecycle/htmx/device_licenses.html
deleted file mode 100644
index 6150c84..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/htmx/device_licenses.html
+++ /dev/null
@@ -1,47 +0,0 @@
-{% load i18n %}
-
-
-
- {% if assignments %}
-
-
-
-
- | {% trans "License" %} |
- {% trans "Manufacturer" %} |
- {% trans "Vendor" %} |
- {% trans "Quantity" %} |
- {% trans "Description" %} |
-
-
-
- {% for assignment in assignments %}
-
- | {{ assignment.license.name }} |
- {{ assignment.license.manufacturer|default:"-" }} |
- {{ assignment.vendor|default:"-" }} |
- {{ assignment.quantity|default:"-" }} |
- {{ assignment.description|default:"-" }} |
-
- {% endfor %}
-
-
- {% if show_all_url %}
-
- {% endif %}
-
- {% else %}
-
- {% trans "None" %}
-
- {% endif %}
-
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/htmx/virtualmachine_contracts.html b/netbox_lifecycle/templates/netbox_lifecycle/htmx/virtualmachine_contracts.html
deleted file mode 100644
index 9d94a89..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/htmx/virtualmachine_contracts.html
+++ /dev/null
@@ -1,130 +0,0 @@
-{% load i18n %}
-{% load netbox_lifecycle_filters %}
-
-
-
- {% if active or future or unspecified or expired_count %}
-
-
-
- {% if active %}
-
-
-
-
- | {% trans "Contract" %} |
- {% trans "SKU" %} |
- {% trans "Vendor" %} |
- {% trans "End Date" %} |
-
-
-
- {% for assignment in active %}
-
- | {{ assignment.contract.contract_id }} |
- {% if assignment.sku %}{{ assignment.sku.sku }}{% else %}-{% endif %} |
- {{ assignment.contract.vendor|default:"-" }} |
- {{ assignment.end_date|date:"Y-m-d" }} |
-
- {% endfor %}
-
-
-
- {% endif %}
- {% if future %}
-
-
-
-
- | {% trans "Contract" %} |
- {% trans "SKU" %} |
- {% trans "Vendor" %} |
- {% trans "Starts" %} |
- {% trans "Ends" %} |
-
-
-
- {% for assignment in future %}
-
- | {{ assignment.contract.contract_id }} |
- {% if assignment.sku %}{{ assignment.sku.sku }}{% else %}-{% endif %} |
- {{ assignment.contract.vendor|default:"-" }} |
- {{ assignment.contract.start|date:"Y-m-d" }} |
- {{ assignment.end_date|date:"Y-m-d" }} |
-
- {% endfor %}
-
-
-
- {% endif %}
- {% if unspecified %}
-
-
-
-
- | {% trans "Contract" %} |
- {% trans "SKU" %} |
- {% trans "Vendor" %} |
- {% trans "Started" %} |
-
-
-
- {% for assignment in unspecified %}
-
- | {{ assignment.contract.contract_id }} |
- {% if assignment.sku %}{{ assignment.sku.sku }}{% else %}-{% endif %} |
- {{ assignment.contract.vendor|default:"-" }} |
- {{ assignment.contract.start|date:"Y-m-d"|default:"-" }} |
-
- {% endfor %}
-
-
-
- {% endif %}
- {% if expired_count %}
-
- {% endif %}
-
-
- {% else %}
-
- {% trans "None" %}
-
- {% endif %}
-
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/htmx/virtualmachine_licenses.html b/netbox_lifecycle/templates/netbox_lifecycle/htmx/virtualmachine_licenses.html
deleted file mode 100644
index 4d9f549..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/htmx/virtualmachine_licenses.html
+++ /dev/null
@@ -1,47 +0,0 @@
-{% load i18n %}
-
-
-
- {% if assignments %}
-
-
-
-
- | {% trans "License" %} |
- {% trans "Manufacturer" %} |
- {% trans "Vendor" %} |
- {% trans "Quantity" %} |
- {% trans "Description" %} |
-
-
-
- {% for assignment in assignments %}
-
- | {{ assignment.license.name }} |
- {{ assignment.license.manufacturer|default:"-" }} |
- {{ assignment.vendor|default:"-" }} |
- {{ assignment.quantity|default:"-" }} |
- {{ assignment.description|default:"-" }} |
-
- {% endfor %}
-
-
- {% if show_all_url %}
-
- {% endif %}
-
- {% else %}
-
- {% trans "None" %}
-
- {% endif %}
-
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/inc/contract_card_placeholder.html b/netbox_lifecycle/templates/netbox_lifecycle/inc/contract_card_placeholder.html
deleted file mode 100644
index 7383d35..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/inc/contract_card_placeholder.html
+++ /dev/null
@@ -1,12 +0,0 @@
-{% load i18n %}
-
-
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/inc/hardware_lifecycle_info.html b/netbox_lifecycle/templates/netbox_lifecycle/inc/hardware_lifecycle_info.html
deleted file mode 100644
index 0f1addc..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/inc/hardware_lifecycle_info.html
+++ /dev/null
@@ -1,43 +0,0 @@
-
-{% load netbox_lifecycle_filters %}
-{% load helpers %}
-{# renders panel on object with lifecycle info assigned to it #}
-
-
-
-
-
- | End of Sale |
- {{ lifecycle_info.end_of_sale|placeholder }} |
-
-
- | End of Maintenance Updates |
- {{ lifecycle_info.end_of_maintenance|placeholder }} |
-
-
- | End of Security Updates |
- {{ lifecycle_info.end_of_security|placeholder }} |
-
-
- | Last Support Contract Attach |
- {{ lifecycle_info.last_contract_attach|placeholder }} |
-
-
- | Last Support Contract Renewal |
- {{ lifecycle_info.last_contract_renewal|placeholder }} |
-
-
- | End of Support |
- {{ lifecycle_info.end_of_support|placeholder }} |
-
-
- {% else %}
-
-
No Lifecycle Dates Defined
- {% endif %}
-
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/inc/license_card_placeholder.html b/netbox_lifecycle/templates/netbox_lifecycle/inc/license_card_placeholder.html
deleted file mode 100644
index eae75b8..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/inc/license_card_placeholder.html
+++ /dev/null
@@ -1,12 +0,0 @@
-{% load i18n %}
-
-
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/inc/support_contract_info.html b/netbox_lifecycle/templates/netbox_lifecycle/inc/support_contract_info.html
deleted file mode 100644
index 6ee8570..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/inc/support_contract_info.html
+++ /dev/null
@@ -1,45 +0,0 @@
-
-{% load netbox_lifecycle_filters %}
-{% load helpers %}
-{# renders panel on object (device) with support contract info assigned to it #}
-
-
-
-
-
- | Vendor |
- {{ support_contract.contract.vendor|linkify|placeholder }} |
-
-
- | Contract Number |
- {{ support_contract.contract|linkify:"contract_id"|placeholder }} |
-
-
- | Support SKU |
- {{ support_contract.sku|linkify|placeholder }} |
-
-
- | Start Date |
- {{ support_contract.contract.start }} |
-
-
- | End Date |
- {% if support_contract.end == None %}
- {{ support_contract.contract.end }} |
- {% else %}
- {{ support_contract.end }} |
- {% endif %}
-
-
- {% else %}
-
-
No Support Contract Assigned
- {% endif %}
-
-
-{% include "netbox_lifecycle/inc/hardware_lifecycle_info.html" %}
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/license.html b/netbox_lifecycle/templates/netbox_lifecycle/license.html
deleted file mode 100644
index b6304f6..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/license.html
+++ /dev/null
@@ -1,46 +0,0 @@
-{% extends 'generic/object.html' %}
-{% load buttons %}
-{% load custom_links %}
-{% load helpers %}
-{% load perms %}
-{% load plugins %}
-{% load tabs %}
-
-{% block content %}
-
-
-
-
-
-
-
- | Manufacturer |
- {{ object.manufacturer|linkify }} |
-
-
- | Name |
- {{ object.name }} |
-
-
- | Description |
- {{ object.description }} |
-
-
-
-
- {% plugin_left_page object %}
- {% include 'inc/panels/tags.html' %}
-
-
- {% include 'inc/panels/related_objects.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/license/assignments.html b/netbox_lifecycle/templates/netbox_lifecycle/license/assignments.html
deleted file mode 100644
index 99ea27d..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/license/assignments.html
+++ /dev/null
@@ -1,49 +0,0 @@
-{% extends 'netbox_lifecycle/generic/base.html' %}
-{% load render_table from django_tables2 %}
-{% load helpers %}
-{% load static %}
-
-{% block content %}
- {% include 'inc/table_controls_htmx.html' with table_modal="LicenseAssignment_config" %}
-
-
-{% endblock %}
-
-{% block modals %}
- {{ block.super }}
- {% table_config_form table %}
-{% endblock modals %}
\ No newline at end of file
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/licenseassignment.html b/netbox_lifecycle/templates/netbox_lifecycle/licenseassignment.html
deleted file mode 100644
index 934e651..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/licenseassignment.html
+++ /dev/null
@@ -1,61 +0,0 @@
-{% extends 'generic/object.html' %}
-{% load buttons %}
-{% load custom_links %}
-{% load helpers %}
-{% load perms %}
-{% load plugins %}
-{% load tabs %}
-
-{% block content %}
-
-
-
-
-
-
-
- | License |
- {{ object.license|linkify }} |
-
-
- | Vendor |
- {{ object.vendor|linkify }} |
-
- {% if object.device %}
-
- | Device |
- {{ object.device|linkify }} |
-
- {% elif object.virtual_machine %}
-
- | Virtual Machine |
- {{ object.virtual_machine|linkify }} |
-
- {% endif %}
-
- | Quantity |
- {{ object.quantity }} |
-
-
- | Description |
- {{ object.description }} |
-
-
-
-
- {% plugin_left_page object %}
- {% include 'inc/panels/tags.html' %}
-
-
- {% include 'inc/panels/related_objects.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/supportcontract.html b/netbox_lifecycle/templates/netbox_lifecycle/supportcontract.html
deleted file mode 100644
index 6325d3d..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/supportcontract.html
+++ /dev/null
@@ -1,78 +0,0 @@
-{% extends 'generic/object.html' %}
-{% load buttons %}
-{% load custom_links %}
-{% load helpers %}
-{% load perms %}
-{% load plugins %}
-{% load tabs %}
-
-{% if perms.netbox_lifecycle.add_supportcontractassignment %}
- {% block extra_controls %}
-
- Add Assignment
-
- {% endblock %}
-{% endif %}
-
-{% block content %}
-
-
-
-
-
-
-
- | Manufacturer |
- {{ object.manufacturer|linkify|placeholder }} |
-
-
- | Vendor |
- {{ object.vendor|linkify|placeholder }} |
-
-
- | Contract ID |
- {{ object.contract_id }} |
-
-
- | Description |
- {{ object.description }} |
-
-
-
-
-
-
-
-
-
- | Start |
- {{ object.start }} |
-
-
- | Last renewal |
- {{ object.renewal }} |
-
-
- | End |
- {{ object.end }} |
-
-
-
-
- {% plugin_left_page object %}
- {% include 'inc/panels/tags.html' %}
-
-
- {% include 'inc/panels/related_objects.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/supportcontract/assignments.html b/netbox_lifecycle/templates/netbox_lifecycle/supportcontract/assignments.html
deleted file mode 100644
index f9942f8..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/supportcontract/assignments.html
+++ /dev/null
@@ -1,49 +0,0 @@
-{% extends 'netbox_lifecycle/generic/base.html' %}
-{% load render_table from django_tables2 %}
-{% load helpers %}
-{% load static %}
-
-{% block content %}
- {% include 'inc/table_controls_htmx.html' with table_modal="ObjectTable_config" %}
-
-
-{% endblock %}
-
-{% block modals %}
- {{ block.super }}
- {% table_config_form table table_name="ObjectTable" %}
-{% endblock modals %}
\ No newline at end of file
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/supportcontractassignment.html b/netbox_lifecycle/templates/netbox_lifecycle/supportcontractassignment.html
deleted file mode 100644
index bb1c2f8..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/supportcontractassignment.html
+++ /dev/null
@@ -1,74 +0,0 @@
-{% extends 'generic/object.html' %}
-{% load buttons %}
-{% load custom_links %}
-{% load helpers %}
-{% load perms %}
-{% load plugins %}
-{% load tabs %}
-
-{% block content %}
-
-
-
-
-
-
-
- | Contract |
- {{ object.contract|linkify }} |
-
-
- | SKU |
- {{ object.sku|linkify }} |
-
- {% if object.device %}
-
- | Device |
- {{ object.device|linkify }} |
-
- {% endif %}
- {% if object.module %}
-
- | Module |
- {{ object.module|linkify }} |
-
- {% endif %}
- {% if object.virtual_machine %}
-
- | Virtual Machine |
- {{ object.virtual_machine|linkify }} |
-
- {% endif %}
- {% if object.license %}
-
- | License |
- {{ object.license|linkify }} |
-
- {% endif %}
-
- | End Date |
- {{ object.end }} |
-
-
- | Description |
- {{ object.description }} |
-
-
-
-
- {% plugin_left_page object %}
- {% include 'inc/panels/tags.html' %}
-
-
- {% include 'inc/panels/related_objects.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/supportsku.html b/netbox_lifecycle/templates/netbox_lifecycle/supportsku.html
deleted file mode 100644
index ef4fd16..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/supportsku.html
+++ /dev/null
@@ -1,46 +0,0 @@
-{% extends 'generic/object.html' %}
-{% load buttons %}
-{% load custom_links %}
-{% load helpers %}
-{% load perms %}
-{% load plugins %}
-{% load tabs %}
-
-{% block content %}
-
-
-
-
-
-
-
- | Manufacturer |
- {{ object.manufacturer|linkify }} |
-
-
- | SKU |
- {{ object.sku }} |
-
-
- | Description |
- {{ object.description }} |
-
-
-
-
- {% plugin_left_page object %}
- {% include 'inc/panels/tags.html' %}
-
-
- {% include 'inc/panels/related_objects.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/ui/panels/tabbed_table.html b/netbox_lifecycle/templates/netbox_lifecycle/ui/panels/tabbed_table.html
new file mode 100644
index 0000000..41fe313
--- /dev/null
+++ b/netbox_lifecycle/templates/netbox_lifecycle/ui/panels/tabbed_table.html
@@ -0,0 +1,39 @@
+{% extends "ui/panels/_base.html" %}
+{% load i18n %}
+
+{% block panel_content %}
+
+
+ {% for tab in tabs %}
+ {% if tab.active %}
+
+ {% else %}
+
+ {% endif %}
+ {% endfor %}
+
+{% endblock panel_content %}
\ No newline at end of file
diff --git a/netbox_lifecycle/templates/netbox_lifecycle/vendor.html b/netbox_lifecycle/templates/netbox_lifecycle/vendor.html
deleted file mode 100644
index de4c8bd..0000000
--- a/netbox_lifecycle/templates/netbox_lifecycle/vendor.html
+++ /dev/null
@@ -1,42 +0,0 @@
-{% extends 'generic/object.html' %}
-{% load buttons %}
-{% load custom_links %}
-{% load helpers %}
-{% load perms %}
-{% load plugins %}
-{% load tabs %}
-
-{% block content %}
-
-
-
-
-
-
-
- | Name |
- {{ object.name }} |
-
-
- | Description |
- {{ object.description }} |
-
-
-
-
- {% plugin_left_page object %}
- {% include 'inc/panels/tags.html' %}
-
-
- {% include 'inc/panels/related_objects.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox_lifecycle/templatetags/__init__.py b/netbox_lifecycle/templatetags/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/netbox_lifecycle/templatetags/netbox_lifecycle_filters.py b/netbox_lifecycle/templatetags/netbox_lifecycle_filters.py
deleted file mode 100644
index 44855d5..0000000
--- a/netbox_lifecycle/templatetags/netbox_lifecycle_filters.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from datetime import datetime, timezone
-
-from dateutil.relativedelta import relativedelta
-from django import template
-from django.utils.safestring import mark_safe
-
-register = template.Library()
-
-
-def is_expired(value):
- return value < datetime.now(tz=timezone.utc).date()
-
-
-def expires_within_six_months(value):
- return value < (datetime.now(tz=timezone.utc).date() + relativedelta(months=+6))
-
-
-@register.filter(is_safe=True)
-def date_badge_class(value):
- if not value:
- return
-
- if is_expired(value):
- return mark_safe('class="badge text-bg-danger"')
- elif expires_within_six_months(value):
- return mark_safe('class="badge text-bg-warning"')
- else:
- return mark_safe('class="badge text-bg-success"')
-
-
-@register.filter(is_safe=True)
-def contract_status_badge(status):
- from netbox_lifecycle.constants import CONTRACT_STATUS_COLOR
-
- if not status or status not in CONTRACT_STATUS_COLOR:
- return ''
- label, color = CONTRACT_STATUS_COLOR[status]
- return mark_safe(f'{label}')
diff --git a/netbox_lifecycle/tests/test_htmx_views.py b/netbox_lifecycle/tests/test_htmx_views.py
deleted file mode 100644
index 6b67a56..0000000
--- a/netbox_lifecycle/tests/test_htmx_views.py
+++ /dev/null
@@ -1,511 +0,0 @@
-from datetime import datetime, timedelta, timezone
-
-from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
-from django.contrib.auth import get_user_model
-from django.test import TestCase
-from django.urls import reverse
-from virtualization.models import Cluster, ClusterType, VirtualMachine
-
-from netbox_lifecycle.models import (
- License,
- LicenseAssignment,
- SupportContract,
- SupportContractAssignment,
- SupportSKU,
- Vendor,
-)
-from netbox_lifecycle.views.htmx import MAX_LICENSE_DISPLAY
-
-User = get_user_model()
-
-
-class DeviceContractsHTMXViewTest(TestCase):
- @classmethod
- def setUpTestData(cls):
- cls.manufacturer = Manufacturer.objects.create(
- name='Test Manufacturer', slug='test-manufacturer'
- )
- device_type = DeviceType.objects.create(
- manufacturer=cls.manufacturer, model='Test Model', slug='test-model'
- )
- device_role = DeviceRole.objects.create(name='Test Role', slug='test-role')
- site = Site.objects.create(name='Test Site', slug='test-site')
- cls.device = Device.objects.create(
- name='Test Device',
- device_type=device_type,
- role=device_role,
- site=site,
- )
- cls.vendor = Vendor.objects.create(name='Test Vendor')
- cls.user = User.objects.create_user(username='testuser', password='testpass')
-
- def test_device_contracts_htmx_view(self):
- contract = SupportContract.objects.create(
- vendor=self.vendor,
- contract_id='TEST-001',
- end=datetime.now(tz=timezone.utc).date() + timedelta(days=30),
- )
- SupportContractAssignment.objects.create(contract=contract, device=self.device)
-
- self.client.force_login(self.user)
- url = reverse(
- 'plugins:netbox_lifecycle:device_contracts_htmx',
- kwargs={'pk': self.device.pk},
- )
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'TEST-001')
- self.assertContains(response, 'Active')
-
- def test_device_contracts_expired_htmx_view(self):
- contract = SupportContract.objects.create(
- vendor=self.vendor,
- contract_id='EXPIRED-001',
- end=datetime.now(tz=timezone.utc).date() - timedelta(days=1),
- )
- SupportContractAssignment.objects.create(contract=contract, device=self.device)
-
- self.client.force_login(self.user)
- url = reverse(
- 'plugins:netbox_lifecycle:device_contracts_expired',
- kwargs={'pk': self.device.pk},
- )
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'EXPIRED-001')
-
- def test_device_contracts_requires_login(self):
- url = reverse(
- 'plugins:netbox_lifecycle:device_contracts_htmx',
- kwargs={'pk': self.device.pk},
- )
- response = self.client.get(url)
- self.assertEqual(response.status_code, 302) # Redirect to login
-
- def test_device_contracts_with_sku(self):
- """Test that SKU is displayed in the contract list."""
- sku = SupportSKU.objects.create(
- manufacturer=self.manufacturer,
- sku='SKU-PREMIUM-24x7',
- )
- contract = SupportContract.objects.create(
- vendor=self.vendor,
- contract_id='TEST-SKU-001',
- end=datetime.now(tz=timezone.utc).date() + timedelta(days=30),
- )
- SupportContractAssignment.objects.create(
- contract=contract, device=self.device, sku=sku
- )
-
- self.client.force_login(self.user)
- url = reverse(
- 'plugins:netbox_lifecycle:device_contracts_htmx',
- kwargs={'pk': self.device.pk},
- )
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'TEST-SKU-001')
- self.assertContains(response, 'SKU-PREMIUM-24x7')
-
- def test_device_contracts_future(self):
- """Test that future contracts are displayed in the Future tab."""
- contract = SupportContract.objects.create(
- vendor=self.vendor,
- contract_id='FUTURE-001',
- start=datetime.now(tz=timezone.utc).date() + timedelta(days=30),
- end=datetime.now(tz=timezone.utc).date() + timedelta(days=365),
- )
- SupportContractAssignment.objects.create(contract=contract, device=self.device)
-
- self.client.force_login(self.user)
- url = reverse(
- 'plugins:netbox_lifecycle:device_contracts_htmx',
- kwargs={'pk': self.device.pk},
- )
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'FUTURE-001')
- self.assertContains(response, 'Future')
-
- def test_device_contracts_unspecified(self):
- """Test that contracts without end date show in Unspecified tab."""
- contract = SupportContract.objects.create(
- vendor=self.vendor,
- contract_id='UNSPEC-001',
- start=datetime.now(tz=timezone.utc).date() - timedelta(days=30),
- end=None,
- )
- SupportContractAssignment.objects.create(contract=contract, device=self.device)
-
- self.client.force_login(self.user)
- url = reverse(
- 'plugins:netbox_lifecycle:device_contracts_htmx',
- kwargs={'pk': self.device.pk},
- )
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'UNSPEC-001')
- self.assertContains(response, 'Unspecified')
-
- def test_device_contracts_no_contracts(self):
- """Test empty state when device has no contracts."""
- self.client.force_login(self.user)
- url = reverse(
- 'plugins:netbox_lifecycle:device_contracts_htmx',
- kwargs={'pk': self.device.pk},
- )
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'None')
-
- def test_device_contracts_multiple_statuses(self):
- """Test that multiple contract statuses are grouped correctly."""
- # Active contract
- active_contract = SupportContract.objects.create(
- vendor=self.vendor,
- contract_id='ACTIVE-MULTI',
- end=datetime.now(tz=timezone.utc).date() + timedelta(days=30),
- )
- SupportContractAssignment.objects.create(
- contract=active_contract, device=self.device
- )
-
- # Future contract
- future_contract = SupportContract.objects.create(
- vendor=self.vendor,
- contract_id='FUTURE-MULTI',
- start=datetime.now(tz=timezone.utc).date() + timedelta(days=30),
- end=datetime.now(tz=timezone.utc).date() + timedelta(days=365),
- )
- SupportContractAssignment.objects.create(
- contract=future_contract, device=self.device
- )
-
- # Expired contract
- expired_contract = SupportContract.objects.create(
- vendor=self.vendor,
- contract_id='EXPIRED-MULTI',
- end=datetime.now(tz=timezone.utc).date() - timedelta(days=1),
- )
- SupportContractAssignment.objects.create(
- contract=expired_contract, device=self.device
- )
-
- self.client.force_login(self.user)
- url = reverse(
- 'plugins:netbox_lifecycle:device_contracts_htmx',
- kwargs={'pk': self.device.pk},
- )
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, 200)
- # Check all tabs are present
- self.assertContains(response, 'Active')
- self.assertContains(response, 'Future')
- self.assertContains(response, 'Expired')
- # Check contracts
- self.assertContains(response, 'ACTIVE-MULTI')
- self.assertContains(response, 'FUTURE-MULTI')
- # Expired is lazy-loaded, so contract ID won't be in initial response
-
- def test_device_contracts_with_license(self):
- """Test contract assignment with license is displayed."""
- license_obj = License.objects.create(
- manufacturer=self.manufacturer,
- name='Enterprise License',
- )
- license_assignment = LicenseAssignment.objects.create(
- license=license_obj,
- vendor=self.vendor,
- device=self.device,
- quantity=1,
- )
- contract = SupportContract.objects.create(
- vendor=self.vendor,
- contract_id='LICENSE-001',
- end=datetime.now(tz=timezone.utc).date() + timedelta(days=30),
- )
- SupportContractAssignment.objects.create(
- contract=contract,
- device=self.device,
- license=license_assignment,
- )
-
- self.client.force_login(self.user)
- url = reverse(
- 'plugins:netbox_lifecycle:device_contracts_htmx',
- kwargs={'pk': self.device.pk},
- )
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'LICENSE-001')
-
- def test_expired_contracts_requires_login(self):
- """Test that expired contracts endpoint requires authentication."""
- url = reverse(
- 'plugins:netbox_lifecycle:device_contracts_expired',
- kwargs={'pk': self.device.pk},
- )
- response = self.client.get(url)
- self.assertEqual(response.status_code, 302) # Redirect to login
-
- def test_expired_contracts_with_sku(self):
- """Test that expired contracts show SKU."""
- sku = SupportSKU.objects.create(
- manufacturer=self.manufacturer,
- sku='SKU-EXPIRED-SUPPORT',
- )
- contract = SupportContract.objects.create(
- vendor=self.vendor,
- contract_id='EXPIRED-SKU-001',
- end=datetime.now(tz=timezone.utc).date() - timedelta(days=1),
- )
- SupportContractAssignment.objects.create(
- contract=contract, device=self.device, sku=sku
- )
-
- self.client.force_login(self.user)
- url = reverse(
- 'plugins:netbox_lifecycle:device_contracts_expired',
- kwargs={'pk': self.device.pk},
- )
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'EXPIRED-SKU-001')
- self.assertContains(response, 'SKU-EXPIRED-SUPPORT')
-
-
-class DeviceLicensesHTMXViewTest(TestCase):
- @classmethod
- def setUpTestData(cls):
- cls.manufacturer = Manufacturer.objects.create(
- name='License Manufacturer', slug='license-manufacturer'
- )
- device_type = DeviceType.objects.create(
- manufacturer=cls.manufacturer, model='License Model', slug='license-model'
- )
- device_role = DeviceRole.objects.create(
- name='License Role', slug='license-role'
- )
- site = Site.objects.create(name='License Site', slug='license-site')
- cls.device = Device.objects.create(
- name='License Device',
- device_type=device_type,
- role=device_role,
- site=site,
- )
- cls.vendor = Vendor.objects.create(name='License Vendor')
- cls.license = License.objects.create(
- manufacturer=cls.manufacturer,
- name='Test License',
- )
- cls.user = User.objects.create_user(username='licenseuser', password='testpass')
-
- def test_device_licenses_requires_login(self):
- url = reverse(
- 'plugins:netbox_lifecycle:device_licenses_htmx',
- kwargs={'pk': self.device.pk},
- )
- response = self.client.get(url)
- self.assertEqual(response.status_code, 302)
-
- def test_device_licenses_empty(self):
- self.client.force_login(self.user)
- url = reverse(
- 'plugins:netbox_lifecycle:device_licenses_htmx',
- kwargs={'pk': self.device.pk},
- )
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'None')
-
- def test_device_licenses_with_assignment(self):
- LicenseAssignment.objects.create(
- license=self.license,
- vendor=self.vendor,
- device=self.device,
- quantity=5,
- )
-
- self.client.force_login(self.user)
- url = reverse(
- 'plugins:netbox_lifecycle:device_licenses_htmx',
- kwargs={'pk': self.device.pk},
- )
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'Test License')
- self.assertContains(response, 'License Manufacturer')
- self.assertContains(response, 'License Vendor')
- self.assertContains(response, '5')
-
- def test_device_licenses_assign_button(self):
- LicenseAssignment.objects.create(
- license=self.license,
- vendor=self.vendor,
- device=self.device,
- )
-
- self.client.force_login(self.user)
- url = reverse(
- 'plugins:netbox_lifecycle:device_licenses_htmx',
- kwargs={'pk': self.device.pk},
- )
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'Assign License')
- self.assertContains(response, f'device={self.device.pk}')
-
- def test_device_licenses_show_all_link(self):
- for i in range(MAX_LICENSE_DISPLAY + 1):
- lic = License.objects.create(
- manufacturer=self.manufacturer,
- name=f'Bulk License {i}',
- )
- LicenseAssignment.objects.create(
- license=lic,
- vendor=self.vendor,
- device=self.device,
- )
-
- self.client.force_login(self.user)
- url = reverse(
- 'plugins:netbox_lifecycle:device_licenses_htmx',
- kwargs={'pk': self.device.pk},
- )
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'Show all Licenses')
-
- def test_device_licenses_no_show_all_when_under_limit(self):
- LicenseAssignment.objects.create(
- license=self.license,
- vendor=self.vendor,
- device=self.device,
- )
-
- self.client.force_login(self.user)
- url = reverse(
- 'plugins:netbox_lifecycle:device_licenses_htmx',
- kwargs={'pk': self.device.pk},
- )
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, 200)
- self.assertNotContains(response, 'Show all Licenses')
-
-
-class VirtualMachineLicensesHTMXViewTest(TestCase):
- @classmethod
- def setUpTestData(cls):
- cls.manufacturer = Manufacturer.objects.create(
- name='VM License Mfg', slug='vm-license-mfg'
- )
- cluster_type = ClusterType.objects.create(
- name='VM Cluster Type', slug='vm-cluster-type'
- )
- cluster = Cluster.objects.create(name='VM Cluster', type=cluster_type)
- cls.virtual_machine = VirtualMachine.objects.create(
- name='License VM',
- cluster=cluster,
- )
- cls.vendor = Vendor.objects.create(name='VM License Vendor')
- cls.license = License.objects.create(
- manufacturer=cls.manufacturer,
- name='VM Test License',
- )
- cls.user = User.objects.create_user(
- username='vmlicenseuser', password='testpass'
- )
-
- def test_vm_licenses_requires_login(self):
- url = reverse(
- 'plugins:netbox_lifecycle:virtualmachine_licenses_htmx',
- kwargs={'pk': self.virtual_machine.pk},
- )
- response = self.client.get(url)
- self.assertEqual(response.status_code, 302)
-
- def test_vm_licenses_empty(self):
- self.client.force_login(self.user)
- url = reverse(
- 'plugins:netbox_lifecycle:virtualmachine_licenses_htmx',
- kwargs={'pk': self.virtual_machine.pk},
- )
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'None')
-
- def test_vm_licenses_with_assignment(self):
- LicenseAssignment.objects.create(
- license=self.license,
- vendor=self.vendor,
- virtual_machine=self.virtual_machine,
- quantity=10,
- )
-
- self.client.force_login(self.user)
- url = reverse(
- 'plugins:netbox_lifecycle:virtualmachine_licenses_htmx',
- kwargs={'pk': self.virtual_machine.pk},
- )
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'VM Test License')
- self.assertContains(response, 'VM License Mfg')
- self.assertContains(response, 'VM License Vendor')
- self.assertContains(response, '10')
-
- def test_vm_licenses_assign_button(self):
- LicenseAssignment.objects.create(
- license=self.license,
- vendor=self.vendor,
- virtual_machine=self.virtual_machine,
- )
-
- self.client.force_login(self.user)
- url = reverse(
- 'plugins:netbox_lifecycle:virtualmachine_licenses_htmx',
- kwargs={'pk': self.virtual_machine.pk},
- )
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'Assign License')
- self.assertContains(response, f'virtual_machine={self.virtual_machine.pk}')
-
- def test_vm_licenses_show_all_link(self):
- for i in range(MAX_LICENSE_DISPLAY + 1):
- lic = License.objects.create(
- manufacturer=self.manufacturer,
- name=f'VM Bulk License {i}',
- )
- LicenseAssignment.objects.create(
- license=lic,
- vendor=self.vendor,
- virtual_machine=self.virtual_machine,
- )
-
- self.client.force_login(self.user)
- url = reverse(
- 'plugins:netbox_lifecycle:virtualmachine_licenses_htmx',
- kwargs={'pk': self.virtual_machine.pk},
- )
- response = self.client.get(url)
-
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'Show all Licenses')
diff --git a/netbox_lifecycle/tests/test_templatetags.py b/netbox_lifecycle/tests/test_templatetags.py
deleted file mode 100644
index baf26f2..0000000
--- a/netbox_lifecycle/tests/test_templatetags.py
+++ /dev/null
@@ -1,39 +0,0 @@
-from django.test import TestCase
-
-from netbox_lifecycle.constants import (
- CONTRACT_STATUS_ACTIVE,
- CONTRACT_STATUS_EXPIRED,
- CONTRACT_STATUS_FUTURE,
- CONTRACT_STATUS_UNSPECIFIED,
-)
-from netbox_lifecycle.templatetags.netbox_lifecycle_filters import contract_status_badge
-
-
-class ContractStatusBadgeFilterTest(TestCase):
- def test_active_status_badge(self):
- result = contract_status_badge(CONTRACT_STATUS_ACTIVE)
- self.assertIn('text-bg-success', result)
- self.assertIn('Active', result)
-
- def test_future_status_badge(self):
- result = contract_status_badge(CONTRACT_STATUS_FUTURE)
- self.assertIn('text-bg-info', result)
- self.assertIn('Future', result)
-
- def test_unspecified_status_badge(self):
- result = contract_status_badge(CONTRACT_STATUS_UNSPECIFIED)
- self.assertIn('text-bg-secondary', result)
- self.assertIn('Unspecified', result)
-
- def test_expired_status_badge(self):
- result = contract_status_badge(CONTRACT_STATUS_EXPIRED)
- self.assertIn('text-bg-danger', result)
- self.assertIn('Expired', result)
-
- def test_invalid_status_returns_empty(self):
- result = contract_status_badge('invalid')
- self.assertEqual(result, '')
-
- def test_none_status_returns_empty(self):
- result = contract_status_badge(None)
- self.assertEqual(result, '')
diff --git a/netbox_lifecycle/ui/__init__.py b/netbox_lifecycle/ui/__init__.py
new file mode 100644
index 0000000..e8fe246
--- /dev/null
+++ b/netbox_lifecycle/ui/__init__.py
@@ -0,0 +1,18 @@
+from .panels import *
+from .attributes import *
+
+__all__ = (
+ 'ColoredDateTimeAttr',
+ 'VendorPanel',
+ 'SupportSKUPanel',
+ 'SupportContractPanel',
+ 'SupportContractDatesPanel',
+ 'SupportContractAssignmentPanel',
+ 'SupportContractAssignmentDevicePanel',
+ 'SupportContractAssignmentVMPanel',
+ 'SupportContractAssignmentLicensePanel',
+ 'LicensePanel',
+ 'LicenseAssignmentPanel',
+ 'HardwareLifecyclePanel',
+ 'HardwareLifecycleDatesPanel',
+)
diff --git a/netbox_lifecycle/ui/attributes/__init__.py b/netbox_lifecycle/ui/attributes/__init__.py
new file mode 100644
index 0000000..656a66f
--- /dev/null
+++ b/netbox_lifecycle/ui/attributes/__init__.py
@@ -0,0 +1,3 @@
+from .datetime import *
+
+__all__ = ('ColoredDateTimeAttr',)
diff --git a/netbox_lifecycle/ui/attributes/datetime.py b/netbox_lifecycle/ui/attributes/datetime.py
new file mode 100644
index 0000000..3a7ffd6
--- /dev/null
+++ b/netbox_lifecycle/ui/attributes/datetime.py
@@ -0,0 +1,35 @@
+import datetime
+
+from django.utils.safestring import mark_safe
+
+from netbox.ui.attrs import DateTimeAttr
+
+__all__ = ('ColoredDateTimeAttr',)
+
+
+class ColoredDateTimeAttr(DateTimeAttr):
+ """
+ A DateTimeAttr that applies a badge to the value when rendered.
+ """
+
+ def get_class(self, value):
+ if datetime.datetime.now(tz=datetime.timezone.utc).date() > value:
+ return 'text-bg-danger'
+ elif (
+ datetime.datetime.now(tz=datetime.timezone.utc).date()
+ + datetime.timedelta(days=183)
+ > value
+ ):
+ return 'text-bg-warning'
+ else:
+ return 'badge-neutral'
+ return ''
+
+ def render(self, obj, context):
+ value = self.get_value(obj)
+ rendered = super().render(obj, context)
+ if value:
+ rendered = mark_safe(
+ f'{rendered}'
+ )
+ return rendered
diff --git a/netbox_lifecycle/ui/panels/__init__.py b/netbox_lifecycle/ui/panels/__init__.py
new file mode 100644
index 0000000..be61e38
--- /dev/null
+++ b/netbox_lifecycle/ui/panels/__init__.py
@@ -0,0 +1,18 @@
+from .contract import *
+from .license import *
+from .hardware import *
+
+__all__ = (
+ 'VendorPanel',
+ 'SupportSKUPanel',
+ 'SupportContractPanel',
+ 'SupportContractDatesPanel',
+ 'SupportContractAssignmentPanel',
+ 'SupportContractAssignmentDevicePanel',
+ 'SupportContractAssignmentVMPanel',
+ 'SupportContractAssignmentLicensePanel',
+ 'LicensePanel',
+ 'LicenseAssignmentPanel',
+ 'HardwareLifecyclePanel',
+ 'HardwareLifecycleDatesPanel',
+)
diff --git a/netbox_lifecycle/ui/panels/contract.py b/netbox_lifecycle/ui/panels/contract.py
new file mode 100644
index 0000000..0e92818
--- /dev/null
+++ b/netbox_lifecycle/ui/panels/contract.py
@@ -0,0 +1,86 @@
+from django.utils.translation import gettext_lazy as _
+
+from netbox.ui import attrs, panels
+
+from netbox_lifecycle.ui.attributes import ColoredDateTimeAttr
+
+__all__ = (
+ 'VendorPanel',
+ 'SupportSKUPanel',
+ 'SupportContractPanel',
+ 'SupportContractDatesPanel',
+ 'SupportContractAssignmentPanel',
+ 'SupportContractAssignmentDevicePanel',
+ 'SupportContractAssignmentVMPanel',
+ 'SupportContractAssignmentLicensePanel',
+)
+
+
+class VendorPanel(panels.ObjectAttributesPanel):
+ name = attrs.TextAttr('name')
+ description = attrs.TextAttr('description')
+
+
+class SupportSKUPanel(panels.ObjectAttributesPanel):
+ manufacturer = attrs.RelatedObjectAttr('manufacturer', linkify=True)
+ sku = attrs.TextAttr('sku', label=_('SKU'))
+ description = attrs.TextAttr('description')
+
+
+class SupportContractPanel(panels.ObjectAttributesPanel):
+ vendor = attrs.RelatedObjectAttr('vendor', linkify=True)
+ contract_id = attrs.TextAttr('contract_id')
+ description = attrs.TextAttr('description')
+
+
+class SupportContractDatesPanel(panels.ObjectAttributesPanel):
+ title = _('Dates')
+ start = attrs.DateTimeAttr('start', spec='date')
+ renewal = attrs.DateTimeAttr('renewal', spec='date')
+ end = ColoredDateTimeAttr('end', spec='date')
+
+
+class SupportContractAssignmentPanel(panels.ObjectAttributesPanel):
+ contract = attrs.RelatedObjectAttr('contract', linkify=True)
+ sku = attrs.RelatedObjectAttr('sku', linkify=True)
+ end = attrs.DateTimeAttr('end', spec='date')
+ description = attrs.TextAttr('description')
+
+
+class SupportContractAssignmentDevicePanel(panels.ObjectAttributesPanel):
+ title = _('Device Assignment')
+
+ device = attrs.RelatedObjectAttr('device', linkify=True)
+ module = attrs.RelatedObjectAttr('module', linkify=True)
+
+ def should_render(self, context):
+ if context.get('object').device:
+ return True
+ return False
+
+
+class SupportContractAssignmentVMPanel(panels.ObjectAttributesPanel):
+ title = _('Virtual Machine Assignment')
+
+ virtual_machine = attrs.RelatedObjectAttr('virtual_machine', linkify=True)
+
+ def should_render(self, context):
+ if context.get('object').virtual_machine:
+ return True
+ return False
+
+
+class SupportContractAssignmentLicensePanel(panels.ObjectAttributesPanel):
+ title = _('License Assignment')
+
+ license = attrs.RelatedObjectAttr('license', linkify=True)
+
+ def should_render(self, context):
+ if context.get('object').license:
+ return True
+ return False
+
+
+class SupportContractAssignmentsPanel(panels.ObjectsTablePanel):
+ model = 'netbox_lifecycle.SupportContractAssignment'
+ title = _('Support Contracts')
diff --git a/netbox_lifecycle/ui/panels/hardware.py b/netbox_lifecycle/ui/panels/hardware.py
new file mode 100644
index 0000000..9b73ea4
--- /dev/null
+++ b/netbox_lifecycle/ui/panels/hardware.py
@@ -0,0 +1,32 @@
+from django.utils.translation import gettext_lazy as _
+
+from netbox.ui import attrs, panels
+
+__all__ = (
+ 'HardwareLifecyclePanel',
+ 'HardwareLifecycleDatesPanel',
+)
+
+from netbox_lifecycle.ui.attributes.datetime import ColoredDateTimeAttr
+
+
+class HardwareLifecyclePanel(panels.ObjectAttributesPanel):
+ manufacturer = attrs.RelatedObjectAttr('assigned_object.manufacturer', linkify=True)
+ assigned_object = attrs.RelatedObjectAttr(
+ 'assigned_object', linkify=True, label=_('Assigned Object')
+ )
+ description = attrs.TextAttr('description')
+
+ def render(self, context):
+
+ return super().render(context)
+
+
+class HardwareLifecycleDatesPanel(panels.ObjectAttributesPanel):
+ title = _('Dates')
+ end_of_sale = ColoredDateTimeAttr('end_of_sale', spec='date')
+ end_of_maintenance = ColoredDateTimeAttr('end_of_maintenance', spec='date')
+ end_of_security = ColoredDateTimeAttr('end_of_security', spec='date')
+ last_contract_attach = ColoredDateTimeAttr('last_contract_attach', spec='date')
+ last_contract_renewal = ColoredDateTimeAttr('last_contract_renewal', spec='date')
+ end_of_support = ColoredDateTimeAttr('end_of_support', spec='date')
diff --git a/netbox_lifecycle/ui/panels/license.py b/netbox_lifecycle/ui/panels/license.py
new file mode 100644
index 0000000..99c90c8
--- /dev/null
+++ b/netbox_lifecycle/ui/panels/license.py
@@ -0,0 +1,42 @@
+from django.utils.translation import gettext_lazy as _
+
+from netbox.ui import attrs, panels
+
+__all__ = (
+ 'LicensePanel',
+ 'LicenseAssignmentPanel',
+)
+
+
+class LicensePanel(panels.ObjectAttributesPanel):
+ manufacturer = attrs.RelatedObjectAttr('manufacturer', linkify=True)
+ name = attrs.TextAttr('name')
+ description = attrs.TextAttr('description')
+
+
+class LicenseAssignmentPanel(panels.ObjectAttributesPanel):
+ license = attrs.RelatedObjectAttr('license', linkify=True)
+ name = attrs.TextAttr('name')
+ description = attrs.TextAttr('description')
+
+
+class LicenseAssignmentDevicePanel(panels.ObjectAttributesPanel):
+ title = _('Device Assignment')
+
+ device = attrs.RelatedObjectAttr('device', linkify=True)
+
+ def should_render(self, context):
+ if context.get('object').device:
+ return True
+ return False
+
+
+class LicenseAssignmentVMPanel(panels.ObjectAttributesPanel):
+ title = _('Virtual Machine Assignment')
+
+ virtual_machine = attrs.RelatedObjectAttr('virtual_machine', linkify=True)
+
+ def should_render(self, context):
+ if context.get('object').virtual_machine:
+ return True
+ return False
diff --git a/netbox_lifecycle/ui/panels/tabbed.py b/netbox_lifecycle/ui/panels/tabbed.py
new file mode 100644
index 0000000..ab4a38f
--- /dev/null
+++ b/netbox_lifecycle/ui/panels/tabbed.py
@@ -0,0 +1,54 @@
+from netbox.ui.panels import Panel, ObjectsTablePanel
+from utilities.querydict import dict_to_querydict
+from utilities.string import title
+from utilities.views import get_viewname
+
+
+class TabbedTablePanel(Panel):
+ """
+ A panel which displays panels within tabs.
+ """
+
+ template_name = 'netbox_lifecycle/ui/panels/tabbed_table.html'
+
+ def __init__(self, tabs: dict, **kwargs):
+ super().__init__(**kwargs)
+ for tab in tabs.values():
+ if tab is ObjectsTablePanel:
+ raise TypeError(
+ f"TabbedTablePanel only accepts ObjectsTablePanel instances, got {type(tab)}"
+ )
+ self.tabs = tabs
+
+ def get_context(self, context):
+ tabs = []
+ first = True
+ for name, panel in self.tabs.items():
+ # If no title is specified, derive one from the model name
+ url_params = {
+ k: v(context) if callable(v) else v for k, v in panel.filters.items()
+ }
+
+ if 'return_url' not in url_params and 'object' in context:
+ url_params['return_url'] = context['object'].get_absolute_url()
+ if panel.include_columns:
+ url_params['include_columns'] = ','.join(panel.include_columns)
+ if panel.exclude_columns:
+ url_params['exclude_columns'] = ','.join(panel.exclude_columns)
+
+ tab = {
+ 'name': name,
+ 'model': panel.model,
+ 'viewname': get_viewname(panel.model, 'list'),
+ 'title': panel.title or title(panel.model._meta.verbose_name_plural),
+ 'active': first,
+ 'url_params': dict_to_querydict(url_params),
+ }
+ first = False
+ tabs.append(tab)
+
+ return {
+ **super().get_context(context),
+ 'title': self.title,
+ 'tabs': tabs,
+ }
diff --git a/netbox_lifecycle/urls.py b/netbox_lifecycle/urls.py
index c0252a3..2a73ce4 100644
--- a/netbox_lifecycle/urls.py
+++ b/netbox_lifecycle/urls.py
@@ -1,288 +1,43 @@
-from django.urls import path
-from netbox.views.generic import ObjectChangeLogView
+from django.urls import path, include
+from utilities.urls import get_model_urls
from . import views
-from .models import (
- HardwareLifecycle,
- License,
- LicenseAssignment,
- SupportContract,
- SupportContractAssignment,
- SupportSKU,
- Vendor,
-)
+
+app_name = 'netbox_lifecycle'
+
urlpatterns = [
path(
- 'lifecycle/',
- views.HardwareLifecycleListView.as_view(),
- name='hardwarelifecycle_list',
- ),
- path(
- 'lifecycle/add',
- views.HardwareLifecycleEditView.as_view(),
- name='hardwarelifecycle_add',
- ),
- path(
- 'lifecycle/edit',
- views.HardwareLifecycleBulkEditView.as_view(),
- name='hardwarelifecycle_bulk_edit',
- ),
- path(
- 'lifecycle/delete',
- views.HardwareLifecycleBulkDeleteView.as_view(),
- name='hardwarelifecycle_bulk_delete',
- ),
- path(
- 'lifecycle/',
- views.HardwareLifecycleView.as_view(),
- name='hardwarelifecycle',
- ),
- path(
- 'lifecycle//edit',
- views.HardwareLifecycleEditView.as_view(),
- name='hardwarelifecycle_edit',
- ),
- path(
- 'lifecycle//delete',
- views.HardwareLifecycleDeleteView.as_view(),
- name='hardwarelifecycle_delete',
- ),
- path(
- 'lifecycle//changelog',
- ObjectChangeLogView.as_view(),
- name='hardwarelifecycle_changelog',
- kwargs={'model': HardwareLifecycle},
- ),
- path('vendors/', views.VendorListView.as_view(), name='vendor_list'),
- path('vendors/add', views.VendorEditView.as_view(), name='vendor_add'),
- path('vendors/edit', views.VendorBulkEditView.as_view(), name='vendor_bulk_edit'),
- path(
- 'vendors/delete',
- views.VendorBulkDeleteView.as_view(),
- name='vendor_bulk_delete',
- ),
- path('vendors/', views.VendorView.as_view(), name='vendor'),
- path('vendors//edit', views.VendorEditView.as_view(), name='vendor_edit'),
- path(
- 'vendors//delete',
- views.VendorDeleteView.as_view(),
- name='vendor_delete',
- ),
- path(
- 'vendors//changelog',
- ObjectChangeLogView.as_view(),
- name='vendor_changelog',
- kwargs={'model': Vendor},
- ),
- path(
- 'contracts/',
- views.SupportContractListView.as_view(),
- name='supportcontract_list',
- ),
- path(
- 'contracts/add',
- views.SupportContractEditView.as_view(),
- name='supportcontract_add',
- ),
- path(
- 'contracts/edit',
- views.SupportContractBulkEditView.as_view(),
- name='supportcontract_bulk_edit',
- ),
- path(
- 'contracts/delete',
- views.SupportContractBulkDeleteView.as_view(),
- name='supportcontract_bulk_delete',
- ),
- path(
- 'contracts/',
- views.SupportContractView.as_view(),
- name='supportcontract',
- ),
- path(
- 'contracts//devices',
- views.SupportContractAssignmentsView.as_view(),
- name='supportcontract_assignments',
- ),
- path(
- 'contracts//edit',
- views.SupportContractEditView.as_view(),
- name='supportcontract_edit',
- ),
- path(
- 'contracts//delete',
- views.SupportContractDeleteView.as_view(),
- name='supportcontract_delete',
- ),
- path(
- 'contracts//changelog',
- ObjectChangeLogView.as_view(),
- name='supportcontract_changelog',
- kwargs={'model': SupportContract},
- ),
- path(
- 'contract-assignment/',
- views.SupportContractAssignmentListView.as_view(),
- name='supportcontractassignment_list',
- ),
- path(
- 'contract-assignment/add',
- views.SupportContractAssignmentEditView.as_view(),
- name='supportcontractassignment_add',
- ),
- path(
- 'contract-assignment/edit/',
- views.SupportContractAssignmentBulkEditView.as_view(),
- name='supportcontractassignment_bulk_edit',
- ),
- path(
- 'contract-assignment/delete/',
- views.SupportContractAssignmentBulkDeleteView.as_view(),
- name='supportcontractassignment_bulk_delete',
- ),
- path(
- 'contract-assignment/',
- views.SupportContractAssignmentView.as_view(),
- name='supportcontractassignment',
- ),
- path(
- 'contract-assignment//edit',
- views.SupportContractAssignmentEditView.as_view(),
- name='supportcontractassignment_edit',
- ),
- path(
- 'contract-assignment//delete',
- views.SupportContractAssignmentDeleteView.as_view(),
- name='supportcontractassignment_delete',
- ),
- path(
- 'contract-assignment//changelog',
- ObjectChangeLogView.as_view(),
- name='supportcontractassignment_changelog',
- kwargs={'model': SupportContractAssignment},
- ),
- path('license/', views.LicenseListView.as_view(), name='license_list'),
- path('license/add', views.LicenseEditView.as_view(), name='license_add'),
- path('license/edit', views.LicenseBulkEditView.as_view(), name='license_bulk_edit'),
- path(
- 'license/delete',
- views.LicenseBulkDeleteView.as_view(),
- name='license_bulk_delete',
- ),
- path('license/', views.LicenseView.as_view(), name='license'),
- path(
- 'license//assignments',
- views.LicenseAssignmentsView.as_view(),
- name='license_assignments',
- ),
- path('license//edit', views.LicenseEditView.as_view(), name='license_edit'),
- path(
- 'license//delete',
- views.LicenseDeleteView.as_view(),
- name='license_delete',
- ),
- path(
- 'license//changelog',
- ObjectChangeLogView.as_view(),
- name='license_changelog',
- kwargs={'model': License},
- ),
- path('sku/', views.SupportSKUListView.as_view(), name='supportsku_list'),
- path('sku/add', views.SupportSKUEditView.as_view(), name='supportsku_add'),
- path(
- 'sku/edit', views.SupportSKUBulkEditView.as_view(), name='supportsku_bulk_edit'
- ),
- path(
- 'sku/delete',
- views.SupportSKUBulkDeleteView.as_view(),
- name='supportsku_bulk_delete',
- ),
- path('sku/', views.SupportSKUView.as_view(), name='supportsku'),
- path(
- 'sku//edit', views.SupportSKUEditView.as_view(), name='supportsku_edit'
- ),
- path(
- 'sku//delete',
- views.SupportSKUDeleteView.as_view(),
- name='supportsku_delete',
- ),
- path(
- 'sku//changelog',
- ObjectChangeLogView.as_view(),
- name='supportsku_changelog',
- kwargs={'model': SupportSKU},
- ),
- path(
- 'license-assignment/',
- views.LicenseAssignmentListView.as_view(),
- name='licenseassignment_list',
- ),
- path(
- 'license-assignment/add',
- views.LicenseAssignmentEditView.as_view(),
- name='licenseassignment_add',
- ),
- path(
- 'license-assignment/edit',
- views.LicenseAssignmentBulkEditView.as_view(),
- name='licenseassignment_bulk_edit',
- ),
- path(
- 'license-assignment/delete/',
- views.LicenseAssignmentBulkDeleteView.as_view(),
- name='licenseassignment_bulk_delete',
- ),
- path(
- 'license-assignment/',
- views.LicenseAssignmentView.as_view(),
- name='licenseassignment',
- ),
- path(
- 'license-assignment//edit',
- views.LicenseAssignmentEditView.as_view(),
- name='licenseassignment_edit',
- ),
- path(
- 'license-assignment//delete',
- views.LicenseAssignmentDeleteView.as_view(),
- name='licenseassignment_delete',
- ),
- path(
- 'license-assignment//changelog',
- ObjectChangeLogView.as_view(),
- name='licenseassignment_changelog',
- kwargs={'model': LicenseAssignment},
+ 'lifecycles/',
+ include(get_model_urls(app_name, 'hardwarelifecycle', detail=False)),
),
- # HTMX endpoints
path(
- 'htmx/device//contracts/',
- views.DeviceContractsHTMXView.as_view(),
- name='device_contracts_htmx',
+ 'lifecycles//', include(get_model_urls(app_name, 'hardwarelifecycle'))
),
+ path('vendor/', include(get_model_urls(app_name, 'vendor', detail=False))),
+ path('vendor//', include(get_model_urls(app_name, 'vendor'))),
path(
- 'htmx/device//contracts/expired/',
- views.DeviceContractsExpiredHTMXView.as_view(),
- name='device_contracts_expired',
+ 'contract/', include(get_model_urls(app_name, 'supportcontract', detail=False))
),
+ path('contract//', include(get_model_urls(app_name, 'supportcontract'))),
path(
- 'htmx/virtualmachine//contracts/',
- views.VirtualMachineContractsHTMXView.as_view(),
- name='virtualmachine_contracts_htmx',
+ 'contract-assignments/',
+ include(get_model_urls(app_name, 'supportcontractassignment', detail=False)),
),
path(
- 'htmx/virtualmachine//contracts/expired/',
- views.VirtualMachineContractsExpiredHTMXView.as_view(),
- name='virtualmachine_contracts_expired',
+ 'contract-assignments//',
+ include(get_model_urls(app_name, 'supportcontractassignment')),
),
+ path('sku/', include(get_model_urls(app_name, 'supportsku', detail=False))),
+ path('sku//', include(get_model_urls(app_name, 'supportsku'))),
+ path('license/', include(get_model_urls(app_name, 'license', detail=False))),
+ path('license//', include(get_model_urls(app_name, 'license'))),
path(
- 'htmx/device//licenses/',
- views.DeviceLicensesHTMXView.as_view(),
- name='device_licenses_htmx',
+ 'license-assignments/',
+ include(get_model_urls(app_name, 'licenseassignment', detail=False)),
),
path(
- 'htmx/virtualmachine//licenses/',
- views.VirtualMachineLicensesHTMXView.as_view(),
- name='virtualmachine_licenses_htmx',
+ 'license-assignments//',
+ include(get_model_urls(app_name, 'licenseassignment')),
),
]
diff --git a/netbox_lifecycle/views/__init__.py b/netbox_lifecycle/views/__init__.py
index 21fcea4..edd31fe 100644
--- a/netbox_lifecycle/views/__init__.py
+++ b/netbox_lifecycle/views/__init__.py
@@ -1,4 +1,3 @@
from .contract import *
from .hardware import *
-from .htmx import *
from .license import *
diff --git a/netbox_lifecycle/views/contract.py b/netbox_lifecycle/views/contract.py
index 11558a6..ed35ec7 100644
--- a/netbox_lifecycle/views/contract.py
+++ b/netbox_lifecycle/views/contract.py
@@ -1,3 +1,4 @@
+from extras.ui.panels import TagsPanel, CustomFieldsPanel
from netbox.views.generic import (
BulkDeleteView,
BulkEditView,
@@ -8,6 +9,7 @@
ObjectListView,
ObjectView,
)
+from netbox.ui import panels, layout
from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
from netbox_lifecycle.filtersets import (
@@ -46,6 +48,16 @@
SupportSKUTable,
VendorTable,
)
+from netbox_lifecycle.ui import (
+ VendorPanel,
+ SupportContractPanel,
+ SupportContractDatesPanel,
+ SupportContractAssignmentPanel,
+ SupportContractAssignmentDevicePanel,
+ SupportContractAssignmentVMPanel,
+ SupportContractAssignmentLicensePanel,
+ SupportSKUPanel,
+)
__all__ = (
# SupportContractAssignment
@@ -84,7 +96,7 @@
)
-@register_model_view(Vendor, name='list')
+@register_model_view(Vendor, name='list', path='', detail=False)
class VendorListView(ObjectListView):
queryset = Vendor.objects.all()
table = VendorTable
@@ -95,6 +107,16 @@ class VendorListView(ObjectListView):
@register_model_view(Vendor)
class VendorView(GetRelatedModelsMixin, ObjectView):
queryset = Vendor.objects.all()
+ template_name = 'generic/object.html'
+ layout = layout.SimpleLayout(
+ left_panels=[
+ VendorPanel(),
+ ],
+ right_panels=[
+ panels.RelatedObjectsPanel(),
+ panels.CommentsPanel(),
+ ],
+ )
def get_extra_context(self, request, instance):
assignments = SupportContractAssignment.objects.filter(
@@ -107,13 +129,19 @@ def get_extra_context(self, request, instance):
}
+@register_model_view(Vendor, 'add', detail=False)
@register_model_view(Vendor, 'edit')
class VendorEditView(ObjectEditView):
queryset = Vendor.objects.all()
form = VendorForm
-@register_model_view(Vendor, 'bulk_edit')
+@register_model_view(Vendor, 'delete')
+class VendorDeleteView(ObjectDeleteView):
+ queryset = Vendor.objects.all()
+
+
+@register_model_view(Vendor, 'bulk_edit', detail=False)
class VendorBulkEditView(BulkEditView):
queryset = Vendor.objects.all()
filterset = VendorFilterSet
@@ -121,25 +149,20 @@ class VendorBulkEditView(BulkEditView):
form = VendorBulkEditForm
-@register_model_view(Vendor, 'delete')
-class VendorDeleteView(ObjectDeleteView):
- queryset = Vendor.objects.all()
-
-
-@register_model_view(Vendor, 'bulk_delete')
+@register_model_view(Vendor, 'bulk_delete', detail=False)
class VendorBulkDeleteView(BulkDeleteView):
queryset = Vendor.objects.all()
filterset = VendorFilterSet
table = VendorTable
-@register_model_view(Vendor, 'bulk_import')
+@register_model_view(Vendor, 'bulk_import', detail=False)
class VendorBulkImportView(BulkImportView):
queryset = Vendor.objects.all()
model_form = VendorImportForm
-@register_model_view(SupportSKU, name='list')
+@register_model_view(SupportSKU, name='list', path='', detail=False)
class SupportSKUListView(ObjectListView):
queryset = SupportSKU.objects.all()
table = SupportSKUTable
@@ -150,43 +173,55 @@ class SupportSKUListView(ObjectListView):
@register_model_view(SupportSKU)
class SupportSKUView(ObjectView):
queryset = SupportSKU.objects.all()
+ template_name = 'generic/object.html'
+ layout = layout.SimpleLayout(
+ left_panels=[
+ SupportSKUPanel(),
+ TagsPanel(),
+ ],
+ right_panels=[
+ panels.CommentsPanel(),
+ panels.RelatedObjectsPanel(),
+ ],
+ )
+@register_model_view(SupportSKU, 'add', detail=False)
@register_model_view(SupportSKU, 'edit')
class SupportSKUEditView(ObjectEditView):
queryset = SupportSKU.objects.all()
form = SupportSKUForm
-@register_model_view(SupportSKU, 'bulk_edit')
-class SupportSKUBulkEditView(BulkEditView):
+@register_model_view(SupportSKU, 'delete')
+class SupportSKUDeleteView(ObjectDeleteView):
queryset = SupportSKU.objects.all()
filterset = SupportSKUFilterSet
table = SupportSKUTable
- form = SupportSKUBulkEditForm
-@register_model_view(SupportSKU, 'delete')
-class SupportSKUDeleteView(ObjectDeleteView):
+@register_model_view(SupportSKU, 'bulk_edit', detail=False)
+class SupportSKUBulkEditView(BulkEditView):
queryset = SupportSKU.objects.all()
filterset = SupportSKUFilterSet
table = SupportSKUTable
+ form = SupportSKUBulkEditForm
-@register_model_view(SupportSKU, 'bulk_delete')
+@register_model_view(SupportSKU, 'bulk_delete', detail=False)
class SupportSKUBulkDeleteView(BulkDeleteView):
queryset = SupportSKU.objects.all()
filterset = SupportSKUFilterSet
table = SupportSKUTable
-@register_model_view(SupportSKU, 'bulk_import')
+@register_model_view(SupportSKU, 'bulk_import', detail=False)
class SupportSKUBulkImportView(BulkImportView):
queryset = SupportSKU.objects.all()
model_form = SupportSKUImportForm
-@register_model_view(SupportContract, name='list')
+@register_model_view(SupportContract, name='list', path='', detail=False)
class SupportContractListView(ObjectListView):
queryset = SupportContract.objects.all()
table = SupportContractTable
@@ -197,16 +232,26 @@ class SupportContractListView(ObjectListView):
@register_model_view(SupportContract)
class SupportContractView(ObjectView):
queryset = SupportContract.objects.all()
+ template_name = 'generic/object.html'
+ layout = layout.SimpleLayout(
+ left_panels=[
+ SupportContractPanel(),
+ SupportContractDatesPanel(),
+ TagsPanel(),
+ ],
+ right_panels=[
+ panels.CommentsPanel(),
+ panels.RelatedObjectsPanel(),
+ ],
+ )
@register_model_view(SupportContract, name='assignments')
class SupportContractAssignmentsView(ObjectChildrenView):
- template_name = 'netbox_lifecycle/supportcontract/assignments.html'
queryset = SupportContract.objects.all()
child_model = SupportContractAssignment
table = SupportContractAssignmentTable
filterset = SupportContractAssignmentFilterSet
- actions = {'add': {'add'}, 'edit': {'change'}, 'delete': {'delete'}}
tab = ViewTab(
label='Assignments',
badge=lambda obj: SupportContractAssignment.objects.filter(
@@ -218,13 +263,19 @@ def get_children(self, request, parent):
return self.child_model.objects.filter(contract=parent)
+@register_model_view(SupportContract, 'add', detail=False)
@register_model_view(SupportContract, 'edit')
class SupportContractEditView(ObjectEditView):
queryset = SupportContract.objects.all()
form = SupportContractForm
-@register_model_view(SupportContract, 'bulk_edit')
+@register_model_view(SupportContract, 'delete')
+class SupportContractDeleteView(ObjectDeleteView):
+ queryset = SupportContract.objects.all()
+
+
+@register_model_view(SupportContract, 'bulk_edit', detail=False)
class SupportContractBulkEditView(BulkEditView):
queryset = SupportContract.objects.all()
filterset = SupportContractFilterSet
@@ -232,25 +283,20 @@ class SupportContractBulkEditView(BulkEditView):
form = SupportContractBulkEditForm
-@register_model_view(SupportContract, 'delete')
-class SupportContractDeleteView(ObjectDeleteView):
- queryset = SupportContract.objects.all()
-
-
-@register_model_view(SupportContract, 'bulk_delete')
+@register_model_view(SupportContract, 'bulk_delete', detail=False)
class SupportContractBulkDeleteView(BulkDeleteView):
queryset = SupportContract.objects.all()
filterset = SupportContractFilterSet
table = SupportContractTable
-@register_model_view(SupportContract, 'bulk_import')
+@register_model_view(SupportContract, 'bulk_import', detail=False)
class SupportContractBulkImportView(BulkImportView):
queryset = SupportContract.objects.all()
model_form = SupportContractImportForm
-@register_model_view(SupportContractAssignment, name='list')
+@register_model_view(SupportContractAssignment, name='list', path='', detail=False)
class SupportContractAssignmentListView(ObjectListView):
queryset = SupportContractAssignment.objects.all()
table = SupportContractAssignmentTable
@@ -264,11 +310,27 @@ class SupportContractAssignmentListView(ObjectListView):
}
-@register_model_view(SupportContract)
+@register_model_view(SupportContractAssignment)
class SupportContractAssignmentView(ObjectView):
queryset = SupportContractAssignment.objects.all()
+ template_name = 'generic/object.html'
+ layout = layout.SimpleLayout(
+ left_panels=[
+ SupportContractAssignmentPanel(),
+ SupportContractAssignmentDevicePanel(),
+ SupportContractAssignmentVMPanel(),
+ SupportContractAssignmentLicensePanel(),
+ TagsPanel(),
+ ],
+ right_panels=[
+ CustomFieldsPanel(),
+ panels.CommentsPanel(),
+ panels.RelatedObjectsPanel(),
+ ],
+ )
+@register_model_view(SupportContractAssignment, 'add', detail=False)
@register_model_view(SupportContractAssignment, 'edit')
class SupportContractAssignmentEditView(ObjectEditView):
queryset = SupportContractAssignment.objects.all()
@@ -280,6 +342,7 @@ class SupportContractAssignmentDeleteView(ObjectDeleteView):
queryset = SupportContractAssignment.objects.all()
+@register_model_view(SupportContractAssignment, 'bulk_edit', detail=False)
class SupportContractAssignmentBulkEditView(BulkEditView):
queryset = SupportContractAssignment.objects.all()
filterset = SupportContractAssignmentFilterSet
@@ -287,13 +350,14 @@ class SupportContractAssignmentBulkEditView(BulkEditView):
form = SupportContractAssignmentBulkEditForm
+@register_model_view(SupportContractAssignment, 'bulk_delete', detail=False)
class SupportContractAssignmentBulkDeleteView(BulkDeleteView):
queryset = SupportContractAssignment.objects.all()
filterset = SupportContractAssignmentFilterSet
table = SupportContractAssignmentTable
-@register_model_view(SupportContractAssignment, 'bulk_import')
+@register_model_view(SupportContractAssignment, 'bulk_import', detail=False)
class SupportContractAssignmentBulkImportView(BulkImportView):
queryset = SupportContractAssignment.objects.all()
model_form = SupportContractAssignmentImportForm
diff --git a/netbox_lifecycle/views/hardware.py b/netbox_lifecycle/views/hardware.py
index 7c8c3d7..ca4edc5 100644
--- a/netbox_lifecycle/views/hardware.py
+++ b/netbox_lifecycle/views/hardware.py
@@ -1,3 +1,4 @@
+from extras.ui.panels import TagsPanel, CustomFieldsPanel
from netbox.views.generic import (
BulkDeleteView,
BulkEditView,
@@ -7,6 +8,8 @@
ObjectListView,
ObjectView,
)
+from netbox.ui import panels, layout
+from netbox_lifecycle.ui import HardwareLifecyclePanel, HardwareLifecycleDatesPanel
from utilities.views import register_model_view
from netbox_lifecycle.filtersets import HardwareLifecycleFilterSet
@@ -30,7 +33,7 @@
)
-@register_model_view(HardwareLifecycle, name='list')
+@register_model_view(HardwareLifecycle, name='list', path='', detail=False)
class HardwareLifecycleListView(ObjectListView):
queryset = HardwareLifecycle.objects.all()
table = HardwareLifecycleTable
@@ -41,19 +44,34 @@ class HardwareLifecycleListView(ObjectListView):
@register_model_view(HardwareLifecycle)
class HardwareLifecycleView(ObjectView):
queryset = HardwareLifecycle.objects.all()
-
- def get_extra_context(self, request, instance):
-
- return {}
-
-
+ template_name = 'generic/object.html'
+ layout = layout.SimpleLayout(
+ left_panels=[
+ HardwareLifecyclePanel(),
+ HardwareLifecycleDatesPanel(),
+ TagsPanel(),
+ ],
+ right_panels=[
+ CustomFieldsPanel(),
+ panels.RelatedObjectsPanel(),
+ panels.CommentsPanel(),
+ ],
+ )
+
+
+@register_model_view(HardwareLifecycle, 'add', detail=False)
@register_model_view(HardwareLifecycle, 'edit')
class HardwareLifecycleEditView(ObjectEditView):
queryset = HardwareLifecycle.objects.all()
form = HardwareLifecycleForm
-@register_model_view(HardwareLifecycle, 'bulk_edit')
+@register_model_view(HardwareLifecycle, 'delete')
+class HardwareLifecycleDeleteView(ObjectDeleteView):
+ queryset = HardwareLifecycle.objects.all()
+
+
+@register_model_view(HardwareLifecycle, 'bulk_edit', detail=False)
class HardwareLifecycleBulkEditView(BulkEditView):
queryset = HardwareLifecycle.objects.all()
filterset = HardwareLifecycleFilterSet
@@ -61,19 +79,14 @@ class HardwareLifecycleBulkEditView(BulkEditView):
form = HardwareLifecycleBulkEditForm
-@register_model_view(HardwareLifecycle, 'delete')
-class HardwareLifecycleDeleteView(ObjectDeleteView):
- queryset = HardwareLifecycle.objects.all()
-
-
-@register_model_view(HardwareLifecycle, 'bulk_delete')
+@register_model_view(HardwareLifecycle, 'bulk_delete', detail=False)
class HardwareLifecycleBulkDeleteView(BulkDeleteView):
queryset = HardwareLifecycle.objects.all()
filterset = HardwareLifecycleFilterSet
table = HardwareLifecycleTable
-@register_model_view(HardwareLifecycle, 'bulk_import')
+@register_model_view(HardwareLifecycle, 'bulk_import', detail=False)
class HardwareLifecycleBulkImportView(BulkImportView):
queryset = HardwareLifecycle.objects.all()
model_form = HardwareLifecycleImportForm
diff --git a/netbox_lifecycle/views/htmx.py b/netbox_lifecycle/views/htmx.py
deleted file mode 100644
index d085f4b..0000000
--- a/netbox_lifecycle/views/htmx.py
+++ /dev/null
@@ -1,192 +0,0 @@
-from dcim.models import Device
-from django.contrib.auth.mixins import LoginRequiredMixin
-from django.shortcuts import get_object_or_404, render
-from django.views import View
-from virtualization.models import VirtualMachine
-
-from netbox_lifecycle.constants import (
- CONTRACT_STATUS_ACTIVE,
- CONTRACT_STATUS_EXPIRED,
- CONTRACT_STATUS_FUTURE,
- CONTRACT_STATUS_UNSPECIFIED,
-)
-from netbox_lifecycle.models import LicenseAssignment, SupportContractAssignment
-
-MAX_LICENSE_DISPLAY = 10
-
-__all__ = (
- 'DeviceContractsExpiredHTMXView',
- 'DeviceContractsHTMXView',
- 'DeviceLicensesHTMXView',
- 'VirtualMachineContractsExpiredHTMXView',
- 'VirtualMachineContractsHTMXView',
- 'VirtualMachineLicensesHTMXView',
-)
-
-
-class DeviceContractsHTMXView(LoginRequiredMixin, View):
- """HTMX endpoint for device contract card content."""
-
- def get(self, request, pk):
- device = get_object_or_404(Device, pk=pk)
- assignments = SupportContractAssignment.objects.filter(
- device=device
- ).select_related(
- 'contract', 'contract__vendor', 'sku', 'sku__manufacturer', 'license'
- )
-
- grouped = {
- CONTRACT_STATUS_ACTIVE: [],
- CONTRACT_STATUS_FUTURE: [],
- CONTRACT_STATUS_UNSPECIFIED: [],
- CONTRACT_STATUS_EXPIRED: [],
- }
- for assignment in assignments:
- grouped[assignment.status].append(assignment)
-
- return render(
- request,
- 'netbox_lifecycle/htmx/device_contracts.html',
- {
- 'device': device,
- 'active': grouped[CONTRACT_STATUS_ACTIVE],
- 'future': grouped[CONTRACT_STATUS_FUTURE],
- 'unspecified': grouped[CONTRACT_STATUS_UNSPECIFIED],
- 'expired_count': len(grouped[CONTRACT_STATUS_EXPIRED]),
- },
- )
-
-
-class DeviceContractsExpiredHTMXView(LoginRequiredMixin, View):
- """HTMX endpoint for expired contracts only."""
-
- def get(self, request, pk):
- device = get_object_or_404(Device, pk=pk)
- expired = [
- a
- for a in SupportContractAssignment.objects.filter(
- device=device
- ).select_related(
- 'contract', 'contract__vendor', 'sku', 'sku__manufacturer', 'license'
- )
- if a.status == CONTRACT_STATUS_EXPIRED
- ]
-
- return render(
- request,
- 'netbox_lifecycle/htmx/contract_list.html',
- {
- 'assignments': expired,
- },
- )
-
-
-class VirtualMachineContractsHTMXView(LoginRequiredMixin, View):
- """HTMX endpoint for virtual machine contract card content."""
-
- def get(self, request, pk):
- virtual_machine = get_object_or_404(VirtualMachine, pk=pk)
- assignments = SupportContractAssignment.objects.filter(
- virtual_machine=virtual_machine
- ).select_related(
- 'contract', 'contract__vendor', 'sku', 'sku__manufacturer', 'license'
- )
-
- grouped = {
- CONTRACT_STATUS_ACTIVE: [],
- CONTRACT_STATUS_FUTURE: [],
- CONTRACT_STATUS_UNSPECIFIED: [],
- CONTRACT_STATUS_EXPIRED: [],
- }
- for assignment in assignments:
- grouped[assignment.status].append(assignment)
-
- return render(
- request,
- 'netbox_lifecycle/htmx/virtualmachine_contracts.html',
- {
- 'virtual_machine': virtual_machine,
- 'active': grouped[CONTRACT_STATUS_ACTIVE],
- 'future': grouped[CONTRACT_STATUS_FUTURE],
- 'unspecified': grouped[CONTRACT_STATUS_UNSPECIFIED],
- 'expired_count': len(grouped[CONTRACT_STATUS_EXPIRED]),
- },
- )
-
-
-class DeviceLicensesHTMXView(LoginRequiredMixin, View):
- """HTMX endpoint for device license card content."""
-
- def get(self, request, pk):
- device = get_object_or_404(Device, pk=pk)
- assignments = LicenseAssignment.objects.filter(device=device).select_related(
- 'license', 'license__manufacturer', 'vendor'
- )
-
- total_count = assignments.count()
- show_all_url = None
- if total_count > MAX_LICENSE_DISPLAY:
- show_all_url = (
- f'/plugins/lifecycle/license-assignment/?device_id={device.pk}'
- )
- assignments = assignments[:MAX_LICENSE_DISPLAY]
-
- return render(
- request,
- 'netbox_lifecycle/htmx/device_licenses.html',
- {
- 'device': device,
- 'assignments': assignments,
- 'show_all_url': show_all_url,
- },
- )
-
-
-class VirtualMachineLicensesHTMXView(LoginRequiredMixin, View):
- """HTMX endpoint for virtual machine license card content."""
-
- def get(self, request, pk):
- virtual_machine = get_object_or_404(VirtualMachine, pk=pk)
- assignments = LicenseAssignment.objects.filter(
- virtual_machine=virtual_machine
- ).select_related('license', 'license__manufacturer', 'vendor')
-
- total_count = assignments.count()
- show_all_url = None
- if total_count > MAX_LICENSE_DISPLAY:
- show_all_url = f'/plugins/lifecycle/license-assignment/?virtual_machine_id={virtual_machine.pk}'
- assignments = assignments[:MAX_LICENSE_DISPLAY]
-
- return render(
- request,
- 'netbox_lifecycle/htmx/virtualmachine_licenses.html',
- {
- 'virtual_machine': virtual_machine,
- 'assignments': assignments,
- 'show_all_url': show_all_url,
- },
- )
-
-
-class VirtualMachineContractsExpiredHTMXView(LoginRequiredMixin, View):
- """HTMX endpoint for expired contracts only (virtual machine)."""
-
- def get(self, request, pk):
- virtual_machine = get_object_or_404(VirtualMachine, pk=pk)
- expired = [
- a
- for a in SupportContractAssignment.objects.filter(
- virtual_machine=virtual_machine
- ).select_related(
- 'contract', 'contract__vendor', 'sku', 'sku__manufacturer', 'license'
- )
- if a.status == CONTRACT_STATUS_EXPIRED
- ]
-
- return render(
- request,
- 'netbox_lifecycle/htmx/contract_list.html',
- {
- 'assignments': expired,
- },
- )
diff --git a/netbox_lifecycle/views/license.py b/netbox_lifecycle/views/license.py
index b10e341..c7f408c 100644
--- a/netbox_lifecycle/views/license.py
+++ b/netbox_lifecycle/views/license.py
@@ -1,3 +1,4 @@
+from extras.ui.panels import TagsPanel, CustomFieldsPanel
from netbox.views.generic import (
BulkDeleteView,
BulkEditView,
@@ -8,6 +9,12 @@
ObjectListView,
ObjectView,
)
+from netbox.ui import panels, layout
+from netbox_lifecycle.ui import LicensePanel, LicenseAssignmentPanel
+from netbox_lifecycle.ui.panels.license import (
+ LicenseAssignmentDevicePanel,
+ LicenseAssignmentVMPanel,
+)
from utilities.views import ViewTab, register_model_view
from netbox_lifecycle.filtersets import LicenseAssignmentFilterSet, LicenseFilterSet
@@ -43,7 +50,7 @@
)
-@register_model_view(License, name='list')
+@register_model_view(License, name='list', path='', detail=False)
class LicenseListView(ObjectListView):
queryset = License.objects.all()
table = LicenseTable
@@ -54,15 +61,33 @@ class LicenseListView(ObjectListView):
@register_model_view(License)
class LicenseView(ObjectView):
queryset = License.objects.all()
+ template_name = 'generic/object.html'
+ layout = layout.SimpleLayout(
+ left_panels=[
+ LicensePanel(),
+ TagsPanel(),
+ ],
+ right_panels=[
+ CustomFieldsPanel(),
+ panels.CommentsPanel(),
+ panels.RelatedObjectsPanel(),
+ ],
+ )
+@register_model_view(License, 'add', detail=False)
@register_model_view(License, 'edit')
class LicenseEditView(ObjectEditView):
queryset = License.objects.all()
form = LicenseForm
-@register_model_view(License, 'bulk_edit')
+@register_model_view(License, 'delete')
+class LicenseDeleteView(ObjectDeleteView):
+ queryset = License.objects.all()
+
+
+@register_model_view(License, 'bulk_edit', detail=False)
class LicenseBulkEditView(BulkEditView):
queryset = License.objects.all()
filterset = LicenseFilterSet
@@ -70,19 +95,14 @@ class LicenseBulkEditView(BulkEditView):
form = LicenseBulkEditForm
-@register_model_view(License, 'delete')
-class LicenseDeleteView(ObjectDeleteView):
- queryset = License.objects.all()
-
-
-@register_model_view(License, 'bulk_delete')
+@register_model_view(License, 'bulk_delete', detail=False)
class LicenseBulkDeleteView(BulkDeleteView):
queryset = License.objects.all()
filterset = LicenseFilterSet
table = LicenseTable
-@register_model_view(License, 'bulk_import')
+@register_model_view(License, 'bulk_import', detail=False)
class LicenseBulkImportView(BulkImportView):
queryset = License.objects.all()
model_form = LicenseImportForm
@@ -90,13 +110,11 @@ class LicenseBulkImportView(BulkImportView):
@register_model_view(License, 'assignments')
class LicenseAssignmentsView(ObjectChildrenView):
- template_name = 'netbox_lifecycle/license/assignments.html'
queryset = License.objects.all()
child_model = LicenseAssignment
table = LicenseAssignmentTable
filterset = LicenseAssignmentFilterSet
viewname = None
- actions = {'add': {'add'}, 'edit': {'change'}, 'delete': {'delete'}}
tab = ViewTab(
label='License Assignments',
badge=lambda obj: LicenseAssignment.objects.filter(license=obj).count(),
@@ -106,7 +124,7 @@ def get_children(self, request, parent):
return self.child_model.objects.filter(license=parent)
-@register_model_view(LicenseAssignment, name='list')
+@register_model_view(LicenseAssignment, name='list', path='', detail=False)
class LicenseAssignmentListView(ObjectListView):
queryset = LicenseAssignment.objects.all()
table = LicenseAssignmentTable
@@ -117,8 +135,23 @@ class LicenseAssignmentListView(ObjectListView):
@register_model_view(LicenseAssignment)
class LicenseAssignmentView(ObjectView):
queryset = LicenseAssignment.objects.all()
+ template_name = 'generic/object.html'
+ layout = layout.SimpleLayout(
+ left_panels=[
+ LicenseAssignmentPanel(),
+ LicenseAssignmentDevicePanel(),
+ LicenseAssignmentVMPanel(),
+ TagsPanel(),
+ ],
+ right_panels=[
+ CustomFieldsPanel(),
+ panels.CommentsPanel(),
+ panels.RelatedObjectsPanel(),
+ ],
+ )
+@register_model_view(LicenseAssignment, 'add', detail=False)
@register_model_view(LicenseAssignment, 'edit')
class LicenseAssignmentEditView(ObjectEditView):
queryset = LicenseAssignment.objects.all()
@@ -130,7 +163,7 @@ class LicenseAssignmentDeleteView(ObjectDeleteView):
queryset = LicenseAssignment.objects.all()
-@register_model_view(LicenseAssignment, 'bulk_edit')
+@register_model_view(LicenseAssignment, 'bulk_edit', detail=False)
class LicenseAssignmentBulkEditView(BulkEditView):
queryset = LicenseAssignment.objects.all()
filterset = LicenseAssignmentFilterSet
@@ -138,14 +171,14 @@ class LicenseAssignmentBulkEditView(BulkEditView):
form = LicenseAssignmentBulkEditForm
-@register_model_view(LicenseAssignment, 'bulk_delete')
+@register_model_view(LicenseAssignment, 'bulk_delete', detail=False)
class LicenseAssignmentBulkDeleteView(BulkDeleteView):
queryset = LicenseAssignment.objects.all()
filterset = LicenseAssignmentFilterSet
table = LicenseAssignmentTable
-@register_model_view(LicenseAssignment, 'bulk_import')
+@register_model_view(LicenseAssignment, 'bulk_import', detail=False)
class LicenseAssignmentBulkImportView(BulkImportView):
queryset = LicenseAssignment.objects.all()
model_form = LicenseAssignmentImportForm
diff --git a/ruff.toml b/ruff.toml
index afe1757..1a750db 100644
--- a/ruff.toml
+++ b/ruff.toml
@@ -9,12 +9,14 @@ output-format = "github"
[lint]
extend-select = ["E1", "E2", "E3", "E501", "W"]
-ignore = ["F403", "F405", "RUF012"]
+ignore = ["F403", "F405", "RUF012", "I001", "RUF022", ]
preview = true
[lint.per-file-ignores]
-"template_code.py" = ["E501"]
-"netbox_lifecycle/graphql/filters.py" = ["F821"]
+"urls.py" = ["F401", ]
+"netbox_lifecycle/graphql/filters.py" = ["F821", ]
+"netbox_lifecycle/ui/panels/contract.py" = ["SIM103", ]
+"netbox_lifecycle/ui/panels/license.py" = ["SIM103", ]
[format]
quote-style = "single"