Merge pull request #10096 from arthanson/art-6454

Fixes #6454 - Adds warning for prerequisite models
This commit is contained in:
Jeremy Stretch 2022-08-22 16:09:59 -04:00 committed by GitHub
commit f35ff105ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 112 additions and 2 deletions

View File

@ -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])

View File

@ -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])

View File

@ -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])

View File

@ -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])

View File

@ -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])

View File

@ -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])

View File

@ -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])

View File

@ -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

View File

@ -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):

View File

@ -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),
}) })

View 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

View File

@ -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 %}

View File

@ -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' %}

View 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>

View File

@ -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])

View File

@ -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])