Merge pull request #6062 from netbox-community/5971-org-object-views

Closes #5971: Dedicated views for organizational models
This commit is contained in:
Jeremy Stretch 2021-03-26 16:06:45 -04:00 committed by GitHub
commit b793ee3aff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1395 additions and 181 deletions

View File

@ -175,7 +175,7 @@ class CircuitType(OrganizationalModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug) return reverse('circuits:circuittype', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (

View File

@ -38,6 +38,7 @@ urlpatterns = [
path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'), path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
path('circuit-types/edit/', views.CircuitTypeBulkEditView.as_view(), name='circuittype_bulk_edit'), path('circuit-types/edit/', views.CircuitTypeBulkEditView.as_view(), name='circuittype_bulk_edit'),
path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'), path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
path('circuit-types/<int:pk>/', views.CircuitTypeView.as_view(), name='circuittype'),
path('circuit-types/<int:pk>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'), path('circuit-types/<int:pk>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
path('circuit-types/<int:pk>/delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'), path('circuit-types/<int:pk>/delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'),
path('circuit-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}), path('circuit-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),

View File

@ -147,6 +147,23 @@ class CircuitTypeListView(generic.ObjectListView):
table = tables.CircuitTypeTable table = tables.CircuitTypeTable
class CircuitTypeView(generic.ObjectView):
queryset = CircuitType.objects.all()
def get_extra_context(self, request, instance):
circuits = Circuit.objects.restrict(request.user, 'view').filter(
type=instance
)
circuits_table = tables.CircuitTable(circuits)
circuits_table.columns.hide('type')
paginate_table(circuits_table, request)
return {
'circuits_table': circuits_table,
}
class CircuitTypeEditView(generic.ObjectEditView): class CircuitTypeEditView(generic.ObjectEditView):
queryset = CircuitType.objects.all() queryset = CircuitType.objects.all()
model_form = forms.CircuitTypeForm model_form = forms.CircuitTypeForm

View File

@ -65,7 +65,7 @@ class Manufacturer(OrganizationalModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug) return reverse('dcim:manufacturer', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (
@ -375,6 +375,9 @@ class DeviceRole(OrganizationalModel):
def __str__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self):
return reverse('dcim:devicerole', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (
self.name, self.name,
@ -436,7 +439,7 @@ class Platform(OrganizationalModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?platform={}".format(reverse('dcim:device_list'), self.slug) return reverse('dcim:platform', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (

View File

@ -67,7 +67,7 @@ class RackRole(OrganizationalModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?role={}".format(reverse('dcim:rack_list'), self.slug) return reverse('dcim:rackrole', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (

View File

@ -56,7 +56,7 @@ class Region(NestedGroupModel):
csv_headers = ['name', 'slug', 'parent', 'description'] csv_headers = ['name', 'slug', 'parent', 'description']
def get_absolute_url(self): def get_absolute_url(self):
return "{}?region={}".format(reverse('dcim:site_list'), self.slug) return reverse('dcim:region', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (
@ -108,7 +108,7 @@ class SiteGroup(NestedGroupModel):
csv_headers = ['name', 'slug', 'parent', 'description'] csv_headers = ['name', 'slug', 'parent', 'description']
def get_absolute_url(self): def get_absolute_url(self):
return "{}?group={}".format(reverse('dcim:site_list'), self.slug) return reverse('dcim:sitegroup', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (
@ -324,7 +324,7 @@ class Location(NestedGroupModel):
] ]
def get_absolute_url(self): def get_absolute_url(self):
return "{}?location_id={}".format(reverse('dcim:rack_list'), self.pk) return reverse('dcim:location', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (

View File

@ -50,6 +50,9 @@ __all__ = (
class DeviceRoleTable(BaseTable): class DeviceRoleTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(
linkify=True
)
device_count = LinkedCountColumn( device_count = LinkedCountColumn(
viewname='dcim:device_list', viewname='dcim:device_list',
url_params={'role': 'slug'}, url_params={'role': 'slug'},
@ -76,6 +79,9 @@ class DeviceRoleTable(BaseTable):
class PlatformTable(BaseTable): class PlatformTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(
linkify=True
)
device_count = LinkedCountColumn( device_count = LinkedCountColumn(
viewname='dcim:device_list', viewname='dcim:device_list',
url_params={'platform': 'slug'}, url_params={'platform': 'slug'},

View File

@ -19,12 +19,14 @@ __all__ = (
# #
# Rack groups # Locations
# #
class LocationTable(BaseTable): class LocationTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = MPTTColumn() name = MPTTColumn(
linkify=True
)
site = tables.Column( site = tables.Column(
linkify=True linkify=True
) )

View File

@ -17,7 +17,9 @@ __all__ = (
class RegionTable(BaseTable): class RegionTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = MPTTColumn() name = MPTTColumn(
linkify=True
)
site_count = tables.Column( site_count = tables.Column(
verbose_name='Sites' verbose_name='Sites'
) )
@ -35,7 +37,9 @@ class RegionTable(BaseTable):
class SiteGroupTable(BaseTable): class SiteGroupTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = MPTTColumn() name = MPTTColumn(
linkify=True
)
site_count = tables.Column( site_count = tables.Column(
verbose_name='Sites' verbose_name='Sites'
) )

View File

@ -14,6 +14,7 @@ urlpatterns = [
path('regions/import/', views.RegionBulkImportView.as_view(), name='region_import'), path('regions/import/', views.RegionBulkImportView.as_view(), name='region_import'),
path('regions/edit/', views.RegionBulkEditView.as_view(), name='region_bulk_edit'), path('regions/edit/', views.RegionBulkEditView.as_view(), name='region_bulk_edit'),
path('regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'), path('regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
path('regions/<int:pk>/', views.RegionView.as_view(), name='region'),
path('regions/<int:pk>/edit/', views.RegionEditView.as_view(), name='region_edit'), path('regions/<int:pk>/edit/', views.RegionEditView.as_view(), name='region_edit'),
path('regions/<int:pk>/delete/', views.RegionDeleteView.as_view(), name='region_delete'), path('regions/<int:pk>/delete/', views.RegionDeleteView.as_view(), name='region_delete'),
path('regions/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}), path('regions/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}),
@ -24,6 +25,7 @@ urlpatterns = [
path('site-groups/import/', views.SiteGroupBulkImportView.as_view(), name='sitegroup_import'), path('site-groups/import/', views.SiteGroupBulkImportView.as_view(), name='sitegroup_import'),
path('site-groups/edit/', views.SiteGroupBulkEditView.as_view(), name='sitegroup_bulk_edit'), path('site-groups/edit/', views.SiteGroupBulkEditView.as_view(), name='sitegroup_bulk_edit'),
path('site-groups/delete/', views.SiteGroupBulkDeleteView.as_view(), name='sitegroup_bulk_delete'), path('site-groups/delete/', views.SiteGroupBulkDeleteView.as_view(), name='sitegroup_bulk_delete'),
path('site-groups/<int:pk>/', views.SiteGroupView.as_view(), name='sitegroup'),
path('site-groups/<int:pk>/edit/', views.SiteGroupEditView.as_view(), name='sitegroup_edit'), path('site-groups/<int:pk>/edit/', views.SiteGroupEditView.as_view(), name='sitegroup_edit'),
path('site-groups/<int:pk>/delete/', views.SiteGroupDeleteView.as_view(), name='sitegroup_delete'), path('site-groups/<int:pk>/delete/', views.SiteGroupDeleteView.as_view(), name='sitegroup_delete'),
path('site-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='sitegroup_changelog', kwargs={'model': SiteGroup}), path('site-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='sitegroup_changelog', kwargs={'model': SiteGroup}),
@ -47,6 +49,7 @@ urlpatterns = [
path('locations/import/', views.LocationBulkImportView.as_view(), name='location_import'), path('locations/import/', views.LocationBulkImportView.as_view(), name='location_import'),
path('locations/edit/', views.LocationBulkEditView.as_view(), name='location_bulk_edit'), path('locations/edit/', views.LocationBulkEditView.as_view(), name='location_bulk_edit'),
path('locations/delete/', views.LocationBulkDeleteView.as_view(), name='location_bulk_delete'), path('locations/delete/', views.LocationBulkDeleteView.as_view(), name='location_bulk_delete'),
path('locations/<int:pk>/', views.LocationView.as_view(), name='location'),
path('locations/<int:pk>/edit/', views.LocationEditView.as_view(), name='location_edit'), path('locations/<int:pk>/edit/', views.LocationEditView.as_view(), name='location_edit'),
path('locations/<int:pk>/delete/', views.LocationDeleteView.as_view(), name='location_delete'), path('locations/<int:pk>/delete/', views.LocationDeleteView.as_view(), name='location_delete'),
path('locations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='location_changelog', kwargs={'model': Location}), path('locations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='location_changelog', kwargs={'model': Location}),
@ -57,6 +60,7 @@ urlpatterns = [
path('rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'), path('rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
path('rack-roles/edit/', views.RackRoleBulkEditView.as_view(), name='rackrole_bulk_edit'), path('rack-roles/edit/', views.RackRoleBulkEditView.as_view(), name='rackrole_bulk_edit'),
path('rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'), path('rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
path('rack-roles/<int:pk>/', views.RackRoleView.as_view(), name='rackrole'),
path('rack-roles/<int:pk>/edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'), path('rack-roles/<int:pk>/edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'),
path('rack-roles/<int:pk>/delete/', views.RackRoleDeleteView.as_view(), name='rackrole_delete'), path('rack-roles/<int:pk>/delete/', views.RackRoleDeleteView.as_view(), name='rackrole_delete'),
path('rack-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}), path('rack-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
@ -93,6 +97,7 @@ urlpatterns = [
path('manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'), path('manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
path('manufacturers/edit/', views.ManufacturerBulkEditView.as_view(), name='manufacturer_bulk_edit'), path('manufacturers/edit/', views.ManufacturerBulkEditView.as_view(), name='manufacturer_bulk_edit'),
path('manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'), path('manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
path('manufacturers/<int:pk>/', views.ManufacturerView.as_view(), name='manufacturer'),
path('manufacturers/<int:pk>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'), path('manufacturers/<int:pk>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
path('manufacturers/<int:pk>/delete/', views.ManufacturerDeleteView.as_view(), name='manufacturer_delete'), path('manufacturers/<int:pk>/delete/', views.ManufacturerDeleteView.as_view(), name='manufacturer_delete'),
path('manufacturers/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}), path('manufacturers/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
@ -179,6 +184,7 @@ urlpatterns = [
path('device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'), path('device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
path('device-roles/edit/', views.DeviceRoleBulkEditView.as_view(), name='devicerole_bulk_edit'), path('device-roles/edit/', views.DeviceRoleBulkEditView.as_view(), name='devicerole_bulk_edit'),
path('device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'), path('device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
path('device-roles/<int:pk>/', views.DeviceRoleView.as_view(), name='devicerole'),
path('device-roles/<int:pk>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'), path('device-roles/<int:pk>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
path('device-roles/<int:pk>/delete/', views.DeviceRoleDeleteView.as_view(), name='devicerole_delete'), path('device-roles/<int:pk>/delete/', views.DeviceRoleDeleteView.as_view(), name='devicerole_delete'),
path('device-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}), path('device-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
@ -189,6 +195,7 @@ urlpatterns = [
path('platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'), path('platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'),
path('platforms/edit/', views.PlatformBulkEditView.as_view(), name='platform_bulk_edit'), path('platforms/edit/', views.PlatformBulkEditView.as_view(), name='platform_bulk_edit'),
path('platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'), path('platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
path('platforms/<int:pk>/', views.PlatformView.as_view(), name='platform'),
path('platforms/<int:pk>/edit/', views.PlatformEditView.as_view(), name='platform_edit'), path('platforms/<int:pk>/edit/', views.PlatformEditView.as_view(), name='platform_edit'),
path('platforms/<int:pk>/delete/', views.PlatformDeleteView.as_view(), name='platform_delete'), path('platforms/<int:pk>/delete/', views.PlatformDeleteView.as_view(), name='platform_delete'),
path('platforms/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}), path('platforms/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),

View File

@ -20,6 +20,7 @@ from secrets.models import Secret
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
from utilities.tables import paginate_table
from utilities.utils import csv_format, count_related from utilities.utils import csv_format, count_related
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
@ -111,6 +112,23 @@ class RegionListView(generic.ObjectListView):
table = tables.RegionTable table = tables.RegionTable
class RegionView(generic.ObjectView):
queryset = Region.objects.all()
def get_extra_context(self, request, instance):
sites = Site.objects.restrict(request.user, 'view').filter(
region=instance
)
sites_table = tables.SiteTable(sites)
sites_table.columns.hide('region')
paginate_table(sites_table, request)
return {
'sites_table': sites_table,
}
class RegionEditView(generic.ObjectEditView): class RegionEditView(generic.ObjectEditView):
queryset = Region.objects.all() queryset = Region.objects.all()
model_form = forms.RegionForm model_form = forms.RegionForm
@ -168,6 +186,23 @@ class SiteGroupListView(generic.ObjectListView):
table = tables.SiteGroupTable table = tables.SiteGroupTable
class SiteGroupView(generic.ObjectView):
queryset = SiteGroup.objects.all()
def get_extra_context(self, request, instance):
sites = Site.objects.restrict(request.user, 'view').filter(
group=instance
)
sites_table = tables.SiteTable(sites)
sites_table.columns.hide('group')
paginate_table(sites_table, request)
return {
'sites_table': sites_table,
}
class SiteGroupEditView(generic.ObjectEditView): class SiteGroupEditView(generic.ObjectEditView):
queryset = SiteGroup.objects.all() queryset = SiteGroup.objects.all()
model_form = forms.SiteGroupForm model_form = forms.SiteGroupForm
@ -290,6 +325,23 @@ class LocationListView(generic.ObjectListView):
table = tables.LocationTable table = tables.LocationTable
class LocationView(generic.ObjectView):
queryset = Location.objects.all()
def get_extra_context(self, request, instance):
devices = Device.objects.restrict(request.user, 'view').filter(
location=instance
)
devices_table = tables.DeviceTable(devices)
devices_table.columns.hide('location')
paginate_table(devices_table, request)
return {
'devices_table': devices_table,
}
class LocationEditView(generic.ObjectEditView): class LocationEditView(generic.ObjectEditView):
queryset = Location.objects.all() queryset = Location.objects.all()
model_form = forms.LocationForm model_form = forms.LocationForm
@ -341,6 +393,23 @@ class RackRoleListView(generic.ObjectListView):
table = tables.RackRoleTable table = tables.RackRoleTable
class RackRoleView(generic.ObjectView):
queryset = RackRole.objects.all()
def get_extra_context(self, request, instance):
racks = Rack.objects.restrict(request.user, 'view').filter(
role=instance
)
racks_table = tables.RackTable(racks)
racks_table.columns.hide('role')
paginate_table(racks_table, request)
return {
'racks_table': racks_table,
}
class RackRoleEditView(generic.ObjectEditView): class RackRoleEditView(generic.ObjectEditView):
queryset = RackRole.objects.all() queryset = RackRole.objects.all()
model_form = forms.RackRoleForm model_form = forms.RackRoleForm
@ -567,6 +636,23 @@ class ManufacturerListView(generic.ObjectListView):
table = tables.ManufacturerTable table = tables.ManufacturerTable
class ManufacturerView(generic.ObjectView):
queryset = Manufacturer.objects.all()
def get_extra_context(self, request, instance):
devicetypes = DeviceType.objects.restrict(request.user, 'view').filter(
manufacturer=instance
)
devicetypes_table = tables.DeviceTypeTable(devicetypes)
devicetypes_table.columns.hide('manufacturer')
paginate_table(devicetypes_table, request)
return {
'devicetypes_table': devicetypes_table,
}
class ManufacturerEditView(generic.ObjectEditView): class ManufacturerEditView(generic.ObjectEditView):
queryset = Manufacturer.objects.all() queryset = Manufacturer.objects.all()
model_form = forms.ManufacturerForm model_form = forms.ManufacturerForm
@ -1017,6 +1103,23 @@ class DeviceRoleListView(generic.ObjectListView):
table = tables.DeviceRoleTable table = tables.DeviceRoleTable
class DeviceRoleView(generic.ObjectView):
queryset = DeviceRole.objects.all()
def get_extra_context(self, request, instance):
devices = Device.objects.restrict(request.user, 'view').filter(
device_role=instance
)
devices_table = tables.DeviceTable(devices)
devices_table.columns.hide('device_role')
paginate_table(devices_table, request)
return {
'devices_table': devices_table,
}
class DeviceRoleEditView(generic.ObjectEditView): class DeviceRoleEditView(generic.ObjectEditView):
queryset = DeviceRole.objects.all() queryset = DeviceRole.objects.all()
model_form = forms.DeviceRoleForm model_form = forms.DeviceRoleForm
@ -1056,6 +1159,23 @@ class PlatformListView(generic.ObjectListView):
table = tables.PlatformTable table = tables.PlatformTable
class PlatformView(generic.ObjectView):
queryset = Platform.objects.all()
def get_extra_context(self, request, instance):
devices = Device.objects.restrict(request.user, 'view').filter(
platform=instance
)
devices_table = tables.DeviceTable(devices)
devices_table.columns.hide('platform')
paginate_table(devices_table, request)
return {
'devices_table': devices_table,
}
class PlatformEditView(generic.ObjectEditView): class PlatformEditView(generic.ObjectEditView):
queryset = Platform.objects.all() queryset = Platform.objects.all()
model_form = forms.PlatformForm model_form = forms.PlatformForm

View File

@ -1,4 +1,5 @@
from django.db import models from django.db import models
from django.urls import reverse
from django.utils.text import slugify from django.utils.text import slugify
from taggit.models import TagBase, GenericTaggedItemBase from taggit.models import TagBase, GenericTaggedItemBase
@ -30,6 +31,9 @@ class Tag(ChangeLoggedModel, TagBase):
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
def get_absolute_url(self):
return reverse('extras:tag', args=[self.pk])
def slugify(self, tag, i=None): def slugify(self, tag, i=None):
# Allow Unicode in Tag slugs (avoids empty slugs for Tags with all-Unicode names) # Allow Unicode in Tag slugs (avoids empty slugs for Tags with all-Unicode names)
slug = slugify(tag, allow_unicode=True) slug = slugify(tag, allow_unicode=True)

View File

@ -38,6 +38,9 @@ OBJECTCHANGE_REQUEST_ID = """
class TagTable(BaseTable): class TagTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(
linkify=True
)
color = ColorColumn() color = ColorColumn()
actions = ButtonsColumn(Tag) actions = ButtonsColumn(Tag)

View File

@ -13,6 +13,7 @@ urlpatterns = [
path('tags/import/', views.TagBulkImportView.as_view(), name='tag_import'), path('tags/import/', views.TagBulkImportView.as_view(), name='tag_import'),
path('tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'), path('tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'),
path('tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'), path('tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
path('tags/<int:pk>/', views.TagView.as_view(), name='tag'),
path('tags/<int:pk>/edit/', views.TagEditView.as_view(), name='tag_edit'), path('tags/<int:pk>/edit/', views.TagEditView.as_view(), name='tag_edit'),
path('tags/<int:pk>/delete/', views.TagDeleteView.as_view(), name='tag_delete'), path('tags/<int:pk>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
path('tags/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}), path('tags/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}),

View File

@ -34,6 +34,17 @@ class TagListView(generic.ObjectListView):
table = tables.TagTable table = tables.TagTable
class TagView(generic.ObjectView):
queryset = Tag.objects.all()
def get_extra_context(self, request, instance):
tagged_items = TaggedItem.objects.filter(tag=instance)
return {
'tagged_item_count': tagged_items.count(),
}
class TagEditView(generic.ObjectEditView): class TagEditView(generic.ObjectEditView):
queryset = Tag.objects.all() queryset = Tag.objects.all()
model_form = forms.TagForm model_form = forms.TagForm
@ -235,11 +246,6 @@ class ObjectChangeLogView(View):
# fall back to using base.html. # fall back to using base.html.
if self.base_template is None: if self.base_template is None:
self.base_template = f"{model._meta.app_label}/{model._meta.model_name}.html" self.base_template = f"{model._meta.app_label}/{model._meta.model_name}.html"
# TODO: This can be removed once an object view has been established for every model.
try:
template.loader.get_template(self.base_template)
except template.TemplateDoesNotExist:
self.base_template = 'base.html'
return render(request, 'extras/object_changelog.html', { return render(request, 'extras/object_changelog.html', {
'object': obj, 'object': obj,
@ -368,11 +374,6 @@ class ObjectJournalView(View):
# fall back to using base.html. # fall back to using base.html.
if self.base_template is None: if self.base_template is None:
self.base_template = f"{model._meta.app_label}/{model._meta.model_name}.html" self.base_template = f"{model._meta.app_label}/{model._meta.model_name}.html"
# TODO: This can be removed once an object view has been established for every model.
try:
template.loader.get_template(self.base_template)
except template.TemplateDoesNotExist:
self.base_template = 'base.html'
return render(request, 'extras/object_journal.html', { return render(request, 'extras/object_journal.html', {
'object': obj, 'object': obj,

View File

@ -66,7 +66,7 @@ class RIR(OrganizationalModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?rir={}".format(reverse('ipam:aggregate_list'), self.slug) return reverse('ipam:rir', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (
@ -216,6 +216,9 @@ class Role(OrganizationalModel):
def __str__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self):
return reverse('ipam:role', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (
self.name, self.name,

View File

@ -70,7 +70,7 @@ class VLANGroup(OrganizationalModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse('ipam:vlangroup_vlans', args=[self.pk]) return reverse('ipam:vlangroup', args=[self.pk])
def clean(self): def clean(self):
super().clean() super().clean()

View File

@ -224,6 +224,9 @@ class AggregateDetailTable(AggregateTable):
class RoleTable(BaseTable): class RoleTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(
linkify=True
)
prefix_count = LinkedCountColumn( prefix_count = LinkedCountColumn(
viewname='ipam:prefix_list', viewname='ipam:prefix_list',
url_params={'role': 'slug'}, url_params={'role': 'slug'},
@ -450,9 +453,8 @@ class VLANTable(BaseTable):
site = tables.Column( site = tables.Column(
linkify=True linkify=True
) )
group = tables.LinkColumn( group = tables.Column(
viewname='ipam:vlangroup_vlans', linkify=True
args=[Accessor('group__pk')]
) )
tenant = TenantColumn() tenant = TenantColumn()
status = ChoiceFieldColumn( status = ChoiceFieldColumn(

View File

@ -37,6 +37,7 @@ urlpatterns = [
path('rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'), path('rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'),
path('rirs/edit/', views.RIRBulkEditView.as_view(), name='rir_bulk_edit'), path('rirs/edit/', views.RIRBulkEditView.as_view(), name='rir_bulk_edit'),
path('rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'), path('rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
path('rirs/<int:pk>/', views.RIRView.as_view(), name='rir'),
path('rirs/<int:pk>/edit/', views.RIREditView.as_view(), name='rir_edit'), path('rirs/<int:pk>/edit/', views.RIREditView.as_view(), name='rir_edit'),
path('rirs/<int:pk>/delete/', views.RIRDeleteView.as_view(), name='rir_delete'), path('rirs/<int:pk>/delete/', views.RIRDeleteView.as_view(), name='rir_delete'),
path('rirs/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}), path('rirs/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}),
@ -59,6 +60,7 @@ urlpatterns = [
path('roles/import/', views.RoleBulkImportView.as_view(), name='role_import'), path('roles/import/', views.RoleBulkImportView.as_view(), name='role_import'),
path('roles/edit/', views.RoleBulkEditView.as_view(), name='role_bulk_edit'), path('roles/edit/', views.RoleBulkEditView.as_view(), name='role_bulk_edit'),
path('roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'), path('roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
path('roles/<int:pk>/', views.RoleView.as_view(), name='role'),
path('roles/<int:pk>/edit/', views.RoleEditView.as_view(), name='role_edit'), path('roles/<int:pk>/edit/', views.RoleEditView.as_view(), name='role_edit'),
path('roles/<int:pk>/delete/', views.RoleDeleteView.as_view(), name='role_delete'), path('roles/<int:pk>/delete/', views.RoleDeleteView.as_view(), name='role_delete'),
path('roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}), path('roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}),
@ -97,9 +99,9 @@ urlpatterns = [
path('vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'), path('vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
path('vlan-groups/edit/', views.VLANGroupBulkEditView.as_view(), name='vlangroup_bulk_edit'), path('vlan-groups/edit/', views.VLANGroupBulkEditView.as_view(), name='vlangroup_bulk_edit'),
path('vlan-groups/delete/', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'), path('vlan-groups/delete/', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
path('vlan-groups/<int:pk>/', views.VLANGroupView.as_view(), name='vlangroup'),
path('vlan-groups/<int:pk>/edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'), path('vlan-groups/<int:pk>/edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
path('vlan-groups/<int:pk>/delete/', views.VLANGroupDeleteView.as_view(), name='vlangroup_delete'), path('vlan-groups/<int:pk>/delete/', views.VLANGroupDeleteView.as_view(), name='vlangroup_delete'),
path('vlan-groups/<int:pk>/vlans/', views.VLANGroupVLANsView.as_view(), name='vlangroup_vlans'),
path('vlan-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}), path('vlan-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}),
# VLANs # VLANs

View File

@ -148,6 +148,23 @@ class RIRListView(generic.ObjectListView):
template_name = 'ipam/rir_list.html' template_name = 'ipam/rir_list.html'
class RIRView(generic.ObjectView):
queryset = RIR.objects.all()
def get_extra_context(self, request, instance):
aggregates = Aggregate.objects.restrict(request.user, 'view').filter(
rir=instance
)
aggregates_table = tables.AggregateTable(aggregates)
aggregates_table.columns.hide('rir')
paginate_table(aggregates_table, request)
return {
'aggregates_table': aggregates_table,
}
class RIREditView(generic.ObjectEditView): class RIREditView(generic.ObjectEditView):
queryset = RIR.objects.all() queryset = RIR.objects.all()
model_form = forms.RIRForm model_form = forms.RIRForm
@ -286,6 +303,23 @@ class RoleListView(generic.ObjectListView):
table = tables.RoleTable table = tables.RoleTable
class RoleView(generic.ObjectView):
queryset = Role.objects.all()
def get_extra_context(self, request, instance):
prefixes = Prefix.objects.restrict(request.user, 'view').filter(
role=instance
)
prefixes_table = tables.PrefixTable(prefixes)
prefixes_table.columns.hide('role')
paginate_table(prefixes_table, request)
return {
'prefixes_table': prefixes_table,
}
class RoleEditView(generic.ObjectEditView): class RoleEditView(generic.ObjectEditView):
queryset = Role.objects.all() queryset = Role.objects.all()
model_form = forms.RoleForm model_form = forms.RoleForm
@ -633,6 +667,29 @@ class VLANGroupListView(generic.ObjectListView):
table = tables.VLANGroupTable table = tables.VLANGroupTable
class VLANGroupView(generic.ObjectView):
queryset = VLANGroup.objects.all()
def get_extra_context(self, request, instance):
vlans = VLAN.objects.restrict(request.user, 'view').filter(group=instance).prefetch_related(
Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user))
)
vlans_count = vlans.count()
vlans = add_available_vlans(instance, vlans)
vlans_table = tables.VLANDetailTable(vlans)
if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'):
vlans_table.columns.show('pk')
vlans_table.columns.hide('site')
vlans_table.columns.hide('group')
paginate_table(vlans_table, request)
return {
'vlans_count': vlans_count,
'vlans_table': vlans_table,
}
class VLANGroupEditView(generic.ObjectEditView): class VLANGroupEditView(generic.ObjectEditView):
queryset = VLANGroup.objects.all() queryset = VLANGroup.objects.all()
model_form = forms.VLANGroupForm model_form = forms.VLANGroupForm
@ -666,38 +723,6 @@ class VLANGroupBulkDeleteView(generic.BulkDeleteView):
table = tables.VLANGroupTable table = tables.VLANGroupTable
class VLANGroupVLANsView(generic.ObjectView):
queryset = VLANGroup.objects.all()
template_name = 'ipam/vlangroup_vlans.html'
def get_extra_context(self, request, instance):
vlans = VLAN.objects.restrict(request.user, 'view').filter(group=instance).prefetch_related(
Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user))
)
vlans = add_available_vlans(instance, vlans)
vlan_table = tables.VLANDetailTable(vlans)
if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'):
vlan_table.columns.show('pk')
vlan_table.columns.hide('site')
vlan_table.columns.hide('group')
paginate_table(vlan_table, request)
# Compile permissions list for rendering the object table
permissions = {
'add': request.user.has_perm('ipam.add_vlan'),
'change': request.user.has_perm('ipam.change_vlan'),
'delete': request.user.has_perm('ipam.delete_vlan'),
}
return {
'first_available_vlan': instance.get_next_available_vid(),
'bulk_querystring': f'group_id={instance.pk}',
'vlan_table': vlan_table,
'permissions': permissions,
}
# #
# VLANs # VLANs
# #

View File

@ -263,7 +263,7 @@ class SecretRole(OrganizationalModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?role={}".format(reverse('secrets:secret_list'), self.slug) return reverse('secrets:secretrole', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (

View File

@ -13,6 +13,7 @@ urlpatterns = [
path('secret-roles/import/', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'), path('secret-roles/import/', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'),
path('secret-roles/edit/', views.SecretRoleBulkEditView.as_view(), name='secretrole_bulk_edit'), path('secret-roles/edit/', views.SecretRoleBulkEditView.as_view(), name='secretrole_bulk_edit'),
path('secret-roles/delete/', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'), path('secret-roles/delete/', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'),
path('secret-roles/<int:pk>/', views.SecretRoleView.as_view(), name='secretrole'),
path('secret-roles/<int:pk>/edit/', views.SecretRoleEditView.as_view(), name='secretrole_edit'), path('secret-roles/<int:pk>/edit/', views.SecretRoleEditView.as_view(), name='secretrole_edit'),
path('secret-roles/<int:pk>/delete/', views.SecretRoleDeleteView.as_view(), name='secretrole_delete'), path('secret-roles/<int:pk>/delete/', views.SecretRoleDeleteView.as_view(), name='secretrole_delete'),
path('secret-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}), path('secret-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}),

View File

@ -7,6 +7,7 @@ from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from netbox.views import generic from netbox.views import generic
from utilities.tables import paginate_table
from utilities.utils import count_related from utilities.utils import count_related
from . import filters, forms, tables from . import filters, forms, tables
from .models import SecretRole, Secret, SessionKey, UserKey from .models import SecretRole, Secret, SessionKey, UserKey
@ -33,6 +34,23 @@ class SecretRoleListView(generic.ObjectListView):
table = tables.SecretRoleTable table = tables.SecretRoleTable
class SecretRoleView(generic.ObjectView):
queryset = SecretRole.objects.all()
def get_extra_context(self, request, instance):
secrets = Secret.objects.restrict(request.user, 'view').filter(
role=instance
)
secrets_table = tables.SecretTable(secrets)
secrets_table.columns.hide('role')
paginate_table(secrets_table, request)
return {
'secrets_table': secrets_table,
}
class SecretRoleEditView(generic.ObjectEditView): class SecretRoleEditView(generic.ObjectEditView):
queryset = SecretRole.objects.all() queryset = SecretRole.objects.all()
model_form = forms.SecretRoleForm model_form = forms.SecretRoleForm

View File

@ -0,0 +1,60 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li><a href="{% url 'circuits:circuittype_list' %}">Circuit Types</a></li>
<li>{{ object }}</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Circuit Type</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ object.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<td>Circuits</td>
<td>
<a href="{% url 'circuits:circuit_list' %}?type_id={{ object.pk }}">{{ circuits_table.rows|length }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% include 'inc/custom_fields_panel.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Circuits</strong>
</div>
{% include 'inc/table.html' with table=circuits_table %}
{% if perms.circuits.add_circuit %}
<div class="panel-footer text-right noprint">
<a href="{% url 'circuits:circuit_add' %}?type={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add circuit
</a>
</div>
{% endif %}
</div>
{% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,76 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li><a href="{% url 'dcim:devicerole_list' %}">Device Roles</a></li>
<li>{{ object }}</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Device Role</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ object.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<td>Color</td>
<td>
<span class="label color-block" style="background-color: #{{ object.color }}">&nbsp;</span>
</td>
</tr>
<tr>
<td>VM Role</td>
<td>
{% if object.vm_role %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
</tr>
<tr>
<td>Devices</td>
<td>
<a href="{% url 'dcim:device_list' %}?role_id={{ object.pk }}">{{ devices_table.rows|length }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% include 'inc/custom_fields_panel.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Devices</strong>
</div>
{% include 'inc/table.html' with table=devices_table %}
{% if perms.dcim.add_device %}
<div class="panel-footer text-right noprint">
<a href="{% url 'dcim:device_add' %}?device_role={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add device
</a>
</div>
{% endif %}
</div>
{% include 'inc/paginator.html' with paginator=devices_table.paginator page=devices_table.page %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,73 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li><a href="{% url 'dcim:location_list' %}">Location</a></li>
{% for location in object.get_ancestors %}
<li><a href="{{ location.get_absolute_url }}">{{ location }}</a></li>
{% endfor %}
<li>{{ object }}</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Location</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ object.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<td>Parent</td>
<td>
{% if object.parent %}
<a href="{{ object.parent.get_absolute_url }}">{{ object.parent }}</a>
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
</tr>
<tr>
<td>Devices</td>
<td>
<a href="{% url 'dcim:device_list' %}?location_id={{ object.pk }}">{{ devices_table.rows|length }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% include 'inc/custom_fields_panel.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Devices</strong>
</div>
{% include 'inc/table.html' with table=devices_table %}
{% if perms.dcim.add_device %}
<div class="panel-footer text-right noprint">
<a href="{% url 'dcim:device_add' %}?location={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add device
</a>
</div>
{% endif %}
</div>
{% include 'inc/paginator.html' with paginator=devices_table.paginator page=devices_table.page %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,60 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li><a href="{% url 'dcim:manufacturer_list' %}">Manufacturers</a></li>
<li>{{ object }}</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Manufacturer</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ object.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<td>Device types</td>
<td>
<a href="{% url 'dcim:devicetype_list' %}?manufacturer_id={{ object.pk }}">{{ devicetypes_table.rows|length }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% include 'inc/custom_fields_panel.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Device Types</strong>
</div>
{% include 'inc/table.html' with table=devicetypes_table %}
{% if perms.dcim.add_devicetype %}
<div class="panel-footer text-right noprint">
<a href="{% url 'dcim:devicetype_add' %}?manufacturer={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add device type
</a>
</div>
{% endif %}
</div>
{% include 'inc/paginator.html' with paginator=devicetypes_table.paginator page=devicetypes_table.page %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,68 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li><a href="{% url 'dcim:platform_list' %}">Platforms</a></li>
<li>{{ object }}</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Platform</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ object.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<td>NAPALM Driver</td>
<td>{{ object.napalm_driver }}</td>
</tr>
<tr>
<td>NAPALM Arguments</td>
<td><pre>{{ object.napalm_args }}</pre></td>
</tr>
<tr>
<td>Devices</td>
<td>
<a href="{% url 'dcim:device_list' %}?platform_id={{ object.pk }}">{{ devices_table.rows|length }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% include 'inc/custom_fields_panel.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Devices</strong>
</div>
{% include 'inc/table.html' with table=devices_table %}
{% if perms.dcim.add_device %}
<div class="panel-footer text-right noprint">
<a href="{% url 'dcim:device_add' %}?device_role={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add device
</a>
</div>
{% endif %}
</div>
{% include 'inc/paginator.html' with paginator=devices_table.paginator page=devices_table.page %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,66 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li><a href="{% url 'dcim:rackrole_list' %}">Rack Roles</a></li>
<li>{{ object }}</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Rack Role</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ object.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<td>Color</td>
<td>
<span class="label color-block" style="background-color: #{{ object.color }}">&nbsp;</span>
</td>
</tr>
<tr>
<td>Racks</td>
<td>
<a href="{% url 'dcim:rack_list' %}?role_id={{ object.pk }}">{{ racks_table.rows|length }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% include 'inc/custom_fields_panel.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Racks</strong>
</div>
{% include 'inc/table.html' with table=racks_table %}
{% if perms.dcim.add_rack %}
<div class="panel-footer text-right noprint">
<a href="{% url 'dcim:rack_add' %}?role={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add rack
</a>
</div>
{% endif %}
</div>
{% include 'inc/paginator.html' with paginator=racks_table.paginator page=racks_table.page %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,73 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li><a href="{% url 'dcim:region_list' %}">Region</a></li>
{% for region in object.get_ancestors %}
<li><a href="{{ region.get_absolute_url }}">{{ region }}</a></li>
{% endfor %}
<li>{{ object }}</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Region</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ object.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<td>Parent</td>
<td>
{% if object.parent %}
<a href="{{ object.parent.get_absolute_url }}">{{ object.parent }}</a>
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
</tr>
<tr>
<td>Sites</td>
<td>
<a href="{% url 'dcim:site_list' %}?region_id={{ object.pk }}">{{ sites_table.rows|length }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% include 'inc/custom_fields_panel.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Sites</strong>
</div>
{% include 'inc/table.html' with table=sites_table %}
{% if perms.dcim.add_site %}
<div class="panel-footer text-right noprint">
<a href="{% url 'dcim:site_add' %}?region={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add site
</a>
</div>
{% endif %}
</div>
{% include 'inc/paginator.html' with paginator=sites_table.paginator page=sites_table.page %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,73 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li><a href="{% url 'dcim:sitegroup_list' %}">Site Groups</a></li>
{% for sitegroup in object.get_ancestors %}
<li><a href="{{ sitegroup.get_absolute_url }}">{{ sitegroup }}</a></li>
{% endfor %}
<li>{{ object }}</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Site Group</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ object.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<td>Parent</td>
<td>
{% if object.parent %}
<a href="{{ object.parent.get_absolute_url }}">{{ object.parent }}</a>
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
</tr>
<tr>
<td>Sites</td>
<td>
<a href="{% url 'dcim:site_list' %}?group_id={{ object.pk }}">{{ sites_table.rows|length }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% include 'inc/custom_fields_panel.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Sites</strong>
</div>
{% include 'inc/table.html' with table=sites_table %}
{% if perms.dcim.add_site %}
<div class="panel-footer text-right noprint">
<a href="{% url 'dcim:site_add' %}?group={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add site
</a>
</div>
{% endif %}
</div>
{% include 'inc/paginator.html' with paginator=sites_table.paginator page=sites_table.page %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -1,98 +1,51 @@
{% extends 'base.html' %} {% extends 'generic/object.html' %}
{% load helpers %} {% load helpers %}
{% load plugins %}
{% block header %} {% block breadcrumbs %}
<div class="row"> <li><a href="{% url 'extras:tag_list' %}">Tags</a></li>
<div class="col-sm-8 col-md-9"> <li>{{ object }}</li>
<ol class="breadcrumb">
<li><a href="{% url 'extras:tag_list' %}">Tags</a></li>
<li>{{ object }}</li>
</ol>
</div>
<div class="col-sm-4 col-md-3">
<form action="{% url 'extras:tag_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="mdi mdi-magnify" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
<div class="pull-right">
{% if perms.taggit.change_tag %}
<a href="{% url 'extras:tag_edit' slug=object.slug %}" class="btn btn-warning">
<span class="mdi mdi-pencil" aria-hidden="true"></span>
Edit this tag
</a>
{% endif %}
{% if perms.taggit.delete_tag %}
<a href="{% url 'extras:tag_delete' slug=object.slug %}" class="btn btn-danger">
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span>
Delete this tag
</a>
{% endif %}
</div>
<h1>{% block title %}Tag: {{ object }}{% endblock %}</h1>
{% include 'inc/created_updated.html' %}
<ul class="nav nav-tabs">
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ object.get_absolute_url }}">Tag</a>
</li>
{% if perms.extras.view_objectchange %}
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'extras:tag_changelog' pk=object.pk %}">Change Log</a>
</li>
{% endif %}
</ul>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Tag</strong> <strong>Tag</strong>
</div> </div>
<table class="table table-hover panel-body attr-table"> <table class="table table-hover panel-body attr-table">
<tr> <tr>
<td>Name</td> <td>Name</td>
<td> <td>{{ object.name }}</td>
{{ object.name }} </tr>
</td> <tr>
</tr> <td>Description</td>
<tr> <td>{{ object.description|placeholder }}</td>
<td>Slug</td> </tr>
<td> <tr>
{{ object.slug }} <td>Color</td>
</td> <td>
</tr> <span class="label color-block" style="background-color: #{{ object.color }}">&nbsp;</span>
<tr> </td>
<td>Tagged Items</td> </tr>
<td> <tr>
{{ items_count }} <td>Tagged Items</td>
</td> <td>
</tr> {{ tagged_item_count }}
<tr> </td>
<td>Color</td> </tr>
<td> </table>
<span class="label color-block" style="background-color: #{{ object.color }}">&nbsp;</span>
</td>
</tr>
<tr>
<td>Description</td>
<td>
{{ object.description|placeholder }}
</td>
</table>
</div>
</div>
<div class="col-md-6">
{% include 'panel_table.html' with table=items_table heading='Tagged Objects' %}
{% include 'inc/paginator.html' with paginator=items_table.paginator page=items_table.page %}
</div>
</div> </div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,70 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li><a href="{% url 'ipam:rir_list' %}">RIRs</a></li>
<li>{{ object }}</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>RIR</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ object.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<td>Private</td>
<td>
{% if object.is_private %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
</tr>
<tr>
<td>Aggregates</td>
<td>
<a href="{% url 'ipam:aggregate_list' %}?rir_id={{ object.pk }}">{{ aggregates_table.rows|length }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% include 'inc/custom_fields_panel.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Aggregates</strong>
</div>
{% include 'inc/table.html' with table=aggregates_table %}
{% if perms.ipam.add_aggregate %}
<div class="panel-footer text-right noprint">
<a href="{% url 'ipam:aggregate_add' %}?rir={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add aggregate
</a>
</div>
{% endif %}
</div>
{% include 'inc/paginator.html' with paginator=aggregates_table.paginator page=aggregates_table.page %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,64 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li><a href="{% url 'ipam:role_list' %}">Roles</a></li>
<li>{{ object }}</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Role</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ object.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<td>Weight</td>
<td>{{ object.weight }}</td>
</tr>
<tr>
<td>Prefixes</td>
<td>
<a href="{% url 'ipam:prefix_list' %}?role_id={{ object.pk }}">{{ prefixes_table.rows|length }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% include 'inc/custom_fields_panel.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Prefixes</strong>
</div>
{% include 'inc/table.html' with table=prefixes_table %}
{% if perms.ipam.add_prefix %}
<div class="panel-footer text-right noprint">
<a href="{% url 'ipam:prefix_add' %}?role={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add prefix
</a>
</div>
{% endif %}
</div>
{% include 'inc/paginator.html' with paginator=prefixes_table.paginator page=prefixes_table.page %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,72 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li><a href="{% url 'ipam:vlangroup_list' %}">VLAN Groups</a></li>
{% if object.scope %}
<li><a href="{{ object.scope.get_absolute_url }}">{{ object.scope }}</a></li>
{% endif %}
<li>{{ object }}</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>VLAN Group</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ object.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<td>Scope</td>
<td>
{% if object.scope %}
<a href="{{ object.scope.get_absolute_url }}">{{ object.scope }}</a>
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</tr>
<tr>
<td>VLANs</td>
<td>
<a href="{% url 'ipam:vlan_list' %}?group_id={{ object.pk }}">{{ vlans_count }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% include 'inc/custom_fields_panel.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<strong>VLANs</strong>
</div>
{% include 'inc/table.html' with table=vlans_table %}
{% if perms.ipam.add_vlan %}
<div class="panel-footer text-right noprint">
<a href="{% url 'ipam:vlan_add' %}?role={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add VLAN
</a>
</div>
{% endif %}
</div>
{% include 'inc/paginator.html' with paginator=vlans_table.paginator page=vlans_table.page %}
</div>
</div>
{% endblock %}

View File

@ -1,24 +0,0 @@
{% extends 'base.html' %}
{% block title %}{{ object }} - VLANs{% endblock %}
{% block content %}
<div class="row noprint">
<div class="col-sm-12 col-md-12">
<ol class="breadcrumb">
<li><a href="{% url 'ipam:vlangroup_list' %}">VLAN Groups</a></li>
{% if object.site %}
<li><a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a></li>
{% endif %}
<li>{{ object }}</li>
</ol>
</div>
</div>
{% include 'ipam/inc/vlangroup_header.html' %}
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with table=vlan_table table_template='panel_table.html' heading='VLANs' bulk_edit_url='ipam:vlan_bulk_edit' bulk_delete_url='ipam:vlan_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,60 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li><a href="{% url 'secrets:secretrole_list' %}">Secret Roles</a></li>
<li>{{ object }}</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Secret Role</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ object.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<td>Secrets</td>
<td>
<a href="{% url 'secrets:secret_list' %}?role_id={{ object.pk }}">{{ secrets_table.rows|length }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% include 'inc/custom_fields_panel.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Secrets</strong>
</div>
{% include 'inc/table.html' with table=secrets_table %}
{% if perms.secrets.add_secret %}
<div class="panel-footer text-right noprint">
<a href="{% url 'secrets:secret_add' %}?role={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add secret
</a>
</div>
{% endif %}
</div>
{% include 'inc/paginator.html' with paginator=secrets_table.paginator page=secrets_table.page %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,73 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li><a href="{% url 'tenancy:tenantgroup_list' %}">Tenant Groups</a></li>
{% for tenantgroup in object.get_ancestors %}
<li><a href="{{ tenantgroup.get_absolute_url }}">{{ tenantgroup }}</a></li>
{% endfor %}
<li>{{ object }}</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Tenant Group</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ object.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<td>Parent</td>
<td>
{% if object.parent %}
<a href="{{ object.parent.get_absolute_url }}">{{ object.parent }}</a>
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
</tr>
<tr>
<td>Sites</td>
<td>
<a href="{% url 'tenancy:tenant_list' %}?group_id={{ object.pk }}">{{ tenants_table.rows|length }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% include 'inc/custom_fields_panel.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Tenants</strong>
</div>
{% include 'inc/table.html' with table=tenants_table %}
{% if perms.tenancy.add_tenant %}
<div class="panel-footer text-right noprint">
<a href="{% url 'tenancy:tenant_add' %}?group={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add tenant
</a>
</div>
{% endif %}
</div>
{% include 'inc/paginator.html' with paginator=tenants_table.paginator page=tenants_table.page %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,60 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li><a href="{% url 'virtualization:clustertype_list' %}">Cluster Groups</a></li>
<li>{{ object }}</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Cluster Group</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ object.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<td>Clusters</td>
<td>
<a href="{% url 'virtualization:cluster_list' %}?group_id={{ object.pk }}">{{ clusters_table.rows|length }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% include 'inc/custom_fields_panel.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Clusters</strong>
</div>
{% include 'inc/table.html' with table=clusters_table %}
{% if perms.virtualization.add_cluster %}
<div class="panel-footer text-right noprint">
<a href="{% url 'virtualization:cluster_add' %}?type={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add cluster
</a>
</div>
{% endif %}
</div>
{% include 'inc/paginator.html' with paginator=clusters_table.paginator page=clusters_table.page %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,60 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li><a href="{% url 'virtualization:clustertype_list' %}">Cluster Types</a></li>
<li>{{ object }}</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Cluster Type</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ object.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<td>Clusters</td>
<td>
<a href="{% url 'virtualization:cluster_list' %}?type_id={{ object.pk }}">{{ clusters_table.rows|length }}</a>
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% include 'inc/custom_fields_panel.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Clusters</strong>
</div>
{% include 'inc/table.html' with table=clusters_table %}
{% if perms.virtualization.add_cluster %}
<div class="panel-footer text-right noprint">
<a href="{% url 'virtualization:cluster_add' %}?type={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add cluster
</a>
</div>
{% endif %}
</div>
{% include 'inc/paginator.html' with paginator=clusters_table.paginator page=clusters_table.page %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -45,7 +45,7 @@ class TenantGroup(NestedGroupModel):
ordering = ['name'] ordering = ['name']
def get_absolute_url(self): def get_absolute_url(self):
return "{}?group={}".format(reverse('tenancy:tenant_list'), self.slug) return reverse('tenancy:tenantgroup', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (

View File

@ -35,7 +35,9 @@ class TenantColumn(tables.TemplateColumn):
class TenantGroupTable(BaseTable): class TenantGroupTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = MPTTColumn() name = MPTTColumn(
linkify=True
)
tenant_count = LinkedCountColumn( tenant_count = LinkedCountColumn(
viewname='tenancy:tenant_list', viewname='tenancy:tenant_list',
url_params={'group': 'slug'}, url_params={'group': 'slug'},

View File

@ -13,6 +13,7 @@ urlpatterns = [
path('tenant-groups/import/', views.TenantGroupBulkImportView.as_view(), name='tenantgroup_import'), path('tenant-groups/import/', views.TenantGroupBulkImportView.as_view(), name='tenantgroup_import'),
path('tenant-groups/edit/', views.TenantGroupBulkEditView.as_view(), name='tenantgroup_bulk_edit'), path('tenant-groups/edit/', views.TenantGroupBulkEditView.as_view(), name='tenantgroup_bulk_edit'),
path('tenant-groups/delete/', views.TenantGroupBulkDeleteView.as_view(), name='tenantgroup_bulk_delete'), path('tenant-groups/delete/', views.TenantGroupBulkDeleteView.as_view(), name='tenantgroup_bulk_delete'),
path('tenant-groups/<int:pk>/', views.TenantGroupView.as_view(), name='tenantgroup'),
path('tenant-groups/<int:pk>/edit/', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'), path('tenant-groups/<int:pk>/edit/', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'),
path('tenant-groups/<int:pk>/delete/', views.TenantGroupDeleteView.as_view(), name='tenantgroup_delete'), path('tenant-groups/<int:pk>/delete/', views.TenantGroupDeleteView.as_view(), name='tenantgroup_delete'),
path('tenant-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='tenantgroup_changelog', kwargs={'model': TenantGroup}), path('tenant-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='tenantgroup_changelog', kwargs={'model': TenantGroup}),

View File

@ -1,9 +1,8 @@
from django.shortcuts import get_object_or_404, render
from circuits.models import Circuit from circuits.models import Circuit
from dcim.models import Site, Rack, Device, RackReservation from dcim.models import Site, Rack, Device, RackReservation
from ipam.models import IPAddress, Prefix, VLAN, VRF from ipam.models import IPAddress, Prefix, VLAN, VRF
from netbox.views import generic from netbox.views import generic
from utilities.tables import paginate_table
from virtualization.models import VirtualMachine, Cluster from virtualization.models import VirtualMachine, Cluster
from . import filters, forms, tables from . import filters, forms, tables
from .models import Tenant, TenantGroup from .models import Tenant, TenantGroup
@ -24,6 +23,23 @@ class TenantGroupListView(generic.ObjectListView):
table = tables.TenantGroupTable table = tables.TenantGroupTable
class TenantGroupView(generic.ObjectView):
queryset = TenantGroup.objects.all()
def get_extra_context(self, request, instance):
tenants = Tenant.objects.restrict(request.user, 'view').filter(
group=instance
)
tenants_table = tables.TenantTable(tenants)
tenants_table.columns.hide('group')
paginate_table(tenants_table, request)
return {
'tenants_table': tenants_table,
}
class TenantGroupEditView(generic.ObjectEditView): class TenantGroupEditView(generic.ObjectEditView):
queryset = TenantGroup.objects.all() queryset = TenantGroup.objects.all()
model_form = forms.TenantGroupForm model_form = forms.TenantGroupForm

View File

@ -1018,6 +1018,7 @@ class ViewTestCases:
maxDiff = None maxDiff = None
class OrganizationalObjectViewTestCase( class OrganizationalObjectViewTestCase(
GetObjectViewTestCase,
GetObjectChangelogViewTestCase, GetObjectChangelogViewTestCase,
CreateObjectViewTestCase, CreateObjectViewTestCase,
EditObjectViewTestCase, EditObjectViewTestCase,

View File

@ -59,7 +59,7 @@ class ClusterType(OrganizationalModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?type={}".format(reverse('virtualization:cluster_list'), self.slug) return reverse('virtualization:clustertype', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (
@ -102,7 +102,7 @@ class ClusterGroup(OrganizationalModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?group={}".format(reverse('virtualization:cluster_list'), self.slug) return reverse('virtualization:clustergroup', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (

View File

@ -14,6 +14,7 @@ urlpatterns = [
path('cluster-types/import/', views.ClusterTypeBulkImportView.as_view(), name='clustertype_import'), path('cluster-types/import/', views.ClusterTypeBulkImportView.as_view(), name='clustertype_import'),
path('cluster-types/edit/', views.ClusterTypeBulkEditView.as_view(), name='clustertype_bulk_edit'), path('cluster-types/edit/', views.ClusterTypeBulkEditView.as_view(), name='clustertype_bulk_edit'),
path('cluster-types/delete/', views.ClusterTypeBulkDeleteView.as_view(), name='clustertype_bulk_delete'), path('cluster-types/delete/', views.ClusterTypeBulkDeleteView.as_view(), name='clustertype_bulk_delete'),
path('cluster-types/<int:pk>/', views.ClusterTypeView.as_view(), name='clustertype'),
path('cluster-types/<int:pk>/edit/', views.ClusterTypeEditView.as_view(), name='clustertype_edit'), path('cluster-types/<int:pk>/edit/', views.ClusterTypeEditView.as_view(), name='clustertype_edit'),
path('cluster-types/<int:pk>/delete/', views.ClusterTypeDeleteView.as_view(), name='clustertype_delete'), path('cluster-types/<int:pk>/delete/', views.ClusterTypeDeleteView.as_view(), name='clustertype_delete'),
path('cluster-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='clustertype_changelog', kwargs={'model': ClusterType}), path('cluster-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='clustertype_changelog', kwargs={'model': ClusterType}),
@ -24,6 +25,7 @@ urlpatterns = [
path('cluster-groups/import/', views.ClusterGroupBulkImportView.as_view(), name='clustergroup_import'), path('cluster-groups/import/', views.ClusterGroupBulkImportView.as_view(), name='clustergroup_import'),
path('cluster-groups/edit/', views.ClusterGroupBulkEditView.as_view(), name='clustergroup_bulk_edit'), path('cluster-groups/edit/', views.ClusterGroupBulkEditView.as_view(), name='clustergroup_bulk_edit'),
path('cluster-groups/delete/', views.ClusterGroupBulkDeleteView.as_view(), name='clustergroup_bulk_delete'), path('cluster-groups/delete/', views.ClusterGroupBulkDeleteView.as_view(), name='clustergroup_bulk_delete'),
path('cluster-groups/<int:pk>/', views.ClusterGroupView.as_view(), name='clustergroup'),
path('cluster-groups/<int:pk>/edit/', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'), path('cluster-groups/<int:pk>/edit/', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'),
path('cluster-groups/<int:pk>/delete/', views.ClusterGroupDeleteView.as_view(), name='clustergroup_delete'), path('cluster-groups/<int:pk>/delete/', views.ClusterGroupDeleteView.as_view(), name='clustergroup_delete'),
path('cluster-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='clustergroup_changelog', kwargs={'model': ClusterGroup}), path('cluster-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='clustergroup_changelog', kwargs={'model': ClusterGroup}),

View File

@ -11,6 +11,7 @@ from ipam.models import IPAddress, Service
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
from netbox.views import generic from netbox.views import generic
from secrets.models import Secret from secrets.models import Secret
from utilities.tables import paginate_table
from utilities.utils import count_related from utilities.utils import count_related
from . import filters, forms, tables from . import filters, forms, tables
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@ -27,6 +28,23 @@ class ClusterTypeListView(generic.ObjectListView):
table = tables.ClusterTypeTable table = tables.ClusterTypeTable
class ClusterTypeView(generic.ObjectView):
queryset = ClusterType.objects.all()
def get_extra_context(self, request, instance):
clusters = Cluster.objects.restrict(request.user, 'view').filter(
type=instance
)
clusters_table = tables.ClusterTable(clusters)
clusters_table.columns.hide('type')
paginate_table(clusters_table, request)
return {
'clusters_table': clusters_table,
}
class ClusterTypeEditView(generic.ObjectEditView): class ClusterTypeEditView(generic.ObjectEditView):
queryset = ClusterType.objects.all() queryset = ClusterType.objects.all()
model_form = forms.ClusterTypeForm model_form = forms.ClusterTypeForm
@ -69,6 +87,23 @@ class ClusterGroupListView(generic.ObjectListView):
table = tables.ClusterGroupTable table = tables.ClusterGroupTable
class ClusterGroupView(generic.ObjectView):
queryset = ClusterGroup.objects.all()
def get_extra_context(self, request, instance):
clusters = Cluster.objects.restrict(request.user, 'view').filter(
group=instance
)
clusters_table = tables.ClusterTable(clusters)
clusters_table.columns.hide('group')
paginate_table(clusters_table, request)
return {
'clusters_table': clusters_table,
}
class ClusterGroupEditView(generic.ObjectEditView): class ClusterGroupEditView(generic.ObjectEditView):
queryset = ClusterGroup.objects.all() queryset = ClusterGroup.objects.all()
model_form = forms.ClusterGroupForm model_form = forms.ClusterGroupForm