diff --git a/src/netbox_slm/api/serializers.py b/src/netbox_slm/api/serializers.py index f76263c..05a4ec3 100644 --- a/src/netbox_slm/api/serializers.py +++ b/src/netbox_slm/api/serializers.py @@ -5,7 +5,7 @@ from netbox.api.serializers import PrimaryModelSerializer, OrganizationalModelSe # NestedRecordSerializer, # NestedNameServerSerializer, # ) -from netbox_slm.models import SoftwareProduct +from netbox_slm.models import SoftwareProduct, SoftwareProductVersion class SoftwareProductSerializer(PrimaryModelSerializer): @@ -22,3 +22,19 @@ class SoftwareProductSerializer(PrimaryModelSerializer): def get_display(self, obj): return obj.name + + +class SoftwareProductVersionSerializer(PrimaryModelSerializer): + display = serializers.SerializerMethodField() + url = serializers.HyperlinkedIdentityField( + view_name="plugins-api:netbox_slm-api:softwareproductversion-detail" + ) + + class Meta: + model = SoftwareProductVersion + fields = [ + 'id', 'display', 'url', 'name', 'software_product', 'tags', 'custom_field_data', 'created', 'last_updated', + ] + + def get_display(self, obj): + return obj.name diff --git a/src/netbox_slm/api/urls.py b/src/netbox_slm/api/urls.py index df53e80..b68ab3a 100644 --- a/src/netbox_slm/api/urls.py +++ b/src/netbox_slm/api/urls.py @@ -1,11 +1,13 @@ from netbox.api import OrderedDefaultRouter from netbox_slm.api.views import ( NetboxSLMRootView, - SoftwareProductViewSet + SoftwareProductViewSet, + SoftwareProductVersionViewSet, ) router = OrderedDefaultRouter() router.APIRootView = NetboxSLMRootView router.register("softwareproducts", SoftwareProductViewSet) +router.register("softwareproductversions", SoftwareProductVersionViewSet) urlpatterns = router.urls \ No newline at end of file diff --git a/src/netbox_slm/api/views.py b/src/netbox_slm/api/views.py index c75c245..5488879 100644 --- a/src/netbox_slm/api/views.py +++ b/src/netbox_slm/api/views.py @@ -4,9 +4,9 @@ from rest_framework.response import Response from rest_framework.routers import APIRootView from extras.api.views import CustomFieldModelViewSet -from netbox_slm.api.serializers import SoftwareProductSerializer -from netbox_slm.filters import SoftwareProductFilter -from netbox_slm.models import SoftwareProduct +from netbox_slm.api.serializers import SoftwareProductSerializer, SoftwareProductVersionSerializer +from netbox_slm.filters import SoftwareProductFilter, SoftwareProductVersionFilter +from netbox_slm.models import SoftwareProduct, SoftwareProductVersion class NetboxSLMRootView(APIRootView): @@ -29,4 +29,16 @@ class SoftwareProductViewSet(CustomFieldModelViewSet): # serializer = RecordSerializer(records, many=True, context={"request": request}) # return Response(serializer.data) + +class SoftwareProductVersionViewSet(CustomFieldModelViewSet): + queryset = SoftwareProductVersion.objects.all() + serializer_class = SoftwareProductVersionSerializer + filterset_class = SoftwareProductVersionFilter + + # @action(detail=True, methods=["get"]) + # def records(self, request, pk=None): + # records = Record.objects.filter(zone=pk) + # serializer = RecordSerializer(records, many=True, context={"request": request}) + # return Response(serializer.data) + # for reference: https://github.com/auroraresearchlab/netbox-dns/blob/main/netbox_dns/api/views.py \ No newline at end of file diff --git a/src/netbox_slm/filters.py b/src/netbox_slm/filters.py index e82c0a1..d92c8cb 100644 --- a/src/netbox_slm/filters.py +++ b/src/netbox_slm/filters.py @@ -1,17 +1,22 @@ import django_filters from django.db.models import Q +from django.utils.translation import gettext as _ from extras.filters import TagFilter from netbox.filtersets import PrimaryModelFilterSet -from netbox_slm.models import SoftwareProduct, SoftwareProductVersion +from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation +from utilities.forms import DynamicModelMultipleChoiceField -class SoftwareProductFilter(PrimaryModelFilterSet): - """Filter capabilities for SoftwareProduct instances.""" - +class BaseFilter(PrimaryModelFilterSet): q = django_filters.CharFilter( method="search", label="Search", ) + + +class SoftwareProductFilter(BaseFilter): + """Filter capabilities for SoftwareProduct instances.""" + name = django_filters.CharFilter( lookup_expr="icontains", ) @@ -29,7 +34,35 @@ class SoftwareProductFilter(PrimaryModelFilterSet): return queryset.filter(qs_filter) -class SoftwareProductVersionFilter(SoftwareProductFilter): +class SoftwareProductVersionFilter(BaseFilter): + name = django_filters.CharFilter( + lookup_expr="icontains", + ) + # software_product_id = django_filters.ModelMultipleChoiceFilter( + # queryset=SoftwareProduct.objects.all(), + # label='SoftwareProduct (ID)', + # ) + # software_product = django_filters.ModelMultipleChoiceFilter( + # field_name='software_product__name', + # queryset=SoftwareProduct.objects.all(), + # to_field_name='name', + # label='SoftwareProduct (name)', + # ) + # tag = TagFilter() + class Meta: model = SoftwareProductVersion - fields = ("name",) # "tag") + fields = ("name", "software_product") # "tag") + + def search(self, queryset, name, value): + """Perform the filtered search.""" + if not value.strip(): + return queryset + qs_filter = Q(name__icontains=value) # | Q(status__icontains=value) + return queryset.filter(qs_filter) + + +class SoftwareProductInstallationFilter(BaseFilter): + class Meta: + model = SoftwareProductInstallation + fields = tuple() # "tag") diff --git a/src/netbox_slm/forms.py b/src/netbox_slm/forms.py index 60b23cc..bc4fc8b 100644 --- a/src/netbox_slm/forms.py +++ b/src/netbox_slm/forms.py @@ -1,5 +1,6 @@ from django import forms from django.urls import reverse_lazy +from django.utils.translation import gettext as _ from extras.forms import ( CustomFieldModelForm, CustomFieldModelCSVForm, @@ -7,8 +8,8 @@ from extras.forms import ( CustomFieldModelBulkEditForm, CustomFieldModelFilterForm, ) -from dcim.models import Manufacturer -from netbox_slm.models import SoftwareProduct, SoftwareProductVersion +from dcim.models import Manufacturer, Device +from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation from utilities.forms import ( BootstrapMixin, DynamicModelChoiceField, APISelect, DynamicModelMultipleChoiceField ) @@ -120,3 +121,73 @@ class SoftwareProductVersionBulkEditForm(BootstrapMixin, AddRemoveTagsForm, Cust class Meta: nullable_fields = [] + + +class SoftwareProductInstallationForm(BootstrapMixin, CustomFieldModelForm): + """Form for creating a new SoftwareProductInstallation object.""" + + device = DynamicModelChoiceField( + queryset=Device.objects.all(), + required=False, + # initial_params={ + # 'device_types': 'device_type' + # } + ) + software_product = DynamicModelChoiceField( + queryset=SoftwareProduct.objects.all(), + required=False, + widget=APISelect( + attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")} + ), + ) + version = DynamicModelChoiceField( + queryset=SoftwareProductVersion.objects.all(), + required=False, + widget=APISelect( + attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")} + ), + query_params={ + 'software_product': '$software_product', + } + ) + + # todo need version and device ? + + # tags = DynamicModelMultipleChoiceField( + # queryset=Tag.objects.all(), + # required=False, + # ) + + class Meta: + model = SoftwareProductInstallation + fields = ("device", "software_product", "version",) # "tags") + + # def clean(self): + # import pdb;pdb.set_trace() + # return super(SoftwareProductInstallationForm, self).clean() + + +class SoftwareProductInstallationFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + """Form for filtering SoftwareProductInstallation instances.""" + + model = SoftwareProductInstallation + + q = forms.CharField(required=False, label="Search") + + # tag = TagFilterField(SoftwareProduct) + + +class SoftwareProductInstallationCSVForm(CustomFieldModelCSVForm): + class Meta: + model = SoftwareProductInstallation + fields = tuple() + + +class SoftwareProductInstallationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=SoftwareProductInstallation.objects.all(), + widget=forms.MultipleHiddenInput(), + ) + + class Meta: + nullable_fields = [] diff --git a/src/netbox_slm/migrations/0002_softwareproductinstallation.py b/src/netbox_slm/migrations/0002_softwareproductinstallation.py new file mode 100644 index 0000000..6ec95ca --- /dev/null +++ b/src/netbox_slm/migrations/0002_softwareproductinstallation.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.8 on 2021-12-08 12:10 + +import django.core.serializers.json +from django.db import migrations, models +import django.db.models.deletion +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0062_clear_secrets_changelog'), + ('dcim', '0133_port_colors'), + ('netbox_slm', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='SoftwareProductInstallation', + fields=[ + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('device', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='softwareproduct_installations', to='dcim.device')), + ('software_product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='software_products', to='netbox_slm.softwareproduct')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ('version', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='softwareproduct_versions', to='netbox_slm.softwareproductversion')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/src/netbox_slm/models.py b/src/netbox_slm/models.py index 5a2857c..43b0677 100644 --- a/src/netbox_slm/models.py +++ b/src/netbox_slm/models.py @@ -44,3 +44,35 @@ class SoftwareProductVersion(PrimaryModel): def get_absolute_url(self): return reverse("plugins:netbox_slm:softwareproductversion", kwargs={"pk": self.pk}) + + +# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') +class SoftwareProductInstallation(PrimaryModel): + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.PROTECT, + related_name='softwareproduct_installations' + ) + # virtualmachine = models.ForeignKey( + # to='virtualization.VirtualMachine', + # on_delete=models.PROTECT, + # related_name='softwareproduct_installations' + # ) + software_product = models.ForeignKey( + to='netbox_slm.SoftwareProduct', + on_delete=models.PROTECT, + related_name='software_products' + ) + version = models.ForeignKey( + to='netbox_slm.SoftwareProductVersion', + on_delete=models.PROTECT, + related_name='softwareproduct_versions' + ) + + objects = RestrictedQuerySet.as_manager() + + def __str__(self): + return f"{self.pk}" + + def get_absolute_url(self): + return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk}) diff --git a/src/netbox_slm/navigation.py b/src/netbox_slm/navigation.py index e461fbd..d34579b 100644 --- a/src/netbox_slm/navigation.py +++ b/src/netbox_slm/navigation.py @@ -42,4 +42,24 @@ menu_items = ( ), ) ), + PluginMenuItem( + link='plugins:netbox_slm:softwareproductinstallation_list', + link_text='Installations', + buttons=( + PluginMenuButton( + "plugins:netbox_slm:softwareproductinstallation_add", + "Add", + "mdi mdi-plus-thick", + ButtonColorChoices.GREEN, + permissions=["netbox_slm.add_softwareproductinstallation"], + ), + PluginMenuButton( + "plugins:netbox_slm:softwareproductinstallation_import", + "Import", + "mdi mdi-upload", + ButtonColorChoices.CYAN, + permissions=["netbox_slm.add_softwareproductinstallation"], + ), + ) + ), ) diff --git a/src/netbox_slm/tables.py b/src/netbox_slm/tables.py index 3b50c86..274b7ef 100644 --- a/src/netbox_slm/tables.py +++ b/src/netbox_slm/tables.py @@ -1,6 +1,6 @@ import django_tables2 as tables from django_tables2.utils import Accessor -from netbox_slm.models import SoftwareProduct, SoftwareProductVersion +from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation from utilities.tables import BaseTable, ChoiceFieldColumn, ToggleColumn @@ -68,3 +68,44 @@ class SoftwareProductVersionTable(BaseTable): "manufacturer", # "tags", ) + + +class SoftwareProductInstallationTable(BaseTable): + """Table for displaying SoftwareProductInstallation objects.""" + + pk = ToggleColumn() + name = tables.LinkColumn() + device = tables.Column( + accessor=Accessor('device'), + linkify=True + ) + software_product = tables.Column( + accessor=Accessor('software_product'), + linkify=True + ) + version = tables.Column( + accessor=Accessor('version'), + linkify=True + ) + + # tags = TagColumn( + # url_name="plugins:netbox_dns:zone_list", + # ) + + class Meta(BaseTable.Meta): + model = SoftwareProductInstallation + fields = ( + "pk", + "name", + "device", + "software_product", + "version", + # "tags", + ) + default_columns = ( + "pk", + "device", + "software_product", + "version", + # "tags", + ) diff --git a/src/netbox_slm/templates/netbox_slm/softwareproductinstallation.html b/src/netbox_slm/templates/netbox_slm/softwareproductinstallation.html new file mode 100644 index 0000000..7ad8300 --- /dev/null +++ b/src/netbox_slm/templates/netbox_slm/softwareproductinstallation.html @@ -0,0 +1,82 @@ +{% extends 'netbox_slm/object.html' %} +{% load helpers %} +{% load plugins %} +{% load render_table from django_tables2 %} +{% load view_helpers %} +{% load perms %} + +{% block extra_controls %} +{{ block.super }} +{% endblock %} + +{% block content %} +
+
+
+
+ Software Product +
+
+ + + + + + + + + +
Installation{{ object }}
bla + + + + + +
+
+
+ {% include 'inc/custom_fields_panel.html' %} +
+
+ {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/src/netbox_slm/urls.py b/src/netbox_slm/urls.py index 85374ea..8adce3e 100644 --- a/src/netbox_slm/urls.py +++ b/src/netbox_slm/urls.py @@ -1,7 +1,7 @@ from django.urls import path from extras.views import ObjectChangeLogView from netbox_slm import views -from netbox_slm.models import SoftwareProduct, SoftwareProductVersion +from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation urlpatterns = [ # Software Products @@ -35,4 +35,20 @@ urlpatterns = [ name="softwareproductversion_changelog", kwargs={"model": SoftwareProductVersion}, ), + + # Software Product Versions + path("installations/", views.SoftwareProductInstallationListView.as_view(), name='softwareproductinstallation_list'), + path("installations/add/", views.SoftwareProductInstallationEditView.as_view(), name='softwareproductinstallation_add'), + path("installations/import/", views.SoftwareProductInstallationBulkImportView.as_view(), name="softwareproductinstallation_import"), + path("installations/edit/", views.SoftwareProductInstallationBulkEditView.as_view(), name="softwareproductinstallation_bulk_edit"), + path("installations/delete/", views.SoftwareProductInstallationBulkDeleteView.as_view(), name="softwareproductinstallation_bulk_delete"), + path("installations//", views.SoftwareProductInstallationView.as_view(), name="softwareproductinstallation"), + path("installations//delete/", views.SoftwareProductInstallationDeleteView.as_view(), name="softwareproductinstallation_delete"), + path("installations//edit/", views.SoftwareProductInstallationEditView.as_view(), name="softwareproductinstallation_edit"), + path( + "installations//changelog/", + ObjectChangeLogView.as_view(), + name="softwareproductinstallation_changelog", + kwargs={"model": SoftwareProductInstallation}, + ), ] diff --git a/src/netbox_slm/views.py b/src/netbox_slm/views.py index 9173f6e..82b96e4 100644 --- a/src/netbox_slm/views.py +++ b/src/netbox_slm/views.py @@ -1,14 +1,15 @@ from netbox.views import generic -from netbox_slm.filters import SoftwareProductFilter, SoftwareProductVersionFilter +from netbox_slm.filters import SoftwareProductFilter, SoftwareProductVersionFilter, SoftwareProductInstallationFilter from netbox_slm.forms import ( SoftwareProductForm, SoftwareProductFilterForm, SoftwareProductCSVForm, SoftwareProductBulkEditForm, SoftwareProductVersionForm, SoftwareProductVersionFilterForm, SoftwareProductVersionCSVForm, - SoftwareProductVersionBulkEditForm + SoftwareProductVersionBulkEditForm, SoftwareProductInstallationForm, SoftwareProductInstallationFilterForm, + SoftwareProductInstallationCSVForm, SoftwareProductInstallationBulkEditForm ) from netbox_slm.models import ( - SoftwareProduct, SoftwareProductVersion + SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation ) -from netbox_slm.tables import SoftwareProductTable, SoftwareProductVersionTable +from netbox_slm.tables import SoftwareProductTable, SoftwareProductVersionTable, SoftwareProductInstallationTable class SoftwareProductListView(generic.ObjectListView): @@ -111,3 +112,54 @@ class SoftwareProductVersionBulkEditView(generic.BulkEditView): class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView): queryset = SoftwareProductVersion.objects.all() table = SoftwareProductVersionTable + + +class SoftwareProductInstallationListView(generic.ObjectListView): + """View for listing all existing SoftwareProductInstallations.""" + + queryset = SoftwareProductInstallation.objects.all() + filterset = SoftwareProductInstallationFilter + filterset_form = SoftwareProductInstallationFilterForm + table = SoftwareProductInstallationTable + template_name = "netbox_slm/object_list.html" + + +class SoftwareProductInstallationView(generic.ObjectView): + """Display SoftwareProductInstallation details""" + + queryset = SoftwareProductVersion.objects.all() + + # def get_extra_context(self, request, instance): + # records = instance.record_set.all() + # return {"records": records} + + +class SoftwareProductInstallationEditView(generic.ObjectEditView): + """View for editing and creating a SoftwareProductInstallation instance.""" + + queryset = SoftwareProductInstallation.objects.all() + model_form = SoftwareProductInstallationForm + + +class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView): + """View for deleting a SoftwareProductInstallation instance""" + + queryset = SoftwareProductInstallation.objects.all() + + +class SoftwareProductInstallationBulkImportView(generic.BulkImportView): + queryset = SoftwareProductInstallation.objects.all() + model_form = SoftwareProductInstallationCSVForm + table = SoftwareProductInstallationTable + + +class SoftwareProductInstallationBulkEditView(generic.BulkEditView): + queryset = SoftwareProductInstallation.objects.all() + filterset = SoftwareProductInstallationFilter + table = SoftwareProductInstallationTable + form = SoftwareProductInstallationBulkEditForm + + +class SoftwareProductInstallationBulkDeleteView(generic.BulkDeleteView): + queryset = SoftwareProductInstallation.objects.all() + table = SoftwareProductInstallationTable