Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions netbox_lifecycle/api/_serializers/hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ class HardwareLifecycleSerializer(NetBoxModelSerializer):
)
assigned_object_type = ContentTypeField(queryset=ContentType.objects.all())

end_of_sale = serializers.DateField()
end_of_maintenance = serializers.DateField(required=False)
end_of_security = serializers.DateField(required=False)
last_contract_attach = serializers.DateField(required=False)
last_contract_renewal = serializers.DateField(required=False)
end_of_support = serializers.DateField()
end_of_sale = serializers.DateField(required=False, allow_null=True)
end_of_maintenance = serializers.DateField(required=False, allow_null=True)
end_of_security = serializers.DateField(required=False, allow_null=True)
last_contract_attach = serializers.DateField(required=False, allow_null=True)
last_contract_renewal = serializers.DateField(required=False, allow_null=True)
end_of_support = serializers.DateField(required=False, allow_null=True)

class Meta:
model = HardwareLifecycle
Expand Down
21 changes: 21 additions & 0 deletions netbox_lifecycle/migrations/0017_optional_lifecycle_dates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('netbox_lifecycle', '0016_add_virtual_machine_support'),
]

operations = [
migrations.AlterField(
model_name='hardwarelifecycle',
name='end_of_sale',
field=models.DateField(blank=True, null=True),
),
migrations.AlterField(
model_name='hardwarelifecycle',
name='end_of_support',
field=models.DateField(blank=True, null=True),
),
]
4 changes: 2 additions & 2 deletions netbox_lifecycle/models/hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ class HardwareLifecycle(PrimaryModel):
ct_field='assigned_object_type', fk_field='assigned_object_id'
)

end_of_sale = models.DateField()
end_of_sale = models.DateField(blank=True, null=True)
end_of_maintenance = models.DateField(blank=True, null=True)
end_of_security = models.DateField(blank=True, null=True)
last_contract_attach = models.DateField(blank=True, null=True)
last_contract_renewal = models.DateField(blank=True, null=True)
end_of_support = models.DateField()
end_of_support = models.DateField(blank=True, null=True)

notice = models.CharField(max_length=500, blank=True, null=True)
documentation = models.CharField(max_length=500, blank=True, null=True)
Expand Down
2 changes: 2 additions & 0 deletions netbox_lifecycle/tables/hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class Meta(NetBoxTable.Meta):
'end_of_maintenance',
'end_of_security',
'end_of_support',
'last_contract_attach',
'last_contract_renewal',
'description',
'comments',
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,27 +35,27 @@ <h5 class="card-header">Dates</h5>
<table class="table table-hover attr-table">
<tr>
<td>End of Sale</td>
<td><span {{ object.end_of_sale|date_badge_class }}>{{ object.end_of_sale }}</span></td>
<td><span {{ object.end_of_sale|date_badge_class }}>{{ object.end_of_sale|placeholder }}</span></td>
</tr>
<tr>
<td>End of Maintenance Updates</td>
<td><span {{ object.end_of_maintenance|date_badge_class }}>{{ object.end_of_maintenance }}</span></td>
<td><span {{ object.end_of_maintenance|date_badge_class }}>{{ object.end_of_maintenance|placeholder }}</span></td>
</tr>
<tr>
<td>End of Security Updates</td>
<td><span {{ object.end_of_security|date_badge_class }}>{{ object.end_of_security }}</span></td>
<td><span {{ object.end_of_security|date_badge_class }}>{{ object.end_of_security|placeholder }}</span></td>
</tr>
<tr>
<td>Last Support Contract Attach</td>
<td><span {{ object.last_contract_attach|date_badge_class }}>{{ object.last_contract_attach }}</span></td>
<td><span {{ object.last_contract_attach|date_badge_class }}>{{ object.last_contract_attach|placeholder }}</span></td>
</tr>
<tr>
<td>Last Support Contract Renewal</td>
<td><span {{ object.last_contract_renewal|date_badge_class }}>{{ object.last_contract_renewal }}</span></td>
<td><span {{ object.last_contract_renewal|date_badge_class }}>{{ object.last_contract_renewal|placeholder }}</span></td>
</tr>
<tr>
<td>End of Support</td>
<td><span {{ object.end_of_support|date_badge_class }}>{{ object.end_of_support }}</span></td>
<td><span {{ object.end_of_support|date_badge_class }}>{{ object.end_of_support|placeholder }}</span></td>
</tr>
</table>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,27 @@ <h5 class="card-header">Lifecycle Dates
<table class="table table-hover attr-table">
<tr>
<td>End of Sale</td>
<td><span {{ lifecycle_info.end_of_sale|date_badge_class }}>{{ lifecycle_info.end_of_sale }}</span></td>
<td><span {{ lifecycle_info.end_of_sale|date_badge_class }}>{{ lifecycle_info.end_of_sale|placeholder }}</span></td>
</tr>
<tr>
<td>End of Maintenance Updates</td>
<td><span {{ lifecycle_info.end_of_maintenance|date_badge_class }}>{{ lifecycle_info.end_of_maintenance }}</span></td>
<td><span {{ lifecycle_info.end_of_maintenance|date_badge_class }}>{{ lifecycle_info.end_of_maintenance|placeholder }}</span></td>
</tr>
<tr>
<td>End of Security Updates</td>
<td><span {{ lifecycle_info.end_of_security|date_badge_class }}>{{ lifecycle_info.end_of_security }}</span></td>
<td><span {{ lifecycle_info.end_of_security|date_badge_class }}>{{ lifecycle_info.end_of_security|placeholder }}</span></td>
</tr>
<tr>
<td>Last Support Contract Attach</td>
<td><span {{ lifecycle_info.last_contract_attach|date_badge_class }}>{{ lifecycle_info.last_contract_attach }}</span></td>
<td><span {{ lifecycle_info.last_contract_attach|date_badge_class }}>{{ lifecycle_info.last_contract_attach|placeholder }}</span></td>
</tr>
<tr>
<td>Last Support Contract Renewal</td>
<td><span {{ lifecycle_info.last_contract_renewal|date_badge_class }}>{{ lifecycle_info.last_contract_renewal }}</span></td>
<td><span {{ lifecycle_info.last_contract_renewal|date_badge_class }}>{{ lifecycle_info.last_contract_renewal|placeholder }}</span></td>
</tr>
<tr>
<td>End of Support</td>
<td><span {{ lifecycle_info.end_of_support|date_badge_class }}>{{ lifecycle_info.end_of_support }}</span></td>
<td><span {{ lifecycle_info.end_of_support|date_badge_class }}>{{ lifecycle_info.end_of_support|placeholder }}</span></td>
</tr>
</table>
{% else %}
Expand Down
73 changes: 73 additions & 0 deletions netbox_lifecycle/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,3 +485,76 @@ def setUpTestData(cls):
'end_of_support': '2050-01-01',
},
]

# Additional device types for null date tests
cls.device_type_for_null_test = DeviceType.objects.create(
model='Device Type Null Test',
manufacturer=manufacturer,
slug='device-type-null',
)
cls.device_type_for_explicit_null = DeviceType.objects.create(
model='Device Type Explicit Null',
manufacturer=manufacturer,
slug='device-type-explicit-null',
)
cls.device_type_for_update_null = DeviceType.objects.create(
model='Device Type Update Null',
manufacturer=manufacturer,
slug='device-type-update-null',
)

def test_create_lifecycle_with_omitted_dates(self):
"""Test creating a hardware lifecycle with omitted date fields."""
self.add_permissions('netbox_lifecycle.add_hardwarelifecycle')
url = reverse('plugins-api:netbox_lifecycle-api:hardwarelifecycle-list')
data = {
'assigned_object_id': self.device_type_for_null_test.pk,
'assigned_object_type': 'dcim.devicetype',
}
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertIsNone(response.data['end_of_sale'])
self.assertIsNone(response.data['end_of_support'])
self.assertIsNone(response.data['end_of_maintenance'])
self.assertIsNone(response.data['end_of_security'])

def test_create_lifecycle_with_explicit_null_dates(self):
"""Test creating a hardware lifecycle with explicit null values for dates."""
self.add_permissions('netbox_lifecycle.add_hardwarelifecycle')
url = reverse('plugins-api:netbox_lifecycle-api:hardwarelifecycle-list')
data = {
'assigned_object_id': self.device_type_for_explicit_null.pk,
'assigned_object_type': 'dcim.devicetype',
'end_of_sale': None,
'end_of_support': None,
'end_of_maintenance': None,
'end_of_security': None,
}
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertIsNone(response.data['end_of_sale'])
self.assertIsNone(response.data['end_of_support'])
self.assertIsNone(response.data['end_of_maintenance'])
self.assertIsNone(response.data['end_of_security'])

def test_update_lifecycle_dates_to_null(self):
"""Test updating a hardware lifecycle to set dates to null."""
self.add_permissions('netbox_lifecycle.change_hardwarelifecycle')
# Create lifecycle with dates
lifecycle = HardwareLifecycle.objects.create(
assigned_object=self.device_type_for_update_null,
end_of_sale='2030-01-01',
end_of_support='2035-01-01',
)
url = reverse(
'plugins-api:netbox_lifecycle-api:hardwarelifecycle-detail',
kwargs={'pk': lifecycle.pk},
)
data = {
'end_of_sale': None,
'end_of_support': None,
}
response = self.client.patch(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertIsNone(response.data['end_of_sale'])
self.assertIsNone(response.data['end_of_support'])
66 changes: 65 additions & 1 deletion netbox_lifecycle/tests/test_forms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.test import TestCase

from dcim.models import Device, Manufacturer
from dcim.models import Device, DeviceType, Manufacturer
from utilities.testing import create_test_device

from netbox_lifecycle.forms import *
Expand Down Expand Up @@ -164,3 +164,67 @@ def test_assignment_with_device_and_license_with_different_device(self):
self.assertFalse(form.is_valid())
with self.assertRaises(ValueError):
form.save()


class HardwareLifecycleTestCase(TestCase):

@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(
name='Manufacturer', slug='manufacturer'
)
cls.device_type = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
)
cls.device_type_2 = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'
)
cls.device_type_3 = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 3', slug='device-type-3'
)

def test_lifecycle_with_all_dates(self):
"""Test creating a hardware lifecycle with all date fields populated."""
form = HardwareLifecycleForm(
data={
'device_type': self.device_type.pk,
'end_of_sale': '2030-01-01',
'end_of_support': '2035-01-01',
'end_of_maintenance': '2032-01-01',
'end_of_security': '2033-01-01',
'last_contract_attach': '2025-01-01',
'last_contract_renewal': '2028-01-01',
}
)
self.assertTrue(form.is_valid())
instance = form.save()
self.assertEqual(str(instance.end_of_sale), '2030-01-01')
self.assertEqual(str(instance.end_of_support), '2035-01-01')

def test_lifecycle_with_no_dates(self):
"""Test creating a hardware lifecycle with no date fields (all null)."""
form = HardwareLifecycleForm(
data={
'device_type': self.device_type_2.pk,
}
)
self.assertTrue(form.is_valid())
instance = form.save()
self.assertIsNone(instance.end_of_sale)
self.assertIsNone(instance.end_of_support)
self.assertIsNone(instance.end_of_maintenance)
self.assertIsNone(instance.end_of_security)

def test_lifecycle_with_partial_dates(self):
"""Test creating a hardware lifecycle with only some date fields."""
form = HardwareLifecycleForm(
data={
'device_type': self.device_type_3.pk,
'end_of_sale': '2030-01-01',
# end_of_support intentionally omitted
}
)
self.assertTrue(form.is_valid())
instance = form.save()
self.assertEqual(str(instance.end_of_sale), '2030-01-01')
self.assertIsNone(instance.end_of_support)