Przeglądaj źródła

Merge pull request #10096 from arthanson/art-6454

Fixes #6454 - Adds warning for prerequisite models
Jeremy Stretch 3 lat temu
rodzic
commit
f35ff105ab

+ 5 - 0
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])
 

+ 14 - 0
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])
 

+ 9 - 0
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])
 

+ 9 - 0
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])
 

+ 4 - 0
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])
 

+ 8 - 0
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])
 

+ 5 - 0
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])
 

+ 8 - 0
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

+ 6 - 2
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):

+ 4 - 0
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),
         })
 

+ 12 - 0
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

+ 4 - 0
netbox/templates/generic/object_edit.html

@@ -40,6 +40,10 @@ Context:
         </div>
       {% endif %}
 
+      {% if prerequisite_model %}
+        {% include 'inc/missing_prerequisites.html' %}
+      {% endif %}
+
       <form action="" method="post" enctype="multipart/form-data" class="form-object-edit mt-5">
         {% csrf_token %}
 

+ 5 - 0
netbox/templates/generic/object_list.html

@@ -100,6 +100,11 @@ Context:
         <input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
 
         {# Object table #}
+
+            {% if prerequisite_model %}
+              {% include 'inc/missing_prerequisites.html' %}
+            {% endif %}
+
         <div class="card">
           <div class="card-body" id="object_list">
             {% include 'htmx/table.html' %}

+ 6 - 0
netbox/templates/inc/missing_prerequisites.html

@@ -0,0 +1,6 @@
+{% load buttons %}
+
+<div class="alert alert-warning" role="alert">
+  <i class="mdi mdi-alert"></i> Before you can add a {{ model|meta:"verbose_name" }} you must first create a
+    <strong>{{ prerequisite_model|meta:"verbose_name"|title }}</strong> which can be added here: {% add_button prerequisite_model %}
+</div>

+ 8 - 0
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])
 

+ 5 - 0
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])