1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merged v2.5.13

This commit is contained in:
Jeremy Stretch
2019-05-31 21:37:41 -04:00
40 changed files with 801 additions and 769 deletions

View File

@ -213,6 +213,33 @@ functionality provided by the front end UI.
---
2.5.13 (2019-05-31)
## Enhancements
* [#2813](https://github.com/digitalocean/netbox/issues/2813) - Add tenant group filters
* [#3085](https://github.com/digitalocean/netbox/issues/3085) - Catch all exceptions during export template rendering
* [#3138](https://github.com/digitalocean/netbox/issues/3138) - Add 2.5GE and 5GE interface form factors
* [#3151](https://github.com/digitalocean/netbox/issues/3151) - Add inventory item count to manufacturers list
* [#3156](https://github.com/digitalocean/netbox/issues/3156) - Add site link to rack reservations overview
* [#3183](https://github.com/digitalocean/netbox/issues/3183) - Enable bulk deletion of sites
* [#3185](https://github.com/digitalocean/netbox/issues/3185) - Improve performance for custom field access within templates
* [#3186](https://github.com/digitalocean/netbox/issues/3186) - Add interface name filter for IP addresses
## Bug Fixes
* [#3031](https://github.com/digitalocean/netbox/issues/3031) - Fixed form field population of tags with spaces
* [#3132](https://github.com/digitalocean/netbox/issues/3132) - Circuit termination missing from available cable termination types
* [#3150](https://github.com/digitalocean/netbox/issues/3150) - Fix formatting of cable length during cable trace
* [#3184](https://github.com/digitalocean/netbox/issues/3184) - Correctly display color block for white cables
* [#3190](https://github.com/digitalocean/netbox/issues/3190) - Fix custom field rendering for Jinja2 export templates
* [#3211](https://github.com/digitalocean/netbox/issues/3211) - Fix error handling when attempting to delete a protected object via API
* [#3223](https://github.com/digitalocean/netbox/issues/3223) - Fix filtering devices by "has power outlets"
* [#3227](https://github.com/digitalocean/netbox/issues/3227) - Fix exception when deleting a circuit with a termination(s)
* [#3228](https://github.com/digitalocean/netbox/issues/3228) - Fixed login link retaining query parameters
---
2.5.12 (2019-05-01)
## Bug Fixes

View File

@ -3,7 +3,7 @@ from django.db.models import Q
from dcim.models import Site
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from .constants import CIRCUIT_STATUS_CHOICES
from .models import Provider, Circuit, CircuitTermination, CircuitType
@ -54,7 +54,7 @@ class CircuitTypeFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug']
class CircuitFilter(CustomFieldFilterSet):
class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@ -87,16 +87,6 @@ class CircuitFilter(CustomFieldFilterSet):
choices=CIRCUIT_STATUS_CHOICES,
null_value=None
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__site',
queryset=Site.objects.all(),

View File

@ -4,6 +4,7 @@ from taggit.forms import TagField
from dcim.models import Site
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import TenancyForm
from tenancy.forms import TenancyFilterForm
from tenancy.models import Tenant
from utilities.forms import (
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField,
@ -265,8 +266,9 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
]
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = Circuit
field_order = ['q', 'type', 'provider', 'status', 'site', 'tenant_group', 'tenant', 'commit_rate']
q = forms.CharField(
required=False,
label='Search'
@ -292,16 +294,6 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
required=False,
widget=StaticSelect2Multiple()
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',

View File

@ -274,11 +274,16 @@ class CircuitTermination(CableTermination):
"""
Reference the parent circuit when recording the change.
"""
try:
related_object = self.circuit
except Circuit.DoesNotExist:
# Parent circuit has been deleted
related_object = None
ObjectChange(
user=user,
request_id=request_id,
changed_object=self,
related_object=self.circuit,
related_object=related_object,
action=action,
object_data=serialize_object(self)
).save()

View File

@ -1,4 +1,4 @@
from django.conf.urls import url
from django.urls import path
from dcim.views import CableCreateView, CableTraceView
from extras.views import ObjectChangeLogView
@ -9,41 +9,42 @@ app_name = 'circuits'
urlpatterns = [
# Providers
url(r'^providers/$', views.ProviderListView.as_view(), name='provider_list'),
url(r'^providers/add/$', views.ProviderCreateView.as_view(), name='provider_add'),
url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'),
url(r'^providers/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
url(r'^providers/(?P<slug>[\w-]+)/$', views.ProviderView.as_view(), name='provider'),
url(r'^providers/(?P<slug>[\w-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'),
url(r'^providers/(?P<slug>[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'),
url(r'^providers/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
path(r'providers/', views.ProviderListView.as_view(), name='provider_list'),
path(r'providers/add/', views.ProviderCreateView.as_view(), name='provider_add'),
path(r'providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'),
path(r'providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
path(r'providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
path(r'providers/<slug:slug>/', views.ProviderView.as_view(), name='provider'),
path(r'providers/<slug:slug>/edit/', views.ProviderEditView.as_view(), name='provider_edit'),
path(r'providers/<slug:slug>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'),
path(r'providers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
# Circuit types
url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'),
url(r'^circuit-types/add/$', views.CircuitTypeCreateView.as_view(), name='circuittype_add'),
url(r'^circuit-types/import/$', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
url(r'^circuit-types/delete/$', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
url(r'^circuit-types/(?P<slug>[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
url(r'^circuit-types/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
path(r'circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),
path(r'circuit-types/add/', views.CircuitTypeCreateView.as_view(), name='circuittype_add'),
path(r'circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
path(r'circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
path(r'circuit-types/<slug:slug>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
path(r'circuit-types/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
# Circuits
url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'),
url(r'^circuits/add/$', views.CircuitCreateView.as_view(), name='circuit_add'),
url(r'^circuits/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'),
url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
url(r'^circuits/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
url(r'^circuits/(?P<pk>\d+)/$', views.CircuitView.as_view(), name='circuit'),
url(r'^circuits/(?P<pk>\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'),
url(r'^circuits/(?P<pk>\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'),
url(r'^circuits/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}),
url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),
path(r'circuits/', views.CircuitListView.as_view(), name='circuit_list'),
path(r'circuits/add/', views.CircuitCreateView.as_view(), name='circuit_add'),
path(r'circuits/import/', views.CircuitBulkImportView.as_view(), name='circuit_import'),
path(r'circuits/edit/', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
path(r'circuits/delete/', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
path(r'circuits/<int:pk>/', views.CircuitView.as_view(), name='circuit'),
path(r'circuits/<int:pk>/edit/', views.CircuitEditView.as_view(), name='circuit_edit'),
path(r'circuits/<int:pk>/delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'),
path(r'circuits/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}),
path(r'circuits/<int:pk>/terminations/swap/', views.circuit_terminations_swap, name='circuit_terminations_swap'),
# Circuit terminations
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'),
url(r'^circuit-terminations/(?P<pk>\d+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
url(r'^circuit-terminations/(?P<pk>\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
url(r'^circuit-terminations/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),
url(r'^circuit-terminations/(?P<pk>\d+)/trace/$', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
path(r'circuits/<int:circuit>/terminations/add/', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'),
path(r'circuit-terminations/<int:pk>/edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
path(r'circuit-terminations/<int:pk>/delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
path(r'circuit-terminations/<int:termination_a_id>/connect/<str:termination_b_type>/', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),
path(r'circuit-terminations/<int:pk>/trace/', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
]

View File

@ -75,6 +75,8 @@ IFACE_TYPE_100ME_FIXED = 800
IFACE_TYPE_1GE_FIXED = 1000
IFACE_TYPE_1GE_GBIC = 1050
IFACE_TYPE_1GE_SFP = 1100
IFACE_TYPE_2GE_FIXED = 1120
IFACE_TYPE_5GE_FIXED = 1130
IFACE_TYPE_10GE_FIXED = 1150
IFACE_TYPE_10GE_CX4 = 1170
IFACE_TYPE_10GE_SFP_PLUS = 1200
@ -150,6 +152,8 @@ IFACE_TYPE_CHOICES = [
[
[IFACE_TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'],
[IFACE_TYPE_1GE_FIXED, '1000BASE-T (1GE)'],
[IFACE_TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'],
[IFACE_TYPE_5GE_FIXED, '5GBASE-T (5GE)'],
[IFACE_TYPE_10GE_FIXED, '10GBASE-T (10GE)'],
[IFACE_TYPE_10GE_CX4, '10GBASE-CX4 (10GE)'],
]
@ -360,7 +364,7 @@ CONNECTION_STATUS_CHOICES = [
# Cable endpoint types
CABLE_TERMINATION_TYPES = [
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport',
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination',
]
# Cable types

View File

@ -6,6 +6,7 @@ from netaddr import EUI
from netaddr.core import AddrFormatError
from extras.filters import CustomFieldFilterSet
from tenancy.filtersets import TenancyFilterSet
from tenancy.models import Tenant
from utilities.constants import COLOR_CHOICES
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
@ -37,7 +38,7 @@ class RegionFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug']
class SiteFilter(CustomFieldFilterSet):
class SiteFilter(TenancyFilterSet, CustomFieldFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@ -61,16 +62,6 @@ class SiteFilter(CustomFieldFilterSet):
to_field_name='slug',
label='Region (slug)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
tag = TagFilter()
class Meta:
@ -125,7 +116,7 @@ class RackRoleFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug', 'color']
class RackFilter(CustomFieldFilterSet):
class RackFilter(TenancyFilterSet, CustomFieldFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@ -154,16 +145,6 @@ class RackFilter(CustomFieldFilterSet):
to_field_name='slug',
label='Group',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
status = django_filters.MultipleChoiceFilter(
choices=RACK_STATUS_CHOICES,
null_value=None
@ -199,7 +180,7 @@ class RackFilter(CustomFieldFilterSet):
)
class RackReservationFilter(django_filters.FilterSet):
class RackReservationFilter(TenancyFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@ -234,16 +215,6 @@ class RackReservationFilter(django_filters.FilterSet):
to_field_name='slug',
label='Group',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
user_id = django_filters.ModelMultipleChoiceFilter(
queryset=User.objects.all(),
label='User (ID)',
@ -449,7 +420,7 @@ class PlatformFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug', 'napalm_driver']
class DeviceFilter(CustomFieldFilterSet):
class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@ -484,16 +455,6 @@ class DeviceFilter(CustomFieldFilterSet):
to_field_name='slug',
label='Role (slug)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
platform_id = django_filters.ModelMultipleChoiceFilter(
queryset=Platform.objects.all(),
label='Platform (ID)',
@ -639,7 +600,7 @@ class DeviceFilter(CustomFieldFilterSet):
return queryset.exclude(powerports__isnull=value)
def _power_outlets(self, queryset, name, value):
return queryset.exclude(poweroutlets_isnull=value)
return queryset.exclude(poweroutlets__isnull=value)
def _interfaces(self, queryset, name, value):
return queryset.exclude(interfaces__isnull=value)

View File

@ -14,7 +14,8 @@ from circuits.models import Circuit, Provider
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from ipam.models import IPAddress, VLAN, VLANGroup
from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from tenancy.forms import TenancyFilterForm
from tenancy.models import Tenant, TenantGroup
from utilities.forms import (
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
@ -256,8 +257,9 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
]
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = Site
field_order = ['q', 'status', 'region', 'tenant_group', 'tenant']
q = forms.CharField(
required=False,
label='Search'
@ -276,16 +278,6 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
value_field="slug",
)
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
)
#
@ -596,8 +588,9 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
]
class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = Rack
field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
q = forms.CharField(
required=False,
label='Search'
@ -619,16 +612,6 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
null_option=True,
)
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
)
status = forms.MultipleChoiceField(
choices=RACK_STATUS_CHOICES,
required=False,
@ -689,40 +672,6 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
return unit_choices
class RackReservationFilterForm(BootstrapMixin, forms.Form):
q = forms.CharField(
required=False,
label='Search'
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
)
)
group_id = FilterChoiceField(
queryset=RackGroup.objects.select_related('site'),
label='Rack group',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/dcim/rack-groups/",
null_option=True,
)
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
)
class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=RackReservation.objects.all(),
@ -751,6 +700,31 @@ class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
nullable_fields = []
class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
field_order = ['q', 'site', 'group_id', 'tenant_group', 'tenant']
q = forms.CharField(
required=False,
label='Search'
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
)
)
group_id = FilterChoiceField(
queryset=RackGroup.objects.select_related('site'),
label='Rack group',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/dcim/rack-groups/",
null_option=True,
)
)
#
# Manufacturers
#
@ -1656,8 +1630,12 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
]
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
class DeviceFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = Device
field_order = [
'q', 'region', 'site', 'rack_group_id', 'rack_id', 'status', 'role', 'tenant_group', 'tenant',
'manufacturer_id', 'device_type_id', 'mac_address', 'has_primary_ip',
]
q = forms.CharField(
required=False,
label='Search'
@ -1715,16 +1693,6 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
value_field="slug",
)
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
)
manufacturer_id = FilterChoiceField(
queryset=Manufacturer.objects.all(),
label='Manufacturer',
@ -3368,11 +3336,33 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
)
)
tenant_group = FilterChoiceField(
queryset=TenantGroup.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenant-groups/",
value_field="slug",
null_option=True,
filter_for={
'tenant': 'group'
}
)
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
)

View File

@ -314,7 +314,12 @@ class RackDetailTable(RackTable):
class RackReservationTable(BaseTable):
pk = ToggleColumn()
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
site = tables.LinkColumn(
viewname='dcim:site',
accessor=Accessor('rack.site'),
args=[Accessor('rack.site.slug')],
)
tenant = tables.TemplateColumn(template_code=COL_TENANT)
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
unit_list = tables.Column(orderable=False, verbose_name='Units')
actions = tables.TemplateColumn(
@ -323,7 +328,7 @@ class RackReservationTable(BaseTable):
class Meta(BaseTable.Meta):
model = RackReservation
fields = ('pk', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions')
fields = ('pk', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions')
#
@ -332,16 +337,26 @@ class RackReservationTable(BaseTable):
class ManufacturerTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
devicetype_count = tables.Column(verbose_name='Device Types')
platform_count = tables.Column(verbose_name='Platforms')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right noprint'}},
verbose_name='')
name = tables.LinkColumn()
devicetype_count = tables.Column(
verbose_name='Device Types'
)
inventoryitem_count = tables.Column(
verbose_name='Inventory Items'
)
platform_count = tables.Column(
verbose_name='Platforms'
)
slug = tables.Column()
actions = tables.TemplateColumn(
template_code=MANUFACTURER_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = Manufacturer
fields = ('pk', 'name', 'devicetype_count', 'platform_count', 'slug', 'actions')
fields = ('pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'slug', 'actions')
#

View File

@ -1,4 +1,4 @@
from django.conf.urls import url
from django.urls import path
from extras.views import ObjectChangeLogView, ImageAttachmentEditView
from ipam.views import ServiceCreateView
@ -14,293 +14,294 @@ app_name = 'dcim'
urlpatterns = [
# Regions
url(r'^regions/$', views.RegionListView.as_view(), name='region_list'),
url(r'^regions/add/$', views.RegionCreateView.as_view(), name='region_add'),
url(r'^regions/import/$', views.RegionBulkImportView.as_view(), name='region_import'),
url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
url(r'^regions/(?P<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
url(r'^regions/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}),
path(r'regions/', views.RegionListView.as_view(), name='region_list'),
path(r'regions/add/', views.RegionCreateView.as_view(), name='region_add'),
path(r'regions/import/', views.RegionBulkImportView.as_view(), name='region_import'),
path(r'regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
path(r'regions/<int:pk>/edit/', views.RegionEditView.as_view(), name='region_edit'),
path(r'regions/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}),
# Sites
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
url(r'^sites/add/$', views.SiteCreateView.as_view(), name='site_add'),
url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),
url(r'^sites/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
url(r'^sites/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
path(r'sites/', views.SiteListView.as_view(), name='site_list'),
path(r'sites/add/', views.SiteCreateView.as_view(), name='site_add'),
path(r'sites/import/', views.SiteBulkImportView.as_view(), name='site_import'),
path(r'sites/edit/', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
path(r'sites/delete/', views.SiteBulkDeleteView.as_view(), name='site_bulk_delete'),
path(r'sites/<slug:slug>/', views.SiteView.as_view(), name='site'),
path(r'sites/<slug:slug>/edit/', views.SiteEditView.as_view(), name='site_edit'),
path(r'sites/<slug:slug>/delete/', views.SiteDeleteView.as_view(), name='site_delete'),
path(r'sites/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
path(r'sites/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
# Rack groups
url(r'^rack-groups/$', views.RackGroupListView.as_view(), name='rackgroup_list'),
url(r'^rack-groups/add/$', views.RackGroupCreateView.as_view(), name='rackgroup_add'),
url(r'^rack-groups/import/$', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'),
url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
url(r'^rack-groups/(?P<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
url(r'^rack-groups/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}),
path(r'rack-groups/', views.RackGroupListView.as_view(), name='rackgroup_list'),
path(r'rack-groups/add/', views.RackGroupCreateView.as_view(), name='rackgroup_add'),
path(r'rack-groups/import/', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'),
path(r'rack-groups/delete/', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
path(r'rack-groups/<int:pk>/edit/', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
path(r'rack-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}),
# Rack roles
url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'),
url(r'^rack-roles/add/$', views.RackRoleCreateView.as_view(), name='rackrole_add'),
url(r'^rack-roles/import/$', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
url(r'^rack-roles/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
path(r'rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'),
path(r'rack-roles/add/', views.RackRoleCreateView.as_view(), name='rackrole_add'),
path(r'rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
path(r'rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
path(r'rack-roles/<int:pk>/edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'),
path(r'rack-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
# Rack reservations
url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'),
url(r'^rack-reservations/edit/$', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'),
url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
url(r'^rack-reservations/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
path(r'rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'),
path(r'rack-reservations/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'),
path(r'rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
path(r'rack-reservations/<int:pk>/edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
path(r'rack-reservations/<int:pk>/delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
path(r'rack-reservations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
# Racks
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
url(r'^rack-elevations/$', views.RackElevationListView.as_view(), name='rack_elevation_list'),
url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'),
url(r'^racks/import/$', views.RackBulkImportView.as_view(), name='rack_import'),
url(r'^racks/edit/$', views.RackBulkEditView.as_view(), name='rack_bulk_edit'),
url(r'^racks/delete/$', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
url(r'^racks/(?P<pk>\d+)/$', views.RackView.as_view(), name='rack'),
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
url(r'^racks/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
url(r'^racks/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
path(r'racks/', views.RackListView.as_view(), name='rack_list'),
path(r'rack-elevations/', views.RackElevationListView.as_view(), name='rack_elevation_list'),
path(r'racks/add/', views.RackEditView.as_view(), name='rack_add'),
path(r'racks/import/', views.RackBulkImportView.as_view(), name='rack_import'),
path(r'racks/edit/', views.RackBulkEditView.as_view(), name='rack_bulk_edit'),
path(r'racks/delete/', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
path(r'racks/<int:pk>/', views.RackView.as_view(), name='rack'),
path(r'racks/<int:pk>/edit/', views.RackEditView.as_view(), name='rack_edit'),
path(r'racks/<int:pk>/delete/', views.RackDeleteView.as_view(), name='rack_delete'),
path(r'racks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
path(r'racks/<int:rack>/reservations/add/', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
path(r'racks/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
# Manufacturers
url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'),
url(r'^manufacturers/add/$', views.ManufacturerCreateView.as_view(), name='manufacturer_add'),
url(r'^manufacturers/import/$', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
url(r'^manufacturers/delete/$', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
url(r'^manufacturers/(?P<slug>[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
url(r'^manufacturers/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
path(r'manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'),
path(r'manufacturers/add/', views.ManufacturerCreateView.as_view(), name='manufacturer_add'),
path(r'manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
path(r'manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
path(r'manufacturers/<slug:slug>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
path(r'manufacturers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
# Device types
url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'),
url(r'^device-types/add/$', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
url(r'^device-types/import/$', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'),
url(r'^device-types/edit/$', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
url(r'^device-types/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
url(r'^device-types/(?P<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'),
url(r'^device-types/(?P<pk>\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
url(r'^device-types/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
path(r'device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'),
path(r'device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
path(r'device-types/import/', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'),
path(r'device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
path(r'device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
path(r'device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
path(r'device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
path(r'device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
path(r'device-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
# Console port templates
url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'),
url(r'^device-types/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'),
path(r'device-types/<int:pk>/console-ports/add/', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'),
path(r'device-types/<int:pk>/console-ports/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'),
# Console server port templates
url(r'^device-types/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateCreateView.as_view(), name='devicetype_add_consoleserverport'),
url(r'^device-types/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'),
path(r'device-types/<int:pk>/console-server-ports/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='devicetype_add_consoleserverport'),
path(r'device-types/<int:pk>/console-server-ports/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'),
# Power port templates
url(r'^device-types/(?P<pk>\d+)/power-ports/add/$', views.PowerPortTemplateCreateView.as_view(), name='devicetype_add_powerport'),
url(r'^device-types/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'),
path(r'device-types/<int:pk>/power-ports/add/', views.PowerPortTemplateCreateView.as_view(), name='devicetype_add_powerport'),
path(r'device-types/<int:pk>/power-ports/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'),
# Power outlet templates
url(r'^device-types/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletTemplateCreateView.as_view(), name='devicetype_add_poweroutlet'),
url(r'^device-types/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'),
path(r'device-types/<int:pk>/power-outlets/add/', views.PowerOutletTemplateCreateView.as_view(), name='devicetype_add_poweroutlet'),
path(r'device-types/<int:pk>/power-outlets/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'),
# Interface templates
url(r'^device-types/(?P<pk>\d+)/interfaces/add/$', views.InterfaceTemplateCreateView.as_view(), name='devicetype_add_interface'),
url(r'^device-types/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'),
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
path(r'device-types/<int:pk>/interfaces/add/', views.InterfaceTemplateCreateView.as_view(), name='devicetype_add_interface'),
path(r'device-types/<int:pk>/interfaces/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'),
path(r'device-types/<int:pk>/interfaces/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
# Front port templates
url(r'^device-types/(?P<pk>\d+)/front-ports/add/$', views.FrontPortTemplateCreateView.as_view(), name='devicetype_add_frontport'),
url(r'^device-types/(?P<pk>\d+)/front-ports/delete/$', views.FrontPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_frontport'),
path(r'device-types/<int:pk>/front-ports/add/', views.FrontPortTemplateCreateView.as_view(), name='devicetype_add_frontport'),
path(r'device-types/<int:pk>/front-ports/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_frontport'),
# Rear port templates
url(r'^device-types/(?P<pk>\d+)/rear-ports/add/$', views.RearPortTemplateCreateView.as_view(), name='devicetype_add_rearport'),
url(r'^device-types/(?P<pk>\d+)/rear-ports/delete/$', views.RearPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_rearport'),
path(r'device-types/<int:pk>/rear-ports/add/', views.RearPortTemplateCreateView.as_view(), name='devicetype_add_rearport'),
path(r'device-types/<int:pk>/rear-ports/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_rearport'),
# Device bay templates
url(r'^device-types/(?P<pk>\d+)/device-bays/add/$', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'),
url(r'^device-types/(?P<pk>\d+)/device-bays/delete/$', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'),
path(r'device-types/<int:pk>/device-bays/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'),
path(r'device-types/<int:pk>/device-bays/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'),
# Device roles
url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'),
url(r'^device-roles/add/$', views.DeviceRoleCreateView.as_view(), name='devicerole_add'),
url(r'^device-roles/import/$', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
url(r'^device-roles/delete/$', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
url(r'^device-roles/(?P<slug>[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
url(r'^device-roles/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
path(r'device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'),
path(r'device-roles/add/', views.DeviceRoleCreateView.as_view(), name='devicerole_add'),
path(r'device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
path(r'device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
path(r'device-roles/<slug:slug>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
path(r'device-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
# Platforms
url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'),
url(r'^platforms/add/$', views.PlatformCreateView.as_view(), name='platform_add'),
url(r'^platforms/import/$', views.PlatformBulkImportView.as_view(), name='platform_import'),
url(r'^platforms/delete/$', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
url(r'^platforms/(?P<slug>[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'),
url(r'^platforms/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),
path(r'platforms/', views.PlatformListView.as_view(), name='platform_list'),
path(r'platforms/add/', views.PlatformCreateView.as_view(), name='platform_add'),
path(r'platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'),
path(r'platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
path(r'platforms/<slug:slug>/edit/', views.PlatformEditView.as_view(), name='platform_edit'),
path(r'platforms/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),
# Devices
url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'),
url(r'^devices/add/$', views.DeviceCreateView.as_view(), name='device_add'),
url(r'^devices/import/$', views.DeviceBulkImportView.as_view(), name='device_import'),
url(r'^devices/import/child-devices/$', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
url(r'^devices/delete/$', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
url(r'^devices/(?P<pk>\d+)/$', views.DeviceView.as_view(), name='device'),
url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
url(r'^devices/(?P<pk>\d+)/config-context/$', views.DeviceConfigContextView.as_view(), name='device_configcontext'),
url(r'^devices/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}),
url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
url(r'^devices/(?P<pk>\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'),
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
url(r'^devices/(?P<pk>\d+)/config/$', views.DeviceConfigView.as_view(), name='device_config'),
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceCreateView.as_view(), name='device_service_assign'),
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
path(r'devices/', views.DeviceListView.as_view(), name='device_list'),
path(r'devices/add/', views.DeviceCreateView.as_view(), name='device_add'),
path(r'devices/import/', views.DeviceBulkImportView.as_view(), name='device_import'),
path(r'devices/import/child-devices/', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
path(r'devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
path(r'devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
path(r'devices/<int:pk>/', views.DeviceView.as_view(), name='device'),
path(r'devices/<int:pk>/edit/', views.DeviceEditView.as_view(), name='device_edit'),
path(r'devices/<int:pk>/delete/', views.DeviceDeleteView.as_view(), name='device_delete'),
path(r'devices/<int:pk>/config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'),
path(r'devices/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}),
path(r'devices/<int:pk>/inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'),
path(r'devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'),
path(r'devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
path(r'devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
path(r'devices/<int:pk>/add-secret/', secret_add, name='device_addsecret'),
path(r'devices/<int:device>/services/assign/', ServiceCreateView.as_view(), name='device_service_assign'),
path(r'devices/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
# Console ports
url(r'^devices/console-ports/add/$', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
url(r'^devices/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
url(r'^console-ports/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
url(r'^console-ports/(?P<pk>\d+)/edit/$', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
url(r'^console-ports/(?P<pk>\d+)/delete/$', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
url(r'^console-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
path(r'devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
path(r'devices/<int:pk>/console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
path(r'devices/<int:pk>/console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
path(r'console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
path(r'console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
path(r'console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
path(r'console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
# Console server ports
url(r'^devices/console-server-ports/add/$', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/edit/$', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
url(r'^console-server-ports/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
url(r'^console-server-ports/(?P<pk>\d+)/delete/$', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
url(r'^console-server-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
url(r'^console-server-ports/rename/$', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
url(r'^console-server-ports/disconnect/$', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
path(r'devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
path(r'devices/<int:pk>/console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
path(r'devices/<int:pk>/console-server-ports/edit/', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'),
path(r'devices/<int:pk>/console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
path(r'console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
path(r'console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
path(r'console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
path(r'console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
path(r'console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
path(r'console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
# Power ports
url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.PowerPortCreateView.as_view(), name='powerport_add'),
url(r'^devices/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
url(r'^power-ports/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
url(r'^power-ports/(?P<pk>\d+)/edit/$', views.PowerPortEditView.as_view(), name='powerport_edit'),
url(r'^power-ports/(?P<pk>\d+)/delete/$', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
url(r'^power-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
path(r'devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
path(r'devices/<int:pk>/power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'),
path(r'devices/<int:pk>/power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
path(r'power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
path(r'power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
path(r'power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
path(r'power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
# Power outlets
url(r'^devices/power-outlets/add/$', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
url(r'^devices/(?P<pk>\d+)/power-outlets/edit/$', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'),
url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
url(r'^power-outlets/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
url(r'^power-outlets/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
url(r'^power-outlets/rename/$', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
url(r'^power-outlets/disconnect/$', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
path(r'devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
path(r'devices/<int:pk>/power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
path(r'devices/<int:pk>/power-outlets/edit/', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'),
path(r'devices/<int:pk>/power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
path(r'power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
path(r'power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
path(r'power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
path(r'power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
path(r'power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
path(r'power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
# Interfaces
url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'),
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
url(r'^interfaces/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
url(r'^interfaces/(?P<pk>\d+)/$', views.InterfaceView.as_view(), name='interface'),
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
url(r'^interfaces/(?P<pk>\d+)/assign-vlans/$', views.InterfaceAssignVLANsView.as_view(), name='interface_assign_vlans'),
url(r'^interfaces/(?P<pk>\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'),
url(r'^interfaces/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
url(r'^interfaces/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
url(r'^interfaces/rename/$', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
url(r'^interfaces/disconnect/$', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
path(r'devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
path(r'devices/<int:pk>/interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'),
path(r'devices/<int:pk>/interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
path(r'devices/<int:pk>/interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
path(r'interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
path(r'interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'),
path(r'interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
path(r'interfaces/<int:pk>/assign-vlans/', views.InterfaceAssignVLANsView.as_view(), name='interface_assign_vlans'),
path(r'interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
path(r'interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
path(r'interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
path(r'interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
path(r'interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
# Front ports
# url(r'^devices/front-ports/add/$', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
url(r'^devices/(?P<pk>\d+)/front-ports/add/$', views.FrontPortCreateView.as_view(), name='frontport_add'),
url(r'^devices/(?P<pk>\d+)/front-ports/edit/$', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'),
url(r'^devices/(?P<pk>\d+)/front-ports/delete/$', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
url(r'^front-ports/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
url(r'^front-ports/(?P<pk>\d+)/edit/$', views.FrontPortEditView.as_view(), name='frontport_edit'),
url(r'^front-ports/(?P<pk>\d+)/delete/$', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
url(r'^front-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
url(r'^front-ports/rename/$', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
url(r'^front-ports/disconnect/$', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
# path(r'devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
path(r'devices/<int:pk>/front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'),
path(r'devices/<int:pk>/front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'),
path(r'devices/<int:pk>/front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
path(r'front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
path(r'front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'),
path(r'front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
path(r'front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
path(r'front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
path(r'front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
# Rear ports
# url(r'^devices/rear-ports/add/$', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
url(r'^devices/(?P<pk>\d+)/rear-ports/add/$', views.RearPortCreateView.as_view(), name='rearport_add'),
url(r'^devices/(?P<pk>\d+)/rear-ports/edit/$', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'),
url(r'^devices/(?P<pk>\d+)/rear-ports/delete/$', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
url(r'^rear-ports/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
url(r'^rear-ports/(?P<pk>\d+)/edit/$', views.RearPortEditView.as_view(), name='rearport_edit'),
url(r'^rear-ports/(?P<pk>\d+)/delete/$', views.RearPortDeleteView.as_view(), name='rearport_delete'),
url(r'^rear-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
url(r'^rear-ports/rename/$', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
url(r'^rear-ports/disconnect/$', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
# path(r'devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
path(r'devices/<int:pk>/rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'),
path(r'devices/<int:pk>/rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'),
path(r'devices/<int:pk>/rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
path(r'rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
path(r'rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
path(r'rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
path(r'rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
path(r'rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
path(r'rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
# Device bays
url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
url(r'^devices/(?P<pk>\d+)/bays/delete/$', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
url(r'^device-bays/(?P<pk>\d+)/edit/$', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
url(r'^device-bays/(?P<pk>\d+)/populate/$', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
url(r'^device-bays/rename/$', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
path(r'devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
path(r'devices/<int:pk>/bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
path(r'devices/<int:pk>/bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
path(r'device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
path(r'device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
path(r'device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
path(r'device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
path(r'device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
# Inventory items
url(r'^inventory-items/$', views.InventoryItemListView.as_view(), name='inventoryitem_list'),
url(r'^inventory-items/import/$', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'),
url(r'^inventory-items/edit/$', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'),
url(r'^inventory-items/delete/$', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'),
url(r'^inventory-items/(?P<pk>\d+)/edit/$', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
url(r'^inventory-items/(?P<pk>\d+)/delete/$', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
url(r'^devices/(?P<device>\d+)/inventory-items/add/$', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),
path(r'inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'),
path(r'inventory-items/import/', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'),
path(r'inventory-items/edit/', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'),
path(r'inventory-items/delete/', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'),
path(r'inventory-items/<int:pk>/edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
path(r'inventory-items/<int:pk>/delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
path(r'devices/<int:device>/inventory-items/add/', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),
# Cables
url(r'^cables/$', views.CableListView.as_view(), name='cable_list'),
url(r'^cables/import/$', views.CableBulkImportView.as_view(), name='cable_import'),
url(r'^cables/edit/$', views.CableBulkEditView.as_view(), name='cable_bulk_edit'),
url(r'^cables/delete/$', views.CableBulkDeleteView.as_view(), name='cable_bulk_delete'),
url(r'^cables/(?P<pk>\d+)/$', views.CableView.as_view(), name='cable'),
url(r'^cables/(?P<pk>\d+)/edit/$', views.CableEditView.as_view(), name='cable_edit'),
url(r'^cables/(?P<pk>\d+)/delete/$', views.CableDeleteView.as_view(), name='cable_delete'),
url(r'^cables/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}),
path(r'cables/', views.CableListView.as_view(), name='cable_list'),
path(r'cables/import/', views.CableBulkImportView.as_view(), name='cable_import'),
path(r'cables/edit/', views.CableBulkEditView.as_view(), name='cable_bulk_edit'),
path(r'cables/delete/', views.CableBulkDeleteView.as_view(), name='cable_bulk_delete'),
path(r'cables/<int:pk>/', views.CableView.as_view(), name='cable'),
path(r'cables/<int:pk>/edit/', views.CableEditView.as_view(), name='cable_edit'),
path(r'cables/<int:pk>/delete/', views.CableDeleteView.as_view(), name='cable_delete'),
path(r'cables/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}),
# Console/power/interface connections (read-only)
url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
url(r'^power-connections/$', views.PowerConnectionsListView.as_view(), name='power_connections_list'),
url(r'^interface-connections/$', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
path(r'console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
path(r'power-connections/', views.PowerConnectionsListView.as_view(), name='power_connections_list'),
path(r'interface-connections/', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
# Virtual chassis
url(r'^virtual-chassis/$', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),
url(r'^virtual-chassis/add/$', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'),
url(r'^virtual-chassis/(?P<pk>\d+)/edit/$', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
url(r'^virtual-chassis/(?P<pk>\d+)/delete/$', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
url(r'^virtual-chassis/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}),
url(r'^virtual-chassis/(?P<pk>\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
url(r'^virtual-chassis-members/(?P<pk>\d+)/delete/$', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
path(r'virtual-chassis/', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),
path(r'virtual-chassis/add/', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'),
path(r'virtual-chassis/<int:pk>/edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
path(r'virtual-chassis/<int:pk>/delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
path(r'virtual-chassis/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}),
path(r'virtual-chassis/<int:pk>/add-member/', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
path(r'virtual-chassis-members/<int:pk>/delete/', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
# Power panels
url(r'^power-panels/$', views.PowerPanelListView.as_view(), name='powerpanel_list'),
url(r'^power-panels/add/$', views.PowerPanelCreateView.as_view(), name='powerpanel_add'),
url(r'^power-panels/import/$', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'),
url(r'^power-panels/delete/$', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'),
url(r'^power-panels/(?P<pk>\d+)/$', views.PowerPanelView.as_view(), name='powerpanel'),
url(r'^power-panels/(?P<pk>\d+)/edit/$', views.PowerPanelEditView.as_view(), name='powerpanel_edit'),
url(r'^power-panels/(?P<pk>\d+)/delete/$', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'),
url(r'^power-panels/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}),
path(r'power-panels/', views.PowerPanelListView.as_view(), name='powerpanel_list'),
path(r'power-panels/add/', views.PowerPanelCreateView.as_view(), name='powerpanel_add'),
path(r'power-panels/import/', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'),
path(r'power-panels/delete/', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'),
path(r'power-panels/<int:pk>/', views.PowerPanelView.as_view(), name='powerpanel'),
path(r'power-panels/<int:pk>/edit/', views.PowerPanelEditView.as_view(), name='powerpanel_edit'),
path(r'power-panels/<int:pk>/delete/', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'),
path(r'power-panels/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}),
# Power feeds
url(r'^power-feeds/$', views.PowerFeedListView.as_view(), name='powerfeed_list'),
url(r'^power-feeds/add/$', views.PowerFeedEditView.as_view(), name='powerfeed_add'),
url(r'^power-feeds/import/$', views.PowerFeedBulkImportView.as_view(), name='powerfeed_import'),
url(r'^power-feeds/edit/$', views.PowerFeedBulkEditView.as_view(), name='powerfeed_bulk_edit'),
url(r'^power-feeds/delete/$', views.PowerFeedBulkDeleteView.as_view(), name='powerfeed_bulk_delete'),
url(r'^power-feeds/(?P<pk>\d+)/$', views.PowerFeedView.as_view(), name='powerfeed'),
url(r'^power-feeds/(?P<pk>\d+)/edit/$', views.PowerFeedEditView.as_view(), name='powerfeed_edit'),
url(r'^power-feeds/(?P<pk>\d+)/delete/$', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'),
url(r'^power-feeds/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}),
path(r'power-feeds/', views.PowerFeedListView.as_view(), name='powerfeed_list'),
path(r'power-feeds/add/', views.PowerFeedEditView.as_view(), name='powerfeed_add'),
path(r'power-feeds/import/', views.PowerFeedBulkImportView.as_view(), name='powerfeed_import'),
path(r'power-feeds/edit/', views.PowerFeedBulkEditView.as_view(), name='powerfeed_bulk_edit'),
path(r'power-feeds/delete/', views.PowerFeedBulkDeleteView.as_view(), name='powerfeed_bulk_delete'),
path(r'power-feeds/<int:pk>/', views.PowerFeedView.as_view(), name='powerfeed'),
path(r'power-feeds/<int:pk>/edit/', views.PowerFeedEditView.as_view(), name='powerfeed_edit'),
path(r'power-feeds/<int:pk>/delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'),
path(r'power-feeds/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}),
]

View File

@ -253,6 +253,14 @@ class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
default_return_url = 'dcim:site_list'
class SiteBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_site'
queryset = Site.objects.select_related('region', 'tenant')
filter = filters.SiteFilter
table = tables.SiteTable
default_return_url = 'dcim:site_list'
#
# Rack groups
#
@ -464,7 +472,7 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class RackReservationListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_rackreservation'
queryset = RackReservation.objects.all()
queryset = RackReservation.objects.select_related('rack__site')
filter = filters.RackReservationFilter
filter_form = forms.RackReservationFilterForm
table = tables.RackReservationTable
@ -523,6 +531,7 @@ class ManufacturerListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_manufacturer'
queryset = Manufacturer.objects.annotate(
devicetype_count=Count('device_types', distinct=True),
inventoryitem_count=Count('inventory_items', distinct=True),
platform_count=Count('platforms', distinct=True),
)
table = tables.ManufacturerTable

View File

@ -30,10 +30,6 @@ def cache_changed_object(instance, **kwargs):
def _record_object_deleted(request, instance, **kwargs):
# Force resolution of request.user in case it's still a SimpleLazyObject. This seems to happen
# occasionally during tests, but haven't been able to determine why.
assert request.user.is_authenticated
# Record that the object was deleted
if hasattr(instance, 'log_change'):
instance.log_change(request.user, request.id, OBJECTCHANGE_ACTION_DELETE)
@ -53,7 +49,7 @@ class ObjectChangeMiddleware(object):
2. Enqueue any relevant webhooks.
3. Increment metric counter for the event type
The post_save and pre_delete signals are employed to catch object modifications, however changes are recorded a bit
The post_save and post_delete signals are employed to catch object modifications, however changes are recorded a bit
differently for each. Objects being saved are cached into thread-local storage for action *after* the response has
completed. This ensures that serialization of the object is performed only after any related objects (e.g. tags)
have been created. Conversely, deletions are acted upon immediately, so that the serialized representation of the
@ -71,10 +67,10 @@ class ObjectChangeMiddleware(object):
# the same request.
request.id = uuid.uuid4()
# Signals don't include the request context, so we're currying it into the pre_delete function ahead of time.
# Signals don't include the request context, so we're currying it into the post_delete function ahead of time.
record_object_deleted = curry(_record_object_deleted, request)
# Connect our receivers to the post_save and pre_delete signals.
# Connect our receivers to the post_save and post_delete signals.
post_save.connect(cache_changed_object, dispatch_uid='record_object_saved')
post_delete.connect(record_object_deleted, dispatch_uid='record_object_deleted')

View File

@ -108,17 +108,22 @@ class Webhook(models.Model):
#
class CustomFieldModel(models.Model):
_cf = None
class Meta:
abstract = True
@property
def cf(self):
"""
Name-based CustomFieldValue accessor for use in templates
"""
if not hasattr(self, 'get_custom_fields'):
return dict()
return {field.name: value for field, value in self.get_custom_fields().items()}
if self._cf is None:
# Cache all custom field values for this instance
self._cf = {
field.name: value for field, value in self.get_custom_fields().items()
}
return self._cf
def get_custom_fields(self):
"""
@ -131,7 +136,7 @@ class CustomFieldModel(models.Model):
# If the object exists, populate its custom fields with values
if hasattr(self, 'pk'):
values = CustomFieldValue.objects.filter(obj_type=content_type, obj_id=self.pk).select_related('field')
values = self.custom_field_values.all()
values_dict = {cfv.field_id: cfv.value for cfv in values}
return OrderedDict([(field, values_dict.get(field.pk)) for field in fields])
else:

View File

@ -1,6 +1,24 @@
from collections import OrderedDict
from django.db.models import Q, QuerySet
class CustomFieldQueryset:
"""
Annotate custom fields on objects within a QuerySet.
"""
def __init__(self, queryset, custom_fields):
self.queryset = queryset
self.model = queryset.model
self.custom_fields = custom_fields
def __iter__(self):
for obj in self.queryset:
values_dict = {cfv.field_id: cfv.value for cfv in obj.custom_field_values.all()}
obj.custom_fields = OrderedDict([(field, values_dict.get(field.pk)) for field in self.custom_fields])
yield obj
class ConfigContextQuerySet(QuerySet):
def get_for_object(self, obj):

View File

@ -1,4 +1,4 @@
from django.conf.urls import url
from django.urls import path
from extras import views
from extras.models import Tag
@ -8,33 +8,33 @@ app_name = 'extras'
urlpatterns = [
# Tags
url(r'^tags/$', views.TagListView.as_view(), name='tag_list'),
url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
url(r'^tags/(?P<slug>[\w-]+)/$', views.TagView.as_view(), name='tag'),
url(r'^tags/(?P<slug>[\w-]+)/edit/$', views.TagEditView.as_view(), name='tag_edit'),
url(r'^tags/(?P<slug>[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'),
url(r'^tags/(?P<slug>[\w-]+)/changelog/$', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}),
path(r'tags/', views.TagListView.as_view(), name='tag_list'),
path(r'tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
path(r'tags/<slug:slug>/', views.TagView.as_view(), name='tag'),
path(r'tags/<slug:slug>/edit/', views.TagEditView.as_view(), name='tag_edit'),
path(r'tags/<slug:slug>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
path(r'tags/<slug:slug>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}),
# Config contexts
url(r'^config-contexts/$', views.ConfigContextListView.as_view(), name='configcontext_list'),
url(r'^config-contexts/add/$', views.ConfigContextCreateView.as_view(), name='configcontext_add'),
url(r'^config-contexts/edit/$', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'),
url(r'^config-contexts/delete/$', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'),
url(r'^config-contexts/(?P<pk>\d+)/$', views.ConfigContextView.as_view(), name='configcontext'),
url(r'^config-contexts/(?P<pk>\d+)/edit/$', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
url(r'^config-contexts/(?P<pk>\d+)/delete/$', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
path(r'config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'),
path(r'config-contexts/add/', views.ConfigContextCreateView.as_view(), name='configcontext_add'),
path(r'config-contexts/edit/', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'),
path(r'config-contexts/delete/', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'),
path(r'config-contexts/<int:pk>/', views.ConfigContextView.as_view(), name='configcontext'),
path(r'config-contexts/<int:pk>/edit/', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
path(r'config-contexts/<int:pk>/delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
# Image attachments
url(r'^image-attachments/(?P<pk>\d+)/edit/$', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
url(r'^image-attachments/(?P<pk>\d+)/delete/$', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
path(r'image-attachments/<int:pk>/edit/', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
path(r'image-attachments/<int:pk>/delete/', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
# Reports
url(r'^reports/$', views.ReportListView.as_view(), name='report_list'),
url(r'^reports/(?P<name>[^/]+\.[^/]+)/$', views.ReportView.as_view(), name='report'),
url(r'^reports/(?P<name>[^/]+\.[^/]+)/run/$', views.ReportRunView.as_view(), name='report_run'),
path(r'reports/', views.ReportListView.as_view(), name='report_list'),
path(r'reports/<str:name>/', views.ReportView.as_view(), name='report'),
path(r'reports/<str:name>/run/', views.ReportRunView.as_view(), name='report_run'),
# Change logging
url(r'^changelog/$', views.ObjectChangeListView.as_view(), name='objectchange_list'),
url(r'^changelog/(?P<pk>\d+)/$', views.ObjectChangeView.as_view(), name='objectchange'),
path(r'changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
path(r'changelog/<int:pk>/', views.ObjectChangeView.as_view(), name='objectchange'),
]

View File

@ -6,14 +6,14 @@ from netaddr.core import AddrFormatError
from dcim.models import Site, Device, Interface
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from virtualization.models import VirtualMachine
from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
class VRFFilter(CustomFieldFilterSet):
class VRFFilter(TenancyFilterSet, CustomFieldFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@ -22,16 +22,6 @@ class VRFFilter(CustomFieldFilterSet):
method='search',
label='Search',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
tag = TagFilter()
def search(self, queryset, name, value):
@ -120,7 +110,7 @@ class RoleFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug']
class PrefixFilter(CustomFieldFilterSet):
class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@ -159,16 +149,6 @@ class PrefixFilter(CustomFieldFilterSet):
to_field_name='rd',
label='VRF (RD)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
@ -267,7 +247,7 @@ class PrefixFilter(CustomFieldFilterSet):
return queryset.filter(prefix__net_mask_length=value)
class IPAddressFilter(CustomFieldFilterSet):
class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@ -298,16 +278,6 @@ class IPAddressFilter(CustomFieldFilterSet):
to_field_name='rd',
label='VRF (RD)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
device = django_filters.CharFilter(
method='filter_device',
field_name='name',
@ -329,6 +299,12 @@ class IPAddressFilter(CustomFieldFilterSet):
to_field_name='name',
label='Virtual machine (name)',
)
interface = django_filters.ModelMultipleChoiceFilter(
field_name='interface__name',
queryset=Interface.objects.all(),
to_field_name='name',
label='Interface (ID)',
)
interface_id = django_filters.ModelMultipleChoiceFilter(
queryset=Interface.objects.all(),
label='Interface (ID)',
@ -408,7 +384,7 @@ class VLANGroupFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug']
class VLANFilter(CustomFieldFilterSet):
class VLANFilter(TenancyFilterSet, CustomFieldFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@ -437,16 +413,6 @@ class VLANFilter(CustomFieldFilterSet):
to_field_name='slug',
label='Group',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
role_id = django_filters.ModelMultipleChoiceFilter(
queryset=Role.objects.all(),
label='Role (ID)',

View File

@ -6,6 +6,7 @@ from taggit.forms import TagField
from dcim.models import Site, Rack, Device, Interface
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import TenancyForm
from tenancy.forms import TenancyFilterForm
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField,
@ -97,22 +98,13 @@ class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm
]
class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = VRF
field_order = ['q', 'tenant_group', 'tenant']
q = forms.CharField(
required=False,
label='Search'
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
)
#
@ -497,8 +489,12 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
]
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = Prefix
field_order = [
'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'status', 'site', 'role', 'tenant_group', 'tenant',
'is_pool', 'expand',
]
q = forms.CharField(
required=False,
label='Search'
@ -533,16 +529,6 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
null_option=True,
)
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
)
status = forms.MultipleChoiceField(
choices=PREFIX_STATUS_CHOICES,
required=False,
@ -949,8 +935,11 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form):
)
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = IPAddress
field_order = [
'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'tenant_group', 'tenant',
]
q = forms.CharField(
required=False,
label='Search'
@ -985,16 +974,6 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
null_option=True,
)
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
)
status = forms.MultipleChoiceField(
choices=IPADDRESS_STATUS_CHOICES,
required=False,
@ -1226,8 +1205,9 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
]
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = VLAN
field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
q = forms.CharField(
required=False,
label='Search'
@ -1251,16 +1231,6 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
null_option=True,
)
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
)
status = forms.MultipleChoiceField(
choices=VLAN_STATUS_CHOICES,
required=False,

View File

@ -319,6 +319,7 @@ class PrefixTable(BaseTable):
class PrefixDetailTable(PrefixTable):
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False)
tenant = tables.TemplateColumn(template_code=COL_TENANT)
class Meta(PrefixTable.Meta):
fields = ('pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description')
@ -351,6 +352,7 @@ class IPAddressDetailTable(IPAddressTable):
nat_inside = tables.LinkColumn(
'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)'
)
tenant = tables.TemplateColumn(template_code=COL_TENANT)
class Meta(IPAddressTable.Meta):
fields = (
@ -426,6 +428,7 @@ class VLANTable(BaseTable):
class VLANDetailTable(VLANTable):
prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes')
tenant = tables.TemplateColumn(template_code=COL_TENANT)
class Meta(VLANTable.Meta):
fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')

View File

@ -1,3 +1,5 @@
import json
from django.urls import reverse
from netaddr import IPNetwork
from rest_framework import status
@ -870,6 +872,8 @@ class VLANTest(APITestCase):
self.vlan2 = VLAN.objects.create(vid=2, name='Test VLAN 2')
self.vlan3 = VLAN.objects.create(vid=3, name='Test VLAN 3')
self.prefix1 = Prefix.objects.create(prefix=IPNetwork('192.168.1.0/24'))
def test_get_vlan(self):
url = reverse('ipam-api:vlan-detail', kwargs={'pk': self.vlan1.pk})
@ -960,6 +964,20 @@ class VLANTest(APITestCase):
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
self.assertEqual(VLAN.objects.count(), 2)
def test_delete_vlan_with_prefix(self):
self.prefix1.vlan = self.vlan1
self.prefix1.save()
url = reverse('ipam-api:vlan-detail', kwargs={'pk': self.vlan1.pk})
response = self.client.delete(url, **self.header)
# can't use assertHttpStatus here because we don't have response.data
self.assertEqual(response.status_code, 409)
content = json.loads(response.content.decode('utf-8'))
self.assertIn('detail', content)
self.assertTrue(content['detail'].startswith('Unable to delete object.'))
class ServiceTest(APITestCase):

View File

@ -1,4 +1,4 @@
from django.conf.urls import url
from django.urls import path
from extras.views import ObjectChangeLogView
from . import views
@ -8,97 +8,97 @@ app_name = 'ipam'
urlpatterns = [
# VRFs
url(r'^vrfs/$', views.VRFListView.as_view(), name='vrf_list'),
url(r'^vrfs/add/$', views.VRFCreateView.as_view(), name='vrf_add'),
url(r'^vrfs/import/$', views.VRFBulkImportView.as_view(), name='vrf_import'),
url(r'^vrfs/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
url(r'^vrfs/(?P<pk>\d+)/$', views.VRFView.as_view(), name='vrf'),
url(r'^vrfs/(?P<pk>\d+)/edit/$', views.VRFEditView.as_view(), name='vrf_edit'),
url(r'^vrfs/(?P<pk>\d+)/delete/$', views.VRFDeleteView.as_view(), name='vrf_delete'),
url(r'^vrfs/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}),
path(r'vrfs/', views.VRFListView.as_view(), name='vrf_list'),
path(r'vrfs/add/', views.VRFCreateView.as_view(), name='vrf_add'),
path(r'vrfs/import/', views.VRFBulkImportView.as_view(), name='vrf_import'),
path(r'vrfs/edit/', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
path(r'vrfs/delete/', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
path(r'vrfs/<int:pk>/', views.VRFView.as_view(), name='vrf'),
path(r'vrfs/<int:pk>/edit/', views.VRFEditView.as_view(), name='vrf_edit'),
path(r'vrfs/<int:pk>/delete/', views.VRFDeleteView.as_view(), name='vrf_delete'),
path(r'vrfs/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}),
# RIRs
url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'),
url(r'^rirs/add/$', views.RIRCreateView.as_view(), name='rir_add'),
url(r'^rirs/import/$', views.RIRBulkImportView.as_view(), name='rir_import'),
url(r'^rirs/delete/$', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
url(r'^rirs/(?P<slug>[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'),
url(r'^vrfs/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}),
path(r'rirs/', views.RIRListView.as_view(), name='rir_list'),
path(r'rirs/add/', views.RIRCreateView.as_view(), name='rir_add'),
path(r'rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'),
path(r'rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
path(r'rirs/<slug:slug>/edit/', views.RIREditView.as_view(), name='rir_edit'),
path(r'vrfs/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}),
# Aggregates
url(r'^aggregates/$', views.AggregateListView.as_view(), name='aggregate_list'),
url(r'^aggregates/add/$', views.AggregateCreateView.as_view(), name='aggregate_add'),
url(r'^aggregates/import/$', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
url(r'^aggregates/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
url(r'^aggregates/(?P<pk>\d+)/$', views.AggregateView.as_view(), name='aggregate'),
url(r'^aggregates/(?P<pk>\d+)/edit/$', views.AggregateEditView.as_view(), name='aggregate_edit'),
url(r'^aggregates/(?P<pk>\d+)/delete/$', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
url(r'^aggregates/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}),
path(r'aggregates/', views.AggregateListView.as_view(), name='aggregate_list'),
path(r'aggregates/add/', views.AggregateCreateView.as_view(), name='aggregate_add'),
path(r'aggregates/import/', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
path(r'aggregates/edit/', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
path(r'aggregates/delete/', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
path(r'aggregates/<int:pk>/', views.AggregateView.as_view(), name='aggregate'),
path(r'aggregates/<int:pk>/edit/', views.AggregateEditView.as_view(), name='aggregate_edit'),
path(r'aggregates/<int:pk>/delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
path(r'aggregates/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}),
# Roles
url(r'^roles/$', views.RoleListView.as_view(), name='role_list'),
url(r'^roles/add/$', views.RoleCreateView.as_view(), name='role_add'),
url(r'^roles/import/$', views.RoleBulkImportView.as_view(), name='role_import'),
url(r'^roles/delete/$', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
url(r'^roles/(?P<slug>[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'),
url(r'^roles/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}),
path(r'roles/', views.RoleListView.as_view(), name='role_list'),
path(r'roles/add/', views.RoleCreateView.as_view(), name='role_add'),
path(r'roles/import/', views.RoleBulkImportView.as_view(), name='role_import'),
path(r'roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
path(r'roles/<slug:slug>/edit/', views.RoleEditView.as_view(), name='role_edit'),
path(r'roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}),
# Prefixes
url(r'^prefixes/$', views.PrefixListView.as_view(), name='prefix_list'),
url(r'^prefixes/add/$', views.PrefixCreateView.as_view(), name='prefix_add'),
url(r'^prefixes/import/$', views.PrefixBulkImportView.as_view(), name='prefix_import'),
url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'),
url(r'^prefixes/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
url(r'^prefixes/(?P<pk>\d+)/$', views.PrefixView.as_view(), name='prefix'),
url(r'^prefixes/(?P<pk>\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'),
url(r'^prefixes/(?P<pk>\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'),
url(r'^prefixes/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}),
url(r'^prefixes/(?P<pk>\d+)/prefixes/$', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'),
url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
path(r'prefixes/', views.PrefixListView.as_view(), name='prefix_list'),
path(r'prefixes/add/', views.PrefixCreateView.as_view(), name='prefix_add'),
path(r'prefixes/import/', views.PrefixBulkImportView.as_view(), name='prefix_import'),
path(r'prefixes/edit/', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'),
path(r'prefixes/delete/', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
path(r'prefixes/<int:pk>/', views.PrefixView.as_view(), name='prefix'),
path(r'prefixes/<int:pk>/edit/', views.PrefixEditView.as_view(), name='prefix_edit'),
path(r'prefixes/<int:pk>/delete/', views.PrefixDeleteView.as_view(), name='prefix_delete'),
path(r'prefixes/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}),
path(r'prefixes/<int:pk>/prefixes/', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'),
path(r'prefixes/<int:pk>/ip-addresses/', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
# IP addresses
url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'),
url(r'^ip-addresses/add/$', views.IPAddressCreateView.as_view(), name='ipaddress_add'),
url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkCreateView.as_view(), name='ipaddress_bulk_add'),
url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
url(r'^ip-addresses/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='ipaddress_changelog', kwargs={'model': IPAddress}),
url(r'^ip-addresses/assign/$', views.IPAddressAssignView.as_view(), name='ipaddress_assign'),
url(r'^ip-addresses/(?P<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'),
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
path(r'ip-addresses/', views.IPAddressListView.as_view(), name='ipaddress_list'),
path(r'ip-addresses/add/', views.IPAddressCreateView.as_view(), name='ipaddress_add'),
path(r'ip-addresses/bulk-add/', views.IPAddressBulkCreateView.as_view(), name='ipaddress_bulk_add'),
path(r'ip-addresses/import/', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
path(r'ip-addresses/edit/', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
path(r'ip-addresses/delete/', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
path(r'ip-addresses/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='ipaddress_changelog', kwargs={'model': IPAddress}),
path(r'ip-addresses/assign/', views.IPAddressAssignView.as_view(), name='ipaddress_assign'),
path(r'ip-addresses/<int:pk>/', views.IPAddressView.as_view(), name='ipaddress'),
path(r'ip-addresses/<int:pk>/edit/', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
path(r'ip-addresses/<int:pk>/delete/', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
# VLAN groups
url(r'^vlan-groups/$', views.VLANGroupListView.as_view(), name='vlangroup_list'),
url(r'^vlan-groups/add/$', views.VLANGroupCreateView.as_view(), name='vlangroup_add'),
url(r'^vlan-groups/import/$', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
url(r'^vlan-groups/(?P<pk>\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
url(r'^vlan-groups/(?P<pk>\d+)/vlans/$', views.VLANGroupVLANsView.as_view(), name='vlangroup_vlans'),
url(r'^vlan-groups/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}),
path(r'vlan-groups/', views.VLANGroupListView.as_view(), name='vlangroup_list'),
path(r'vlan-groups/add/', views.VLANGroupCreateView.as_view(), name='vlangroup_add'),
path(r'vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
path(r'vlan-groups/delete/', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
path(r'vlan-groups/<int:pk>/edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
path(r'vlan-groups/<int:pk>/vlans/', views.VLANGroupVLANsView.as_view(), name='vlangroup_vlans'),
path(r'vlan-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}),
# VLANs
url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'),
url(r'^vlans/add/$', views.VLANCreateView.as_view(), name='vlan_add'),
url(r'^vlans/import/$', views.VLANBulkImportView.as_view(), name='vlan_import'),
url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
url(r'^vlans/delete/$', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
url(r'^vlans/(?P<pk>\d+)/$', views.VLANView.as_view(), name='vlan'),
url(r'^vlans/(?P<pk>\d+)/members/$', views.VLANMembersView.as_view(), name='vlan_members'),
url(r'^vlans/(?P<pk>\d+)/edit/$', views.VLANEditView.as_view(), name='vlan_edit'),
url(r'^vlans/(?P<pk>\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'),
url(r'^vlans/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}),
path(r'vlans/', views.VLANListView.as_view(), name='vlan_list'),
path(r'vlans/add/', views.VLANCreateView.as_view(), name='vlan_add'),
path(r'vlans/import/', views.VLANBulkImportView.as_view(), name='vlan_import'),
path(r'vlans/edit/', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
path(r'vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
path(r'vlans/<int:pk>/', views.VLANView.as_view(), name='vlan'),
path(r'vlans/<int:pk>/members/', views.VLANMembersView.as_view(), name='vlan_members'),
path(r'vlans/<int:pk>/edit/', views.VLANEditView.as_view(), name='vlan_edit'),
path(r'vlans/<int:pk>/delete/', views.VLANDeleteView.as_view(), name='vlan_delete'),
path(r'vlans/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}),
# Services
url(r'^services/$', views.ServiceListView.as_view(), name='service_list'),
url(r'^services/edit/$', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'),
url(r'^services/delete/$', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'),
url(r'^services/(?P<pk>\d+)/$', views.ServiceView.as_view(), name='service'),
url(r'^services/(?P<pk>\d+)/edit/$', views.ServiceEditView.as_view(), name='service_edit'),
url(r'^services/(?P<pk>\d+)/delete/$', views.ServiceDeleteView.as_view(), name='service_delete'),
url(r'^services/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}),
path(r'services/', views.ServiceListView.as_view(), name='service_list'),
path(r'services/edit/', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'),
path(r'services/delete/', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'),
path(r'services/<int:pk>/', views.ServiceView.as_view(), name='service'),
path(r'services/<int:pk>/edit/', views.ServiceEditView.as_view(), name='service_edit'),
path(r'services/<int:pk>/delete/', views.ServiceDeleteView.as_view(), name='service_delete'),
path(r'services/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}),
]

View File

@ -1,5 +1,6 @@
from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls import include
from django.urls import path, re_path
from django.views.static import serve
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
@ -24,63 +25,63 @@ schema_view = get_schema_view(
_patterns = [
# Base views
url(r'^$', HomeView.as_view(), name='home'),
url(r'^search/$', SearchView.as_view(), name='search'),
path(r'', HomeView.as_view(), name='home'),
path(r'search/', SearchView.as_view(), name='search'),
# Login/logout
url(r'^login/$', LoginView.as_view(), name='login'),
url(r'^logout/$', LogoutView.as_view(), name='logout'),
path(r'login/', LoginView.as_view(), name='login'),
path(r'logout/', LogoutView.as_view(), name='logout'),
# Apps
url(r'^circuits/', include('circuits.urls')),
url(r'^dcim/', include('dcim.urls')),
url(r'^extras/', include('extras.urls')),
url(r'^ipam/', include('ipam.urls')),
url(r'^secrets/', include('secrets.urls')),
url(r'^tenancy/', include('tenancy.urls')),
url(r'^user/', include('users.urls')),
url(r'^virtualization/', include('virtualization.urls')),
path(r'circuits/', include('circuits.urls')),
path(r'dcim/', include('dcim.urls')),
path(r'extras/', include('extras.urls')),
path(r'ipam/', include('ipam.urls')),
path(r'secrets/', include('secrets.urls')),
path(r'tenancy/', include('tenancy.urls')),
path(r'user/', include('users.urls')),
path(r'virtualization/', include('virtualization.urls')),
# API
url(r'^api/$', APIRootView.as_view(), name='api-root'),
url(r'^api/circuits/', include('circuits.api.urls')),
url(r'^api/dcim/', include('dcim.api.urls')),
url(r'^api/extras/', include('extras.api.urls')),
url(r'^api/ipam/', include('ipam.api.urls')),
url(r'^api/secrets/', include('secrets.api.urls')),
url(r'^api/tenancy/', include('tenancy.api.urls')),
url(r'^api/virtualization/', include('virtualization.api.urls')),
url(r'^api/docs/$', schema_view.with_ui('swagger'), name='api_docs'),
url(r'^api/redoc/$', schema_view.with_ui('redoc'), name='api_redocs'),
url(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(), name='schema_swagger'),
path(r'api/', APIRootView.as_view(), name='api-root'),
path(r'api/circuits/', include('circuits.api.urls')),
path(r'api/dcim/', include('dcim.api.urls')),
path(r'api/extras/', include('extras.api.urls')),
path(r'api/ipam/', include('ipam.api.urls')),
path(r'api/secrets/', include('secrets.api.urls')),
path(r'api/tenancy/', include('tenancy.api.urls')),
path(r'api/virtualization/', include('virtualization.api.urls')),
path(r'api/docs/', schema_view.with_ui('swagger'), name='api_docs'),
path(r'api/redoc/', schema_view.with_ui('redoc'), name='api_redocs'),
re_path(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(), name='schema_swagger'),
# Serving static media in Django to pipe it through LoginRequiredMiddleware
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
path(r'media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT}),
# Admin
url(r'^admin/', admin_site.urls),
path(r'admin/', admin_site.urls),
]
if settings.WEBHOOKS_ENABLED:
_patterns += [
url(r'^admin/webhook-backend-status/', include('django_rq.urls')),
path(r'admin/webhook-backend-status/', include('django_rq.urls')),
]
if settings.DEBUG:
import debug_toolbar
_patterns += [
url(r'^__debug__/', include(debug_toolbar.urls)),
path(r'__debug__/', include(debug_toolbar.urls)),
]
if settings.METRICS_ENABLED:
_patterns += [
url('', include('django_prometheus.urls')),
path('', include('django_prometheus.urls')),
]
# Prepend BASE_PATH
urlpatterns = [
url(r'^{}'.format(settings.BASE_PATH), include(_patterns))
path(r'{}'.format(settings.BASE_PATH), include(_patterns))
]
handler500 = 'utilities.views.server_error'

View File

@ -559,6 +559,7 @@ table.report th a {
.color-block {
display: block;
width: 80px;
border: 1px solid grey;
}
.text-nowrap {
white-space: nowrap;

View File

@ -267,6 +267,10 @@ $(document).ready(function() {
processResults: function (data) {
var results = $.map(data.results, function (obj) {
// If tag contains space add double quotes
if (/\s/.test(obj.name))
obj.name = '"' + obj.name + '"'
return {
id: obj.name,
text: obj.name

View File

@ -1,4 +1,4 @@
from django.conf.urls import url
from django.urls import path
from extras.views import ObjectChangeLogView
from . import views
@ -8,21 +8,21 @@ app_name = 'secrets'
urlpatterns = [
# Secret roles
url(r'^secret-roles/$', views.SecretRoleListView.as_view(), name='secretrole_list'),
url(r'^secret-roles/add/$', views.SecretRoleCreateView.as_view(), name='secretrole_add'),
url(r'^secret-roles/import/$', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'),
url(r'^secret-roles/delete/$', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'),
url(r'^secret-roles/(?P<slug>[\w-]+)/edit/$', views.SecretRoleEditView.as_view(), name='secretrole_edit'),
url(r'^secret-roles/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}),
path(r'secret-roles/', views.SecretRoleListView.as_view(), name='secretrole_list'),
path(r'secret-roles/add/', views.SecretRoleCreateView.as_view(), name='secretrole_add'),
path(r'secret-roles/import/', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'),
path(r'secret-roles/delete/', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'),
path(r'secret-roles/<slug:slug>/edit/', views.SecretRoleEditView.as_view(), name='secretrole_edit'),
path(r'secret-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}),
# Secrets
url(r'^secrets/$', views.SecretListView.as_view(), name='secret_list'),
url(r'^secrets/import/$', views.SecretBulkImportView.as_view(), name='secret_import'),
url(r'^secrets/edit/$', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'),
url(r'^secrets/delete/$', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'),
url(r'^secrets/(?P<pk>\d+)/$', views.SecretView.as_view(), name='secret'),
url(r'^secrets/(?P<pk>\d+)/edit/$', views.secret_edit, name='secret_edit'),
url(r'^secrets/(?P<pk>\d+)/delete/$', views.SecretDeleteView.as_view(), name='secret_delete'),
url(r'^secrets/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='secret_changelog', kwargs={'model': Secret}),
path(r'secrets/', views.SecretListView.as_view(), name='secret_list'),
path(r'secrets/import/', views.SecretBulkImportView.as_view(), name='secret_import'),
path(r'secrets/edit/', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'),
path(r'secrets/delete/', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'),
path(r'secrets/<int:pk>/', views.SecretView.as_view(), name='secret'),
path(r'secrets/<int:pk>/edit/', views.secret_edit, name='secret_edit'),
path(r'secrets/<int:pk>/delete/', views.SecretDeleteView.as_view(), name='secret_delete'),
path(r'secrets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='secret_changelog', kwargs={'model': Secret}),
]

View File

@ -31,7 +31,7 @@
</h4>
<p><span class="label label-{% if cable.status %}success{% else %}info{% endif %}">{{ cable.get_status_display }}</span></p>
<p>{{ cable.get_type_display|default:"" }}</p>
{% if cable.length %}- {{ cable.length }}{{ cable.get_length_unit_display }}{% endif %}
{% if cable.length %}{{ cable.length }} {{ cable.get_length_unit_display }}{% endif %}
<span class="label color-block center-block" style="background-color: #{{ cable.color }}">&nbsp;</span>
{% else %}
<h4 class="text-muted">No Cable</h4>

View File

@ -12,7 +12,7 @@
<h1>{% block title %}Sites{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:site_bulk_edit' %}
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:site_bulk_edit' bulk_delete_url='dcim:site_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}

View File

@ -435,7 +435,12 @@
</ul>
</li>
{% else %}
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in"></i> Log in</a></li>
{% url 'login' as login_url %}
{% if request.path == login_url %}
<li><a href="{{ request.get_full_path }}"><i class="fa fa-sign-in"></i> Log in</a></li>
{% else %}
<li><a href="{{ login_url }}?next={{ request.get_full_path | urlencode }}"><i class="fa fa-sign-in"></i> Log in</a></li>
{% endif %}
{% endif %}
</ul>
<form action="{% url 'search' %}" method="get" class="navbar-form navbar-right" id="navbar_search" role="search">

View File

@ -25,6 +25,7 @@
<div class="panel-body">
{% csrf_token %}
{% if 'next' in request.GET %}<input type="hidden" name="next" value="{{ request.GET.next }}" />{% endif %}
{% if 'next' in request.POST %}<input type="hidden" name="next" value="{{ request.POST.next }}" />{% endif %}
{% render_form form %}
</div>
<div class="panel-footer text-right">

View File

@ -0,0 +1,27 @@
import django_filters
from .models import Tenant, TenantGroup
class TenancyFilterSet(django_filters.FilterSet):
tenant_group_id = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__group__id',
queryset=TenantGroup.objects.all(),
to_field_name='id',
label='Tenant Group (ID)',
)
tenant_group = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__group__slug',
queryset=TenantGroup.objects.all(),
to_field_name='slug',
label='Tenant Group (slug)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)

View File

@ -1,5 +1,4 @@
from django import forms
from django.db.models import Count
from taggit.forms import TagField
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
@ -117,7 +116,7 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
#
# Tenancy form extension
# Form extensions
#
class TenancyForm(ChainedFieldsMixin, forms.Form):
@ -155,3 +154,29 @@ class TenancyForm(ChainedFieldsMixin, forms.Form):
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
class TenancyFilterForm(forms.Form):
tenant_group = FilterChoiceField(
queryset=TenantGroup.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenant-groups/",
value_field="slug",
null_option=True,
filter_for={
'tenant': 'group'
}
)
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
)

View File

@ -1,4 +1,4 @@
from django.conf.urls import url
from django.urls import path
from extras.views import ObjectChangeLogView
from . import views
@ -8,22 +8,22 @@ app_name = 'tenancy'
urlpatterns = [
# Tenant groups
url(r'^tenant-groups/$', views.TenantGroupListView.as_view(), name='tenantgroup_list'),
url(r'^tenant-groups/add/$', views.TenantGroupCreateView.as_view(), name='tenantgroup_add'),
url(r'^tenant-groups/import/$', views.TenantGroupBulkImportView.as_view(), name='tenantgroup_import'),
url(r'^tenant-groups/delete/$', views.TenantGroupBulkDeleteView.as_view(), name='tenantgroup_bulk_delete'),
url(r'^tenant-groups/(?P<slug>[\w-]+)/edit/$', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'),
url(r'^tenant-groups/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='tenantgroup_changelog', kwargs={'model': TenantGroup}),
path(r'tenant-groups/', views.TenantGroupListView.as_view(), name='tenantgroup_list'),
path(r'tenant-groups/add/', views.TenantGroupCreateView.as_view(), name='tenantgroup_add'),
path(r'tenant-groups/import/', views.TenantGroupBulkImportView.as_view(), name='tenantgroup_import'),
path(r'tenant-groups/delete/', views.TenantGroupBulkDeleteView.as_view(), name='tenantgroup_bulk_delete'),
path(r'tenant-groups/<slug:slug>/edit/', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'),
path(r'tenant-groups/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='tenantgroup_changelog', kwargs={'model': TenantGroup}),
# Tenants
url(r'^tenants/$', views.TenantListView.as_view(), name='tenant_list'),
url(r'^tenants/add/$', views.TenantCreateView.as_view(), name='tenant_add'),
url(r'^tenants/import/$', views.TenantBulkImportView.as_view(), name='tenant_import'),
url(r'^tenants/edit/$', views.TenantBulkEditView.as_view(), name='tenant_bulk_edit'),
url(r'^tenants/delete/$', views.TenantBulkDeleteView.as_view(), name='tenant_bulk_delete'),
url(r'^tenants/(?P<slug>[\w-]+)/$', views.TenantView.as_view(), name='tenant'),
url(r'^tenants/(?P<slug>[\w-]+)/edit/$', views.TenantEditView.as_view(), name='tenant_edit'),
url(r'^tenants/(?P<slug>[\w-]+)/delete/$', views.TenantDeleteView.as_view(), name='tenant_delete'),
url(r'^tenants/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='tenant_changelog', kwargs={'model': Tenant}),
path(r'tenants/', views.TenantListView.as_view(), name='tenant_list'),
path(r'tenants/add/', views.TenantCreateView.as_view(), name='tenant_add'),
path(r'tenants/import/', views.TenantBulkImportView.as_view(), name='tenant_import'),
path(r'tenants/edit/', views.TenantBulkEditView.as_view(), name='tenant_bulk_edit'),
path(r'tenants/delete/', views.TenantBulkDeleteView.as_view(), name='tenant_bulk_delete'),
path(r'tenants/<slug:slug>/', views.TenantView.as_view(), name='tenant'),
path(r'tenants/<slug:slug>/edit/', views.TenantEditView.as_view(), name='tenant_edit'),
path(r'tenants/<slug:slug>/delete/', views.TenantDeleteView.as_view(), name='tenant_delete'),
path(r'tenants/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='tenant_changelog', kwargs={'model': Tenant}),
]

View File

@ -1,18 +1,18 @@
from django.conf.urls import url
from django.urls import path
from . import views
app_name = 'user'
urlpatterns = [
url(r'^profile/$', views.ProfileView.as_view(), name='profile'),
url(r'^password/$', views.ChangePasswordView.as_view(), name='change_password'),
url(r'^api-tokens/$', views.TokenListView.as_view(), name='token_list'),
url(r'^api-tokens/add/$', views.TokenEditView.as_view(), name='token_add'),
url(r'^api-tokens/(?P<pk>\d+)/edit/$', views.TokenEditView.as_view(), name='token_edit'),
url(r'^api-tokens/(?P<pk>\d+)/delete/$', views.TokenDeleteView.as_view(), name='token_delete'),
url(r'^user-key/$', views.UserKeyView.as_view(), name='userkey'),
url(r'^user-key/edit/$', views.UserKeyEditView.as_view(), name='userkey_edit'),
url(r'^session-key/delete/$', views.SessionKeyDeleteView.as_view(), name='sessionkey_delete'),
path(r'profile/', views.ProfileView.as_view(), name='profile'),
path(r'password/', views.ChangePasswordView.as_view(), name='change_password'),
path(r'api-tokens/', views.TokenListView.as_view(), name='token_list'),
path(r'api-tokens/add/', views.TokenEditView.as_view(), name='token_add'),
path(r'api-tokens/<int:pk>/edit/', views.TokenEditView.as_view(), name='token_edit'),
path(r'api-tokens/<int:pk>/delete/', views.TokenDeleteView.as_view(), name='token_delete'),
path(r'user-key/', views.UserKeyView.as_view(), name='userkey'),
path(r'user-key/edit/', views.UserKeyEditView.as_view(), name='userkey_edit'),
path(r'session-key/delete/', views.SessionKeyDeleteView.as_view(), name='sessionkey_delete'),
]

View File

@ -4,7 +4,7 @@ import pytz
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDoesNotExist
from django.db.models import ManyToManyField
from django.db.models import ManyToManyField, ProtectedError
from django.http import Http404
from rest_framework.exceptions import APIException
from rest_framework.permissions import BasePermission
@ -274,6 +274,19 @@ class ModelViewSet(_ModelViewSet):
# Fall back to the hard-coded serializer class
return self.serializer_class
def dispatch(self, request, *args, **kwargs):
try:
return super().dispatch(request, *args, **kwargs)
except ProtectedError as e:
models = ['{} ({})'.format(o, o._meta) for o in e.protected_objects.all()]
msg = 'Unable to delete object. The following dependent objects were found: {}'.format(', '.join(models))
return self.finalize_response(
request,
Response({'detail': msg}, status=409),
*args,
**kwargs
)
def list(self, *args, **kwargs):
"""
Call to super to allow for caching

View File

@ -2,6 +2,7 @@ from django.conf import settings
from django.db import ProgrammingError
from django.http import Http404, HttpResponseRedirect
from django.urls import reverse
import urllib
from .views import server_error
@ -22,7 +23,12 @@ class LoginRequiredMiddleware(object):
# performs its own authentication. Also metrics can be read without login.
api_path = reverse('api-root')
if not request.path_info.startswith((api_path, '/metrics')) and request.path_info != settings.LOGIN_URL:
return HttpResponseRedirect('{}?next={}'.format(settings.LOGIN_URL, request.path_info))
return HttpResponseRedirect(
'{}?next={}'.format(
settings.LOGIN_URL,
urllib.parse.quote(request.get_full_path_info())
)
)
return self.get_response(request)

View File

@ -1,5 +1,4 @@
import sys
from collections import OrderedDict
from copy import deepcopy
from django.conf import settings
@ -12,7 +11,7 @@ from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHidd
from django.http import HttpResponse, HttpResponseServerError
from django.shortcuts import get_object_or_404, redirect, render
from django.template import loader
from django.template.exceptions import TemplateDoesNotExist, TemplateSyntaxError
from django.template.exceptions import TemplateDoesNotExist
from django.urls import reverse
from django.utils.html import escape
from django.utils.http import is_safe_url
@ -23,6 +22,7 @@ from django.views.generic import View
from django_tables2 import RequestConfig
from extras.models import CustomField, CustomFieldValue, ExportTemplate
from extras.querysets import CustomFieldQueryset
from utilities.forms import BootstrapMixin, CSVDataField
from utilities.utils import csv_format
from .error_handlers import handle_protectederror
@ -30,23 +30,6 @@ from .forms import ConfirmationForm
from .paginator import EnhancedPaginator
class CustomFieldQueryset:
"""
Annotate custom fields on objects within a QuerySet.
"""
def __init__(self, queryset, custom_fields):
self.queryset = queryset
self.model = queryset.model
self.custom_fields = custom_fields
def __iter__(self):
for obj in self.queryset:
values_dict = {cfv.field_id: cfv.value for cfv in obj.custom_field_values.all()}
obj.custom_fields = OrderedDict([(field, values_dict.get(field.pk)) for field in self.custom_fields])
yield obj
class GetReturnURLMixin(object):
"""
Provides logic for determining where a user should be redirected after processing a form.
@ -115,8 +98,9 @@ class ObjectListView(View):
self.queryset = self.filter(request.GET, self.queryset).qs
# If this type of object has one or more custom fields, prefetch any relevant custom field values
custom_fields = CustomField.objects.filter(obj_type=ContentType.objects.get_for_model(model))\
.prefetch_related('choices')
custom_fields = CustomField.objects.filter(
obj_type=ContentType.objects.get_for_model(model)
).prefetch_related('choices')
if custom_fields:
self.queryset = self.queryset.prefetch_related('custom_field_values')
@ -126,10 +110,12 @@ class ObjectListView(View):
queryset = CustomFieldQueryset(self.queryset, custom_fields) if custom_fields else self.queryset
try:
return et.render_to_response(queryset)
except TemplateSyntaxError:
except Exception as e:
messages.error(
request,
"There was an error rendering the selected export template ({}).".format(et.name)
"There was an error rendering the selected export template ({}): {}".format(
et.name, e
)
)
# Fall back to built-in CSV formatting if export requested but no template specified

View File

@ -5,7 +5,7 @@ from netaddr.core import AddrFormatError
from dcim.models import DeviceRole, Interface, Platform, Region, Site
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
from .constants import VM_STATUS_CHOICES
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@ -79,7 +79,7 @@ class ClusterFilter(CustomFieldFilterSet):
)
class VirtualMachineFilter(CustomFieldFilterSet):
class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@ -150,16 +150,6 @@ class VirtualMachineFilter(CustomFieldFilterSet):
to_field_name='slug',
label='Role (slug)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
platform_id = django_filters.ModelMultipleChoiceFilter(
queryset=Platform.objects.all(),
label='Platform (ID)',

View File

@ -8,6 +8,7 @@ from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, S
from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
from ipam.models import IPAddress
from tenancy.forms import TenancyForm
from tenancy.forms import TenancyFilterForm
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
@ -336,8 +337,8 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class Meta:
model = VirtualMachine
fields = [
'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data',
'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4',
'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data',
]
help_texts = {
'local_context_data': "Local config context data overwrites all sources contexts in the final rendered "
@ -520,8 +521,12 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB
]
class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm):
class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = VirtualMachine
field_order = [
'q', 'cluster_group', 'cluster_type', 'cluster_id', 'status', 'role', 'region', 'site', 'tenant_group',
'tenant', 'platform',
]
q = forms.CharField(
required=False,
label='Search'
@ -591,16 +596,6 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm):
required=False,
widget=StaticSelect2Multiple()
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url='/api/tenancy/tenants/',
value_field="slug",
null_option=True,
)
)
platform = FilterChoiceField(
queryset=Platform.objects.all(),
to_field_name='slug',

View File

@ -1,4 +1,4 @@
from django.conf.urls import url
from django.urls import path
from extras.views import ObjectChangeLogView
from ipam.views import ServiceCreateView
@ -9,53 +9,53 @@ app_name = 'virtualization'
urlpatterns = [
# Cluster types
url(r'^cluster-types/$', views.ClusterTypeListView.as_view(), name='clustertype_list'),
url(r'^cluster-types/add/$', views.ClusterTypeCreateView.as_view(), name='clustertype_add'),
url(r'^cluster-types/import/$', views.ClusterTypeBulkImportView.as_view(), name='clustertype_import'),
url(r'^cluster-types/delete/$', views.ClusterTypeBulkDeleteView.as_view(), name='clustertype_bulk_delete'),
url(r'^cluster-types/(?P<slug>[\w-]+)/edit/$', views.ClusterTypeEditView.as_view(), name='clustertype_edit'),
url(r'^cluster-types/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='clustertype_changelog', kwargs={'model': ClusterType}),
path(r'cluster-types/', views.ClusterTypeListView.as_view(), name='clustertype_list'),
path(r'cluster-types/add/', views.ClusterTypeCreateView.as_view(), name='clustertype_add'),
path(r'cluster-types/import/', views.ClusterTypeBulkImportView.as_view(), name='clustertype_import'),
path(r'cluster-types/delete/', views.ClusterTypeBulkDeleteView.as_view(), name='clustertype_bulk_delete'),
path(r'cluster-types/<slug:slug>/edit/', views.ClusterTypeEditView.as_view(), name='clustertype_edit'),
path(r'cluster-types/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='clustertype_changelog', kwargs={'model': ClusterType}),
# Cluster groups
url(r'^cluster-groups/$', views.ClusterGroupListView.as_view(), name='clustergroup_list'),
url(r'^cluster-groups/add/$', views.ClusterGroupCreateView.as_view(), name='clustergroup_add'),
url(r'^cluster-groups/import/$', views.ClusterGroupBulkImportView.as_view(), name='clustergroup_import'),
url(r'^cluster-groups/delete/$', views.ClusterGroupBulkDeleteView.as_view(), name='clustergroup_bulk_delete'),
url(r'^cluster-groups/(?P<slug>[\w-]+)/edit/$', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'),
url(r'^cluster-groups/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='clustergroup_changelog', kwargs={'model': ClusterGroup}),
path(r'cluster-groups/', views.ClusterGroupListView.as_view(), name='clustergroup_list'),
path(r'cluster-groups/add/', views.ClusterGroupCreateView.as_view(), name='clustergroup_add'),
path(r'cluster-groups/import/', views.ClusterGroupBulkImportView.as_view(), name='clustergroup_import'),
path(r'cluster-groups/delete/', views.ClusterGroupBulkDeleteView.as_view(), name='clustergroup_bulk_delete'),
path(r'cluster-groups/<slug:slug>/edit/', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'),
path(r'cluster-groups/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='clustergroup_changelog', kwargs={'model': ClusterGroup}),
# Clusters
url(r'^clusters/$', views.ClusterListView.as_view(), name='cluster_list'),
url(r'^clusters/add/$', views.ClusterCreateView.as_view(), name='cluster_add'),
url(r'^clusters/import/$', views.ClusterBulkImportView.as_view(), name='cluster_import'),
url(r'^clusters/edit/$', views.ClusterBulkEditView.as_view(), name='cluster_bulk_edit'),
url(r'^clusters/delete/$', views.ClusterBulkDeleteView.as_view(), name='cluster_bulk_delete'),
url(r'^clusters/(?P<pk>\d+)/$', views.ClusterView.as_view(), name='cluster'),
url(r'^clusters/(?P<pk>\d+)/edit/$', views.ClusterEditView.as_view(), name='cluster_edit'),
url(r'^clusters/(?P<pk>\d+)/delete/$', views.ClusterDeleteView.as_view(), name='cluster_delete'),
url(r'^clusters/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='cluster_changelog', kwargs={'model': Cluster}),
url(r'^clusters/(?P<pk>\d+)/devices/add/$', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'),
url(r'^clusters/(?P<pk>\d+)/devices/remove/$', views.ClusterRemoveDevicesView.as_view(), name='cluster_remove_devices'),
path(r'clusters/', views.ClusterListView.as_view(), name='cluster_list'),
path(r'clusters/add/', views.ClusterCreateView.as_view(), name='cluster_add'),
path(r'clusters/import/', views.ClusterBulkImportView.as_view(), name='cluster_import'),
path(r'clusters/edit/', views.ClusterBulkEditView.as_view(), name='cluster_bulk_edit'),
path(r'clusters/delete/', views.ClusterBulkDeleteView.as_view(), name='cluster_bulk_delete'),
path(r'clusters/<int:pk>/', views.ClusterView.as_view(), name='cluster'),
path(r'clusters/<int:pk>/edit/', views.ClusterEditView.as_view(), name='cluster_edit'),
path(r'clusters/<int:pk>/delete/', views.ClusterDeleteView.as_view(), name='cluster_delete'),
path(r'clusters/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='cluster_changelog', kwargs={'model': Cluster}),
path(r'clusters/<int:pk>/devices/add/', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'),
path(r'clusters/<int:pk>/devices/remove/', views.ClusterRemoveDevicesView.as_view(), name='cluster_remove_devices'),
# Virtual machines
url(r'^virtual-machines/$', views.VirtualMachineListView.as_view(), name='virtualmachine_list'),
url(r'^virtual-machines/add/$', views.VirtualMachineCreateView.as_view(), name='virtualmachine_add'),
url(r'^virtual-machines/import/$', views.VirtualMachineBulkImportView.as_view(), name='virtualmachine_import'),
url(r'^virtual-machines/edit/$', views.VirtualMachineBulkEditView.as_view(), name='virtualmachine_bulk_edit'),
url(r'^virtual-machines/delete/$', views.VirtualMachineBulkDeleteView.as_view(), name='virtualmachine_bulk_delete'),
url(r'^virtual-machines/(?P<pk>\d+)/$', views.VirtualMachineView.as_view(), name='virtualmachine'),
url(r'^virtual-machines/(?P<pk>\d+)/edit/$', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'),
url(r'^virtual-machines/(?P<pk>\d+)/delete/$', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'),
url(r'^virtual-machines/(?P<pk>\d+)/config-context/$', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'),
url(r'^virtual-machines/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='virtualmachine_changelog', kwargs={'model': VirtualMachine}),
url(r'^virtual-machines/(?P<virtualmachine>\d+)/services/assign/$', ServiceCreateView.as_view(), name='virtualmachine_service_assign'),
path(r'virtual-machines/', views.VirtualMachineListView.as_view(), name='virtualmachine_list'),
path(r'virtual-machines/add/', views.VirtualMachineCreateView.as_view(), name='virtualmachine_add'),
path(r'virtual-machines/import/', views.VirtualMachineBulkImportView.as_view(), name='virtualmachine_import'),
path(r'virtual-machines/edit/', views.VirtualMachineBulkEditView.as_view(), name='virtualmachine_bulk_edit'),
path(r'virtual-machines/delete/', views.VirtualMachineBulkDeleteView.as_view(), name='virtualmachine_bulk_delete'),
path(r'virtual-machines/<int:pk>/', views.VirtualMachineView.as_view(), name='virtualmachine'),
path(r'virtual-machines/<int:pk>/edit/', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'),
path(r'virtual-machines/<int:pk>/delete/', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'),
path(r'virtual-machines/<int:pk>/config-context/', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'),
path(r'virtual-machines/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='virtualmachine_changelog', kwargs={'model': VirtualMachine}),
path(r'virtual-machines/<int:virtualmachine>/services/assign/', ServiceCreateView.as_view(), name='virtualmachine_service_assign'),
# VM interfaces
url(r'^virtual-machines/interfaces/add/$', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_interface'),
url(r'^virtual-machines/(?P<pk>\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'),
url(r'^virtual-machines/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
url(r'^virtual-machines/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
url(r'^vm-interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
url(r'^vm-interfaces/(?P<pk>\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'),
path(r'virtual-machines/interfaces/add/', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_interface'),
path(r'virtual-machines/<int:pk>/interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'),
path(r'virtual-machines/<int:pk>/interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
path(r'virtual-machines/<int:pk>/interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
path(r'vm-interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
path(r'vm-interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
]

View File

@ -10,8 +10,13 @@ django-tables2==2.0.6
django-taggit==1.1.0
django-taggit-serializer==0.1.7
django-timezone-field==3.0
<<<<<<< HEAD
djangorestframework==3.9.2
drf-yasg[validation]==1.15.0
=======
djangorestframework==3.9.1
drf-yasg[validation]==1.14.0
>>>>>>> develop
graphviz==0.10.1
Jinja2==2.10.1
Markdown==2.6.11

View File

@ -5,6 +5,8 @@
# Once the script completes, remember to restart the WSGI service (e.g.
# gunicorn or uWSGI).
cd "$(dirname "$0")"
PYTHON="python3"
PIP="pip3"