Merge branch 'develop' into 8580-filter-connected

This commit is contained in:
Arthur
2022-09-12 10:11:20 -07:00
16 changed files with 125 additions and 55 deletions

View File

@@ -1096,7 +1096,7 @@ class InterfacePoETypeChoices(ChoiceSet):
(PASSIVE_24V_2PAIR, 'Passive 24V (2-pair)'),
(PASSIVE_24V_4PAIR, 'Passive 24V (4-pair)'),
(PASSIVE_48V_2PAIR, 'Passive 48V (2-pair)'),
(PASSIVE_48V_2PAIR, 'Passive 48V (4-pair)'),
(PASSIVE_48V_4PAIR, 'Passive 48V (4-pair)'),
)
),
)

View File

@@ -1331,6 +1331,12 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
label='VRF'
)
wwn = forms.CharField(
empty_value=None,
required=False,
label='WWN'
)
fieldsets = (
('Interface', ('device', 'module', 'name', 'type', 'speed', 'duplex', 'label', 'description', 'tags')),
('Addressing', ('vrf', 'mac_address', 'wwn')),

View File

@@ -281,15 +281,11 @@ class CableTermination(models.Model):
# Validate interface type (if applicable)
if self.termination_type.model == 'interface' and self.termination.type in NONCONNECTABLE_IFACE_TYPES:
raise ValidationError({
'termination': f'Cables cannot be terminated to {self.termination.get_type_display()} interfaces'
})
raise ValidationError(f"Cables cannot be terminated to {self.termination.get_type_display()} interfaces")
# A CircuitTermination attached to a ProviderNetwork cannot have a Cable
if self.termination_type.model == 'circuittermination' and self.termination.provider_network is not None:
raise ValidationError({
'termination': "Circuit terminations attached to a provider network may not be cabled."
})
raise ValidationError("Circuit terminations attached to a provider network may not be cabled.")
def save(self, *args, **kwargs):

View File

@@ -3,7 +3,7 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.forms import SimpleArrayField
from django.utils.safestring import mark_safe
from extras.choices import CustomFieldTypeChoices
from extras.choices import CustomFieldVisibilityChoices, CustomFieldTypeChoices
from extras.models import *
from extras.utils import FeatureQuery
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelForm, CSVMultipleContentTypeField, SlugField
@@ -38,6 +38,10 @@ class CustomFieldCSVForm(CSVModelForm):
required=False,
help_text='Comma-separated list of field choices'
)
ui_visibility = CSVChoiceField(
choices=CustomFieldVisibilityChoices,
help_text='How the custom field is displayed in the user interface'
)
class Meta:
model = CustomField

View File

@@ -14,7 +14,7 @@ from django.utils.safestring import mark_safe
from extras.choices import *
from extras.utils import FeatureQuery
from netbox.models import ChangeLoggedModel
from netbox.models.features import ExportTemplatesMixin, WebhooksMixin
from netbox.models.features import CloningMixin, ExportTemplatesMixin, WebhooksMixin
from utilities import filters
from utilities.forms import (
CSVChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
@@ -41,7 +41,7 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
return self.get_queryset().filter(content_types=content_type)
class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
content_types = models.ManyToManyField(
to=ContentType,
related_name='custom_fields',
@@ -143,8 +143,14 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
verbose_name='UI visibility',
help_text='Specifies the visibility of custom field in the UI'
)
objects = CustomFieldManager()
clone_fields = (
'content_types', 'type', 'object_type', 'group_name', 'description', 'required', 'filter_logic', 'default',
'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', 'ui_visibility',
)
class Meta:
ordering = ['group_name', 'weight', 'name']

View File

@@ -21,7 +21,7 @@ from extras.conditions import ConditionSet
from extras.utils import FeatureQuery, image_upload
from netbox.models import ChangeLoggedModel
from netbox.models.features import (
CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, JobResultsMixin, TagsMixin, WebhooksMixin,
CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, JobResultsMixin, TagsMixin, WebhooksMixin,
)
from utilities.querysets import RestrictedQuerySet
from utilities.utils import render_jinja2
@@ -187,7 +187,7 @@ class Webhook(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
return render_jinja2(self.payload_url, context)
class CustomLink(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
class CustomLink(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
"""
A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
code to be rendered with an object as context.
@@ -230,6 +230,10 @@ class CustomLink(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
help_text="Force link to open in a new window"
)
clone_fields = (
'content_type', 'enabled', 'weight', 'group_name', 'button_class', 'new_window',
)
class Meta:
ordering = ['group_name', 'weight', 'name']

View File

@@ -21,6 +21,14 @@ __all__ = (
AVAILABLE_LABEL = mark_safe('<span class="badge bg-success">Available</span>')
PREFIX_LINK = """
{% if record.pk %}
<a href="{{ record.get_absolute_url }}">{{ record.prefix }}</a>
{% else %}
<a href="{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.site %}&site={{ object.site.pk }}{% endif %}{% if object.tenant %}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}{% endif %}">{{ record.prefix }}</a>
{% endif %}
"""
PREFIX_LINK_WITH_DEPTH = """
{% load helpers %}
{% if record.depth %}
<div class="record-depth">
@@ -29,8 +37,7 @@ PREFIX_LINK = """
{% endfor %}
</div>
{% endif %}
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.site %}&site={{ object.site.pk }}{% endif %}{% if object.tenant %}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
"""
""" + PREFIX_LINK
IPADDRESS_LINK = """
{% if record.pk %}
@@ -216,14 +223,15 @@ class PrefixUtilizationColumn(columns.UtilizationColumn):
class PrefixTable(TenancyColumnsMixin, NetBoxTable):
prefix = columns.TemplateColumn(
template_code=PREFIX_LINK,
template_code=PREFIX_LINK_WITH_DEPTH,
export_raw=True,
attrs={'td': {'class': 'text-nowrap'}}
)
prefix_flat = tables.Column(
prefix_flat = columns.TemplateColumn(
accessor=Accessor('prefix'),
linkify=True,
verbose_name='Prefix (Flat)',
template_code=PREFIX_LINK,
export_raw=True,
verbose_name='Prefix (Flat)'
)
depth = tables.Column(
accessor=Accessor('_depth'),

View File

@@ -2,7 +2,6 @@ from django.core.validators import ValidationError
from django.db import models
from mptt.models import MPTTModel, TreeForeignKey
from extras.utils import is_taggable
from utilities.mptt import TreeManager
from utilities.querysets import RestrictedQuerySet
from netbox.models.features import *
@@ -32,7 +31,7 @@ class NetBoxFeatureSet(
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.
showing prerequisite warnings in the UI on the list and detail views.
"""
return []
@@ -52,7 +51,7 @@ class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, models.Model)
abstract = True
class NetBoxModel(NetBoxFeatureSet, models.Model):
class NetBoxModel(CloningMixin, NetBoxFeatureSet, models.Model):
"""
Primary models represent real objects within the infrastructure being modeled.
"""
@@ -61,25 +60,6 @@ class NetBoxModel(NetBoxFeatureSet, models.Model):
class Meta:
abstract = True
def clone(self):
"""
Return a dictionary of attributes suitable for creating a copy of the current instance. This is used for pre-
populating an object creation form in the UI.
"""
attrs = {}
for field_name in getattr(self, 'clone_fields', []):
field = self._meta.get_field(field_name)
field_value = field.value_from_object(self)
if field_value not in (None, ''):
attrs[field_name] = field_value
# Include tags (if applicable)
if is_taggable(self):
attrs['tags'] = [tag.pk for tag in self.tags.all()]
return attrs
class NestedGroupModel(NetBoxFeatureSet, MPTTModel):
"""

View File

@@ -10,12 +10,13 @@ from django.db import models
from taggit.managers import TaggableManager
from extras.choices import CustomFieldVisibilityChoices, ObjectChangeActionChoices
from extras.utils import register_features
from extras.utils import is_taggable, register_features
from netbox.signals import post_clean
from utilities.utils import serialize_object
__all__ = (
'ChangeLoggingMixin',
'CloningMixin',
'CustomFieldsMixin',
'CustomLinksMixin',
'CustomValidationMixin',
@@ -82,6 +83,33 @@ class ChangeLoggingMixin(models.Model):
return objectchange
class CloningMixin(models.Model):
"""
Provides the clone() method used to prepare a copy of existing objects.
"""
class Meta:
abstract = True
def clone(self):
"""
Return a dictionary of attributes suitable for creating a copy of the current instance. This is used for pre-
populating an object creation form in the UI.
"""
attrs = {}
for field_name in getattr(self, 'clone_fields', []):
field = self._meta.get_field(field_name)
field_value = field.value_from_object(self)
if field_value not in (None, ''):
attrs[field_name] = field_value
# Include tags (if applicable)
if is_taggable(self):
attrs['tags'] = [tag.pk for tag in self.tags.all()]
return attrs
class CustomFieldsMixin(models.Model):
"""
Enables support for custom fields.