diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py index c78ea81c7..c08b5473a 100644 --- a/netbox/circuits/models/circuits.py +++ b/netbox/circuits/models/circuits.py @@ -1,3 +1,4 @@ +from django.apps import apps from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from django.db import models @@ -136,6 +137,10 @@ class Circuit(NetBoxModel): def __str__(self): return self.cid + @classmethod + def get_prerequisite_models(cls): + return [apps.get_model('circuits.Provider'), CircuitType] + def get_absolute_url(self): return reverse('circuits:circuit', args=[self.pk]) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 136fcf6cf..092df3a0e 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -1,6 +1,8 @@ import decimal import yaml + +from django.apps import apps from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator @@ -159,6 +161,10 @@ class DeviceType(NetBoxModel): self._original_front_image = self.front_image self._original_rear_image = self.rear_image + @classmethod + def get_prerequisite_models(cls): + return [Manufacturer, ] + def get_absolute_url(self): return reverse('dcim:devicetype', args=[self.pk]) @@ -338,6 +344,10 @@ class ModuleType(NetBoxModel): def __str__(self): return self.model + @classmethod + def get_prerequisite_models(cls): + return [Manufacturer, ] + def get_absolute_url(self): return reverse('dcim:moduletype', args=[self.pk]) @@ -658,6 +668,10 @@ class Device(NetBoxModel, ConfigContextModel): return f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})' return super().__str__() + @classmethod + def get_prerequisite_models(cls): + return [apps.get_model('dcim.Site'), DeviceRole, DeviceType, ] + def get_absolute_url(self): return reverse('dcim:device', args=[self.pk]) diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index c275691c0..83eead67f 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -1,3 +1,4 @@ +from django.apps import apps from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator @@ -54,6 +55,10 @@ class PowerPanel(NetBoxModel): def __str__(self): return self.name + @classmethod + def get_prerequisite_models(cls): + return [apps.get_model('dcim.Site'), ] + def get_absolute_url(self): return reverse('dcim:powerpanel', args=[self.pk]) @@ -138,6 +143,10 @@ class PowerFeed(NetBoxModel, PathEndpoint, CabledObjectModel): def __str__(self): return self.name + @classmethod + def get_prerequisite_models(cls): + return [PowerPanel, ] + def get_absolute_url(self): return reverse('dcim:powerfeed', args=[self.pk]) diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 50c91b52e..22fca8cf6 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -1,5 +1,6 @@ import decimal +from django.apps import apps from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.models import ContentType @@ -201,6 +202,10 @@ class Rack(NetBoxModel): return f'{self.name} ({self.facility_id})' return self.name + @classmethod + def get_prerequisite_models(cls): + return [apps.get_model('dcim.Site'), ] + def get_absolute_url(self): return reverse('dcim:rack', args=[self.pk]) @@ -477,6 +482,10 @@ class RackReservation(NetBoxModel): def __str__(self): return "Reservation for rack {}".format(self.rack) + @classmethod + def get_prerequisite_models(cls): + return [apps.get_model('dcim.Site'), Rack, ] + def get_absolute_url(self): return reverse('dcim:rackreservation', args=[self.pk]) diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index 67bcc6e4c..f5c8e6d9d 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -411,6 +411,10 @@ class Location(NestedGroupModel): super().validate_unique(exclude=exclude) + @classmethod + def get_prerequisite_models(cls): + return [Site, ] + def get_absolute_url(self): return reverse('dcim:location', args=[self.pk]) diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 26cee8100..2d3f4d291 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -124,6 +124,10 @@ class ASN(NetBoxModel): def __str__(self): return f'AS{self.asn_with_asdot}' + @classmethod + def get_prerequisite_models(cls): + return [RIR, ] + def get_absolute_url(self): return reverse('ipam:asn', args=[self.pk]) @@ -185,6 +189,10 @@ class Aggregate(GetAvailablePrefixesMixin, NetBoxModel): def __str__(self): return str(self.prefix) + @classmethod + def get_prerequisite_models(cls): + return [RIR, ] + def get_absolute_url(self): return reverse('ipam:aggregate', args=[self.pk]) diff --git a/netbox/ipam/models/l2vpn.py b/netbox/ipam/models/l2vpn.py index 0e948b18e..809007033 100644 --- a/netbox/ipam/models/l2vpn.py +++ b/netbox/ipam/models/l2vpn.py @@ -1,3 +1,4 @@ +from django.apps import apps from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError @@ -103,6 +104,10 @@ class L2VPNTermination(NetBoxModel): return f'{self.assigned_object} <> {self.l2vpn}' return super().__str__() + @classmethod + def get_prerequisite_models(cls): + return [apps.get_model('ipam.L2VPN'), ] + def get_absolute_url(self): return reverse('ipam:l2vpntermination', args=[self.pk]) diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index b9d585952..4c65094ca 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -28,6 +28,14 @@ class NetBoxFeatureSet( class Meta: abstract = True + @classmethod + def get_prerequisite_models(cls): + """ + Return a list of model types that are required to create this model or empty list if none. This is used for + showing prequisite warnings in the UI on the list and detail views. + """ + return [] + # # Base model classes diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 60ad4a2cc..7340ea2a0 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -26,6 +26,7 @@ from utilities.permissions import get_permission_for_model from utilities.views import GetReturnURLMixin from .base import BaseMultiObjectView from .mixins import ActionsMixin, TableMixin +from .utils import get_prerequisite_model __all__ = ( 'BulkComponentCreateView', @@ -165,13 +166,16 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin): 'table': table, }) - return render(request, self.template_name, { + context = { 'model': model, 'table': table, 'actions': actions, 'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None, + 'prerequisite_model': get_prerequisite_model(self.queryset), **self.get_extra_context(request), - }) + } + + return render(request, self.template_name, context) class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView): diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 5ff0cfdff..19401f79a 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -21,6 +21,7 @@ from utilities.utils import get_viewname, normalize_querydict, prepare_cloned_fi from utilities.views import GetReturnURLMixin from .base import BaseObjectView from .mixins import ActionsMixin, TableMixin +from .utils import get_prerequisite_model __all__ = ( 'ComponentCreateView', @@ -340,15 +341,18 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): """ obj = self.get_object(**kwargs) obj = self.alter_object(obj, request, args, kwargs) + model = self.queryset.model initial_data = normalize_querydict(request.GET) form = self.form(instance=obj, initial=initial_data) restrict_form_fields(form, request.user) return render(request, self.template_name, { + 'model': model, 'object': obj, 'form': form, 'return_url': self.get_return_url(request, obj), + 'prerequisite_model': get_prerequisite_model(self.queryset), **self.get_extra_context(request, obj), }) diff --git a/netbox/netbox/views/generic/utils.py b/netbox/netbox/views/generic/utils.py new file mode 100644 index 000000000..61c6dc242 --- /dev/null +++ b/netbox/netbox/views/generic/utils.py @@ -0,0 +1,12 @@ +def get_prerequisite_model(queryset): + model = queryset.model + + if not queryset.exists(): + if hasattr(model, 'get_prerequisite_models'): + prerequisites = model.get_prerequisite_models() + if prerequisites: + for prereq in prerequisites: + if not prereq.objects.exists(): + return prereq + + return None diff --git a/netbox/templates/generic/object_edit.html b/netbox/templates/generic/object_edit.html index 892c7d2b1..8047dc59d 100644 --- a/netbox/templates/generic/object_edit.html +++ b/netbox/templates/generic/object_edit.html @@ -40,6 +40,10 @@ Context: {% endif %} + {% if prerequisite_model %} + {% include 'inc/missing_prerequisites.html' %} + {% endif %} +
{% csrf_token %} diff --git a/netbox/templates/generic/object_list.html b/netbox/templates/generic/object_list.html index 1e2ae796f..60eba6097 100644 --- a/netbox/templates/generic/object_list.html +++ b/netbox/templates/generic/object_list.html @@ -100,6 +100,11 @@ Context: {# Object table #} + + {% if prerequisite_model %} + {% include 'inc/missing_prerequisites.html' %} + {% endif %} +
{% include 'htmx/table.html' %} diff --git a/netbox/templates/inc/missing_prerequisites.html b/netbox/templates/inc/missing_prerequisites.html new file mode 100644 index 000000000..5814b72eb --- /dev/null +++ b/netbox/templates/inc/missing_prerequisites.html @@ -0,0 +1,6 @@ +{% load buttons %} + + diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 21bc799be..b8131c1ce 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -167,6 +167,10 @@ class Cluster(NetBoxModel): def __str__(self): return self.name + @classmethod + def get_prerequisite_models(cls): + return [ClusterType, ] + def get_absolute_url(self): return reverse('virtualization:cluster', args=[self.pk]) @@ -312,6 +316,10 @@ class VirtualMachine(NetBoxModel, ConfigContextModel): def __str__(self): return self.name + @classmethod + def get_prerequisite_models(cls): + return [Cluster, ] + def get_absolute_url(self): return reverse('virtualization:virtualmachine', args=[self.pk]) diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index 0540e9c45..36410b83b 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -1,3 +1,4 @@ +from django.apps import apps from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse @@ -190,6 +191,10 @@ class WirelessLink(WirelessAuthenticationBase, NetBoxModel): def __str__(self): return f'#{self.pk}' + @classmethod + def get_prerequisite_models(cls): + return [apps.get_model('dcim.Interface'), ] + def get_absolute_url(self): return reverse('wireless:wirelesslink', args=[self.pk])