Merge remote-tracking branch 'upstream/develop' into multipoint_circuits

This commit is contained in:
Nathan Gotz 2016-09-14 16:28:09 -05:00
commit bbc428359d
18 changed files with 254 additions and 422 deletions

View File

@ -1,4 +1,4 @@
This section entails features of NetBox which are not crucial to its primary functions, but that provide additional value. This section entails features of NetBox which are not crucial to its primary functions, but provide additional value.
# Custom Fields # Custom Fields
@ -17,15 +17,15 @@ Custom fields must be created through the admin UI under Extras > Custom Fields.
Assign the field a name. This should be a simple database-friendly string, e.g. `tps_report`. You may optionally assign the field a human-friendly label (e.g. "TPS report") as well; the label will be displayed on forms. If a description is provided, it will appear beneath the field in a form. Assign the field a name. This should be a simple database-friendly string, e.g. `tps_report`. You may optionally assign the field a human-friendly label (e.g. "TPS report") as well; the label will be displayed on forms. If a description is provided, it will appear beneath the field in a form.
Marking the field as required will force the user to provide a value for the field when creating a new object or when saving an existing object. A default value for the field may also be provided. Use "true" or "false" for boolean fields. (The default value has no effect for selection fields.) Marking the field as required will require the user to provide a value for the field when creating a new object or when saving an existing object. A default value for the field may also be provided. Use "true" or "false" for boolean fields. (The default value has no effect for selection fields.)
When creating a selection field, you must create at least two choices. These choices will be arranged first by weight, with lower weights appearing higher in the list, and then alphabetically. When creating a selection field, you should create at least two choices. These choices will be arranged first by weight, with lower weights appearing higher in the list, and then alphabetically.
## Using Custom Fields ## Using Custom Fields
When a single object is edited, the form will include any custom fields which have been defined for its type. These fields are included in the "Custom Fields" panel. Each custom field value must be saved independently from the core object, so it's best to avoid adding too many custom fields per object. When a single object is edited, the form will include any custom fields which have been defined for the object type. These fields are included in the "Custom Fields" panel. On the backend, each custom field value is saved separately from the core object as an independent database call, so it's best to avoid adding too many custom fields per object.
When editing multiple objects, values are saved in bulk per field. That is, there is no significant difference in overhead when saving a custom field value for 100 objects versus one object. However, the bulk operation must be performed separately for each custom field. When editing multiple objects, custom field values are saved in bulk. There is no significant difference in overhead when saving a custom field value for 100 objects versus one object. However, the bulk operation must be performed separately for each custom field.
# Export Templates # Export Templates

View File

@ -6,7 +6,8 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
from tenancy.forms import bulkedit_tenant_choices from tenancy.forms import bulkedit_tenant_choices
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, Livesearch, SmallTextarea, SlugField, APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, FilterChoiceField, Livesearch, SmallTextarea,
SlugField, get_filter_choices,
) )
from .models import Circuit, CircuitType, Provider, Termination from .models import Circuit, CircuitType, Provider, Termination
@ -64,8 +65,7 @@ def provider_site_choices():
class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm): class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Provider model = Provider
site = forms.MultipleChoiceField(required=False, choices=provider_site_choices, site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug'))
widget=forms.SelectMultiple(attrs={'size': 8}))
# #
@ -85,6 +85,19 @@ class CircuitTypeForm(forms.ModelForm, BootstrapMixin):
# #
class CircuitForm(BootstrapMixin, CustomFieldForm): class CircuitForm(BootstrapMixin, CustomFieldForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, label='Rack',
widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}',
attrs={'filter-for': 'device'}))
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device',
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
attrs={'filter-for': 'interface'}))
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
)
interface = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Interface',
widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
disabled_indicator='is_connected'))
comments = CommentField() comments = CommentField()
class Meta: class Meta:
@ -95,126 +108,15 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
help_texts = { help_texts = {
'cid': "Unique circuit ID", 'cid': "Unique circuit ID",
'install_date': "Format: YYYY-MM-DD", 'install_date': "Format: YYYY-MM-DD",
}
def __init__(self, *args, **kwargs):
super(CircuitForm, self).__init__(*args, **kwargs)
class CircuitFromCSVForm(forms.ModelForm):
provider = forms.ModelChoiceField(Provider.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Provider not found.'})
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Invalid circuit type.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
class Meta:
model = Circuit
fields = ['cid', 'provider', 'type', 'tenant', 'install_date']
class CircuitImportForm(BulkImportForm, BootstrapMixin):
csv = CSVDataField(csv_form=CircuitFromCSVForm)
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
comments = CommentField()
def circuit_type_choices():
type_choices = CircuitType.objects.annotate(circuit_count=Count('circuits'))
return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in type_choices]
def circuit_provider_choices():
provider_choices = Provider.objects.annotate(circuit_count=Count('circuits'))
return [(p.slug, u'{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices]
def circuit_tenant_choices():
tenant_choices = Tenant.objects.annotate(circuit_count=Count('circuits'))
return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in tenant_choices]
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Circuit
type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices)
provider = forms.MultipleChoiceField(required=False, choices=circuit_provider_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
tenant = forms.MultipleChoiceField(required=False, choices=circuit_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
#
# Terminations
#
class TerminationForm(BootstrapMixin, CustomFieldForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
rack = forms.ModelChoiceField(
queryset=Rack.objects.all(),
required=False,
label='Rack',
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{site}}',
attrs={'filter-for': 'device'}
)
)
device = forms.ModelChoiceField(
queryset=Device.objects.all(),
required=False,
label='Device',
widget=APISelect(
api_url='/api/dcim/devices/?rack_id={{rack}}',
attrs={'filter-for': 'interface'}
)
)
livesearch = forms.CharField(
required=False,
label='Device',
widget=Livesearch(
query_key='q',
query_url='dcim-api:device_list',
field_to_update='device'
)
)
interface = forms.ModelChoiceField(
queryset=Interface.objects.all(),
required=False,
label='Interface',
widget=APISelect(
api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
disabled_indicator='is_connected'
)
)
comments = CommentField()
class Meta:
model = Termination
fields = [
'tid', 'site', 'rack', 'device', 'livesearch',
'interface', 'port_speed', 'upstream_speed', 'commit_rate',
'xconnect_id', 'pp_info', 'comments'
]
help_texts = {
'tid': "Termination ID",
'port_speed': "Physical circuit speed", 'port_speed': "Physical circuit speed",
'commit_rate': "Commited rate", 'commit_rate': "Commited rate",
'xconnect_id': "ID of the local cross-connect", 'xconnect_id': "ID of the local cross-connect",
'pp_info': "Patch panel ID and port number(s)" 'pp_info': "Patch panel ID and port number(s)"
} }
widgets = {
'circuit': forms.HiddenInput(),
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(TerminationForm, self).__init__(*args, **kwargs) super(CircuitForm, self).__init__(*args, **kwargs)
# If this circuit has been assigned to an interface, initialize rack and device # If this circuit has been assigned to an interface, initialize rack and device
if self.instance.interface: if self.instance.interface:
@ -240,11 +142,11 @@ class TerminationForm(BootstrapMixin, CustomFieldForm):
# Limit interface choices # Limit interface choices
if self.is_bound and self.data.get('device'): if self.is_bound and self.data.get('device'):
interfaces = Interface.objects.filter(device=self.data['device'])\ interfaces = Interface.objects.filter(device=self.data['device'])\
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b') .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b')
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface') self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
elif self.initial.get('device'): elif self.initial.get('device'):
interfaces = Interface.objects.filter(device=self.initial['device'])\ interfaces = Interface.objects.filter(device=self.initial['device'])\
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b') .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b')
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface') self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
else: else:
interfaces = [] interfaces = []
@ -254,3 +156,40 @@ class TerminationForm(BootstrapMixin, CustomFieldForm):
'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'), 'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'),
}) for iface in interfaces }) for iface in interfaces
] ]
class CircuitFromCSVForm(forms.ModelForm):
provider = forms.ModelChoiceField(Provider.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Provider not found.'})
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Invalid circuit type.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
site = forms.ModelChoiceField(Site.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Site not found.'})
class Meta:
model = Circuit
fields = ['cid', 'provider', 'type', 'tenant', 'install_date']
class CircuitImportForm(BulkImportForm, BootstrapMixin):
csv = CSVDataField(csv_form=CircuitFromCSVForm)
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
comments = CommentField()
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Circuit
type = FilterChoiceField(choices=get_filter_choices(CircuitType, id_field='slug', count_field='circuits'))
provider = FilterChoiceField(choices=get_filter_choices(Provider, id_field='slug', count_field='circuits'))
tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='circuits'))
site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='circuits'))

View File

@ -1,7 +1,7 @@
import re import re
from django import forms from django import forms
from django.db.models import Count, Q from django.db.models import Q
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from ipam.models import IPAddress from ipam.models import IPAddress
@ -9,7 +9,8 @@ from tenancy.forms import bulkedit_tenant_choices
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
APISelect, add_blank_choice, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField, APISelect, add_blank_choice, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField,
FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
get_filter_choices
) )
from .models import ( from .models import (
@ -117,15 +118,9 @@ class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
def site_tenant_choices():
tenant_choices = Tenant.objects.annotate(site_count=Count('sites'))
return [(t.slug, u'{} ({})'.format(t.name, t.site_count)) for t in tenant_choices]
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Site model = Site
tenant = forms.MultipleChoiceField(required=False, choices=site_tenant_choices, tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='sites'))
widget=forms.SelectMultiple(attrs={'size': 8}))
# #
@ -140,14 +135,8 @@ class RackGroupForm(forms.ModelForm, BootstrapMixin):
fields = ['site', 'name', 'slug'] fields = ['site', 'name', 'slug']
def rackgroup_site_choices():
site_choices = Site.objects.annotate(rack_count=Count('rack_groups'))
return [(s.slug, u'{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
class RackGroupFilterForm(forms.Form, BootstrapMixin): class RackGroupFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=rackgroup_site_choices, site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='rack_groups'))
widget=forms.SelectMultiple(attrs={'size': 8}))
# #
@ -254,36 +243,13 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
comments = CommentField() comments = CommentField()
def rack_site_choices():
site_choices = Site.objects.annotate(rack_count=Count('racks'))
return [(s.slug, u'{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
def rack_group_choices():
group_choices = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
return [(g.pk, u'{} ({})'.format(g, g.rack_count)) for g in group_choices]
def rack_tenant_choices():
tenant_choices = Tenant.objects.annotate(rack_count=Count('racks'))
return [(t.slug, u'{} ({})'.format(t.name, t.rack_count)) for t in tenant_choices]
def rack_role_choices():
role_choices = RackRole.objects.annotate(rack_count=Count('racks'))
return [(r.slug, u'{} ({})'.format(r.name, r.rack_count)) for r in role_choices]
class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Rack model = Rack
site = forms.MultipleChoiceField(required=False, choices=rack_site_choices, site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='racks'))
widget=forms.SelectMultiple(attrs={'size': 8})) group_id = FilterChoiceField(choices=get_filter_choices(RackGroup, select_related=['site'], count_field='racks'),
group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices, label='Rack Group', label='Rack Group')
widget=forms.SelectMultiple(attrs={'size': 8})) tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='racks'))
tenant = forms.MultipleChoiceField(required=False, choices=rack_tenant_choices, role = FilterChoiceField(choices=get_filter_choices(RackRole, id_field='slug', count_field='racks'))
widget=forms.SelectMultiple(attrs={'size': 8}))
role = forms.MultipleChoiceField(required=False, choices=rack_role_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
# #
@ -317,14 +283,9 @@ class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
u_height = forms.IntegerField(min_value=1, required=False) u_height = forms.IntegerField(min_value=1, required=False)
def devicetype_manufacturer_choices():
manufacturer_choices = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
return [(m.slug, u'{} ({})'.format(m.name, m.devicetype_count)) for m in manufacturer_choices]
class DeviceTypeFilterForm(forms.Form, BootstrapMixin): class DeviceTypeFilterForm(forms.Form, BootstrapMixin):
manufacturer = forms.MultipleChoiceField(required=False, choices=devicetype_manufacturer_choices, manufacturer = FilterChoiceField(choices=get_filter_choices(Manufacturer, id_field='slug',
widget=forms.SelectMultiple(attrs={'size': 8})) count_field='device_types'))
# #
@ -627,49 +588,18 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
serial = forms.CharField(max_length=50, required=False, label='Serial Number') serial = forms.CharField(max_length=50, required=False, label='Serial Number')
def device_site_choices():
site_choices = Site.objects.annotate(device_count=Count('racks__devices'))
return [(s.slug, u'{} ({})'.format(s.name, s.device_count)) for s in site_choices]
def device_rack_group_choices():
group_choices = RackGroup.objects.select_related('site').annotate(device_count=Count('racks__devices'))
return [(g.pk, u'{} ({})'.format(g, g.device_count)) for g in group_choices]
def device_role_choices():
role_choices = DeviceRole.objects.annotate(device_count=Count('devices'))
return [(r.slug, u'{} ({})'.format(r.name, r.device_count)) for r in role_choices]
def device_tenant_choices():
tenant_choices = Tenant.objects.annotate(device_count=Count('devices'))
return [(t.slug, u'{} ({})'.format(t.name, t.device_count)) for t in tenant_choices]
def device_type_choices():
type_choices = DeviceType.objects.select_related('manufacturer').annotate(device_count=Count('instances'))
return [(t.pk, u'{} ({})'.format(t, t.device_count)) for t in type_choices]
def device_platform_choices():
platform_choices = Platform.objects.annotate(device_count=Count('devices'))
return [(p.slug, u'{} ({})'.format(p.name, p.device_count)) for p in platform_choices]
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Device model = Device
site = forms.MultipleChoiceField(required=False, choices=device_site_choices, site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='racks__devices'))
widget=forms.SelectMultiple(attrs={'size': 8})) rack_group_id = FilterChoiceField(choices=get_filter_choices(RackGroup, select_related=['site'],
rack_group_id = forms.MultipleChoiceField(required=False, choices=device_rack_group_choices, label='Rack Group', count_field='racks__devices'),
widget=forms.SelectMultiple(attrs={'size': 8})) label='Rack Group')
role = forms.MultipleChoiceField(required=False, choices=device_role_choices, role = FilterChoiceField(choices=get_filter_choices(DeviceRole, id_field='slug', count_field='devices'))
widget=forms.SelectMultiple(attrs={'size': 8})) tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='devices'))
tenant = forms.MultipleChoiceField(required=False, choices=device_tenant_choices, device_type_id = FilterChoiceField(choices=get_filter_choices(DeviceType, select_related=['manufacturer'],
widget=forms.SelectMultiple(attrs={'size': 8})) count_field='instances'),
device_type_id = forms.MultipleChoiceField(required=False, choices=device_type_choices, label='Type', label='Type')
widget=forms.SelectMultiple(attrs={'size': 8})) platform = FilterChoiceField(choices=get_filter_choices(Platform, id_field='slug', count_field='devices'))
platform = forms.MultipleChoiceField(required=False, choices=device_platform_choices)
status = forms.NullBooleanField(required=False, widget=forms.Select(choices=FORM_STATUS_CHOICES)) status = forms.NullBooleanField(required=False, widget=forms.Select(choices=FORM_STATUS_CHOICES))

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-09-13 15:20
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0018_device_add_asset_tag'),
]
operations = [
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
),
]

View File

@ -77,23 +77,39 @@ ROLE_COLOR_CHOICES = [
[COLOR_GRAY3, 'Dark Gray'], [COLOR_GRAY3, 'Dark Gray'],
] ]
# Virtual
IFACE_FF_VIRTUAL = 0 IFACE_FF_VIRTUAL = 0
IFACE_FF_100M_COPPER = 800 # Ethernet
IFACE_FF_1GE_COPPER = 1000 IFACE_FF_100ME_FIXED = 800
IFACE_FF_GBIC = 1050 IFACE_FF_1GE_FIXED = 1000
IFACE_FF_SFP = 1100 IFACE_FF_1GE_GBIC = 1050
IFACE_FF_10GE_COPPER = 1150 IFACE_FF_1GE_SFP = 1100
IFACE_FF_SFP_PLUS = 1200 IFACE_FF_10GE_FIXED = 1150
IFACE_FF_XFP = 1300 IFACE_FF_10GE_SFP_PLUS = 1200
IFACE_FF_QSFP_PLUS = 1400 IFACE_FF_10GE_XFP = 1300
IFACE_FF_CFP = 1500 IFACE_FF_10GE_XENPAK = 1310
IFACE_FF_QSFP28 = 1600 IFACE_FF_10GE_X2 = 1320
IFACE_FF_25GE_SFP28 = 1350
IFACE_FF_40GE_QSFP_PLUS = 1400
IFACE_FF_100GE_CFP = 1500
IFACE_FF_100GE_QSFP28 = 1600
# Fibrechannel
IFACE_FF_1GFC_SFP = 3010
IFACE_FF_2GFC_SFP = 3020
IFACE_FF_4GFC_SFP = 3040
IFACE_FF_8GFC_SFP_PLUS = 3080
IFACE_FF_16GFC_SFP_PLUS = 3160
# Serial
IFACE_FF_T1 = 4000 IFACE_FF_T1 = 4000
IFACE_FF_E1 = 4010 IFACE_FF_E1 = 4010
IFACE_FF_T3 = 4040 IFACE_FF_T3 = 4040
IFACE_FF_E3 = 4050 IFACE_FF_E3 = 4050
# Stacking
IFACE_FF_STACKWISE = 5000 IFACE_FF_STACKWISE = 5000
IFACE_FF_STACKWISE_PLUS = 5050 IFACE_FF_STACKWISE_PLUS = 5050
# Other
IFACE_FF_OTHER = 32767
IFACE_FF_CHOICES = [ IFACE_FF_CHOICES = [
[ [
'Virtual interfaces', 'Virtual interfaces',
@ -102,23 +118,36 @@ IFACE_FF_CHOICES = [
] ]
], ],
[ [
'Ethernet', 'Ethernet (fixed)',
[ [
[IFACE_FF_100M_COPPER, '100BASE-TX (10/100M)'], [IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'],
[IFACE_FF_1GE_COPPER, '1000BASE-T (1GE)'], [IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'],
[IFACE_FF_10GE_COPPER, '10GBASE-T (10GE)'], [IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'],
] ]
], ],
[ [
'Modular', 'Ethernet (modular)',
[ [
[IFACE_FF_GBIC, 'GBIC (1GE)'], [IFACE_FF_1GE_GBIC, 'GBIC (1GE)'],
[IFACE_FF_SFP, 'SFP (1GE)'], [IFACE_FF_1GE_SFP, 'SFP (1GE)'],
[IFACE_FF_XFP, 'XFP (10GE)'], [IFACE_FF_10GE_SFP_PLUS, 'SFP+ (10GE)'],
[IFACE_FF_SFP_PLUS, 'SFP+ (10GE)'], [IFACE_FF_10GE_XFP, 'XFP (10GE)'],
[IFACE_FF_QSFP_PLUS, 'QSFP+ (40GE)'], [IFACE_FF_10GE_XENPAK, 'XENPAK (10GE)'],
[IFACE_FF_CFP, 'CFP (100GE)'], [IFACE_FF_10GE_X2, 'X2 (10GE)'],
[IFACE_FF_QSFP28, 'QSFP28 (100GE)'], [IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'],
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
[IFACE_FF_100GE_CFP, 'CFP (100GE)'],
[IFACE_FF_100GE_QSFP28, 'QSFP28 (100GE)'],
]
],
[
'FibreChannel',
[
[IFACE_FF_1GFC_SFP, 'SFP (1GFC)'],
[IFACE_FF_2GFC_SFP, 'SFP (2GFC)'],
[IFACE_FF_4GFC_SFP, 'SFP (4GFC)'],
[IFACE_FF_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
[IFACE_FF_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
] ]
], ],
[ [
@ -137,6 +166,12 @@ IFACE_FF_CHOICES = [
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'], [IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
] ]
], ],
[
'Other',
[
[IFACE_FF_OTHER, 'Other'],
]
],
] ]
STATUS_ACTIVE = True STATUS_ACTIVE = True
@ -647,7 +682,7 @@ class InterfaceTemplate(models.Model):
""" """
device_type = models.ForeignKey('DeviceType', related_name='interface_templates', on_delete=models.CASCADE) device_type = models.ForeignKey('DeviceType', related_name='interface_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30) name = models.CharField(max_length=30)
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_SFP_PLUS) form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
mgmt_only = models.BooleanField(default=False, verbose_name='Management only') mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
objects = InterfaceTemplateManager() objects = InterfaceTemplateManager()
@ -1023,7 +1058,7 @@ class Interface(models.Model):
""" """
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE) device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
name = models.CharField(max_length=30) name = models.CharField(max_length=30)
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_SFP_PLUS) form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address') mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
mgmt_only = models.BooleanField(default=False, verbose_name='OOB Management', mgmt_only = models.BooleanField(default=False, verbose_name='OOB Management',
help_text="This interface is used only for out-of-band management") help_text="This interface is used only for out-of-band management")

View File

@ -29,8 +29,8 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
elif cf.type == CF_TYPE_BOOLEAN: elif cf.type == CF_TYPE_BOOLEAN:
choices = ( choices = (
(None, '---------'), (None, '---------'),
(True, 'True'), (1, 'True'),
(False, 'False'), (0, 'False'),
) )
if cf.default.lower() in ['true', 'yes', '1']: if cf.default.lower() in ['true', 'yes', '1']:
initial = True initial = True

View File

@ -234,10 +234,10 @@ class ExportTemplate(models.Model):
""" """
template = Template(self.template_code) template = Template(self.template_code)
mime_type = 'text/plain' if not self.mime_type else self.mime_type mime_type = 'text/plain' if not self.mime_type else self.mime_type
response = HttpResponse( output = template.render(Context(context_dict))
template.render(Context(context_dict)), # Replace CRLF-style line terminators
content_type=mime_type output = output.replace('\r\n', '\n')
) response = HttpResponse(output, content_type=mime_type)
if self.file_extension: if self.file_extension:
filename += '.{}'.format(self.file_extension) filename += '.{}'.format(self.file_extension)
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename) response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)

View File

@ -5,7 +5,10 @@ from dcim.models import Site, Device, Interface
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import bulkedit_tenant_choices from tenancy.forms import bulkedit_tenant_choices
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import BootstrapMixin, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField from utilities.forms import (
APISelect, BootstrapMixin, CSVDataField, BulkImportForm, FilterChoiceField, Livesearch, SlugField,
get_filter_choices,
)
from .models import ( from .models import (
Aggregate, IPAddress, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF, Aggregate, IPAddress, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
@ -14,6 +17,11 @@ from .models import (
FORM_PREFIX_STATUS_CHOICES = (('', '---------'),) + PREFIX_STATUS_CHOICES FORM_PREFIX_STATUS_CHOICES = (('', '---------'),) + PREFIX_STATUS_CHOICES
FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES
IP_FAMILY_CHOICES = [
('', 'All'),
(4, 'IPv4'),
(6, 'IPv6'),
]
def bulkedit_vrf_choices(): def bulkedit_vrf_choices():
@ -64,15 +72,9 @@ class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
description = forms.CharField(max_length=100, required=False) description = forms.CharField(max_length=100, required=False)
def vrf_tenant_choices():
tenant_choices = Tenant.objects.annotate(vrf_count=Count('vrfs'))
return [(t.slug, u'{} ({})'.format(t.name, t.vrf_count)) for t in tenant_choices]
class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm): class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = VRF model = VRF
tenant = forms.MultipleChoiceField(required=False, choices=vrf_tenant_choices, tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='vrfs'))
widget=forms.SelectMultiple(attrs={'size': 8}))
# #
@ -123,15 +125,10 @@ class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
description = forms.CharField(max_length=100, required=False) description = forms.CharField(max_length=100, required=False)
def aggregate_rir_choices():
rir_choices = RIR.objects.annotate(aggregate_count=Count('aggregates'))
return [(r.slug, u'{} ({})'.format(r.name, r.aggregate_count)) for r in rir_choices]
class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm): class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Aggregate model = Aggregate
rir = forms.MultipleChoiceField(required=False, choices=aggregate_rir_choices, label='RIR', family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
widget=forms.SelectMultiple(attrs={'size': 8})) rir = FilterChoiceField(choices=get_filter_choices(RIR, id_field='slug', count_field='aggregates'), label='RIR')
# #
@ -262,21 +259,6 @@ class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
description = forms.CharField(max_length=100, required=False) description = forms.CharField(max_length=100, required=False)
def prefix_vrf_choices():
vrf_choices = VRF.objects.annotate(prefix_count=Count('prefixes'))
return [(v.pk, u'{} ({})'.format(v.name, v.prefix_count)) for v in vrf_choices]
def tenant_choices():
tenant_choices = Tenant.objects.all()
return [(t.slug, t.name) for t in tenant_choices]
def prefix_site_choices():
site_choices = Site.objects.annotate(prefix_count=Count('prefixes'))
return [(s.slug, u'{} ({})'.format(s.name, s.prefix_count)) for s in site_choices]
def prefix_status_choices(): def prefix_status_choices():
status_counts = {} status_counts = {}
for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'): for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'):
@ -284,26 +266,18 @@ def prefix_status_choices():
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES] return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
def prefix_role_choices():
role_choices = Role.objects.annotate(prefix_count=Count('prefixes'))
return [(r.slug, u'{} ({})'.format(r.name, r.prefix_count)) for r in role_choices]
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Prefix model = Prefix
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={ parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
'placeholder': 'Network', 'placeholder': 'Network',
})) }))
vrf = forms.MultipleChoiceField(required=False, choices=prefix_vrf_choices, label='VRF', family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
widget=forms.SelectMultiple(attrs={'size': 6})) vrf = FilterChoiceField(choices=get_filter_choices(VRF, count_field='prefixes'), label='VRF')
tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant', tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='prefixes'),
widget=forms.SelectMultiple(attrs={'size': 6})) label='Tenant')
status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices, status = FilterChoiceField(choices=prefix_status_choices)
widget=forms.SelectMultiple(attrs={'size': 6})) site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='prefixes'))
site = forms.MultipleChoiceField(required=False, choices=prefix_site_choices, role = FilterChoiceField(choices=get_filter_choices(Role, id_field='slug', count_field='prefixes'))
widget=forms.SelectMultiple(attrs={'size': 6}))
role = forms.MultipleChoiceField(required=False, choices=prefix_role_choices,
widget=forms.SelectMultiple(attrs={'size': 6}))
expand = forms.BooleanField(required=False, label='Expand prefix hierarchy') expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
@ -434,25 +408,15 @@ class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
description = forms.CharField(max_length=100, required=False) description = forms.CharField(max_length=100, required=False)
def ipaddress_family_choices():
return [('', 'All'), (4, 'IPv4'), (6, 'IPv6')]
def ipaddress_vrf_choices():
vrf_choices = VRF.objects.annotate(ipaddress_count=Count('ip_addresses'))
return [(v.pk, u'{} ({})'.format(v.name, v.ipaddress_count)) for v in vrf_choices]
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = IPAddress model = IPAddress
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={ parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
'placeholder': 'Prefix', 'placeholder': 'Prefix',
})) }))
family = forms.ChoiceField(required=False, choices=ipaddress_family_choices, label='Address Family') family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
vrf = forms.MultipleChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF', vrf = FilterChoiceField(choices=get_filter_choices(VRF, count_field='ip_addresses'), label='VRF')
widget=forms.SelectMultiple(attrs={'size': 6})) tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='prefixes'),
tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant', label='Tenant')
widget=forms.SelectMultiple(attrs={'size': 6}))
# #
@ -467,14 +431,8 @@ class VLANGroupForm(forms.ModelForm, BootstrapMixin):
fields = ['site', 'name', 'slug'] fields = ['site', 'name', 'slug']
def vlangroup_site_choices():
site_choices = Site.objects.annotate(vlangroup_count=Count('vlan_groups'))
return [(s.slug, u'{} ({})'.format(s.name, s.vlangroup_count)) for s in site_choices]
class VLANGroupFilterForm(forms.Form, BootstrapMixin): class VLANGroupFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=vlangroup_site_choices, site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='vlan_groups'))
widget=forms.SelectMultiple(attrs={'size': 8}))
# #
@ -552,21 +510,6 @@ class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
description = forms.CharField(max_length=100, required=False) description = forms.CharField(max_length=100, required=False)
def vlan_site_choices():
site_choices = Site.objects.annotate(vlan_count=Count('vlans'))
return [(s.slug, u'{} ({})'.format(s.name, s.vlan_count)) for s in site_choices]
def vlan_group_choices():
group_choices = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
return [(g.pk, u'{} ({})'.format(g, g.vlan_count)) for g in group_choices]
def vlan_tenant_choices():
tenant_choices = Tenant.objects.annotate(vrf_count=Count('vlans'))
return [(t.slug, u'{} ({})'.format(t.name, t.vrf_count)) for t in tenant_choices]
def vlan_status_choices(): def vlan_status_choices():
status_counts = {} status_counts = {}
for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'): for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
@ -574,19 +517,11 @@ def vlan_status_choices():
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES] return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
def vlan_role_choices():
role_choices = Role.objects.annotate(vlan_count=Count('vlans'))
return [(r.slug, u'{} ({})'.format(r.name, r.vlan_count)) for r in role_choices]
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = VLAN model = VLAN
site = forms.MultipleChoiceField(required=False, choices=vlan_site_choices, site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='vlans'))
widget=forms.SelectMultiple(attrs={'size': 8})) group_id = FilterChoiceField(choices=get_filter_choices(VLANGroup, select_related=['site'], count_field='vlans'),
group_id = forms.MultipleChoiceField(required=False, choices=vlan_group_choices, label='VLAN Group', label='VLAN Group')
widget=forms.SelectMultiple(attrs={'size': 8})) tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='vlans'))
tenant = forms.MultipleChoiceField(required=False, choices=vlan_tenant_choices, status = FilterChoiceField(choices=vlan_status_choices)
widget=forms.SelectMultiple(attrs={'size': 8})) role = FilterChoiceField(choices=get_filter_choices(Role, id_field='slug', count_field='vlans'))
status = forms.MultipleChoiceField(required=False, choices=vlan_status_choices)
role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))

View File

@ -12,7 +12,7 @@ except ImportError:
"the documentation.") "the documentation.")
VERSION = '1.5.3-dev' VERSION = '1.6.1-dev'
# Import local configuration # Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:

View File

@ -34,7 +34,8 @@ body {
footer p { footer p {
margin: 20px 0; margin: 20px 0;
} }
@media (max-width: 1120px) {
@media (max-width: 1200px) {
.navbar-header { .navbar-header {
float: none; float: none;
} }
@ -54,6 +55,7 @@ footer p {
} }
.navbar-collapse.collapse { .navbar-collapse.collapse {
display: none!important; display: none!important;
max-height: none;
} }
.navbar-nav { .navbar-nav {
float: none!important; float: none!important;
@ -84,13 +86,11 @@ th.pk, td.pk {
width: 30px; width: 30px;
} }
/* Paginator */ /* Paginator */
nav ul.pagination { nav ul.pagination {
margin-top: 0; margin-top: 0;
} }
/* Racks */ /* Racks */
div.rack_header { div.rack_header {
margin-left: 36px; margin-left: 36px;

View File

@ -32,61 +32,6 @@ $(document).ready(function() {
}) })
} }
// Helper select fields
$('select.helper-parent').change(function () {
// Resolve child field by ID specified in parent
var child_field = $('#id_' + $(this).attr('child'));
// Wipe out any existing options within the child field
child_field.empty();
child_field.append($("<option></option>").attr("value", "").text(""));
// If the parent has a value set, fetch a list of child options via the API and populate the child field with them
if ($(this).val()) {
// Construct the API request URL
var api_url = $(this).attr('child-source');
var parent_accessor = $(this).attr('parent-accessor');
if (parent_accessor) {
api_url += '?' + parent_accessor + '=' + $(this).val();
} else {
api_url += '?' + $(this).attr('name') + '_id=' + $(this).val();
}
var api_url_extra = $(this).attr('child-filter');
if (api_url_extra) {
api_url += '&' + api_url_extra;
}
var disabled_indicator = $(this).attr('disabled-indicator');
var disabled_exempt = child_field.attr('exempt');
var child_display = $(this).attr('child-display');
if (!child_display) {
child_display = 'name';
}
$.ajax({
url: api_url,
dataType: 'json',
success: function (response, status) {
console.log(response);
$.each(response, function (index, choice) {
var option = $("<option></option>").attr("value", choice.id).text(choice[child_display]);
if (disabled_indicator && choice[disabled_indicator] && choice.id != disabled_exempt) {
option.attr("disabled", "disabled")
}
child_field.append(option);
});
}
});
}
// Trigger change event in case the child field is the parent of another field
child_field.change();
});
// API select widget // API select widget
$('select[filter-for]').change(function () { $('select[filter-for]').change(function () {

View File

@ -2,10 +2,11 @@ from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from django import forms from django import forms
from django.db.models import Count
from dcim.models import Device from dcim.models import Device
from utilities.forms import BootstrapMixin, BulkImportForm, CSVDataField, SlugField from utilities.forms import (
BootstrapMixin, BulkImportForm, CSVDataField, FilterChoiceField, SlugField, get_filter_choices,
)
from .models import Secret, SecretRole, UserKey from .models import Secret, SecretRole, UserKey
@ -95,13 +96,8 @@ class SecretBulkEditForm(forms.Form, BootstrapMixin):
name = forms.CharField(max_length=100, required=False) name = forms.CharField(max_length=100, required=False)
def secret_role_choices():
role_choices = SecretRole.objects.annotate(secret_count=Count('secrets'))
return [(r.slug, u'{} ({})'.format(r.name, r.secret_count)) for r in role_choices]
class SecretFilterForm(forms.Form, BootstrapMixin): class SecretFilterForm(forms.Form, BootstrapMixin):
role = forms.MultipleChoiceField(required=False, choices=secret_role_choices) role = FilterChoiceField(choices=get_filter_choices(SecretRole, id_field='slug', count_field='secrets'))
# #

View File

@ -167,7 +167,7 @@
{% if perms.ipam.add_rir or perms.ipam.add_role %} {% if perms.ipam.add_rir or perms.ipam.add_role %}
<li class="divider"></li> <li class="divider"></li>
{% endif %} {% endif %}
<li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefix/VLAN Roles</a></li> <li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefix Roles</a></li>
{% if perms.ipam.add_role %} {% if perms.ipam.add_role %}
<li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li> <li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li>
{% endif %} {% endif %}
@ -186,6 +186,11 @@
{% if perms.ipam.add_vlangroup %} {% if perms.ipam.add_vlangroup %}
<li><a href="{% url 'ipam:vlangroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN Group</a></li> <li><a href="{% url 'ipam:vlangroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN Group</a></li>
{% endif %} {% endif %}
<li class="divider"></li>
<li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLAN Roles</a></li>
{% if perms.ipam.add_role %}
<li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li>
{% endif %}
</ul> </ul>
</li> </li>
<li class="dropdown{% if request.path|startswith:'/circuits/' %} active{% endif %}"> <li class="dropdown{% if request.path|startswith:'/circuits/' %} active{% endif %}">

View File

@ -83,7 +83,7 @@
</tr> </tr>
<tr> <tr>
<td>Face</td> <td>Face</td>
<td>Rack face; front or rear (optional)</td> <td>Rack face; front or rear (required if position is set)</td>
<td>Rear</td> <td>Rear</td>
</tr> </tr>
</tbody> </tbody>

View File

@ -1,8 +1,9 @@
from django import forms from django import forms
from django.db.models import Count
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from utilities.forms import BootstrapMixin, BulkImportForm, CommentField, CSVDataField, SlugField from utilities.forms import (
BootstrapMixin, BulkImportForm, CommentField, CSVDataField, FilterChoiceField, SlugField, get_filter_choices,
)
from .models import Tenant, TenantGroup from .models import Tenant, TenantGroup
@ -74,12 +75,6 @@ class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
group = forms.TypedChoiceField(choices=bulkedit_tenantgroup_choices, coerce=int, required=False, label='Group') group = forms.TypedChoiceField(choices=bulkedit_tenantgroup_choices, coerce=int, required=False, label='Group')
def tenant_group_choices():
group_choices = TenantGroup.objects.annotate(tenant_count=Count('tenants'))
return [(g.slug, u'{} ({})'.format(g.name, g.tenant_count)) for g in group_choices]
class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm): class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Tenant model = Tenant
group = forms.MultipleChoiceField(required=False, choices=tenant_group_choices, group = FilterChoiceField(choices=get_filter_choices(TenantGroup, id_field='slug', count_field='tenants'))
widget=forms.SelectMultiple(attrs={'size': 8}))

View File

@ -3,6 +3,7 @@ import re
from django import forms from django import forms
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
from django.db.models import Count
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -34,6 +35,27 @@ def add_blank_choice(choices):
return ((None, '---------'),) + choices return ((None, '---------'),) + choices
def get_filter_choices(model, id_field='pk', select_related=[], count_field=None):
"""
Return a list of choices suitable for a ChoiceField.
:param model: The base model to use for the queryset
:param id_field: Field to use as the object identifier
:param select_related: Any related tables to include
:param count: The field to use for a child COUNT() (optional)
:return:
"""
queryset = model.objects.all()
if select_related:
queryset = queryset.select_related(*select_related)
if count_field:
queryset = queryset.annotate(child_count=Count(count_field))
return [(getattr(obj, id_field), u'{} ({})'.format(obj, obj.child_count)) for obj in queryset]
else:
return [(getattr(obj, id_field), u'{}'.format(obj)) for obj in queryset]
# #
# Widgets # Widgets
# #
@ -222,6 +244,16 @@ class SlugField(forms.SlugField):
self.widget.attrs['slug-source'] = slug_source self.widget.attrs['slug-source'] = slug_source
class FilterChoiceField(forms.MultipleChoiceField):
def __init__(self, *args, **kwargs):
if 'required' not in kwargs:
kwargs['required'] = False
if 'widget' not in kwargs:
kwargs['widget'] = forms.SelectMultiple(attrs={'size': 6})
super(FilterChoiceField, self).__init__(*args, **kwargs)
# #
# Forms # Forms
# #

View File

@ -1,7 +1,6 @@
from django_tables2 import RequestConfig from django_tables2 import RequestConfig
from django.contrib import messages from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
@ -381,10 +380,6 @@ class BulkDeleteView(View):
template_name = 'utilities/confirm_bulk_delete.html' template_name = 'utilities/confirm_bulk_delete.html'
default_redirect_url = None default_redirect_url = None
@method_decorator(staff_member_required)
def dispatch(self, *args, **kwargs):
return super(BulkDeleteView, self).dispatch(*args, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
# Attempt to derive parent object if a parent class has been given # Attempt to derive parent object if a parent class has been given

0
scripts/docker-build.sh Normal file → Executable file
View File