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

Merged develop

This commit is contained in:
Jeremy Stretch
2017-02-28 16:10:53 -05:00
parent 8f42f59a80
commit c0152940f9
29 changed files with 437 additions and 51 deletions

View File

@ -6,6 +6,10 @@ How you define sites will depend on the nature of your organization, but typical
Sites can be assigned an optional facility ID to identify the actual facility housing colocated equipment.
### Regions
Sites can optionally be arranged by geographic region. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy.
---
# Racks

View File

@ -119,9 +119,17 @@ class CircuitListView(ObjectListView):
def circuit(request, pk):
circuit = get_object_or_404(Circuit, pk=pk)
termination_a = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_A).first()
termination_z = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_Z).first()
circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
termination_a = CircuitTermination.objects.select_related(
'site__region', 'interface__device'
).filter(
circuit=circuit, term_side=TERM_SIDE_A
).first()
termination_z = CircuitTermination.objects.select_related(
'site__region', 'interface__device'
).filter(
circuit=circuit, term_side=TERM_SIDE_Z
).first()
return render(request, 'circuits/circuit.html', {
'circuit': circuit,

View File

@ -1,13 +1,24 @@
from django.contrib import admin
from django.db.models import Count
from mptt.admin import MPTTModelAdmin
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform,
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Site,
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region,
Site,
)
@admin.register(Region)
class RegionAdmin(MPTTModelAdmin):
list_display = ['name', 'parent', 'slug']
prepopulated_fields = {
'slug': ['name'],
}
@admin.register(Site)
class SiteAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'facility', 'asn']

View File

@ -6,18 +6,46 @@ from dcim.models import (
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
RACK_WIDTH_CHOICES, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
)
from extras.api.serializers import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer
#
# Regions
#
class NestedRegionSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
class Meta:
model = Region
fields = ['id', 'url', 'name', 'slug']
class RegionSerializer(serializers.ModelSerializer):
parent = NestedRegionSerializer()
class Meta:
model = Region
fields = ['id', 'url', 'name', 'slug', 'parent']
class WritableRegionSerializer(serializers.ModelSerializer):
class Meta:
model = Region
fields = ['id', 'name', 'slug', 'parent']
#
# Sites
#
class SiteSerializer(CustomFieldModelSerializer):
region = NestedRegionSerializer()
tenant = NestedTenantSerializer()
class Meta:

View File

@ -10,6 +10,7 @@ from . import views
router = routers.DefaultRouter()
# Sites
router.register(r'regions', views.RegionViewSet)
router.register(r'sites', views.SiteViewSet)
# Racks

View File

@ -13,7 +13,7 @@ from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module,
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
RackRole, Site,
RackRole, Region, Site,
)
from dcim import filters
from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer
@ -25,6 +25,16 @@ from .exceptions import MissingFilterException
from . import serializers
#
# Regions
#
class RegionViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Region.objects.all()
serializer_class = serializers.RegionSerializer
write_serializer_class = serializers.WritableRegionSerializer
#
# Sites
#

View File

@ -10,7 +10,7 @@ from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, InterfaceTemplate,
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Site, VIRTUAL_IFACE_TYPES,
RackReservation, RackRole, Region, Site, VIRTUAL_IFACE_TYPES,
)
@ -19,6 +19,17 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
action='search',
label='Search',
)
region_id = NullableModelMultipleChoiceFilter(
name='region',
queryset=Region.objects.all(),
label='Region (ID)',
)
region = NullableModelMultipleChoiceFilter(
name='region',
queryset=Region.objects.all(),
to_field_name='slug',
label='Region (slug)',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),

View File

@ -1,5 +1,7 @@
import re
from mptt.forms import TreeNodeChoiceField
from django import forms
from django.contrib.postgres.forms.array import SimpleArrayField
from django.core.exceptions import ValidationError
@ -11,7 +13,7 @@ from tenancy.models import Tenant
from utilities.forms import (
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField,
CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled,
SmallTextarea, SlugField,
SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField,
)
from .formfields import MACAddressFormField
@ -20,7 +22,7 @@ from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
VIRTUAL_IFACE_TYPES
)
@ -63,18 +65,33 @@ class DeviceComponentForm(BootstrapMixin, forms.Form):
super(DeviceComponentForm, self).__init__(*args, **kwargs)
#
# Regions
#
class RegionForm(BootstrapMixin, forms.ModelForm):
slug = SlugField()
class Meta:
model = Region
fields = ['parent', 'name', 'slug']
#
# Sites
#
class SiteForm(BootstrapMixin, CustomFieldForm):
region = TreeNodeChoiceField(queryset=Region.objects.all())
slug = SlugField()
comments = CommentField()
class Meta:
model = Site
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'contact_name',
'contact_phone', 'contact_email', 'comments']
fields = [
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
'contact_name', 'contact_phone', 'contact_email', 'comments',
]
widgets = {
'physical_address': SmallTextarea(attrs={'rows': 3}),
'shipping_address': SmallTextarea(attrs={'rows': 3}),
@ -89,12 +106,22 @@ class SiteForm(BootstrapMixin, CustomFieldForm):
class SiteFromCSVForm(forms.ModelForm):
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
region = forms.ModelChoiceField(
Region.objects.all(), to_field_name='name', required=False, error_messages={
'invalid_choice': 'Tenant not found.'
}
)
tenant = forms.ModelChoiceField(
Tenant.objects.all(), to_field_name='name', required=False, error_messages={
'invalid_choice': 'Tenant not found.'
}
)
class Meta:
model = Site
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email']
fields = [
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email',
]
class SiteImportForm(BootstrapMixin, BulkImportForm):
@ -103,18 +130,27 @@ class SiteImportForm(BootstrapMixin, BulkImportForm):
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False)
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN')
class Meta:
nullable_fields = ['tenant', 'asn']
nullable_fields = ['region', 'tenant', 'asn']
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Site
q = forms.CharField(required=False, label='Search')
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('sites')), to_field_name='slug',
null_option=(0, 'None'))
region = FilterTreeNodeMultipleChoiceField(
queryset=Region.objects.annotate(filter_count=Count('sites')),
to_field_name='slug',
required=False,
)
tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('sites')),
to_field_name='slug',
null_option=(0, 'None')
)
#

View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-02-28 17:14
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
class Migration(migrations.Migration):
dependencies = [
('dcim', '0030_interface_add_lag'),
]
operations = [
migrations.CreateModel(
name='Region',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True)),
('slug', models.SlugField(unique=True)),
('lft', models.PositiveIntegerField(db_index=True, editable=False)),
('rght', models.PositiveIntegerField(db_index=True, editable=False)),
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
('level', models.PositiveIntegerField(db_index=True, editable=False)),
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.Region')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='site',
name='region',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='dcim.Region'),
),
]

View File

@ -1,5 +1,7 @@
from collections import OrderedDict
from mptt.models import MPTTModel, TreeForeignKey
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
@ -200,6 +202,29 @@ RPC_CLIENT_CHOICES = [
]
#
# Regions
#
@python_2_unicode_compatible
class Region(MPTTModel):
"""
Sites can be grouped within geographic Regions.
"""
parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True)
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
class MPTTMeta:
order_insertion_by = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return "{}?region={}".format(reverse('dcim:site_list'), self.slug)
#
# Sites
#
@ -218,7 +243,8 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='sites', on_delete=models.PROTECT)
region = models.ForeignKey('Region', related_name='sites', blank=True, null=True, on_delete=models.SET_NULL)
tenant = models.ForeignKey(Tenant, related_name='sites', blank=True, null=True, on_delete=models.PROTECT)
facility = models.CharField(max_length=50, blank=True)
asn = ASNField(blank=True, null=True, verbose_name='ASN')
physical_address = models.CharField(max_length=200, blank=True)
@ -244,6 +270,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
return csv_format([
self.name,
self.slug,
self.region.name if self.region else None,
self.tenant.name if self.tenant else None,
self.facility,
self.asn,
@ -1248,8 +1275,8 @@ class Interface(models.Model):
)
})
# A LAG interface cannot have a parent LAG
if self.form_factor == IFACE_FF_LAG and self.lag is not None:
# A virtual interface cannot have a parent LAG
if self.form_factor in VIRTUAL_IFACE_TYPES and self.lag is not None:
raise ValidationError({
'lag': u"{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display())
})

View File

@ -6,10 +6,28 @@ from utilities.tables import BaseTable, ToggleColumn
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
RackGroup, Site,
RackGroup, Region, Site,
)
REGION_LINK = """
{% if record.get_children %}
<span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i></a>
{% else %}
<span style="padding-left: {{ record.get_ancestors|length }}9px">
{% endif %}
{{ record.name }}
</span>
"""
SITE_REGION_LINK = """
{% if record.region %}
<a href="{% url 'dcim:site_list' %}?region={{ record.region.slug }}">{{ record.region }}</a>
{% else %}
&mdash;
{% endif %}
"""
COLOR_LABEL = """
<label class="label" style="background-color: #{{ record.color }}">{{ record }}</label>
"""
@ -20,6 +38,12 @@ DEVICE_LINK = """
</a>
"""
REGION_ACTIONS = """
{% if perms.dcim.change_region %}
<a href="{% url 'dcim:region_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
RACKGROUP_ACTIONS = """
{% if perms.dcim.change_rackgroup %}
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
@ -76,6 +100,27 @@ UTILIZATION_GRAPH = """
"""
#
# Regions
#
class RegionTable(BaseTable):
pk = ToggleColumn()
# name = tables.LinkColumn(verbose_name='Name')
name = tables.TemplateColumn(template_code=REGION_LINK, orderable=False)
site_count = tables.Column(verbose_name='Sites')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(
template_code=REGION_ACTIONS,
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = Region
fields = ('pk', 'name', 'site_count', 'slug', 'actions')
#
# Sites
#
@ -84,6 +129,7 @@ class SiteTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name')
facility = tables.Column(verbose_name='Facility')
region = tables.TemplateColumn(template_code=SITE_REGION_LINK, verbose_name='Region')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
asn = tables.Column(verbose_name='ASN')
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
@ -94,8 +140,10 @@ class SiteTable(BaseTable):
class Meta(BaseTable.Meta):
model = Site
fields = ('pk', 'name', 'facility', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
'vlan_count', 'circuit_count')
fields = (
'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
'vlan_count', 'circuit_count',
)
#

View File

@ -17,6 +17,7 @@ class SiteTest(APITestCase):
'id',
'name',
'slug',
'region',
'tenant',
'facility',
'asn',

View File

@ -8,6 +8,12 @@ from . import views
urlpatterns = [
# Regions
url(r'^regions/$', views.RegionListView.as_view(), name='region_list'),
url(r'^regions/add/$', views.RegionEditView.as_view(), name='region_add'),
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'),
# Sites
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'),

View File

@ -26,7 +26,7 @@ from .models import (
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Site,
RackReservation, RackRole, Region, Site,
)
@ -129,12 +129,37 @@ class ComponentDeleteView(ObjectDeleteView):
return obj.device.get_absolute_url()
#
# Regions
#
class RegionListView(ObjectListView):
queryset = Region.objects.annotate(site_count=Count('sites'))
table = tables.RegionTable
template_name = 'dcim/region_list.html'
class RegionEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_region'
model = Region
form_class = forms.RegionForm
def get_return_url(self, obj):
return reverse('dcim:region_list')
class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_region'
cls = Region
default_return_url = 'dcim:region_list'
#
# Sites
#
class SiteListView(ObjectListView):
queryset = Site.objects.select_related('tenant')
queryset = Site.objects.select_related('region', 'tenant')
filter = filters.SiteFilter
filter_form = forms.SiteFilterForm
table = tables.SiteTable
@ -143,7 +168,7 @@ class SiteListView(ObjectListView):
def site(request, slug):
site = get_object_or_404(Site, slug=slug)
site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug)
stats = {
'rack_count': Rack.objects.filter(site=site).count(),
'device_count': Device.objects.filter(rack__site=site).count(),
@ -263,7 +288,7 @@ class RackListView(ObjectListView):
def rack(request, pk):
rack = get_object_or_404(Rack, pk=pk)
rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True)\
.select_related('device_type__manufacturer')
@ -638,7 +663,9 @@ class DeviceListView(ObjectListView):
def device(request, pk):
device = get_object_or_404(Device, pk=pk)
device = get_object_or_404(Device.objects.select_related(
'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
), pk=pk)
console_ports = natsorted(
ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name')
)

View File

@ -393,7 +393,9 @@ class PrefixListView(ObjectListView):
def prefix(request, pk):
prefix = get_object_or_404(Prefix.objects.select_related('site', 'vlan', 'role'), pk=pk)
prefix = get_object_or_404(Prefix.objects.select_related(
'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role'
), pk=pk)
try:
aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix))
@ -731,7 +733,7 @@ class VLANListView(ObjectListView):
def vlan(request, pk):
vlan = get_object_or_404(VLAN.objects.select_related('site', 'role'), pk=pk)
vlan = get_object_or_404(VLAN.objects.select_related('site__region', 'tenant__group', 'role'), pk=pk)
prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
prefix_table = tables.PrefixBriefTable(list(prefixes))
prefix_table.exclude = ('vlan',)

View File

@ -104,6 +104,7 @@ INSTALLED_APPS = (
'django.contrib.humanize',
'debug_toolbar',
'django_tables2',
'mptt',
'rest_framework',
'rest_framework_swagger',
'circuits',

View File

@ -37,6 +37,11 @@
<li><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Sites</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'dcim:region_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Regions</a></li>
{% if perms.dcim.add_region %}
<li><a href="{% url 'dcim:region_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Region</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'tenancy:tenant_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenants</a></li>
{% if perms.tenancy.add_tenant %}
<li><a href="{% url 'tenancy:tenant_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant</a></li>

View File

@ -66,6 +66,10 @@
<td>Tenant</td>
<td>
{% if circuit.tenant %}
{% if circuit.tenant.group %}
<a href="{{ circuit.tenant.group.get_absolute_url }}">{{ circuit.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>
{% else %}
<span class="text-muted">None</span>

View File

@ -27,6 +27,10 @@
<tr>
<td>Site</td>
<td>
{% if termination.site.region %}
<a href="{{ termination.site.region.get_absolute_url }}">{{ termination.site.region }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'dcim:site' slug=termination.site.slug %}">{{ termination.site }}</a>
</td>
</tr>
@ -34,7 +38,8 @@
<td>Termination</td>
<td>
{% if termination.interface %}
<span><a href="{% url 'dcim:device' pk=termination.interface.device.pk %}">{{ termination.interface.device }}</a> {{ termination.interface }}</span>
<a href="{% url 'dcim:device' pk=termination.interface.device.pk %}">{{ termination.interface.device }}</a>
<i class="fa fa-angle-right"></i> {{ termination.interface }}
{% else %}
<span class="text-muted">Not defined</span>
{% endif %}

View File

@ -14,19 +14,13 @@
<strong>Device</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Tenant</td>
<td>
{% if device.tenant %}
<a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Site</td>
<td>
{% if device.site.region %}
<a href="{{ device.site.region.get_absolute_url }}">{{ device.site.region }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a>
</td>
</tr>
@ -34,7 +28,11 @@
<td>Rack</td>
<td>
{% if device.rack %}
<span><a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack.name }}</a>{% if device.rack.facility_id %} ({{ device.rack.facility_id }}){% endif %}</span>
{% if device.rack.group %}
<a href="{{ device.rack.group.get_absolute_url }}">{{ device.rack.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack.name }}</a>{% if device.rack.facility_id %} ({{ device.rack.facility_id }}){% endif %}
{% else %}
<span class="text-muted">None</span>
{% endif %}
@ -57,6 +55,20 @@
{% endif %}
</td>
</tr>
<tr>
<td>Tenant</td>
<td>
{% if device.tenant %}
{% if device.tenant.group %}
<a href="{{ device.tenant.group.get_absolute_url }}">{{ device.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Device Type</td>
<td>
@ -393,7 +405,7 @@
{% endif %}
{% endif %}
{% if interfaces or device.device_type.is_network_device %}
{% if perms.dcim.delete_interface %}
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
<form method="post">
{% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" />

View File

@ -64,6 +64,10 @@
<tr>
<td>Site</td>
<td>
{% if rack.site.region %}
<a href="{{ rack.site.region.get_absolute_url }}">{{ rack.site.region }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'dcim:site' slug=rack.site.slug %}">{{ rack.site }}</a>
</td>
</tr>
@ -91,6 +95,10 @@
<td>Tenant</td>
<td>
{% if rack.tenant %}
{% if rack.tenant.group %}
<a href="{{ rack.tenant.group.get_absolute_url }}">{{ rack.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ rack.tenant.get_absolute_url }}">{{ rack.tenant }}</a>
{% else %}
<span class="text-muted">None</span>

View File

@ -0,0 +1,21 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Regions{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_region %}
<a href="{% url 'dcim:region_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a region
</a>
{% endif %}
</div>
<h1>{{ block.title }}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@ -9,7 +9,12 @@
<div class="row">
<div class="col-sm-8 col-md-9">
<ol class="breadcrumb">
<li><a href="{% url 'dcim:site_list' %}">Sites</a></li>
{% if site.region %}
{% for region in site.region.get_ancestors %}
<li><a href="{{ region.get_absolute_url }}">{{ region }}</a></li>
{% endfor %}
<li><a href="{{ site.region.get_absolute_url }}">{{ site.region }}</a></li>
{% endif %}
<li>{{ site }}</li>
</ol>
</div>
@ -55,10 +60,28 @@
<strong>Site</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Region</td>
<td>
{% if site.region %}
{% for region in site.region.get_ancestors %}
<a href="{{ region.get_absolute_url }}">{{ region }}</a>
<i class="fa fa-angle-right"></i>
{% endfor %}
<a href="{{ site.region.get_absolute_url }}">{{ site.region }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Tenant</td>
<td>
{% if site.tenant %}
{% if site.tenant.group %}
<a href="{{ site.tenant.group.get_absolute_url }}">{{ site.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ site.tenant.get_absolute_url }}">{{ site.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
@ -85,6 +108,13 @@
{% endif %}
</td>
</tr>
</table>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Contact Info</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Physical Address</td>
<td>

View File

@ -7,6 +7,7 @@
<div class="panel-body">
{% render_field form.name %}
{% render_field form.slug %}
{% render_field form.region %}
{% render_field form.tenant %}
{% render_field form.facility %}
{% render_field form.asn %}

View File

@ -38,6 +38,11 @@
<td>URL-friendly name</td>
<td>ash4-south</td>
</tr>
<tr>
<td>Region</td>
<td>Name of region (optional)</td>
<td>North America</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
@ -71,7 +76,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>ASH-4 South,ash4-south,Pied Piper,Equinix DC6,65000,Hank Hill,+1-214-555-1234,hhill@example.com</pre>
<pre>ASH-4 South,ash4-south,North America,Pied Piper,Equinix DC6,65000,Hank Hill,+1-214-555-1234,hhill@example.com</pre>
</div>
</div>
{% endblock %}

View File

@ -30,8 +30,16 @@
<td>Tenant</td>
<td>
{% if prefix.tenant %}
{% if prefix.tenant.group %}
<a href="{{ prefix.tenant.group.get_absolute_url }}">{{ prefix.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ prefix.tenant.get_absolute_url }}">{{ prefix.tenant }}</a>
{% elif prefix.vrf.tenant %}
{% if prefix.vrf.tenant.group %}
<a href="{{ prefix.vrf.tenant.group.get_absolute_url }}">{{ prefix.vrf.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ prefix.vrf.tenant.get_absolute_url }}">{{ prefix.vrf.tenant }}</a>
<label class="label label-info">Inherited</label>
{% else %}
@ -53,6 +61,10 @@
<td>Site</td>
<td>
{% if prefix.site %}
{% if prefix.site.region %}
<a href="{{ prefix.site.region.get_absolute_url }}">{{ prefix.site.region }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'dcim:site' slug=prefix.site.slug %}">{{ prefix.site }}</a>
{% else %}
<span class="text-muted">None</span>
@ -63,6 +75,10 @@
<td>VLAN</td>
<td>
{% if prefix.vlan %}
{% if prefix.vlan.group %}
<a href="{{ prefix.vlan.group.get_absolute_url }}">{{ prefix.vlan.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'ipam:vlan' pk=prefix.vlan.pk %}">{{ prefix.vlan.display_name }}</a>
{% else %}
<span class="text-muted">None</span>
@ -79,7 +95,7 @@
<td>Role</td>
<td>
{% if prefix.role %}
<span>{{ prefix.role }}</span>
<a href="{% url 'ipam:prefix_list' %}?role={{ prefix.role.slug }}">{{ prefix.role }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}

View File

@ -57,6 +57,10 @@
<td>Site</td>
<td>
{% if vlan.site %}
{% if vlan.site.region %}
<a href="{{ vlan.site.region.get_absolute_url }}">{{ vlan.site.region }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'dcim:site' slug=vlan.site.slug %}">{{ vlan.site }}</a>
{% else %}
<span class="text-muted">None</span>
@ -85,6 +89,10 @@
<td>Tenant</td>
<td>
{% if vlan.tenant %}
{% if vlan.tenant.group %}
<a href="{{ vlan.tenant.group.get_absolute_url }}">{{ vlan.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ vlan.tenant.get_absolute_url }}">{{ vlan.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
@ -101,7 +109,7 @@
<td>Role</td>
<td>
{% if vlan.role %}
<span>{{ vlan.role }}</span>
<a href="{% url 'ipam:vlan_list' %}?role={{ vlan.role.slug }}">{{ vlan.role }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}

View File

@ -2,6 +2,8 @@ import csv
import itertools
import re
from mptt.forms import TreeNodeMultipleChoiceField
from django import forms
from django.conf import settings
from django.core.urlresolvers import reverse_lazy
@ -365,7 +367,7 @@ class SlugField(forms.SlugField):
self.widget.attrs['slug-source'] = slug_source
class FilterChoiceField(forms.ModelMultipleChoiceField):
class FilterChoiceFieldMixin(object):
iterator = forms.models.ModelChoiceIterator
def __init__(self, null_option=None, *args, **kwargs):
@ -374,12 +376,13 @@ class FilterChoiceField(forms.ModelMultipleChoiceField):
kwargs['required'] = False
if 'widget' not in kwargs:
kwargs['widget'] = forms.SelectMultiple(attrs={'size': 6})
super(FilterChoiceField, self).__init__(*args, **kwargs)
super(FilterChoiceFieldMixin, self).__init__(*args, **kwargs)
def label_from_instance(self, obj):
label = super(FilterChoiceFieldMixin, self).label_from_instance(obj)
if hasattr(obj, 'filter_count'):
return u'{} ({})'.format(obj, obj.filter_count)
return force_text(obj)
return u'{} ({})'.format(label, obj.filter_count)
return label
def _get_choices(self):
if hasattr(self, '_choices'):
@ -391,6 +394,14 @@ class FilterChoiceField(forms.ModelMultipleChoiceField):
choices = property(_get_choices, forms.ChoiceField._set_choices)
class FilterChoiceField(FilterChoiceFieldMixin, forms.ModelMultipleChoiceField):
pass
class FilterTreeNodeMultipleChoiceField(FilterChoiceFieldMixin, TreeNodeMultipleChoiceField):
pass
class LaxURLField(forms.URLField):
"""
Custom URLField which allows any valid URL scheme

View File

@ -3,6 +3,7 @@ cryptography>=1.4
Django>=1.10
django-debug-toolbar>=1.6
django-filter==0.15.3
django-mptt==0.8.7
django-rest-swagger==0.3.10
django-tables2>=1.2.5
djangorestframework>=3.5.0