mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 04:02:52 -06:00
Merge pull request #10096 from arthanson/art-6454
Fixes #6454 - Adds warning for prerequisite models
This commit is contained in:
commit
f35ff105ab
@ -1,3 +1,4 @@
|
|||||||
|
from django.apps import apps
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -136,6 +137,10 @@ class Circuit(NetBoxModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.cid
|
return self.cid
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_prerequisite_models(cls):
|
||||||
|
return [apps.get_model('circuits.Provider'), CircuitType]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('circuits:circuit', args=[self.pk])
|
return reverse('circuits:circuit', args=[self.pk])
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import decimal
|
import decimal
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
@ -159,6 +161,10 @@ class DeviceType(NetBoxModel):
|
|||||||
self._original_front_image = self.front_image
|
self._original_front_image = self.front_image
|
||||||
self._original_rear_image = self.rear_image
|
self._original_rear_image = self.rear_image
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_prerequisite_models(cls):
|
||||||
|
return [Manufacturer, ]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:devicetype', args=[self.pk])
|
return reverse('dcim:devicetype', args=[self.pk])
|
||||||
|
|
||||||
@ -338,6 +344,10 @@ class ModuleType(NetBoxModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.model
|
return self.model
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_prerequisite_models(cls):
|
||||||
|
return [Manufacturer, ]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:moduletype', args=[self.pk])
|
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 f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})'
|
||||||
return super().__str__()
|
return super().__str__()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_prerequisite_models(cls):
|
||||||
|
return [apps.get_model('dcim.Site'), DeviceRole, DeviceType, ]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:device', args=[self.pk])
|
return reverse('dcim:device', args=[self.pk])
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.apps import apps
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
@ -54,6 +55,10 @@ class PowerPanel(NetBoxModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_prerequisite_models(cls):
|
||||||
|
return [apps.get_model('dcim.Site'), ]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:powerpanel', args=[self.pk])
|
return reverse('dcim:powerpanel', args=[self.pk])
|
||||||
|
|
||||||
@ -138,6 +143,10 @@ class PowerFeed(NetBoxModel, PathEndpoint, CabledObjectModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_prerequisite_models(cls):
|
||||||
|
return [PowerPanel, ]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:powerfeed', args=[self.pk])
|
return reverse('dcim:powerfeed', args=[self.pk])
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import decimal
|
import decimal
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
@ -201,6 +202,10 @@ class Rack(NetBoxModel):
|
|||||||
return f'{self.name} ({self.facility_id})'
|
return f'{self.name} ({self.facility_id})'
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_prerequisite_models(cls):
|
||||||
|
return [apps.get_model('dcim.Site'), ]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:rack', args=[self.pk])
|
return reverse('dcim:rack', args=[self.pk])
|
||||||
|
|
||||||
@ -477,6 +482,10 @@ class RackReservation(NetBoxModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Reservation for rack {}".format(self.rack)
|
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):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:rackreservation', args=[self.pk])
|
return reverse('dcim:rackreservation', args=[self.pk])
|
||||||
|
|
||||||
|
@ -411,6 +411,10 @@ class Location(NestedGroupModel):
|
|||||||
|
|
||||||
super().validate_unique(exclude=exclude)
|
super().validate_unique(exclude=exclude)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_prerequisite_models(cls):
|
||||||
|
return [Site, ]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:location', args=[self.pk])
|
return reverse('dcim:location', args=[self.pk])
|
||||||
|
|
||||||
|
@ -124,6 +124,10 @@ class ASN(NetBoxModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'AS{self.asn_with_asdot}'
|
return f'AS{self.asn_with_asdot}'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_prerequisite_models(cls):
|
||||||
|
return [RIR, ]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('ipam:asn', args=[self.pk])
|
return reverse('ipam:asn', args=[self.pk])
|
||||||
|
|
||||||
@ -185,6 +189,10 @@ class Aggregate(GetAvailablePrefixesMixin, NetBoxModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.prefix)
|
return str(self.prefix)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_prerequisite_models(cls):
|
||||||
|
return [RIR, ]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('ipam:aggregate', args=[self.pk])
|
return reverse('ipam:aggregate', args=[self.pk])
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.apps import apps
|
||||||
from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@ -103,6 +104,10 @@ class L2VPNTermination(NetBoxModel):
|
|||||||
return f'{self.assigned_object} <> {self.l2vpn}'
|
return f'{self.assigned_object} <> {self.l2vpn}'
|
||||||
return super().__str__()
|
return super().__str__()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_prerequisite_models(cls):
|
||||||
|
return [apps.get_model('ipam.L2VPN'), ]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('ipam:l2vpntermination', args=[self.pk])
|
return reverse('ipam:l2vpntermination', args=[self.pk])
|
||||||
|
|
||||||
|
@ -28,6 +28,14 @@ class NetBoxFeatureSet(
|
|||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
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
|
# Base model classes
|
||||||
|
@ -26,6 +26,7 @@ from utilities.permissions import get_permission_for_model
|
|||||||
from utilities.views import GetReturnURLMixin
|
from utilities.views import GetReturnURLMixin
|
||||||
from .base import BaseMultiObjectView
|
from .base import BaseMultiObjectView
|
||||||
from .mixins import ActionsMixin, TableMixin
|
from .mixins import ActionsMixin, TableMixin
|
||||||
|
from .utils import get_prerequisite_model
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'BulkComponentCreateView',
|
'BulkComponentCreateView',
|
||||||
@ -165,13 +166,16 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
|||||||
'table': table,
|
'table': table,
|
||||||
})
|
})
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
context = {
|
||||||
'model': model,
|
'model': model,
|
||||||
'table': table,
|
'table': table,
|
||||||
'actions': actions,
|
'actions': actions,
|
||||||
'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
|
'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),
|
**self.get_extra_context(request),
|
||||||
})
|
}
|
||||||
|
|
||||||
|
return render(request, self.template_name, context)
|
||||||
|
|
||||||
|
|
||||||
class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView):
|
class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView):
|
||||||
|
@ -21,6 +21,7 @@ from utilities.utils import get_viewname, normalize_querydict, prepare_cloned_fi
|
|||||||
from utilities.views import GetReturnURLMixin
|
from utilities.views import GetReturnURLMixin
|
||||||
from .base import BaseObjectView
|
from .base import BaseObjectView
|
||||||
from .mixins import ActionsMixin, TableMixin
|
from .mixins import ActionsMixin, TableMixin
|
||||||
|
from .utils import get_prerequisite_model
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ComponentCreateView',
|
'ComponentCreateView',
|
||||||
@ -340,15 +341,18 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
|||||||
"""
|
"""
|
||||||
obj = self.get_object(**kwargs)
|
obj = self.get_object(**kwargs)
|
||||||
obj = self.alter_object(obj, request, args, kwargs)
|
obj = self.alter_object(obj, request, args, kwargs)
|
||||||
|
model = self.queryset.model
|
||||||
|
|
||||||
initial_data = normalize_querydict(request.GET)
|
initial_data = normalize_querydict(request.GET)
|
||||||
form = self.form(instance=obj, initial=initial_data)
|
form = self.form(instance=obj, initial=initial_data)
|
||||||
restrict_form_fields(form, request.user)
|
restrict_form_fields(form, request.user)
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
|
'model': model,
|
||||||
'object': obj,
|
'object': obj,
|
||||||
'form': form,
|
'form': form,
|
||||||
'return_url': self.get_return_url(request, obj),
|
'return_url': self.get_return_url(request, obj),
|
||||||
|
'prerequisite_model': get_prerequisite_model(self.queryset),
|
||||||
**self.get_extra_context(request, obj),
|
**self.get_extra_context(request, obj),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
12
netbox/netbox/views/generic/utils.py
Normal file
12
netbox/netbox/views/generic/utils.py
Normal file
@ -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
|
@ -40,6 +40,10 @@ Context:
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if prerequisite_model %}
|
||||||
|
{% include 'inc/missing_prerequisites.html' %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<form action="" method="post" enctype="multipart/form-data" class="form-object-edit mt-5">
|
<form action="" method="post" enctype="multipart/form-data" class="form-object-edit mt-5">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
|
@ -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 %}" />
|
<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 #}
|
{# Object table #}
|
||||||
|
|
||||||
|
{% if prerequisite_model %}
|
||||||
|
{% include 'inc/missing_prerequisites.html' %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body" id="object_list">
|
<div class="card-body" id="object_list">
|
||||||
{% include 'htmx/table.html' %}
|
{% include 'htmx/table.html' %}
|
||||||
|
6
netbox/templates/inc/missing_prerequisites.html
Normal file
6
netbox/templates/inc/missing_prerequisites.html
Normal file
@ -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>
|
@ -167,6 +167,10 @@ class Cluster(NetBoxModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_prerequisite_models(cls):
|
||||||
|
return [ClusterType, ]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('virtualization:cluster', args=[self.pk])
|
return reverse('virtualization:cluster', args=[self.pk])
|
||||||
|
|
||||||
@ -312,6 +316,10 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_prerequisite_models(cls):
|
||||||
|
return [Cluster, ]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('virtualization:virtualmachine', args=[self.pk])
|
return reverse('virtualization:virtualmachine', args=[self.pk])
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.apps import apps
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -190,6 +191,10 @@ class WirelessLink(WirelessAuthenticationBase, NetBoxModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'#{self.pk}'
|
return f'#{self.pk}'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_prerequisite_models(cls):
|
||||||
|
return [apps.get_model('dcim.Interface'), ]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('wireless:wirelesslink', args=[self.pk])
|
return reverse('wireless:wirelesslink', args=[self.pk])
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user