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) * [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)) * [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: Python 3:
```no-highlight ```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 # update-alternatives --install /usr/bin/python python /usr/bin/python3 1
``` ```
Python 2: Python 2:
```no-highlight ```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** **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 Alias /static /opt/netbox/netbox/static
# Needed to allow token-based API authentication
WSGIPassAuthorization on
<Directory /opt/netbox/netbox/static> <Directory /opt/netbox/netbox/static>
Options Indexes FollowSymLinks MultiViews Options Indexes FollowSymLinks MultiViews
AllowOverride None AllowOverride None

View File

@ -581,9 +581,18 @@ class WritablePowerPortSerializer(serializers.ModelSerializer):
# Interfaces # 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): class InterfaceSerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES) form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
lag = NestedInterfaceSerializer()
connection = serializers.SerializerMethodField(read_only=True) connection = serializers.SerializerMethodField(read_only=True)
connected_interface = serializers.SerializerMethodField(read_only=True) connected_interface = serializers.SerializerMethodField(read_only=True)

View File

@ -477,6 +477,11 @@ class InterfaceFilter(DeviceComponentFilterSet):
method='filter_type', method='filter_type',
label='Interface type', label='Interface type',
) )
lag_id = django_filters.ModelMultipleChoiceFilter(
name='lag',
queryset=Interface.objects.all(),
label='LAG interface (ID)',
)
mac_address = django_filters.CharFilter( mac_address = django_filters.CharFilter(
method='_mac_address', method='_mac_address',
label='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', queryset=Platform.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid platform.'} 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: class Meta:
fields = [] fields = []
@ -692,8 +692,12 @@ class BaseDeviceFromCSVForm(forms.ModelForm):
except DeviceType.DoesNotExist: except DeviceType.DoesNotExist:
self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name)) self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
def clean_status_name(self): def clean_status(self):
return dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']] 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): class DeviceFromCSVForm(BaseDeviceFromCSVForm):
@ -707,8 +711,8 @@ class DeviceFromCSVForm(BaseDeviceFromCSVForm):
class Meta(BaseDeviceFromCSVForm.Meta): class Meta(BaseDeviceFromCSVForm.Meta):
fields = [ fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
'status_name', 'site', 'rack_name', 'position', 'face', 'site', 'rack_name', 'position', 'face',
] ]
def clean(self): def clean(self):
@ -751,8 +755,8 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
class Meta(BaseDeviceFromCSVForm.Meta): class Meta(BaseDeviceFromCSVForm.Meta):
fields = [ fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
'status_name', 'parent', 'device_bay_name', 'parent', 'device_bay_name',
] ]
def clean(self): def clean(self):
@ -817,13 +821,15 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
rack_id = FilterChoiceField( rack_id = FilterChoiceField(
queryset=Rack.objects.annotate(filter_count=Count('devices')), queryset=Rack.objects.annotate(filter_count=Count('devices')),
label='Rack', label='Rack',
null_option=(0, 'None'),
) )
role = FilterChoiceField( role = FilterChoiceField(
queryset=DeviceRole.objects.annotate(filter_count=Count('devices')), queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
to_field_name='slug', to_field_name='slug',
) )
tenant = FilterChoiceField( 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'), null_option=(0, 'None'),
) )
manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer') manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
@ -1207,7 +1213,7 @@ class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
) )
power_outlet = ChainedModelChoiceField( power_outlet = ChainedModelChoiceField(
queryset=PowerOutlet.objects.all(), queryset=PowerOutlet.objects.all(),
chains={'device': 'device'}, chains={'device': 'pdu'},
label='Outlet', label='Outlet',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/power-outlets/?device_id={{pdu}}', api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
@ -1441,7 +1447,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
label='Interface', label='Interface',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical', 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 import forms
from django.core.exceptions import ValidationError
from django.db.models import Count from django.db.models import Count
from dcim.models import Site, Rack, Device, Interface from dcim.models import Site, Rack, Device, Interface
@ -195,14 +196,16 @@ class PrefixFromCSVForm(forms.ModelForm):
error_messages={'invalid_choice': 'Site not found.'}) error_messages={'invalid_choice': 'Site not found.'})
vlan_group_name = forms.CharField(required=False) vlan_group_name = forms.CharField(required=False)
vlan_vid = forms.IntegerField(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', role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid role.'}) error_messages={'invalid_choice': 'Invalid role.'})
class Meta: class Meta:
model = Prefix model = Prefix
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role', 'is_pool', fields = [
'description'] 'prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status', 'role', 'is_pool',
'description',
]
def clean(self): def clean(self):
@ -237,12 +240,12 @@ class PrefixFromCSVForm(forms.ModelForm):
except VLAN.MultipleObjectsReturned: except VLAN.MultipleObjectsReturned:
self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid)) self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
def save(self, *args, **kwargs): def clean_status(self):
status_choices = {s[1].lower(): s[0] for s in PREFIX_STATUS_CHOICES}
# Assign Prefix status by name try:
self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']] return status_choices[self.cleaned_data['status'].lower()]
except KeyError:
return super(PrefixFromCSVForm, self).save(*args, **kwargs) raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
class PrefixImportForm(BootstrapMixin, BulkImportForm): class PrefixImportForm(BootstrapMixin, BulkImportForm):
@ -491,7 +494,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
error_messages={'invalid_choice': 'VRF not found.'}) error_messages={'invalid_choice': 'VRF not found.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False, tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'}) 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', device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Device not found.'}) error_messages={'invalid_choice': 'Device not found.'})
interface_name = forms.CharField(required=False) interface_name = forms.CharField(required=False)
@ -499,7 +502,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
class Meta: class Meta:
model = IPAddress 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): def clean(self):
@ -522,10 +525,14 @@ class IPAddressFromCSVForm(forms.ModelForm):
if is_primary and not device: if is_primary and not device:
self.add_error('is_primary', "No device specified; cannot set as primary IP") 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 def save(self, *args, **kwargs):
self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
# Set interface # Set interface
if self.cleaned_data['device'] and self.cleaned_data['interface_name']: 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): class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
site = forms.ModelChoiceField( site = forms.ModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False,
widget=forms.Select( widget=forms.Select(
attrs={'filter-for': 'group', 'nullable': 'true'} attrs={'filter-for': 'group', 'nullable': 'true'}
) )
@ -649,7 +657,7 @@ class VLANFromCSVForm(forms.ModelForm):
Tenant.objects.all(), to_field_name='name', required=False, Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'} 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( role = forms.ModelChoiceField(
queryset=Role.objects.all(), required=False, to_field_name='name', queryset=Role.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid role.'} error_messages={'invalid_choice': 'Invalid role.'}
@ -657,7 +665,7 @@ class VLANFromCSVForm(forms.ModelForm):
class Meta: class Meta:
model = VLAN 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): def clean(self):
@ -671,6 +679,13 @@ class VLANFromCSVForm(forms.ModelForm):
except VLANGroup.DoesNotExist: except VLANGroup.DoesNotExist:
self.add_error('group_name', "Invalid VLAN group {}.".format(group_name)) 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): def save(self, *args, **kwargs):
vlan = super(VLANFromCSVForm, self).save(commit=False) vlan = super(VLANFromCSVForm, self).save(commit=False)
@ -679,9 +694,6 @@ class VLANFromCSVForm(forms.ModelForm):
if self.cleaned_data['group_name']: if self.cleaned_data['group_name']:
vlan.group = VLANGroup.objects.get(site=self.cleaned_data['site'], name=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'): if kwargs.get('commit'):
vlan.save() vlan.save()
return vlan return vlan

View File

@ -70,9 +70,9 @@ IPADDRESS_LINK = """
{% if record.pk %} {% if record.pk %}
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a> <a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
{% elif perms.ipam.add_ipaddress %} {% 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 %} {% else %}
{{ record.0 }} {% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
{% endif %} {% endif %}
""" """

View File

@ -525,6 +525,7 @@ def prefix_ipaddresses(request, pk):
'prefix': prefix, 'prefix': prefix,
'ip_table': ip_table, 'ip_table': ip_table,
'permissions': permissions, '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 # Import local configuration
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None ALLOWED_HOSTS = DATABASE = SECRET_KEY = None

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,24 +11,25 @@
{% if page %} {% if page %}
<div class="col-md-9"> <div class="col-md-9">
<div style="white-space: nowrap; overflow-x: scroll;"> <div style="white-space: nowrap; overflow-x: scroll;">
{% for rack in page %} {% for rack in page %}
<div style="display: inline-block; width: 266px"> <div style="display: inline-block; width: 266px">
<div class="rack_header"> <div class="rack_header">
<h4><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></h4> <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> </div>
{% if face_id %} {% endfor %}
{% 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 %}
</div> </div>
{% include 'paginator.html' %} <br />
{% include 'inc/paginator.html' %}
</div> </div>
{% else %} {% else %}
<div class="col-md-9"> <div class="col-md-9">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
{% load helpers %} {% load helpers %}
<div class="paginator pull-right" style="margin-top: 20px"> <div class="paginator pull-right">
{% if paginator.num_pages > 1 %} {% if paginator.num_pages > 1 %}
<nav> <nav>
<ul class="pagination pull-right"> <ul class="pagination pull-right">
{% if page.has_previous %} {% 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 %} {% endif %}
{% for p in page.smart_pages %} {% for p in page.smart_pages %}
{% if p %} {% if p %}
@ -15,13 +15,14 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if page.has_next %} {% 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 %} {% endif %}
</ul> </ul>
</nav> </nav>
{% endif %} {% endif %}
<div class="clearfix"></div> {% if page %}
<div class="text-right text-muted"> <div class="text-right text-muted">
Showing {{ page.start_index }}-{{ page.end_index }} of {{ total_count }} Showing {{ page.start_index }}-{{ page.end_index }} of {{ page.paginator.count }}
</div> </div>
{% endif %}
</div> </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' %} {% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% block title %}Aggregate: {{ aggregate }}{% endblock %} {% block title %}Aggregate: {{ aggregate }}{% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% block title %}VLAN {{ vlan.display_name }}{% endblock %} {% block title %}VLAN {{ vlan.display_name }}{% endblock %}
@ -136,7 +135,7 @@
<div class="panel-heading"> <div class="panel-heading">
<strong>Prefixes</strong> <strong>Prefixes</strong>
</div> </div>
{% render_table prefix_table %} {% include 'responsive_table.html' with table=prefix_table %}
{% if perms.ipam.add_prefix %} {% if perms.ipam.add_prefix %}
<div class="panel-footer text-right"> <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"> <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 panel-default">
<div class="panel-heading"><strong>VLAN</strong></div> <div class="panel-heading"><strong>VLAN</strong></div>
<div class="panel-body"> <div class="panel-body">
{% render_field form.site %}
{% render_field form.group %}
{% render_field form.vid %} {% render_field form.vid %}
{% render_field form.name %} {% render_field form.name %}
{% render_field form.status %} {% render_field form.status %}
{% render_field form.site %}
{% render_field form.group %}
{% render_field form.role %} {% render_field form.role %}
{% render_field form.description %} {% render_field form.description %}
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
{% load render_table from django_tables2 %}
{% load helpers %} {% load helpers %}
{% if permissions.change or permissions.delete %} {% if permissions.change or permissions.delete %}
<form method="post" class="form form-horizontal"> <form method="post" class="form form-horizontal">
@ -15,12 +14,12 @@
</div> </div>
<div class="pull-right"> <div class="pull-right">
{% if bulk_edit_url and permissions.change %} {% 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 <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit All
</button> </button>
{% endif %} {% endif %}
{% if bulk_delete_url and permissions.delete %} {% 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 <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete All
</button> </button>
{% endif %} {% endif %}
@ -28,7 +27,7 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% render_table table table_template|default:'table.html' %} {% include table_template|default:'responsive_table.html' %}
{% block extra_actions %}{% endblock %} {% block extra_actions %}{% endblock %}
{% if bulk_edit_url and permissions.change %} {% 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"> <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 %} {% endif %}
</form> </form>
{% else %} {% else %}
{% render_table table table_template|default:'table.html' %} {% include table_template|default:'responsive_table.html' %}
{% endif %} {% endif %}
<div class="clearfix"></div> <div class="clearfix"></div>

View File

@ -443,12 +443,10 @@ class ChainedFieldsMixin(forms.BaseForm):
filters_dict = {} filters_dict = {}
for db_field, parent_field in field.chains.items(): for db_field, parent_field in field.chains.items():
if self.is_bound: if self.is_bound and self.data.get(parent_field):
filters_dict[db_field] = self.data.get(parent_field) or None filters_dict[db_field] = self.data[parent_field]
elif self.initial.get(parent_field): elif self.initial.get(parent_field):
filters_dict[db_field] = self.initial[parent_field] filters_dict[db_field] = self.initial[parent_field]
else:
filters_dict[db_field] = None
if filters_dict: if filters_dict:
field.queryset = field.queryset.filter(**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.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import transaction, IntegrityError from django.db import transaction, IntegrityError
from django.db.models import ProtectedError from django.db.models import ProtectedError
from django.forms import CharField, ModelMultipleChoiceField, MultipleHiddenInput, TypedChoiceField from django.forms import CharField, ModelMultipleChoiceField, MultipleHiddenInput, TypedChoiceField