Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Hardware Replacement Tracker #206

Open
wants to merge 2 commits into
base: ltm-1.6
Choose a base branch
from
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
38 changes: 38 additions & 0 deletions nautobot_device_lifecycle_mgmt/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
ContractLCM,
DeviceSoftwareValidationResult,
HardwareLCM,
HardwareReplacementLCM,
InventoryItemSoftwareValidationResult,
ProviderLCM,
SoftwareImageLCM,
Expand Down Expand Up @@ -368,3 +369,40 @@ class Meta: # pylint: disable=too-few-public-methods
"valid_software",
"url",
]


class HardwareReplacementLCMSerializer(*serializer_base_classes): # pylint: disable=R0901,too-few-public-methods
"""API serializer."""

url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:nautobot_device_lifecycle_mgmt-api:hardwarereplacementlcm-detail"
)
current_device_type = NestedDeviceTypeSerializer(
many=False, read_only=False, required=False, help_text="Current Device Type for Hardware Replacement"
)
replacement_device_type = NestedDeviceTypeSerializer(
many=False, read_only=False, required=False, help_text="Replacement Device Type for Hardware Replacement"
)
current_inventory_item = NestedInventoryItemSerializer(
many=False, read_only=False, required=False, help_text="Current Inventory Item for Hardware Replacement"
)
replacement_inventory_item = NestedInventoryItemSerializer(
many=False, read_only=False, required=False, help_text="Replacement Inventory Item for Hardware Replacement"
)

class Meta: # pylint: disable=too-few-public-methods
"""Meta attributes."""

model = HardwareReplacementLCM
fields = [
"url",
"current_device_type",
"current_inventory_item",
"replacement_device_type",
"replacement_inventory_item",
"device_roles",
"object_tags",
"valid_since",
"valid_until",
"use_case",
]
2 changes: 2 additions & 0 deletions nautobot_device_lifecycle_mgmt/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
VulnerabilityLCMViewSet,
DeviceSoftwareValidationResultListViewSet,
InventoryItemSoftwareValidationResultListViewSet,
HardwareReplacementLCMListViewSet,
)

router = routers.DefaultRouter()
Expand All @@ -28,6 +29,7 @@
router.register(r"vulnerability", VulnerabilityLCMViewSet)
router.register(r"device-validated-software-result", DeviceSoftwareValidationResultListViewSet)
router.register(r"inventory-item-validated-software-result", InventoryItemSoftwareValidationResultListViewSet)
router.register(r"hardware-replacement", HardwareReplacementLCMListViewSet)

app_name = "nautobot_device_lifecycle_mgmt"

Expand Down
11 changes: 11 additions & 0 deletions nautobot_device_lifecycle_mgmt/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
VulnerabilityLCM,
DeviceSoftwareValidationResult,
InventoryItemSoftwareValidationResult,
HardwareReplacementLCM,
)
from nautobot_device_lifecycle_mgmt.filters import (
HardwareLCMFilterSet,
Expand All @@ -28,6 +29,7 @@
VulnerabilityLCMFilterSet,
DeviceSoftwareValidationResultFilterSet,
InventoryItemSoftwareValidationResultFilterSet,
HardwareReplacementLCMFilterSet,
)

from .serializers import (
Expand All @@ -42,6 +44,7 @@
VulnerabilityLCMSerializer,
DeviceSoftwareValidationResultSerializer,
InventoryItemSoftwareValidationResultSerializer,
HardwareReplacementLCMSerializer,
)


Expand Down Expand Up @@ -140,3 +143,11 @@ class InventoryItemSoftwareValidationResultListViewSet(CustomFieldModelViewSet):

# Disabling POST as these should only be created via Job.
http_method_names = ["get", "head", "options"]


class HardwareReplacementLCMListViewSet(CustomFieldModelViewSet):
"""REST API viewset for HardwareReplacementLCM records."""

queryset = HardwareReplacementLCM.objects.all()
serializer_class = HardwareReplacementLCMSerializer
filterset_class = HardwareReplacementLCMFilterSet
71 changes: 71 additions & 0 deletions nautobot_device_lifecycle_mgmt/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
ContractLCM,
DeviceSoftwareValidationResult,
HardwareLCM,
HardwareReplacementLCM,
InventoryItemSoftwareValidationResult,
ProviderLCM,
SoftwareImageLCM,
Expand Down Expand Up @@ -856,3 +857,73 @@ def search(self, queryset, name, value): # pylint: disable=unused-argument, no-
| Q(inventory_item__name__icontains=value)
)
return queryset.filter(qs_filter)


class HardwareReplacementLCMFilterSet(NautobotFilterSet):
"""Filter for HardwareReplacementLCM."""

current_device_type = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(),
field_name="current_device_type__model",
to_field_name="model",
label="Current Device Type",
)
replacement_device_type = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(),
field_name="replacement_device_type__model",
to_field_name="model",
label="Replacement Device Type",
)
current_inventory_item = django_filters.ModelMultipleChoiceFilter(
queryset=InventoryItem.objects.all(),
field_name="current_inventory_type__slug",
to_field_name="slug",
label="Current Inventory Item",
)
replacement_inventory_item = django_filters.ModelMultipleChoiceFilter(
queryset=InventoryItem.objects.all(),
field_name="replacement_inventory_type__slug",
to_field_name="slug",
label="Replacement Inventory Item",
)
device_roles = django_filters.ModelMultipleChoiceFilter(
field_name="device_roles__slug",
queryset=DeviceRole.objects.all(),
to_field_name="slug",
label="Device Roles (slug)",
)
object_tags = django_filters.ModelMultipleChoiceFilter(
field_name="object_tags__slug",
queryset=Tag.objects.all(),
to_field_name="slug",
label="Object Tags (slug)",
)
valid_since = django_filters.DateTimeFromToRangeFilter()
valid_until = django_filters.DateTimeFromToRangeFilter()
valid = django_filters.BooleanFilter(method="valid_search", label="Currently valid")

class Meta:
"""Meta attributes for filter."""

model = HardwareReplacementLCM

fields = [
"current_device_type",
"replacement_device_type",
"current_inventory_item",
"replacement_inventory_item",
"device_roles",
"object_tags",
"valid_since",
"valid_until",
"valid",
]

def valid_search(self, queryset, name, value): # pylint: disable=unused-argument, no-self-use
"""Perform the valid_search search."""
today = datetime.date.today()
if value is True:
qs_filter = Q(valid_since__lte=today, valid_until=None) | Q(valid_since__lte=today, valid_until__gte=today)
else:
qs_filter = Q(valid_since__gt=today) | Q(valid_until__lt=today)
return queryset.filter(qs_filter)
171 changes: 171 additions & 0 deletions nautobot_device_lifecycle_mgmt/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
ContractLCM,
DeviceSoftwareValidationResult,
HardwareLCM,
HardwareReplacementLCM,
InventoryItemSoftwareValidationResult,
ProviderLCM,
SoftwareImageLCM,
Expand Down Expand Up @@ -1213,3 +1214,173 @@ class Meta:
*VulnerabilityLCM.csv_headers,
"tags",
]


class HardwareReplacementLCMFilterForm(BootstrapMixin, forms.ModelForm):
"""Filter form to filter searches."""

current_device_type = DynamicModelMultipleChoiceField(
queryset=DeviceType.objects.all(),
to_field_name="model",
required=False,
)
current_inventory_item = DynamicModelMultipleChoiceField(
queryset=InventoryItem.objects.all(),
to_field_name="name",
required=False,
)
replacement_device_type = DynamicModelMultipleChoiceField(
queryset=DeviceType.objects.all(),
to_field_name="model",
required=False,
)
replacement_inventory_item = DynamicModelMultipleChoiceField(
queryset=InventoryItem.objects.all(),
to_field_name="name",
required=False,
)
device_roles = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
to_field_name="slug",
required=False,
)
object_tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
to_field_name="slug",
required=False,
)
valid_since_before = forms.DateField(label="Valid Since Date Before", required=False, widget=DatePicker())
valid_since_after = forms.DateField(label="Valid Since Date After", required=False, widget=DatePicker())
valid_until_before = forms.DateField(label="Valid Until Date Before", required=False, widget=DatePicker())
valid_until_after = forms.DateField(label="Valid Until Date After", required=False, widget=DatePicker())
valid = forms.BooleanField(
label="Valid Now", required=False, widget=StaticSelect2(choices=BOOLEAN_WITH_BLANK_CHOICES)
)

class Meta:
"""Meta attributes for the HardwareLCMFilterForm class."""

model = HardwareReplacementLCM
# Define the fields above for ordering and widget purposes
fields = [
"current_device_type",
"current_inventory_item",
"replacement_device_type",
"replacement_inventory_item",
"device_roles",
"object_tags",
"valid_since_before",
"valid_since_after",
"valid",
]


class HardwareReplacementLCMForm(BootstrapMixin, CustomFieldModelFormMixin, RelationshipModelFormMixin):
"""Hardware Device Lifecycle creation/edit form."""

current_device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all(), required=False, help_text="The device type that needs to be replaced"
)
current_inventory_item = DynamicModelChoiceField(
queryset=InventoryItem.objects.all(), required=False, help_text="The inventory item that needs to be replaced"
)
replacement_device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all(), required=False, help_text="The device type that will be the replacement"
)
replacement_inventory_item = DynamicModelChoiceField(
queryset=InventoryItem.objects.all(),
required=False,
help_text="The inventory item that will be the replacement",
)
device_roles = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
required=False,
help_text="Apply this replacement only to products with any of the following device role(s)",
)
object_tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False,
help_text="Apply this replacement only to products with any of the following object tag(s)",
)

def clean(self):
"""Validate the form ensuring that at least one current and replacement product was chosen and that the valid dates make sense."""
super().clean()
cleaned_data = self.cleaned_data
if cleaned_data.get("current_device_type"):
if not cleaned_data.get("replacement_device_type"):
self.add_error("replacement_device_type", "A replacement device type must be chosen.")
if cleaned_data.get("replacement_inventory_item"):
self.add_error(
"replacement_inventory_item",
"Inventory item cannot be selected as a replacement for a device type.",
)
elif cleaned_data.get("current_inventory_item"):
if not cleaned_data.get("replacement_inventory_item"):
self.add_error("replacement_inventory_item", "A replacement inventory item must be chosen.")
if cleaned_data.get("replacement_device_type"):
self.add_error(
"replacement_device_type", "Device Type cannot be selected as a replacement for an inventory item."
)
else:
msg = "One of the product types must be chosen for current product"
self.add_error("current_device_type", msg)
self.add_error("current_inventory_item", msg)
if cleaned_data.get("valid_until") and cleaned_data.get("valid_until") < cleaned_data.get("valid_since"):
self.add_error("valid_until", "Valid Until date must be after the Valid Since date.")

class Meta:
"""Meta attributes for the HardwareLCMForm class."""

model = HardwareReplacementLCM
fields = [
"current_device_type",
"current_inventory_item",
"device_roles",
"object_tags",
"replacement_device_type",
"replacement_inventory_item",
"use_case",
"valid_since",
"valid_until",
]

widgets = {
"valid_since": DatePicker(),
"valid_until": DatePicker(),
}


class HardwareReplacementLCMCSVForm(CustomFieldModelCSVForm):
"""Form for bulk creating HardwareReplacementLCM objects."""

current_device_type = CSVModelChoiceField(
queryset=DeviceType.objects.all(), required=False, to_field_name="slug", help_text="Current Device Type"
)
current_inventory_item = CSVModelChoiceField(
queryset=InventoryItem.objects.all(), required=False, to_field_name="slug", help_text="Current Inventory Item"
)
replacement_device_type = CSVModelChoiceField(
queryset=DeviceType.objects.all(), required=False, to_field_name="slug", help_text="Replacement Device Type"
)
replacement_inventory_item = CSVModelChoiceField(
queryset=InventoryItem.objects.all(),
required=False,
to_field_name="slug",
help_text="Replacement Inventory Item",
)
device_roles = CSVMultipleModelChoiceField(
queryset=DeviceRole.objects.all(),
required=False,
to_field_name="model",
help_text="Comma-separated list of DeviceRole Models",
)
object_tags = CSVMultipleModelChoiceField(
queryset=Tag.objects.all(), required=False, to_field_name="slug", help_text="Comma-separated list of Tag Slugs"
)

class Meta:
"""Meta attributes for the HardwareReplacementLCMCSVForm class."""

model = HardwareReplacementLCM
fields = HardwareReplacementLCM.csv_headers
Loading