Merge pull request #1181 from digitalocean/develop

Release v2.0.2
This commit is contained in:
Jeremy Stretch 2017-05-15 13:23:33 -04:00 committed by GitHub
commit 43e1e0dbc8
34 changed files with 624 additions and 485 deletions

View File

@ -25,7 +25,7 @@ server {
server_name netbox.example.com;
access_log off;
client_max_body_size 25m;
location /static/ {
alias /opt/netbox/netbox/static/;

View File

@ -3,10 +3,11 @@ from django.db.models import Count
from dcim.models import Site, Device, Interface, Rack, VIRTUAL_IFACE_TYPES
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, FilterChoiceField, Livesearch, SmallTextarea,
SlugField,
APISelect, BootstrapMixin, BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField,
FilterChoiceField, Livesearch, SmallTextarea, SlugField,
)
from .models import Circuit, CircuitTermination, CircuitType, Provider
@ -83,12 +84,15 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
# Circuits
#
class CircuitForm(BootstrapMixin, CustomFieldForm):
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
comments = CommentField()
class Meta:
model = Circuit
fields = ['cid', 'type', 'provider', 'tenant', 'install_date', 'commit_rate', 'description', 'comments']
fields = [
'cid', 'type', 'provider', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
'comments',
]
help_texts = {
'cid': "Unique circuit ID",
'install_date': "Format: YYYY-MM-DD",
@ -152,15 +156,16 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
# Circuit terminations
#
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
widget=forms.Select(
attrs={'filter-for': 'rack'}
)
)
rack = forms.ModelChoiceField(
rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains={'site': 'site'},
required=False,
label='Rack',
widget=APISelect(
@ -168,8 +173,9 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
attrs={'filter-for': 'device', 'nullable': 'true'}
)
)
device = forms.ModelChoiceField(
device = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains={'site': 'site', 'rack': 'rack'},
required=False,
label='Device',
widget=APISelect(
@ -187,8 +193,11 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
field_to_update='device'
)
)
interface = forms.ModelChoiceField(
queryset=Interface.objects.all(),
interface = ChainedModelChoiceField(
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b'
),
chains={'device': 'device'},
required=False,
label='Interface',
widget=APISelect(
@ -212,49 +221,17 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
def __init__(self, *args, **kwargs):
# Initialize helper selectors
instance = kwargs.get('instance')
if instance and instance.interface is not None:
initial = kwargs.get('initial', {})
initial['rack'] = instance.interface.device.rack
initial['device'] = instance.interface.device
kwargs['initial'] = initial
super(CircuitTerminationForm, self).__init__(*args, **kwargs)
# If an interface has been assigned, initialize rack and device
if self.instance.interface:
self.initial['rack'] = self.instance.interface.device.rack
self.initial['device'] = self.instance.interface.device
# Limit rack choices
if self.is_bound:
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
elif self.initial.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
else:
self.fields['rack'].choices = []
# Limit device choices
if self.is_bound and self.data.get('rack'):
self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
elif self.initial.get('rack'):
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
else:
self.fields['device'].choices = []
# Limit interface choices
if self.is_bound and self.data.get('device'):
interfaces = Interface.objects.filter(device=self.data['device']).exclude(
form_factor__in=VIRTUAL_IFACE_TYPES
).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b'
)
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
elif self.initial.get('device'):
interfaces = Interface.objects.filter(device=self.initial['device']).exclude(
form_factor__in=VIRTUAL_IFACE_TYPES
).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b'
)
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
else:
interfaces = []
# Mark connected interfaces as disabled
self.fields['interface'].choices = [
(iface.id, {
'label': iface.name,
'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'),
}) for iface in interfaces
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface'].queryset
]

View File

@ -79,7 +79,13 @@ class CircuitSearchTable(SearchTable):
cid = tables.LinkColumn(verbose_name='ID')
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
a_side = tables.LinkColumn(
'dcim:site', accessor=Accessor('termination_a.site'), args=[Accessor('termination_a.site.slug')]
)
z_side = tables.LinkColumn(
'dcim:site', accessor=Accessor('termination_z.site'), args=[Accessor('termination_z.site.slug')]
)
class Meta(SearchTable.Meta):
model = Circuit
fields = ('cid', 'type', 'provider', 'tenant', 'description')
fields = ('cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')

View File

@ -608,6 +608,7 @@ class InterfaceSerializer(serializers.ModelSerializer):
class PeerInterfaceSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
device = NestedDeviceSerializer()
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
class Meta:
model = Interface

View File

@ -8,11 +8,13 @@ from django.db.models import Count, Q
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from ipam.models import IPAddress
from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
BulkImportForm, CommentField, CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField,
Livesearch, SelectWithDisabled, SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField,
BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField, ExpandableNameField,
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
FilterTreeNodeMultipleChoiceField,
)
from .formfields import MACAddressFormField
@ -80,7 +82,7 @@ class RegionForm(BootstrapMixin, forms.ModelForm):
# Sites
#
class SiteForm(BootstrapMixin, CustomFieldForm):
class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False)
slug = SlugField()
comments = CommentField()
@ -88,8 +90,8 @@ class SiteForm(BootstrapMixin, CustomFieldForm):
class Meta:
model = Site
fields = [
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
'contact_name', 'contact_phone', 'contact_email', 'comments',
'name', 'slug', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'physical_address',
'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
]
widgets = {
'physical_address': SmallTextarea(attrs={'rows': 3}),
@ -184,16 +186,23 @@ class RackRoleForm(BootstrapMixin, forms.ModelForm):
# Racks
#
class RackForm(BootstrapMixin, CustomFieldForm):
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group', widget=APISelect(
api_url='/api/dcim/rack-groups/?site_id={{site}}',
))
class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
group = ChainedModelChoiceField(
queryset=RackGroup.objects.all(),
chains={'site': 'site'},
required=False,
widget=APISelect(
api_url='/api/dcim/rack-groups/?site_id={{site}}',
)
)
comments = CommentField()
class Meta:
model = Rack
fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
'comments']
fields = [
'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'role', 'type', 'width', 'u_height',
'desc_units', 'comments',
]
help_texts = {
'site': "The site at which the rack exists",
'name': "Organizational rack name",
@ -204,18 +213,6 @@ class RackForm(BootstrapMixin, CustomFieldForm):
'site': forms.Select(attrs={'filter-for': 'group'}),
}
def __init__(self, *args, **kwargs):
super(RackForm, self).__init__(*args, **kwargs)
# Limit rack group choices
if self.is_bound and self.data.get('site'):
self.fields['group'].queryset = RackGroup.objects.filter(site__pk=self.data['site'])
elif self.initial.get('site'):
self.fields['group'].queryset = RackGroup.objects.filter(site=self.initial['site'])
else:
self.fields['group'].choices = []
class RackFromCSVForm(forms.ModelForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
@ -538,33 +535,54 @@ class PlatformForm(BootstrapMixin, forms.ModelForm):
# Devices
#
class DeviceForm(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, widget=APISelect(
class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
widget=forms.Select(
attrs={'filter-for': 'rack'}
)
)
rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains={'site': 'site'},
required=False,
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{site}}',
display_field='display_name',
attrs={'filter-for': 'position'}
)
)
position = forms.TypedChoiceField(
required=False, empty_value=None, help_text="The lowest-numbered unit occupied by the device",
widget=APISelect(api_url='/api/dcim/racks/{{rack}}/units/?face={{face}}', disabled_indicator='device')
required=False,
empty_value=None,
help_text="The lowest-numbered unit occupied by the device",
widget=APISelect(
api_url='/api/dcim/racks/{{rack}}/units/?face={{face}}',
disabled_indicator='device'
)
)
manufacturer = forms.ModelChoiceField(
queryset=Manufacturer.objects.all(), widget=forms.Select(attrs={'filter-for': 'device_type'})
queryset=Manufacturer.objects.all(),
widget=forms.Select(
attrs={'filter-for': 'device_type'}
)
)
device_type = forms.ModelChoiceField(
queryset=DeviceType.objects.all(), label='Device type',
widget=APISelect(api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}', display_field='model')
device_type = ChainedModelChoiceField(
queryset=DeviceType.objects.all(),
chains={'manufacturer': 'manufacturer'},
label='Device type',
widget=APISelect(
api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}',
display_field='model'
)
)
comments = CommentField()
class Meta:
model = Device
fields = [
'name', 'device_role', 'tenant', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face',
'status', 'platform', 'primary_ip4', 'primary_ip6', 'comments',
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face', 'status',
'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments',
]
help_texts = {
'device_role': "The function this device serves",
@ -572,19 +590,22 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
}
widgets = {
'face': forms.Select(attrs={'filter-for': 'position'}),
'manufacturer': forms.Select(attrs={'filter-for': 'device_type'}),
}
def __init__(self, *args, **kwargs):
# Initialize helper selectors
instance = kwargs.get('instance')
# Using hasattr() instead of "is not None" to avoid RelatedObjectDoesNotExist on required field
if instance and hasattr(instance, 'device_type'):
initial = kwargs.get('initial', {})
initial['manufacturer'] = instance.device_type.manufacturer
kwargs['initial'] = initial
super(DeviceForm, self).__init__(*args, **kwargs)
if self.instance.pk:
# Initialize helper selections
self.initial['site'] = self.instance.site
self.initial['manufacturer'] = self.instance.device_type.manufacturer
# Compile list of choices for primary IPv4 and IPv6 addresses
for family in [4, 6]:
ip_choices = []
@ -607,14 +628,6 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
self.fields['primary_ip6'].choices = []
self.fields['primary_ip6'].widget.attrs['readonly'] = True
# Limit rack choices
if self.is_bound and self.data.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
elif self.initial.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
else:
self.fields['rack'].choices = []
# Rack position
pk = self.instance.pk if self.instance.pk else None
try:
@ -635,16 +648,6 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
}) for p in position_choices
]
# Limit device_type choices
if self.is_bound:
self.fields['device_type'].queryset = DeviceType.objects.filter(manufacturer__pk=self.data['manufacturer'])\
.select_related('manufacturer')
elif self.initial.get('manufacturer'):
self.fields['device_type'].queryset = DeviceType.objects.filter(manufacturer=self.initial['manufacturer'])\
.select_related('manufacturer')
else:
self.fields['device_type'].choices = []
# Disable rack assignment if this is a child device installed in a parent device
if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
self.fields['site'].disabled = True
@ -811,6 +814,10 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__devices')),
label='Rack group',
)
rack_id = FilterChoiceField(
queryset=Rack.objects.annotate(filter_count=Count('devices')),
label='Rack',
)
role = FilterChoiceField(
queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
to_field_name='slug',
@ -940,21 +947,23 @@ class ConsoleConnectionImportForm(BootstrapMixin, BulkImportForm):
self.cleaned_data['csv'] = connection_list
class ConsolePortConnectionForm(BootstrapMixin, forms.ModelForm):
class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
widget=forms.HiddenInput(),
)
rack = forms.ModelChoiceField(
rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains={'site': 'site'},
label='Rack',
required=False,
widget=forms.Select(
attrs={'filter-for': 'console_server', 'nullable': 'true'}
)
)
console_server = forms.ModelChoiceField(
queryset=Device.objects.all(),
console_server = ChainedModelChoiceField(
queryset=Device.objects.filter(device_type__is_console_server=True),
chains={'site': 'site', 'rack': 'rack'},
label='Console Server',
required=False,
widget=APISelect(
@ -972,8 +981,9 @@ class ConsolePortConnectionForm(BootstrapMixin, forms.ModelForm):
field_to_update='console_server',
)
)
cs_port = forms.ModelChoiceField(
cs_port = ChainedModelChoiceField(
queryset=ConsoleServerPort.objects.all(),
chains={'device': 'console_server'},
label='Port',
widget=APISelect(
api_url='/api/dcim/console-server-ports/?device_id={{console_server}}',
@ -996,32 +1006,6 @@ class ConsolePortConnectionForm(BootstrapMixin, forms.ModelForm):
if not self.instance.pk:
raise RuntimeError("ConsolePortConnectionForm must be initialized with an existing ConsolePort instance.")
# Initialize rack choices if site is set
if self.initial.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
else:
self.fields['rack'].choices = []
# Initialize console_server choices if rack or site is set
if self.initial.get('rack'):
self.fields['console_server'].queryset = Device.objects.filter(
rack=self.initial['rack'], device_type__is_console_server=True
)
elif self.initial.get('site'):
self.fields['console_server'].queryset = Device.objects.filter(
site=self.initial['site'], rack__isnull=True, device_type__is_console_server=True
)
else:
self.fields['console_server'].choices = []
# Initialize CS port choices if console_server is set
if self.initial.get('console_server'):
self.fields['cs_port'].queryset = ConsoleServerPort.objects.filter(
device=self.initial['console_server']
)
else:
self.fields['cs_port'].choices = []
#
# Console server ports
@ -1041,21 +1025,23 @@ class ConsoleServerPortCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name')
class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
widget=forms.HiddenInput(),
)
rack = forms.ModelChoiceField(
rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains={'site': 'site'},
label='Rack',
required=False,
widget=forms.Select(
attrs={'filter-for': 'device', 'nullable': 'true'}
)
)
device = forms.ModelChoiceField(
device = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains={'site': 'site', 'rack': 'rack'},
label='Device',
required=False,
widget=APISelect(
@ -1073,8 +1059,9 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
field_to_update='device'
)
)
port = forms.ModelChoiceField(
port = ChainedModelChoiceField(
queryset=ConsolePort.objects.all(),
chains={'device': 'device'},
label='Port',
widget=APISelect(
api_url='/api/dcim/console-ports/?device_id={{device}}',
@ -1096,30 +1083,6 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
'connection_status': 'Status',
}
def __init__(self, *args, **kwargs):
super(ConsoleServerPortConnectionForm, self).__init__(*args, **kwargs)
# Initialize rack choices if site is set
if self.initial.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
else:
self.fields['rack'].choices = []
# Initialize device choices if rack or site is set
if self.initial.get('rack'):
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
elif self.initial.get('site'):
self.fields['device'].queryset = Device.objects.filter(site=self.initial['site'], rack__isnull=True)
else:
self.fields['device'].choices = []
# Initialize port choices if device is set
if self.initial.get('device'):
self.fields['port'].queryset = ConsolePort.objects.filter(device=self.initial['device'])
else:
self.fields['port'].choices = []
#
# Power ports
@ -1211,18 +1174,20 @@ class PowerConnectionImportForm(BootstrapMixin, BulkImportForm):
self.cleaned_data['csv'] = connection_list
class PowerPortConnectionForm(BootstrapMixin, forms.ModelForm):
class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.HiddenInput())
rack = forms.ModelChoiceField(
rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains={'site': 'site'},
label='Rack',
required=False,
widget=forms.Select(
attrs={'filter-for': 'pdu', 'nullable': 'true'}
)
)
pdu = forms.ModelChoiceField(
pdu = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains={'site': 'site', 'rack': 'rack'},
label='PDU',
required=False,
widget=APISelect(
@ -1240,8 +1205,9 @@ class PowerPortConnectionForm(BootstrapMixin, forms.ModelForm):
field_to_update='pdu'
)
)
power_outlet = forms.ModelChoiceField(
power_outlet = ChainedModelChoiceField(
queryset=PowerOutlet.objects.all(),
chains={'device': 'device'},
label='Outlet',
widget=APISelect(
api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
@ -1264,30 +1230,6 @@ class PowerPortConnectionForm(BootstrapMixin, forms.ModelForm):
if not self.instance.pk:
raise RuntimeError("PowerPortConnectionForm must be initialized with an existing PowerPort instance.")
# Initialize rack choices if site is set
if self.initial.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
else:
self.fields['rack'].choices = []
# Initialize pdu choices if rack or site is set
if self.initial.get('rack'):
self.fields['pdu'].queryset = Device.objects.filter(
rack=self.initial['rack'], device_type__is_pdu=True
)
elif self.initial.get('site'):
self.fields['pdu'].queryset = Device.objects.filter(
site=self.initial['site'], rack__isnull=True, device_type__is_pdu=True
)
else:
self.fields['pdu'].choices = []
# Initialize power outlet choices if pdu is set
if self.initial.get('pdu'):
self.fields['power_outlet'].queryset = PowerOutlet.objects.filter(device=self.initial['pdu'])
else:
self.fields['power_outlet'].choices = []
#
# Power outlets
@ -1307,21 +1249,23 @@ class PowerOutletCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name')
class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
widget=forms.HiddenInput()
)
rack = forms.ModelChoiceField(
rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains={'site': 'site'},
label='Rack',
required=False,
widget=forms.Select(
attrs={'filter-for': 'device', 'nullable': 'true'}
)
)
device = forms.ModelChoiceField(
device = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains={'site': 'site', 'rack': 'rack'},
label='Device',
required=False,
widget=APISelect(
@ -1339,8 +1283,9 @@ class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
field_to_update='device'
)
)
port = forms.ModelChoiceField(
port = ChainedModelChoiceField(
queryset=PowerPort.objects.all(),
chains={'device': 'device'},
label='Port',
widget=APISelect(
api_url='/api/dcim/power-ports/?device_id={{device}}',
@ -1362,30 +1307,6 @@ class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
'connection_status': 'Status',
}
def __init__(self, *args, **kwargs):
super(PowerOutletConnectionForm, self).__init__(*args, **kwargs)
# Initialize rack choices if site is set
if self.initial.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
else:
self.fields['rack'].choices = []
# Initialize device choices if rack or site is set
if self.initial.get('rack'):
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
elif self.initial.get('site'):
self.fields['device'].queryset = Device.objects.filter(site=self.initial['site'], rack__isnull=True)
else:
self.fields['device'].choices = []
# Initialize port choices if device is set
if self.initial.get('device'):
self.fields['port'].queryset = PowerPort.objects.filter(device=self.initial['device'])
else:
self.fields['port'].choices = []
#
# Interfaces
@ -1468,7 +1389,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
# Interface connections
#
class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
interface_a = forms.ChoiceField(
choices=[],
widget=SelectWithDisabled,
@ -1482,8 +1403,9 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
attrs={'filter-for': 'rack_b'}
)
)
rack_b = forms.ModelChoiceField(
rack_b = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains={'site': 'site_b'},
label='Rack',
required=False,
widget=APISelect(
@ -1491,8 +1413,9 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
attrs={'filter-for': 'device_b', 'nullable': 'true'}
)
)
device_b = forms.ModelChoiceField(
device_b = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains={'site': 'site_b', 'rack': 'rack_b'},
label='Device',
required=False,
widget=APISelect(
@ -1510,8 +1433,11 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
field_to_update='device_b'
)
)
interface_b = forms.ModelChoiceField(
queryset=Interface.objects.all(),
interface_b = ChainedModelChoiceField(
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b'
),
chains={'device': 'device_b'},
label='Interface',
widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical',
@ -1537,31 +1463,9 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
]
# Initialize rack_b choices if site_b is set
if self.initial.get('site_b'):
self.fields['rack_b'].queryset = Rack.objects.filter(site=self.initial['site_b'])
else:
self.fields['rack_b'].choices = []
# Initialize device_b choices if rack_b or site_b is set
if self.initial.get('rack_b'):
self.fields['device_b'].queryset = Device.objects.filter(rack=self.initial['rack_b'])
elif self.initial.get('site_b'):
self.fields['device_b'].queryset = Device.objects.filter(site=self.initial['site_b'], rack__isnull=True)
else:
self.fields['device_b'].choices = []
# Initialize interface_b choices if device_b is set
if self.initial.get('device_b'):
device_b_interfaces = Interface.objects.filter(device=self.initial['device_b']).exclude(
form_factor__in=VIRTUAL_IFACE_TYPES
).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b'
)
else:
device_b_interfaces = []
# Mark connected interfaces as disabled
self.fields['interface_b'].choices = [
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_b_interfaces
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface_b'].queryset
]

View File

@ -410,7 +410,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
]
def __str__(self):
return self.display_name
return self.display_name or super(Rack, self).__str__()
def get_absolute_url(self):
return reverse('dcim:rack', args=[self.pk])
@ -467,7 +467,9 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
def display_name(self):
if self.facility_id:
return u"{} ({})".format(self.name, self.facility_id)
return self.name
elif self.name:
return self.name
return u""
def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False):
"""
@ -810,13 +812,13 @@ class InterfaceManager(models.Manager):
def order_naturally(self, method=IFACE_ORDERING_POSITION):
"""
Naturally order interfaces by their name and numeric position. The sort method must be one of the defined
Naturally order interfaces by their type and numeric position. The sort method must be one of the defined
IFACE_ORDERING_CHOICES (typically indicated by a parent Device's DeviceType).
To order interfaces naturally, the `name` field is split into five distinct components: leading text (name),
To order interfaces naturally, the `name` field is split into six distinct components: leading text (type),
slot, subslot, position, channel, and virtual circuit:
{name}{slot}/{subslot}/{position}:{channel}.{vc}
{type}{slot}/{subslot}/{position}:{channel}.{vc}
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet0/1 would
be parsed as follows:
@ -828,16 +830,17 @@ class InterfaceManager(models.Manager):
channel = None
vc = 0
The chosen sorting method will determine which fields are ordered first in the query.
The original `name` field is taken as a whole to serve as a fallback in the event interfaces do not match any of
the prescribed fields.
"""
queryset = self.get_queryset()
sql_col = '{}.name'.format(queryset.model._meta.db_table)
ordering = {
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_name'),
IFACE_ORDERING_NAME: ('_name', '_slot', '_subslot', '_position', '_channel', '_vc'),
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_type', 'name'),
IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_channel', '_vc', 'name'),
}[method]
return queryset.extra(select={
'_name': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
'_type': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
'_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
@ -983,7 +986,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
unique_together = ['rack', 'position', 'face']
def __str__(self):
return self.display_name
return self.display_name or super(Device, self).__str__()
def get_absolute_url(self):
return reverse('dcim:device', args=[self.pk])
@ -1102,12 +1105,9 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
def display_name(self):
if self.name:
return self.name
elif self.position:
return u"{} ({} U{})".format(self.device_type, self.rack.name, self.position)
elif self.rack:
return u"{} ({})".format(self.device_type, self.rack.name)
else:
return u"{} ({})".format(self.device_type, self.site.name)
elif hasattr(self, 'device_type'):
return u"{}".format(self.device_type)
return u""
@property
def identifier(self):

View File

@ -105,6 +105,9 @@ class ComponentCreateView(View):
new_components.append(component_form.save(commit=False))
else:
for field, errors in component_form.errors.as_data().items():
# Assign errors on the child form's name field to name_pattern on the parent form
if field == 'name':
field = 'name_pattern'
for e in errors:
form.add_error(field, u'{}: {}'.format(name, ', '.join(e)))

View File

@ -13,6 +13,8 @@ from django.template import Template, Context
from django.utils.encoding import python_2_unicode_compatible
from django.utils.safestring import mark_safe
from utilities.utils import foreground_color
CUSTOMFIELD_MODELS = (
'site', 'rack', 'devicetype', 'device', # DCIM
@ -316,7 +318,7 @@ class TopologyMap(models.Model):
def render(self, img_format='png'):
from circuits.models import CircuitTermination
from dcim.models import Device, InterfaceConnection
from dcim.models import CONNECTION_STATUS_CONNECTED, Device, InterfaceConnection
# Construct the graph
graph = graphviz.Graph()
@ -336,8 +338,9 @@ class TopologyMap(models.Model):
for query in device_set.split(';'): # Split regexes on semicolons
devices += Device.objects.filter(name__regex=query).select_related('device_role')
for d in devices:
fillcolor = '#{}'.format(d.device_role.color)
subgraph.node(d.name, style='filled', fillcolor=fillcolor)
bg_color = '#{}'.format(d.device_role.color)
fg_color = '#{}'.format(foreground_color(d.device_role.color))
subgraph.node(d.name, style='filled', fillcolor=bg_color, fontcolor=fg_color, fontname='sans')
# Add an invisible connection to each successive device in a set to enforce horizontal order
for j in range(0, len(devices) - 1):
@ -357,7 +360,8 @@ class TopologyMap(models.Model):
interface_a__device__in=devices, interface_b__device__in=devices
)
for c in connections:
graph.edge(c.interface_a.device.name, c.interface_b.device.name)
style = 'solid' if c.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
graph.edge(c.interface_a.device.name, c.interface_b.device.name, style=style)
# Add all circuits to the graph
for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices):

View File

@ -3,10 +3,11 @@ from django.db.models import Count
from dcim.models import Site, Rack, Device, Interface
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, CSVDataField, ExpandableIPAddressField,
FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice,
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, ChainedModelChoiceField, CSVDataField,
ExpandableIPAddressField, FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice,
)
from .models import (
@ -32,11 +33,11 @@ IPADDRESS_MASK_LENGTH_CHOICES = PREFIX_MASK_LENGTH_CHOICES + [(128, 128)]
# VRFs
#
class VRFForm(BootstrapMixin, CustomFieldForm):
class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class Meta:
model = VRF
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
fields = ['name', 'rd', 'enforce_unique', 'description', 'tenant_group', 'tenant']
labels = {
'rd': "RD",
}
@ -163,30 +164,27 @@ class RoleForm(BootstrapMixin, forms.ModelForm):
# Prefixes
#
class PrefixForm(BootstrapMixin, CustomFieldForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
widget=forms.Select(attrs={'filter-for': 'vlan', 'nullable': 'true'}))
vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN',
widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}',
display_field='display_name'))
class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select(
attrs={'filter-for': 'vlan', 'nullable': 'true'}
)
)
vlan = ChainedModelChoiceField(
queryset=VLAN.objects.all(), chains={'site': 'site'}, required=False, label='VLAN', widget=APISelect(
api_url='/api/ipam/vlans/?site_id={{site}}', display_field='display_name'
)
)
class Meta:
model = Prefix
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan', 'status', 'role', 'is_pool', 'description']
fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant']
def __init__(self, *args, **kwargs):
super(PrefixForm, self).__init__(*args, **kwargs)
self.fields['vrf'].empty_label = 'Global'
# Initialize field without choices to avoid pulling all VLANs from the database
if self.is_bound and self.data.get('site'):
self.fields['vlan'].queryset = VLAN.objects.filter(site__pk=self.data['site'])
elif self.initial.get('site'):
self.fields['vlan'].queryset = VLAN.objects.filter(site=self.initial['site'])
else:
self.fields['vlan'].queryset = VLAN.objects.filter(site=None)
class PrefixFromCSVForm(forms.ModelForm):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
@ -214,7 +212,6 @@ class PrefixFromCSVForm(forms.ModelForm):
vlan_group_name = self.cleaned_data.get('vlan_group_name')
vlan_vid = self.cleaned_data.get('vlan_vid')
vlan_group = None
vlan = None
# Validate VLAN group
if vlan_group_name:
@ -310,85 +307,123 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
# IP addresses
#
class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm):
class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm):
interface_site = forms.ModelChoiceField(
queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select(
queryset=Site.objects.all(),
required=False,
label='Site',
widget=forms.Select(
attrs={'filter-for': 'interface_rack'}
)
)
interface_rack = forms.ModelChoiceField(
queryset=Rack.objects.all(), required=False, label='Rack', widget=APISelect(
api_url='/api/dcim/racks/?site_id={{interface_site}}', display_field='display_name',
interface_rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains={'site': 'interface_site'},
required=False,
label='Rack',
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{interface_site}}',
display_field='display_name',
attrs={'filter-for': 'interface_device', 'nullable': 'true'}
)
)
interface_device = forms.ModelChoiceField(
queryset=Device.objects.all(), required=False, label='Device', widget=APISelect(
interface_device = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains={'site': 'interface_site', 'rack': 'interface_rack'},
required=False,
label='Device',
widget=APISelect(
api_url='/api/dcim/devices/?site_id={{interface_site}}&rack_id={{interface_rack}}',
display_field='display_name', attrs={'filter-for': 'interface'}
display_field='display_name',
attrs={'filter-for': 'interface'}
)
)
interface = ChainedModelChoiceField(
queryset=Interface.objects.all(),
chains={'device': 'interface_device'},
required=False,
widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{interface_device}}'
)
)
nat_site = forms.ModelChoiceField(
queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select(
queryset=Site.objects.all(),
required=False,
label='Site',
widget=forms.Select(
attrs={'filter-for': 'nat_device'}
)
)
nat_device = forms.ModelChoiceField(
queryset=Device.objects.all(), required=False, label='Device', widget=APISelect(
api_url='/api/dcim/devices/?site_id={{nat_site}}', display_field='display_name',
nat_rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains={'site': 'nat_site'},
required=False,
label='Rack',
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{interface_site}}',
display_field='display_name',
attrs={'filter-for': 'nat_device', 'nullable': 'true'}
)
)
nat_device = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains={'site': 'nat_site'},
required=False,
label='Device',
widget=APISelect(
api_url='/api/dcim/devices/?site_id={{nat_site}}',
display_field='display_name',
attrs={'filter-for': 'nat_inside'}
)
)
nat_inside = ChainedModelChoiceField(
queryset=IPAddress.objects.all(),
chains={'interface__device': 'nat_device'},
required=False,
label='IP Address',
widget=APISelect(
api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}',
display_field='address'
)
)
livesearch = forms.CharField(
required=False, label='IP Address', widget=Livesearch(
query_key='q', query_url='ipam-api:ipaddress-list', field_to_update='nat_inside', obj_label='address'
required=False,
label='IP Address',
widget=Livesearch(
query_key='q',
query_url='ipam-api:ipaddress-list',
field_to_update='nat_inside',
obj_label='address'
)
)
primary_for_device = forms.BooleanField(required=False, label='Make this the primary IP for the device')
class Meta:
model = IPAddress
fields = ['address', 'vrf', 'tenant', 'status', 'description', 'interface', 'primary_for_device', 'nat_inside']
widgets = {
'interface': APISelect(api_url='/api/dcim/interfaces/?device_id={{interface_device}}'),
'nat_inside': APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}', display_field='address')
}
fields = [
'address', 'vrf', 'status', 'description', 'interface', 'primary_for_device', 'nat_inside', 'tenant_group',
'tenant',
]
def __init__(self, *args, **kwargs):
# Initialize helper selectors
instance = kwargs.get('instance')
initial = kwargs.get('initial', {})
if instance and instance.interface is not None:
initial['interface_site'] = instance.interface.device.site
initial['interface_rack'] = instance.interface.device.rack
initial['interface_device'] = instance.interface.device
if instance and instance.nat_inside is not None:
initial['nat_site'] = instance.nat_inside.device.site
initial['nat_rack'] = instance.nat_inside.device.rack
initial['nat_device'] = instance.nat_inside.device
kwargs['initial'] = initial
super(IPAddressForm, self).__init__(*args, **kwargs)
self.fields['vrf'].empty_label = 'Global'
# If an interface has been assigned, initialize site, rack, and device
if self.instance.interface:
self.initial['interface_site'] = self.instance.interface.device.site
self.initial['interface_rack'] = self.instance.interface.device.rack
self.initial['interface_device'] = self.instance.interface.device
# Limit rack choices
if self.is_bound and self.data.get('interface_site'):
self.fields['interface_rack'].queryset = Rack.objects.filter(site__pk=self.data['interface_site'])
elif self.initial.get('interface_site'):
self.fields['interface_rack'].queryset = Rack.objects.filter(site=self.initial['interface_site'])
else:
self.fields['interface_rack'].choices = []
# Limit device choices
if self.is_bound and self.data.get('interface_rack'):
self.fields['interface_device'].queryset = Device.objects.filter(rack=self.data['interface_rack'])
elif self.initial.get('interface_rack'):
self.fields['interface_device'].queryset = Device.objects.filter(rack=self.initial['interface_rack'])
else:
self.fields['interface_device'].choices = []
# Limit interface choices
if self.is_bound and self.data.get('interface_device'):
self.fields['interface'].queryset = Interface.objects.filter(device=self.data['interface_device'])
elif self.initial.get('interface_device'):
self.fields['interface'].queryset = Interface.objects.filter(device=self.initial['interface_device'])
else:
self.fields['interface'].choices = []
# Initialize primary_for_device if IP address is already assigned
if self.instance.interface is not None:
device = self.instance.interface.device
@ -398,38 +433,6 @@ class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm):
):
self.initial['primary_for_device'] = True
if self.instance.nat_inside:
nat_inside = self.instance.nat_inside
# If the IP is assigned to an interface, populate site/device fields accordingly
if self.instance.nat_inside.interface:
self.initial['nat_site'] = self.instance.nat_inside.interface.device.site.pk
self.initial['nat_device'] = self.instance.nat_inside.interface.device.pk
self.fields['nat_device'].queryset = Device.objects.filter(
site=nat_inside.interface.device.site
)
self.fields['nat_inside'].queryset = IPAddress.objects.filter(
interface__device=nat_inside.interface.device
)
else:
self.fields['nat_inside'].queryset = IPAddress.objects.filter(pk=nat_inside.pk)
else:
# Initialize nat_device choices if nat_site is set
if self.is_bound and self.data.get('nat_site'):
self.fields['nat_device'].queryset = Device.objects.filter(site__pk=self.data['nat_site'])
elif self.initial.get('nat_site'):
self.fields['nat_device'].queryset = Device.objects.filter(site=self.initial['nat_site'])
else:
self.fields['nat_device'].choices = []
# Initialize nat_inside choices if nat_device is set
if self.is_bound and self.data.get('nat_device'):
self.fields['nat_inside'].queryset = IPAddress.objects.filter(
interface__device__pk=self.data['nat_device'])
elif self.initial.get('nat_device'):
self.fields['nat_inside'].queryset = IPAddress.objects.filter(
interface__device__pk=self.initial['nat_device'])
else:
self.fields['nat_inside'].choices = []
def clean(self):
super(IPAddressForm, self).clean()
@ -468,15 +471,19 @@ class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm):
return ipaddress
class IPAddressBulkAddForm(BootstrapMixin, CustomFieldForm):
address_pattern = ExpandableIPAddressField(label='Address Pattern')
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF', empty_label='Global')
class IPAddressPatternForm(BootstrapMixin, forms.Form):
pattern = ExpandableIPAddressField(label='Address pattern')
pattern_map = ('address_pattern', 'address')
class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class Meta:
model = IPAddress
fields = ['address_pattern', 'vrf', 'tenant', 'status', 'description']
fields = ['address', 'status', 'vrf', 'description', 'tenant_group', 'tenant']
def __init__(self, *args, **kwargs):
super(IPAddressBulkAddForm, self).__init__(*args, **kwargs)
self.fields['vrf'].empty_label = 'Global'
class IPAddressFromCSVForm(forms.ModelForm):
@ -602,14 +609,26 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
# VLANs
#
class VLANForm(BootstrapMixin, CustomFieldForm):
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, label='Group', widget=APISelect(
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
))
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
widget=forms.Select(
attrs={'filter-for': 'group', 'nullable': 'true'}
)
)
group = ChainedModelChoiceField(
queryset=VLANGroup.objects.all(),
chains={'site': 'site'},
required=False,
label='Group',
widget=APISelect(
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
)
)
class Meta:
model = VLAN
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
fields = ['site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant']
help_texts = {
'site': "Leave blank if this VLAN spans multiple sites",
'group': "VLAN group (optional)",
@ -618,21 +637,6 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
'status': "Operational status of this VLAN",
'role': "The primary function of this VLAN",
}
widgets = {
'site': forms.Select(attrs={'filter-for': 'group', 'nullable': 'true'}),
}
def __init__(self, *args, **kwargs):
super(VLANForm, self).__init__(*args, **kwargs)
# Limit VLAN group choices
if self.is_bound and self.data.get('site'):
self.fields['group'].queryset = VLANGroup.objects.filter(site__pk=self.data['site'])
elif self.initial.get('site'):
self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
else:
self.fields['group'].queryset = VLANGroup.objects.filter(site=None)
class VLANFromCSVForm(forms.ModelForm):
@ -663,7 +667,7 @@ class VLANFromCSVForm(forms.ModelForm):
group_name = self.cleaned_data.get('group_name')
if group_name:
try:
vlan_group = VLANGroup.objects.get(site=self.cleaned_data.get('site'), name=group_name)
VLANGroup.objects.get(site=self.cleaned_data.get('site'), name=group_name)
except VLANGroup.DoesNotExist:
self.add_error('group_name', "Invalid VLAN group {}.".format(group_name))
@ -697,7 +701,7 @@ class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
description = forms.CharField(max_length=100, required=False)
class Meta:
nullable_fields = ['group', 'tenant', 'role', 'description']
nullable_fields = ['site', 'group', 'tenant', 'role', 'description']
def vlan_status_choices():

View File

@ -538,7 +538,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
verbose_name_plural = 'VLANs'
def __str__(self):
return self.display_name
return self.display_name or super(VLAN, self).__str__()
def get_absolute_url(self):
return reverse('ipam:vlan', args=[self.pk])
@ -565,7 +565,9 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
@property
def display_name(self):
return u'{} ({})'.format(self.vid, self.name)
if self.vid and self.name:
return u"{} ({})".format(self.vid, self.name)
return None
def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status]

View File

@ -76,6 +76,15 @@ IPADDRESS_LINK = """
{% endif %}
"""
IPADDRESS_DEVICE = """
{% if record.interface %}
<a href="{{ record.interface.device.get_absolute_url }}">{{ record.interface.device }}</a>
({{ record.interface.name }})
{% else %}
&mdash;
{% endif %}
"""
VRF_LINK = """
{% if record.vrf %}
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
@ -281,12 +290,14 @@ class IPAddressTable(BaseTable):
status = tables.TemplateColumn(STATUS_LABEL)
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
tenant = tables.TemplateColumn(TENANT_LINK)
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False)
interface = tables.Column(orderable=False)
nat_inside = tables.LinkColumn(
'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)'
)
device = tables.TemplateColumn(IPADDRESS_DEVICE, orderable=False)
class Meta(BaseTable.Meta):
model = IPAddress
fields = ('pk', 'address', 'status', 'vrf', 'tenant', 'device', 'interface', 'description')
fields = ('pk', 'address', 'status', 'vrf', 'tenant', 'nat_inside', 'device', 'description')
row_attrs = {
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
}

View File

@ -2,15 +2,12 @@ from django_tables2 import RequestConfig
import netaddr
from django.conf import settings
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib import messages
from django.db.models import Count, Q
from django.shortcuts import get_object_or_404, redirect, render
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from dcim.models import Device
from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator
from utilities.views import (
BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
@ -536,7 +533,7 @@ def prefix_ipaddresses(request, pk):
#
class IPAddressListView(ObjectListView):
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device')
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')
filter = filters.IPAddressFilter
filter_form = forms.IPAddressFilterForm
table = tables.IPAddressTable
@ -587,8 +584,9 @@ class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
permission_required = 'ipam.add_ipaddress'
form = forms.IPAddressBulkAddForm
model_form = forms.IPAddressForm
pattern_form = forms.IPAddressPatternForm
model_form = forms.IPAddressBulkAddForm
pattern_target = 'address'
template_name = 'ipam/ipaddress_bulk_add.html'
default_return_url = 'ipam:ipaddress_list'

View File

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

View File

@ -36,7 +36,7 @@ SEARCH_TYPES = {
'url': 'circuits:provider_list',
},
'circuit': {
'queryset': Circuit.objects.select_related('type', 'provider', 'tenant'),
'queryset': Circuit.objects.select_related('type', 'provider', 'tenant').prefetch_related('terminations__site'),
'filter': CircuitFilter,
'table': CircuitSearchTable,
'url': 'circuits:circuit_list',

View File

@ -44,6 +44,7 @@ class SecretTable(BaseTable):
class SecretSearchTable(SearchTable):
device = tables.LinkColumn()
class Meta(SearchTable.Meta):
model = Secret

View File

@ -8,12 +8,18 @@
{% render_field form.provider %}
{% render_field form.cid %}
{% render_field form.type %}
{% render_field form.tenant %}
{% render_field form.install_date %}
{% render_field form.commit_rate %}
{% render_field form.description %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Tenancy</strong></div>
<div class="panel-body">
{% render_field form.tenant_group %}
{% render_field form.tenant %}
</div>
</div>
{% if form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>

View File

@ -7,7 +7,6 @@
<div class="panel-body">
{% render_field form.name %}
{% render_field form.device_role %}
{% render_field form.tenant %}
</div>
</div>
<div class="panel panel-default">
@ -63,6 +62,13 @@
{% endif %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Tenancy</strong></div>
<div class="panel-body">
{% render_field form.tenant_group %}
{% render_field form.tenant %}
</div>
</div>
{% if form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>

View File

@ -31,24 +31,83 @@
{% block javascript %}
<script type="text/javascript">
$(document).ready(function() {
var site_list = $('#id_site');
var rack_group_list = $('#id_rack_group_id');
var rack_list = $('#id_rack_id');
var manufacturer_list = $('#id_manufacturer_id');
var model_list = $('#id_device_type_id');
$('#id_manufacturer_id').change(function() {
model_list.empty();
// Update device type options based on selected manufacturer
manufacturer_list.change(function() {
var selected_manufacturers = $(this).val();
if (selected_manufacturers) {
var api_url = netbox_api_path + 'dcim/device-types/?manufacturer_id=' + selected_manufacturers.join('&manufacturer_id=');
model_list.empty();
$.ajax({
url: api_url,
url: netbox_api_path + 'dcim/device-types/?limit=500&manufacturer_id=' + selected_manufacturers.join('&manufacturer_id='),
dataType: 'json',
success: function (response, status) {
$.each(response, function (index, device_type) {
var option = $("<option></option>").attr("value", device_type.id).text(device_type["model"] + " (" + device_type["instance_count"] + ")");
$.each(response["results"], function (index, device_type) {
var option = $("<option></option>").attr("value", device_type.id).text(device_type.model + " (" + device_type.instance_count + ")");
model_list.append(option);
});
}
});
}
});
// Update rack group and rack options based on selected site
site_list.change(function() {
var selected_sites = $(this).val();
if (selected_sites) {
// Update rack group options
rack_group_list.empty();
$.ajax({
url: netbox_api_path + 'dcim/rack-groups/?limit=500&site=' + selected_sites.join('&site='),
dataType: 'json',
success: function (response, status) {
$.each(response["results"], function (index, group) {
var option = $("<option></option>").attr("value", group.id).text(group.name);
rack_group_list.append(option);
});
}
});
// Update rack options
rack_list.empty();
$.ajax({
url: netbox_api_path + 'dcim/racks/?limit=500&site=' + selected_sites.join('&site='),
dataType: 'json',
success: function (response, status) {
$.each(response["results"], function (index, rack) {
var option = $("<option></option>").attr("value", rack.id).text(rack.display_name);
rack_list.append(option);
});
}
});
}
});
// Update rack options based on selected rack group
rack_group_list.change(function() {
var selected_rack_groups = $(this).val();
if (selected_rack_groups) {
rack_list.empty();
$.ajax({
url: netbox_api_path + 'dcim/racks/?limit=500&group_id=' + selected_rack_groups.join('&group_id='),
dataType: 'json',
success: function (response, status) {
$.each(response["results"], function (index, rack) {
var option = $("<option></option>").attr("value", rack.id).text(rack.display_name);
rack_list.append(option);
});
}
});
}
});
});
</script>
{% endblock %}

View File

@ -134,7 +134,7 @@
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
</td>
<td class="text-right">
{% if perms.ipam.edit_ipaddress %}
{% if perms.ipam.change_ipaddress %}
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
</a>

View File

@ -6,11 +6,22 @@
<div class="panel-heading"><strong>Rack</strong></div>
<div class="panel-body">
{% render_field form.site %}
{% render_field form.group %}
{% render_field form.name %}
{% render_field form.facility_id %}
{% render_field form.tenant %}
{% render_field form.group %}
{% render_field form.role %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Tenancy</strong></div>
<div class="panel-body">
{% render_field form.tenant_group %}
{% render_field form.tenant %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Dimensions</strong></div>
<div class="panel-body">
{% render_field form.type %}
{% render_field form.width %}
{% render_field form.u_height %}

View File

@ -14,7 +14,7 @@
{% for rack in page %}
<div style="display: inline-block; width: 266px">
<div class="rack_header">
<h4>{{ rack.name }}</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 %}
@ -23,7 +23,7 @@
{% endif %}
<div class="clearfix"></div>
<div class="rack_header">
<h4>{{ rack.name }}</h4>
<h4><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></h4>
</div>
</div>
{% endfor %}

View File

@ -8,11 +8,17 @@
{% render_field form.name %}
{% render_field form.slug %}
{% render_field form.region %}
{% render_field form.tenant %}
{% render_field form.facility %}
{% render_field form.asn %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Tenancy</strong></div>
<div class="panel-body">
{% render_field form.tenant_group %}
{% render_field form.tenant %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Contact Info</strong></div>
<div class="panel-body">

View File

@ -12,18 +12,24 @@
<div class="panel panel-default">
<div class="panel-heading"><strong>IP Addresses</strong></div>
<div class="panel-body">
{% render_field form.address_pattern %}
{% render_field form.vrf %}
{% render_field form.tenant %}
{% render_field form.status %}
{% render_field form.description %}
{% render_field pattern_form.pattern %}
{% render_field model_form.status %}
{% render_field model_form.vrf %}
{% render_field model_form.description %}
</div>
</div>
{% if form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Tenancy</strong></div>
<div class="panel-body">
{% render_field model_form.tenant_group %}
{% render_field model_form.tenant %}
</div>
</div>
{% if model_form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>
<div class="panel-body">
{% render_custom_fields form %}
{% render_custom_fields model_form %}
</div>
</div>
{% endif %}

View File

@ -13,12 +13,18 @@
<div class="panel-heading"><strong>IP Address</strong></div>
<div class="panel-body">
{% render_field form.address %}
{% render_field form.vrf %}
{% render_field form.tenant %}
{% render_field form.status %}
{% render_field form.vrf %}
{% render_field form.description %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Tenancy</strong></div>
<div class="panel-body">
{% render_field form.tenant_group %}
{% render_field form.tenant %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Interface Assignment</strong>

View File

@ -6,14 +6,20 @@
<div class="panel-heading"><strong>Prefix</strong></div>
<div class="panel-body">
{% render_field form.prefix %}
{% render_field form.status %}
{% render_field form.vrf %}
{% render_field form.tenant %}
{% render_field form.site %}
{% render_field form.vlan %}
{% render_field form.status %}
{% render_field form.role %}
{% render_field form.is_pool %}
{% render_field form.description %}
{% render_field form.is_pool %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Tenancy</strong></div>
<div class="panel-body">
{% render_field form.tenant_group %}
{% render_field form.tenant %}
</div>
</div>
{% if form.custom_fields %}

View File

@ -9,12 +9,18 @@
{% render_field form.group %}
{% render_field form.vid %}
{% render_field form.name %}
{% render_field form.tenant %}
{% render_field form.status %}
{% render_field form.role %}
{% render_field form.description %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Tenancy</strong></div>
<div class="panel-body">
{% render_field form.tenant_group %}
{% render_field form.tenant %}
</div>
</div>
{% if form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>

View File

@ -7,11 +7,17 @@
<div class="panel-body">
{% render_field form.name %}
{% render_field form.rd %}
{% render_field form.tenant %}
{% render_field form.enforce_unique %}
{% render_field form.description %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Tenancy</strong></div>
<div class="panel-body">
{% render_field form.tenant_group %}
{% render_field form.tenant %}
</div>
</div>
{% if form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>

View File

@ -7,7 +7,7 @@
<nav>
<ul class="pagination pull-right">
{% if table.page.has_previous %}
<li><a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}">&laquo;</a></li>
<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 %}
@ -17,18 +17,20 @@
{% endif %}
{% endfor %}
{% if table.page.has_next %}
<li><a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}">&raquo;</a></li>
<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">
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 %}
{% 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

@ -2,8 +2,10 @@ from django import forms
from django.db.models import Count
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from utilities.forms import BootstrapMixin, BulkImportForm, CommentField, CSVDataField, FilterChoiceField, SlugField
from utilities.forms import (
APISelect, BootstrapMixin, BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField,
FilterChoiceField, SlugField,
)
from .models import Tenant, TenantGroup
@ -61,3 +63,36 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
to_field_name='slug',
null_option=(0, 'None')
)
#
# Tenancy form extension
#
class TenancyForm(ChainedFieldsMixin, forms.Form):
tenant_group = forms.ModelChoiceField(
queryset=TenantGroup.objects.all(),
required=False,
widget=forms.Select(
attrs={'filter-for': 'tenant', 'nullable': 'true'}
)
)
tenant = ChainedModelChoiceField(
queryset=Tenant.objects.all(),
chains={'group': 'tenant_group'},
required=False,
widget=APISelect(
api_url='/api/tenancy/tenants/?group_id={{tenant_group}}'
)
)
def __init__(self, *args, **kwargs):
# Initialize helper selector
instance = kwargs.get('instance')
if instance and instance.tenant is not None:
initial = kwargs.get('initial', {})
initial['tenant_group'] = instance.tenant.group
kwargs['initial'] = initial
super(TenancyForm, self).__init__(*args, **kwargs)

View File

@ -44,6 +44,7 @@ class TenantTable(BaseTable):
class TenantSearchTable(SearchTable):
name = tables.LinkColumn()
class Meta(SearchTable.Meta):
model = Tenant

View File

@ -216,10 +216,13 @@ class TokenEditView(LoginRequiredMixin, View):
token.user = request.user
token.save()
msg = "Token updated" if pk else "New token created"
msg = "Modified token {}".format(token) if pk else "Created token {}".format(token)
messages.success(request, msg)
return redirect('user:token_list')
if '_addanother' in request.POST:
return redirect(request.path)
else:
return redirect('user:token_list')
class TokenDeleteView(LoginRequiredMixin, View):

View File

@ -331,6 +331,25 @@ class FlexibleModelChoiceField(forms.ModelChoiceField):
return value
class ChainedModelChoiceField(forms.ModelChoiceField):
"""
A ModelChoiceField which is initialized based on the values of other fields within a form. `chains` is a dictionary
mapping of model fields to peer fields within the form. For example:
country1 = forms.ModelChoiceField(queryset=Country.objects.all())
city1 = ChainedModelChoiceField(queryset=City.objects.all(), chains={'country': 'country1'}
The queryset of the `city1` field will be modified as
.filter(country=<value>)
where <value> is the value of the `country1` field. (Note: The form must inherit from ChainedFieldsMixin.)
"""
def __init__(self, chains=None, *args, **kwargs):
self.chains = chains
super(ChainedModelChoiceField, self).__init__(*args, **kwargs)
class SlugField(forms.SlugField):
def __init__(self, slug_source='name', *args, **kwargs):
@ -411,6 +430,32 @@ class BootstrapMixin(forms.BaseForm):
field.widget.attrs['placeholder'] = field.label
class ChainedFieldsMixin(forms.BaseForm):
"""
Iterate through all ChainedModelChoiceFields in the form and modify their querysets based on chained fields.
"""
def __init__(self, *args, **kwargs):
super(ChainedFieldsMixin, self).__init__(*args, **kwargs)
for field_name, field in self.fields.items():
if isinstance(field, ChainedModelChoiceField):
filters_dict = {}
for db_field, parent_field in field.chains.items():
if self.is_bound:
filters_dict[db_field] = self.data.get(parent_field) or None
elif self.initial.get(parent_field):
filters_dict[db_field] = self.initial[parent_field]
else:
filters_dict[db_field] = None
if filters_dict:
field.queryset = field.queryset.filter(**filters_dict)
else:
field.queryset = field.queryset.none()
class ReturnURLForm(forms.Form):
"""
Provides a hidden return URL field to control where the user is directed after the form is submitted.

View File

@ -24,3 +24,15 @@ def csv_format(data):
csv.append(u'{}'.format(value))
return u','.join(csv)
def foreground_color(bg_color):
"""
Return the ideal foreground color (black or white) for a given background color in hexadecimal RGB format.
"""
bg_color = bg_color.strip('#')
r, g, b = [int(bg_color[c:c + 2], 16) for c in (0, 2, 4)]
if r * 0.299 + g * 0.587 + b * 0.114 > 186:
return '000000'
else:
return 'ffffff'

View File

@ -290,66 +290,78 @@ class BulkAddView(View):
"""
Create new objects in bulk.
form: Form class
pattern_form: Form class which provides the `pattern` field
model_form: The ModelForm used to create individual objects
template_name: The name of the template
default_return_url: Name of the URL to which the user is redirected after creating the objects
"""
form = None
pattern_form = None
model_form = None
pattern_target = ''
template_name = None
default_return_url = 'home'
def get(self, request):
form = self.form()
pattern_form = self.pattern_form()
model_form = self.model_form()
return render(request, self.template_name, {
'obj_type': self.model_form._meta.model._meta.verbose_name,
'form': form,
'pattern_form': pattern_form,
'model_form': model_form,
'return_url': reverse(self.default_return_url),
})
def post(self, request):
model = self.model_form._meta.model
form = self.form(request.POST)
if form.is_valid():
pattern_form = self.pattern_form(request.POST)
model_form = self.model_form(request.POST)
# Read the pattern field and target from the form's pattern_map
pattern_field, pattern_target = form.pattern_map
pattern = form.cleaned_data[pattern_field]
model_form_data = form.cleaned_data
if pattern_form.is_valid():
pattern = pattern_form.cleaned_data['pattern']
new_objs = []
try:
with transaction.atomic():
# Validate and save each object individually
# Create objects from the expanded. Abort the transaction on the first validation error.
for value in pattern:
model_form_data[pattern_target] = value
model_form = self.model_form(model_form_data)
# Reinstantiate the model form each time to avoid overwriting the same instance. Use a mutable
# copy of the POST QueryDict so that we can update the target field value.
model_form = self.model_form(request.POST.copy())
model_form.data[self.pattern_target] = value
# Validate each new object independently.
if model_form.is_valid():
obj = model_form.save()
new_objs.append(obj)
else:
for error in model_form.errors.as_data().values():
form.add_error(None, error)
# Abort the creation of all objects if errors exist
if form.errors:
raise ValidationError("Validation of one or more model forms failed.")
except ValidationError:
# Copy any errors on the pattern target field to the pattern form.
errors = model_form.errors.as_data()
if errors.get(self.pattern_target):
pattern_form.add_error('pattern', errors[self.pattern_target])
# Raise an IntegrityError to break the for loop and abort the transaction.
raise IntegrityError()
# If we make it to this point, validation has succeeded on all new objects.
msg = u"Added {} {}".format(len(new_objs), model._meta.verbose_name_plural)
messages.success(request, msg)
UserAction.objects.log_bulk_create(request.user, ContentType.objects.get_for_model(model), msg)
if '_addanother' in request.POST:
return redirect(request.path)
return redirect(self.default_return_url)
except IntegrityError:
pass
if not form.errors:
msg = u"Added {} {}".format(len(new_objs), model._meta.verbose_name_plural)
messages.success(request, msg)
UserAction.objects.log_bulk_create(request.user, ContentType.objects.get_for_model(model), msg)
if '_addanother' in request.POST:
return redirect(request.path)
return redirect(self.default_return_url)
return render(request, self.template_name, {
'form': form,
'pattern_form': pattern_form,
'model_form': model_form,
'obj_type': model._meta.verbose_name,
'return_url': reverse(self.default_return_url),
})