Merge pull request #1201 from digitalocean/develop

Release v2.0.3
This commit is contained in:
Jeremy Stretch 2017-05-18 14:37:19 -04:00 committed by GitHub
commit ad95b86fdd
59 changed files with 215 additions and 229 deletions

View File

@ -33,3 +33,4 @@ Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for inst
* [Docker container](https://github.com/digitalocean/netbox-docker)
* [Heroku deployment](https://heroku.com/deploy?template=https://github.com/BILDQUADRAT/netbox/tree/heroku) (via [@mraerino](https://github.com/BILDQUADRAT/netbox/tree/heroku))
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant)

View File

@ -5,14 +5,14 @@
Python 3:
```no-highlight
# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
# update-alternatives --install /usr/bin/python python /usr/bin/python3 1
```
Python 2:
```no-highlight
# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
```
**CentOS/RHEL**

View File

@ -73,6 +73,9 @@ Once Apache is installed, proceed with the following configuration (Be sure to m
Alias /static /opt/netbox/netbox/static
# Needed to allow token-based API authentication
WSGIPassAuthorization on
<Directory /opt/netbox/netbox/static>
Options Indexes FollowSymLinks MultiViews
AllowOverride None

View File

@ -581,9 +581,18 @@ class WritablePowerPortSerializer(serializers.ModelSerializer):
# Interfaces
#
class NestedInterfaceSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
class Meta:
model = Interface
fields = ['id', 'url', 'name']
class InterfaceSerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer()
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
lag = NestedInterfaceSerializer()
connection = serializers.SerializerMethodField(read_only=True)
connected_interface = serializers.SerializerMethodField(read_only=True)

View File

@ -477,6 +477,11 @@ class InterfaceFilter(DeviceComponentFilterSet):
method='filter_type',
label='Interface type',
)
lag_id = django_filters.ModelMultipleChoiceFilter(
name='lag',
queryset=Interface.objects.all(),
label='LAG interface (ID)',
)
mac_address = django_filters.CharFilter(
method='_mac_address',
label='MAC address',

View File

@ -674,7 +674,7 @@ class BaseDeviceFromCSVForm(forms.ModelForm):
queryset=Platform.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid platform.'}
)
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in STATUS_CHOICES])
status = forms.CharField()
class Meta:
fields = []
@ -692,8 +692,12 @@ class BaseDeviceFromCSVForm(forms.ModelForm):
except DeviceType.DoesNotExist:
self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
def clean_status_name(self):
return dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
def clean_status(self):
status_choices = {s[1].lower(): s[0] for s in STATUS_CHOICES}
try:
return status_choices[self.cleaned_data['status'].lower()]
except KeyError:
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
class DeviceFromCSVForm(BaseDeviceFromCSVForm):
@ -707,8 +711,8 @@ class DeviceFromCSVForm(BaseDeviceFromCSVForm):
class Meta(BaseDeviceFromCSVForm.Meta):
fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
'status_name', 'site', 'rack_name', 'position', 'face',
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
'site', 'rack_name', 'position', 'face',
]
def clean(self):
@ -751,8 +755,8 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
class Meta(BaseDeviceFromCSVForm.Meta):
fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
'status_name', 'parent', 'device_bay_name',
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
'parent', 'device_bay_name',
]
def clean(self):
@ -817,13 +821,15 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
rack_id = FilterChoiceField(
queryset=Rack.objects.annotate(filter_count=Count('devices')),
label='Rack',
null_option=(0, 'None'),
)
role = FilterChoiceField(
queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
to_field_name='slug',
)
tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug',
queryset=Tenant.objects.annotate(filter_count=Count('devices')),
to_field_name='slug',
null_option=(0, 'None'),
)
manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
@ -1207,7 +1213,7 @@ class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
)
power_outlet = ChainedModelChoiceField(
queryset=PowerOutlet.objects.all(),
chains={'device': 'device'},
chains={'device': 'pdu'},
label='Outlet',
widget=APISelect(
api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
@ -1441,7 +1447,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
label='Interface',
widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical',
disabled_indicator='is_connected'
disabled_indicator='connection'
)
)

View File

@ -1,4 +1,5 @@
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Count
from dcim.models import Site, Rack, Device, Interface
@ -195,14 +196,16 @@ class PrefixFromCSVForm(forms.ModelForm):
error_messages={'invalid_choice': 'Site not found.'})
vlan_group_name = forms.CharField(required=False)
vlan_vid = forms.IntegerField(required=False)
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in PREFIX_STATUS_CHOICES])
status = forms.CharField()
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid role.'})
class Meta:
model = Prefix
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role', 'is_pool',
'description']
fields = [
'prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status', 'role', 'is_pool',
'description',
]
def clean(self):
@ -237,12 +240,12 @@ class PrefixFromCSVForm(forms.ModelForm):
except VLAN.MultipleObjectsReturned:
self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
def save(self, *args, **kwargs):
# Assign Prefix status by name
self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
return super(PrefixFromCSVForm, self).save(*args, **kwargs)
def clean_status(self):
status_choices = {s[1].lower(): s[0] for s in PREFIX_STATUS_CHOICES}
try:
return status_choices[self.cleaned_data['status'].lower()]
except KeyError:
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
class PrefixImportForm(BootstrapMixin, BulkImportForm):
@ -491,7 +494,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
error_messages={'invalid_choice': 'VRF not found.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in IPADDRESS_STATUS_CHOICES])
status = forms.CharField()
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Device not found.'})
interface_name = forms.CharField(required=False)
@ -499,7 +502,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
class Meta:
model = IPAddress
fields = ['address', 'vrf', 'tenant', 'status_name', 'device', 'interface_name', 'is_primary', 'description']
fields = ['address', 'vrf', 'tenant', 'status', 'device', 'interface_name', 'is_primary', 'description']
def clean(self):
@ -522,10 +525,14 @@ class IPAddressFromCSVForm(forms.ModelForm):
if is_primary and not device:
self.add_error('is_primary', "No device specified; cannot set as primary IP")
def save(self, *args, **kwargs):
def clean_status(self):
status_choices = {s[1].lower(): s[0] for s in IPADDRESS_STATUS_CHOICES}
try:
return status_choices[self.cleaned_data['status'].lower()]
except KeyError:
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
# Assign status by name
self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
def save(self, *args, **kwargs):
# Set interface
if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
@ -612,6 +619,7 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
required=False,
widget=forms.Select(
attrs={'filter-for': 'group', 'nullable': 'true'}
)
@ -649,7 +657,7 @@ class VLANFromCSVForm(forms.ModelForm):
Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'}
)
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES])
status = forms.CharField()
role = forms.ModelChoiceField(
queryset=Role.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid role.'}
@ -657,7 +665,7 @@ class VLANFromCSVForm(forms.ModelForm):
class Meta:
model = VLAN
fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
def clean(self):
@ -671,6 +679,13 @@ class VLANFromCSVForm(forms.ModelForm):
except VLANGroup.DoesNotExist:
self.add_error('group_name', "Invalid VLAN group {}.".format(group_name))
def clean_status(self):
status_choices = {s[1].lower(): s[0] for s in VLAN_STATUS_CHOICES}
try:
return status_choices[self.cleaned_data['status'].lower()]
except KeyError:
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
def save(self, *args, **kwargs):
vlan = super(VLANFromCSVForm, self).save(commit=False)
@ -679,9 +694,6 @@ class VLANFromCSVForm(forms.ModelForm):
if self.cleaned_data['group_name']:
vlan.group = VLANGroup.objects.get(site=self.cleaned_data['site'], name=self.cleaned_data['group_name'])
# Assign VLAN status by name
vlan.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
if kwargs.get('commit'):
vlan.save()
return vlan

View File

@ -70,9 +70,9 @@ IPADDRESS_LINK = """
{% if record.pk %}
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
{% elif perms.ipam.add_ipaddress %}
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Lots of{% endif %} free IP{{ record.0|pluralize }}</a>
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
{% else %}
{{ record.0 }}
{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
{% endif %}
"""

View File

@ -525,6 +525,7 @@ def prefix_ipaddresses(request, pk):
'prefix': prefix,
'ip_table': ip_table,
'permissions': permissions,
'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix),
})

View File

@ -13,7 +13,7 @@ except ImportError:
)
VERSION = '2.0.2'
VERSION = '2.0.3'
# Import local configuration
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None

View File

@ -1,3 +1,4 @@
from collections import OrderedDict
import sys
from rest_framework.views import APIView
@ -27,91 +28,91 @@ from .forms import SearchForm
SEARCH_MAX_RESULTS = 15
SEARCH_TYPES = {
SEARCH_TYPES = OrderedDict((
# Circuits
'provider': {
('provider', {
'queryset': Provider.objects.all(),
'filter': ProviderFilter,
'table': ProviderSearchTable,
'url': 'circuits:provider_list',
},
'circuit': {
}),
('circuit', {
'queryset': Circuit.objects.select_related('type', 'provider', 'tenant').prefetch_related('terminations__site'),
'filter': CircuitFilter,
'table': CircuitSearchTable,
'url': 'circuits:circuit_list',
},
}),
# DCIM
'site': {
('site', {
'queryset': Site.objects.select_related('region', 'tenant'),
'filter': SiteFilter,
'table': SiteSearchTable,
'url': 'dcim:site_list',
},
'rack': {
}),
('rack', {
'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role'),
'filter': RackFilter,
'table': RackSearchTable,
'url': 'dcim:rack_list',
},
'devicetype': {
}),
('devicetype', {
'queryset': DeviceType.objects.select_related('manufacturer'),
'filter': DeviceTypeFilter,
'table': DeviceTypeSearchTable,
'url': 'dcim:devicetype_list',
},
'device': {
}),
('device', {
'queryset': Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack'),
'filter': DeviceFilter,
'table': DeviceSearchTable,
'url': 'dcim:device_list',
},
}),
# IPAM
'vrf': {
('vrf', {
'queryset': VRF.objects.select_related('tenant'),
'filter': VRFFilter,
'table': VRFSearchTable,
'url': 'ipam:vrf_list',
},
'aggregate': {
}),
('aggregate', {
'queryset': Aggregate.objects.select_related('rir'),
'filter': AggregateFilter,
'table': AggregateSearchTable,
'url': 'ipam:aggregate_list',
},
'prefix': {
}),
('prefix', {
'queryset': Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
'filter': PrefixFilter,
'table': PrefixSearchTable,
'url': 'ipam:prefix_list',
},
'ipaddress': {
}),
('ipaddress', {
'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device'),
'filter': IPAddressFilter,
'table': IPAddressSearchTable,
'url': 'ipam:ipaddress_list',
},
'vlan': {
}),
('vlan', {
'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role'),
'filter': VLANFilter,
'table': VLANSearchTable,
'url': 'ipam:vlan_list',
},
}),
# Secrets
'secret': {
('secret', {
'queryset': Secret.objects.select_related('role', 'device'),
'filter': SecretFilter,
'table': SecretSearchTable,
'url': 'secrets:secret_list',
},
}),
# Tenancy
'tenant': {
('tenant', {
'queryset': Tenant.objects.select_related('group'),
'filter': TenantFilter,
'table': TenantSearchTable,
'url': 'tenancy:tenant_list',
},
}
}),
))
def home(request):

View File

@ -74,6 +74,13 @@ footer p {
}
}
/* Hide the nav search bar on displays less than 1600px wide */
@media (max-width: 1599px) {
#navbar_search {
display: none;
}
}
/* Forms */
label {
font-weight: normal;

View File

@ -16,7 +16,7 @@ $(document).ready(function() {
// Adding/editing a secret
$('form').submit(function(event) {
$(this).find('input.requires-session-key').each(function() {
$(this).find('.requires-session-key').each(function() {
if (this.value && document.cookie.indexOf('session_key') == -1) {
console.log('Field ' + this.value + ' requires a session key');
$('#privkey_modal').modal('show');

View File

@ -246,8 +246,8 @@
<ul class="nav navbar-nav navbar-right">
{% if request.user.is_authenticated %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
{{ request.user }} <span class="caret"></span>
<a href="#" class="dropdown-toggle" data-toggle="dropdown" title="{{ request.user }}" role="button" aria-haspopup="true" aria-expanded="false">
{{ request.user|truncatechars:"30" }} <span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li><a href="{% url 'user:profile' %}"><i class="fa fa-user" aria-hidden="true"></i> Profile</a></li>
@ -262,7 +262,7 @@
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in" aria-hidden="true"></i> Log in</a></li>
{% endif %}
</ul>
<form action="{% url 'search' %}" method="get" class="navbar-form navbar-right" role="search">
<form action="{% url 'search' %}" method="get" class="navbar-form navbar-right" id="navbar_search" role="search">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Search">
<span class="input-group-btn">

View File

@ -1,6 +1,4 @@
{% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}Circuit Import{% endblock %}

View File

@ -1,6 +1,4 @@
{% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}Provider Import{% endblock %}

View File

@ -1,6 +1,4 @@
{% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}Console Connections Import{% endblock %}

View File

@ -1,5 +1,4 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% block title %}Console Connections{% endblock %}
@ -16,7 +15,7 @@
<h1>Console Connections</h1>
<div class="row">
<div class="col-md-9">
{% render_table table 'table.html' %}
{% include 'responsive_table.html' %}
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}

View File

@ -1,6 +1,5 @@
{% extends '_base.html' %}
{% load static from staticfiles %}
{% load render_table from django_tables2 %}
{% load helpers %}
{% block title %}{{ device }}{% endblock %}

View File

@ -1,5 +1,4 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}Device Import{% endblock %}

View File

@ -1,5 +1,4 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}Device Import{% endblock %}

View File

@ -76,6 +76,7 @@ $(document).ready(function() {
// Update rack options
rack_list.empty();
rack_list.append($("<option></option>").attr("value", "0").text("None"));
$.ajax({
url: netbox_api_path + 'dcim/racks/?limit=500&site=' + selected_sites.join('&site='),
dataType: 'json',

View File

@ -1,6 +1,5 @@
{% extends '_base.html' %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}

View File

@ -1,4 +1,3 @@
{% load render_table from django_tables2 %}
{% if perms.dcim.change_devicetype %}
<form method="post">
{% csrf_token %}
@ -19,7 +18,7 @@
{% endif %}
</div>
</div>
{% render_table table 'table.html' %}
{% include 'responsive_table.html' %}
<div class="panel-footer">
{% if table.rows %}
{% if edit_url %}
@ -48,6 +47,6 @@
<div class="panel-heading">
<strong>{{ title }}</strong>
</div>
{% render_table table 'table.html' %}
{% include 'responsive_table.html' %}
</div>
{% endif %}

View File

@ -1,6 +1,4 @@
{% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}Interface Connections Import{% endblock %}

View File

@ -1,5 +1,4 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% block title %}Interface Connections{% endblock %}
@ -16,7 +15,7 @@
<h1>Interface Connections</h1>
<div class="row">
<div class="col-md-9">
{% render_table table 'table.html' %}
{% include 'responsive_table.html' %}
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}

View File

@ -1,6 +1,4 @@
{% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}Power Connections Import{% endblock %}

View File

@ -1,5 +1,4 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% block title %}Power Connections{% endblock %}
@ -16,7 +15,7 @@
<h1>Power Connections</h1>
<div class="row">
<div class="col-md-9">
{% render_table table 'table.html' %}
{% include 'responsive_table.html' %}
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}

View File

@ -1,6 +1,5 @@
{% extends '_base.html' %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% block title %}{{ rack.site }} - Rack {{ rack.name }}{% endblock %}

View File

@ -11,24 +11,25 @@
{% if page %}
<div class="col-md-9">
<div style="white-space: nowrap; overflow-x: scroll;">
{% for rack in page %}
<div style="display: inline-block; width: 266px">
<div class="rack_header">
<h4><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></h4>
{% for rack in page %}
<div style="display: inline-block; width: 266px">
<div class="rack_header">
<h4><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></h4>
</div>
{% if face_id %}
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 %}
{% else %}
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_front_elevation secondary_face=rack.get_rear_elevation face_id=0 %}
{% endif %}
<div class="clearfix"></div>
<div class="rack_header">
<h4><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></h4>
</div>
</div>
{% if face_id %}
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 %}
{% else %}
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_front_elevation secondary_face=rack.get_rear_elevation face_id=0 %}
{% endif %}
<div class="clearfix"></div>
<div class="rack_header">
<h4><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></h4>
</div>
</div>
{% endfor %}
{% endfor %}
</div>
{% include 'paginator.html' %}
<br />
{% include 'inc/paginator.html' %}
</div>
{% else %}
<div class="col-md-9">

View File

@ -1,6 +1,4 @@
{% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}Rack Import{% endblock %}

View File

@ -1,6 +1,5 @@
{% extends '_base.html' %}
{% load static from staticfiles %}
{% load render_table from django_tables2 %}
{% load helpers %}
{% block title %}{{ site }}{% endblock %}

View File

@ -1,5 +1,4 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}Site Import{% endblock %}

View File

@ -1,5 +1,4 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% block content %}
{% include 'search_form.html' %}

View File

@ -1,9 +1,8 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% block content %}
<h1>{% block title %}Import Completed{% endblock %}</h1>
{% render_table table %}
{% include 'responsive_table.html' %}
<a href="{{ request.path }}" class="btn btn-primary">
<span class="fa fa-download" aria-hidden="true"></span>
Import more

View File

@ -1,11 +1,11 @@
{% load helpers %}
<div class="paginator pull-right" style="margin-top: 20px">
<div class="paginator pull-right">
{% if paginator.num_pages > 1 %}
<nav>
<ul class="pagination pull-right">
{% if page.has_previous %}
<li><a href="{% querystring request page=page.previous_page_number %}">&laquo;</a></li>
<li><a href="{% querystring request page=page.previous_page_number %}"><i class="fa fa-angle-double-left"></i></a></li>
{% endif %}
{% for p in page.smart_pages %}
{% if p %}
@ -15,13 +15,14 @@
{% endif %}
{% endfor %}
{% if page.has_next %}
<li><a href="{% querystring request page=page.next_page_number %}">&raquo;</a></li>
<li><a href="{% querystring request page=page.next_page_number %}"><i class="fa fa-angle-double-right"></i></a></li>
{% endif %}
</ul>
</nav>
{% endif %}
<div class="clearfix"></div>
<div class="text-right text-muted">
Showing {{ page.start_index }}-{{ page.end_index }} of {{ total_count }}
</div>
{% if page %}
<div class="text-right text-muted">
Showing {{ page.start_index }}-{{ page.end_index }} of {{ page.paginator.count }}
</div>
{% endif %}
</div>

View File

@ -0,0 +1,41 @@
{% load django_tables2 %}
<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
{% if table.show_header %}
<thead>
<tr>
{% for column in table.columns %}
{% if column.orderable %}
<th {{ column.attrs.th.as_html }}><a href="{% querystring page=column.order_by_alias.next %}">{{ column.header }}</a></th>
{% else %}
<th {{ column.attrs.th.as_html }}>{{ column.header }}</th>
{% endif %}
{% endfor %}
</tr>
</thead>
{% endif %}
<tbody>
{% for row in table.page.object_list|default:table.rows %}
<tr {{ row.attrs.as_html }}>
{% for column, cell in row.items %}
<td {{ column.attrs.td.as_html }}>{{ cell }}</td>
{% endfor %}
</tr>
{% empty %}
{% if table.empty_text %}
<tr>
<td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
{% if table.has_footer %}
<tfoot>
<tr>
{% for column in table.columns %}
<td>{{ column.footer }}</td>
{% endfor %}
</tr>
</tfoot>
{% endif %}
</table>

View File

@ -1,5 +1,4 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% block title %}Aggregate: {{ aggregate }}{% endblock %}

View File

@ -1,6 +1,4 @@
{% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}Aggregate Import{% endblock %}

View File

@ -1,5 +1,4 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% block title %}{{ ipaddress }}{% endblock %}
@ -133,17 +132,11 @@
{% endwith %}
</div>
<div class="col-md-6">
{% with heading='Parent Prefixes' %}
{% render_table parent_prefixes_table 'panel_table.html' %}
{% endwith %}
{% include 'panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
{% if duplicate_ips_table.rows %}
{% with heading='Duplicate IP Addresses' panel_class='danger' %}
{% render_table duplicate_ips_table 'panel_table.html' %}
{% endwith %}
{% include 'panel_table.html' with table=duplicate_ips_table heading='Duplicate IP Addresses' panel_class='danger' %}
{% endif %}
{% with heading='Related IP Addresses' %}
{% render_table related_ips_table 'panel_table.html' %}
{% endwith %}
{% include 'panel_table.html' with table=related_ips_table heading='Related IP Addresses' %}
</div>
</div>
{% endblock %}

View File

@ -1,6 +1,4 @@
{% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}IP Address Import{% endblock %}

View File

@ -1,5 +1,4 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% block title %}{{ prefix }}{% endblock %}
@ -134,13 +133,9 @@
</div>
<div class="col-md-7">
{% if duplicate_prefix_table.rows %}
{% with heading='Duplicate Prefixes' panel_class='danger' %}
{% render_table duplicate_prefix_table 'panel_table.html' %}
{% endwith %}
{% include 'panel_table.html' with table=duplicate_prefix_table heading='Duplicate Prefixes' panel_class='danger' %}
{% endif %}
{% with heading='Parent Prefixes' %}
{% render_table parent_prefix_table 'panel_table.html' %}
{% endwith %}
{% include 'panel_table.html' with table=parent_prefix_table heading='Parent Prefixes' %}
</div>
</div>
<div class="row">

View File

@ -1,6 +1,4 @@
{% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}Prefix Import{% endblock %}

View File

@ -1,5 +1,4 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% block title %}{{ prefix }}{% endblock %}

View File

@ -1,5 +1,4 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% block title %}VLAN {{ vlan.display_name }}{% endblock %}
@ -136,7 +135,7 @@
<div class="panel-heading">
<strong>Prefixes</strong>
</div>
{% render_table prefix_table %}
{% include 'responsive_table.html' with table=prefix_table %}
{% if perms.ipam.add_prefix %}
<div class="panel-footer text-right">
<a href="{% url 'ipam:prefix_add' %}?{% if vlan.tenant %}tenant={{ vlan.tenant.pk }}&{% endif %}site={{ vlan.site.pk }}&vlan={{ vlan.pk }}" class="btn btn-primary btn-xs">

View File

@ -5,11 +5,11 @@
<div class="panel panel-default">
<div class="panel-heading"><strong>VLAN</strong></div>
<div class="panel-body">
{% render_field form.site %}
{% render_field form.group %}
{% render_field form.vid %}
{% render_field form.name %}
{% render_field form.status %}
{% render_field form.site %}
{% render_field form.group %}
{% render_field form.role %}
{% render_field form.description %}
</div>

View File

@ -1,6 +1,4 @@
{% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}VLAN Import{% endblock %}
@ -17,7 +15,7 @@
<tbody>
<tr>
<td>Site</td>
<td>Name of assigned site</td>
<td>Name of assigned site (optional)</td>
<td>LAS2</td>
</tr>
<tr>

View File

@ -1,5 +1,4 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% block title %}VRF {{ vrf }}{% endblock %}
@ -92,7 +91,7 @@
<div class="panel-heading">
<strong>Prefixes</strong>
</div>
{% render_table prefix_table %}
{% include 'responsive_table.html' with table=prefix_table %}
</div>
</div>
</div>

View File

@ -1,6 +1,4 @@
{% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}VRF Import{% endblock %}

View File

@ -1,10 +1,5 @@
{% extends 'django_tables2/table.html' %}
{% load django_tables2 %}
{% load i18n %}
{% load render_table from django_tables2 %}
{# Wraps a table inside a Bootstrap panel and includes custom pagination rendering #}
{% block table %}
<div class="panel panel-{{ panel_class|default:'default' }}">
{% if heading %}
<div class="panel-heading">
@ -12,15 +7,14 @@
</div>
{% endif %}
{% if table.rows %}
{{ block.super }}
{% render_table table 'inc/table.html' %}
{% else %}
<div class="panel-body text-muted">None</div>
{% endif %}
</div>
{% endblock %}
{% block pagination %}
{% if not hide_paginator %}
{% include 'table_paginator.html' %}
{% endif %}
{% endblock pagination %}
{% if table.rows and not hide_paginator %}
{% with paginator=table.paginator page=table.page %}
{% include 'inc/paginator.html' %}
{% endwith %}
{% endif %}

View File

@ -0,0 +1,8 @@
{% load render_table from django_tables2 %}
<div class="table-responsive">
{% render_table table 'inc/table.html' %}
</div>
{% with paginator=table.paginator page=table.page %}
{% include 'inc/paginator.html' %}
{% endwith %}

View File

@ -1,6 +1,5 @@
{% extends '_base.html' %}
{% load static from staticfiles %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}Secret Import{% endblock %}

View File

@ -1,10 +0,0 @@
{% extends 'django_tables2/bootstrap-responsive.html' %}
{% load django_tables2 %}
{# Extends the stock django_tables2 template to provide custom formatting of the pagination controls #}
{% block pagination %}
{% if not hide_paginator %}
{% include 'table_paginator.html' %}
{% endif %}
{% endblock pagination %}

View File

@ -1,36 +0,0 @@
{% load django_tables2 %}
{# Custom pagination controls to render nicely with Bootstrap CSS. smart_pages requires EnhancedPaginator. #}
<div class="paginator pull-right">
{% if table.paginator.num_pages > 1 %}
<nav>
<ul class="pagination pull-right">
{% if table.page.has_previous %}
<li><a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}"><i class="fa fa-angle-double-left"></i></a></li>
{% endif %}
{% for p in table.page.smart_pages %}
{% if p %}
<li{% ifequal table.page.number p %} class="active"{% endifequal %}><a href="{% querystring table.prefixed_page_field=p %}">{{ p }}</a></li>
{% else %}
<li class="disabled"><span>&hellip;</span></li>
{% endif %}
{% endfor %}
{% if table.page.has_next %}
<li><a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}"><i class="fa fa-angle-double-right"></i></a></li>
{% endif %}
</ul>
</nav>
{% endif %}
<div class="clearfix"></div>
<div class="text-right text-muted">
{% with table.page.paginator.count as total %}
Showing {{ table.page.start_index }}-{{ table.page.end_index }} of {{ total }}
{% if total == 1 %}
{{ table.data.verbose_name }}
{% else %}
{{ table.data.verbose_name_plural }}
{% endif %}
{% endwith %}
</div>
</div>

View File

@ -1,6 +1,4 @@
{% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block title %}Tenant Import{% endblock %}

View File

@ -1,5 +1,4 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block content %}

View File

@ -1,4 +1,3 @@
{% load render_table from django_tables2 %}
{% load helpers %}
{% if permissions.change or permissions.delete %}
<form method="post" class="form form-horizontal">
@ -15,12 +14,12 @@
</div>
<div class="pull-right">
{% if bulk_edit_url and permissions.change %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled="disabled">
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if bulk_querystring %}?{{ bulk_querystring }}{% elif request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled="disabled">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit All
</button>
{% endif %}
{% if bulk_delete_url and permissions.delete %}
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled="disabled">
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if bulk_querystring %}?{{ bulk_querystring }}{% elif request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled="disabled">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete All
</button>
{% endif %}
@ -28,7 +27,7 @@
</div>
</div>
{% endif %}
{% render_table table table_template|default:'table.html' %}
{% include table_template|default:'responsive_table.html' %}
{% block extra_actions %}{% endblock %}
{% if bulk_edit_url and permissions.change %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
@ -42,6 +41,6 @@
{% endif %}
</form>
{% else %}
{% render_table table table_template|default:'table.html' %}
{% include table_template|default:'responsive_table.html' %}
{% endif %}
<div class="clearfix"></div>

View File

@ -443,12 +443,10 @@ class ChainedFieldsMixin(forms.BaseForm):
filters_dict = {}
for db_field, parent_field in field.chains.items():
if self.is_bound:
filters_dict[db_field] = self.data.get(parent_field) or None
if self.is_bound and self.data.get(parent_field):
filters_dict[db_field] = self.data[parent_field]
elif self.initial.get(parent_field):
filters_dict[db_field] = self.initial[parent_field]
else:
filters_dict[db_field] = None
if filters_dict:
field.queryset = field.queryset.filter(**filters_dict)

View File

@ -4,7 +4,6 @@ from django_tables2 import RequestConfig
from django.conf import settings
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import transaction, IntegrityError
from django.db.models import ProtectedError
from django.forms import CharField, ModelMultipleChoiceField, MultipleHiddenInput, TypedChoiceField