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

Merge branch 'import_headers' into develop

This commit is contained in:
Jeremy Stretch
2017-06-07 15:54:59 -04:00
35 changed files with 838 additions and 1516 deletions

View File

@ -8,8 +8,8 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
APISelect, BootstrapMixin, BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField,
FilterChoiceField, Livesearch, SmallTextarea, SlugField,
APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField,
SmallTextarea, SlugField,
)
from .models import Circuit, CircuitTermination, CircuitType, Provider
@ -39,15 +39,18 @@ class ProviderForm(BootstrapMixin, CustomFieldForm):
}
class ProviderFromCSVForm(forms.ModelForm):
class ProviderCSVForm(forms.ModelForm):
slug = SlugField()
class Meta:
model = Provider
fields = ['name', 'slug', 'asn', 'account', 'portal_url']
class ProviderImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=ProviderFromCSVForm)
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'comments']
help_texts = {
'name': 'Provider name',
'asn': '32-bit autonomous system number',
'portal_url': 'Portal URL',
'comments': 'Free-form comments',
}
class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@ -102,21 +105,36 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
}
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 CircuitCSVForm(forms.ModelForm):
provider = forms.ModelChoiceField(
queryset=Provider.objects.all(),
to_field_name='name',
help_text='Name of parent provider',
error_messages={
'invalid_choice': 'Provider not found.'
}
)
type = forms.ModelChoiceField(
queryset=CircuitType.objects.all(),
to_field_name='name',
help_text='Type of circuit',
error_messages={
'invalid_choice': 'Invalid circuit type.'
}
)
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text='Name of assigned tenant',
error_messages={
'invalid_choice': 'Tenant not found.'
}
)
class Meta:
model = Circuit
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description']
class CircuitImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=CircuitFromCSVForm)
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments']
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):

View File

@ -65,9 +65,8 @@ class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'circuits.add_provider'
form = forms.ProviderImportForm
model_form = forms.ProviderCSVForm
table = tables.ProviderTable
template_name = 'circuits/provider_import.html'
default_return_url = 'circuits:provider_list'
@ -163,9 +162,8 @@ class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'circuits.add_circuit'
form = forms.CircuitImportForm
model_form = forms.CircuitCSVForm
table = tables.CircuitTable
template_name = 'circuits/circuit_import.html'
default_return_url = 'circuits:circuit_list'

View File

@ -5,7 +5,6 @@ import re
from django import forms
from django.contrib.postgres.forms.array import SimpleArrayField
from django.core.exceptions import ValidationError
from django.db.models import Count, Q
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
@ -14,18 +13,18 @@ from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField, ExpandableNameField,
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVChoiceField, ExpandableNameField, FilterChoiceField,
FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
FilterTreeNodeMultipleChoiceField,
)
from .formfields import MACAddressFormField
from .models import (
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate,
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES,
SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, VIRTUAL_IFACE_TYPES,
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, ConsolePort,
ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, Interface,
IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_FACE_CHOICES,
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, RACK_WIDTH_19IN, RACK_WIDTH_23IN,
Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, VIRTUAL_IFACE_TYPES,
)
@ -50,14 +49,6 @@ def get_device_by_name_or_pk(name):
return device
def validate_connection_status(value):
"""
Custom validator for connection statuses. value must be either "planned" or "connected" (case-insensitive).
"""
if value.lower() not in ['planned', 'connected']:
raise ValidationError('Invalid connection status ({}); must be either "planned" or "connected".'.format(value))
class DeviceComponentForm(BootstrapMixin, forms.Form):
"""
Allow inclusion of the parent device as context for limiting field choices.
@ -107,27 +98,37 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
}
class SiteFromCSVForm(forms.ModelForm):
class SiteCSVForm(forms.ModelForm):
region = forms.ModelChoiceField(
Region.objects.all(), to_field_name='name', required=False, error_messages={
'invalid_choice': 'Tenant not found.'
queryset=Region.objects.all(),
required=False,
to_field_name='name',
help_text='Name of assigned region',
error_messages={
'invalid_choice': 'Region not found.',
}
)
tenant = forms.ModelChoiceField(
Tenant.objects.all(), to_field_name='name', required=False, error_messages={
'invalid_choice': 'Tenant not found.'
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text='Name of assigned tenant',
error_messages={
'invalid_choice': 'Tenant not found.',
}
)
class Meta:
model = Site
fields = [
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email',
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
'contact_name', 'contact_phone', 'contact_email', 'comments',
]
class SiteImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=SiteFromCSVForm)
help_texts = {
'name': 'Site name',
'slug': 'URL-friendly slug',
'asn': '32-bit autonomous system number',
}
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@ -217,49 +218,73 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
}
class RackFromCSVForm(forms.ModelForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Site not found.'})
group_name = forms.CharField(required=False)
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
role = forms.ModelChoiceField(RackRole.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Role not found.'})
type = forms.CharField(required=False)
class RackCSVForm(forms.ModelForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
to_field_name='name',
help_text='Name of parent site',
error_messages={
'invalid_choice': 'Site not found.',
}
)
group_name = forms.CharField(
help_text='Name of rack group',
required=False
)
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text='Name of assigned tenant',
error_messages={
'invalid_choice': 'Tenant not found.',
}
)
role = forms.ModelChoiceField(
queryset=RackRole.objects.all(),
required=False,
to_field_name='name',
help_text='Name of assigned role',
error_messages={
'invalid_choice': 'Role not found.',
}
)
type = CSVChoiceField(
choices=RACK_TYPE_CHOICES,
required=False,
help_text='Rack type'
)
width = forms.ChoiceField(
choices = (
(RACK_WIDTH_19IN, '19'),
(RACK_WIDTH_23IN, '23'),
),
help_text='Rail-to-rail width (in inches)'
)
class Meta:
model = Rack
fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height',
'desc_units']
fields = [
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
]
help_texts = {
'name': 'Rack name',
'u_height': 'Height in rack units',
}
def clean(self):
super(RackCSVForm, self).clean()
site = self.cleaned_data.get('site')
group = self.cleaned_data.get('group_name')
group_name = self.cleaned_data.get('group_name')
# Validate rack group
if site and group:
if group_name:
try:
self.instance.group = RackGroup.objects.get(site=site, name=group)
self.instance.group = RackGroup.objects.get(site=site, name=group_name)
except RackGroup.DoesNotExist:
self.add_error('group_name', "Invalid rack group ({})".format(group))
def clean_type(self):
rack_type = self.cleaned_data['type']
if not rack_type:
return None
try:
choices = {v.lower(): k for k, v in RACK_TYPE_CHOICES}
return choices[rack_type.lower()]
except KeyError:
raise forms.ValidationError('Invalid rack type ({}). Valid choices are: {}.'.format(
rack_type,
', '.join({v: k for k, v in RACK_TYPE_CHOICES}),
))
class RackImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=RackFromCSVForm)
raise forms.ValidationError("Rack group {} not found for site {}".format(group_name, site))
class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@ -663,32 +688,60 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
self.initial['rack'] = self.instance.parent_bay.device.rack_id
class BaseDeviceFromCSVForm(forms.ModelForm):
class BaseDeviceCSVForm(forms.ModelForm):
device_role = forms.ModelChoiceField(
queryset=DeviceRole.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Invalid device role.'}
queryset=DeviceRole.objects.all(),
to_field_name='name',
help_text='Name of assigned role',
error_messages={
'invalid_choice': 'Invalid device role.',
}
)
tenant = forms.ModelChoiceField(
Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'}
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text='Name of assigned tenant',
error_messages={
'invalid_choice': 'Tenant not found.',
}
)
manufacturer = forms.ModelChoiceField(
queryset=Manufacturer.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Invalid manufacturer.'}
queryset=Manufacturer.objects.all(),
to_field_name='name',
help_text='Device type manufacturer',
error_messages={
'invalid_choice': 'Invalid manufacturer.',
}
)
model_name = forms.CharField(
help_text='Device type model name'
)
model_name = forms.CharField()
platform = forms.ModelChoiceField(
queryset=Platform.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid platform.'}
queryset=Platform.objects.all(),
required=False,
to_field_name='name',
help_text='Name of assigned platform',
error_messages={
'invalid_choice': 'Invalid platform.',
}
)
status = CSVChoiceField(
choices=STATUS_CHOICES,
help_text='Operational status of device'
)
status = forms.CharField()
class Meta:
fields = []
model = Device
help_texts = {
'name': 'Device name',
}
def clean(self):
super(BaseDeviceCSVForm, self).clean()
manufacturer = self.cleaned_data.get('manufacturer')
model_name = self.cleaned_data.get('model_name')
@ -697,70 +750,73 @@ class BaseDeviceFromCSVForm(forms.ModelForm):
try:
self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
except DeviceType.DoesNotExist:
self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
def clean_status(self):
status_choices = {s[1].lower(): s[0] for s in STATUS_CHOICES}
try:
return status_choices[self.cleaned_data['status'].lower()]
except KeyError:
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
raise forms.ValidationError("Device type {} {} not found".format(manufacturer, model_name))
class DeviceFromCSVForm(BaseDeviceFromCSVForm):
class DeviceCSVForm(BaseDeviceCSVForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(), to_field_name='name', error_messages={
queryset=Site.objects.all(),
to_field_name='name',
help_text='Name of parent site',
error_messages={
'invalid_choice': 'Invalid site name.',
}
)
rack_name = forms.CharField(required=False)
face = forms.CharField(required=False)
rack_group = forms.CharField(
required=False,
help_text='Parent rack\'s group (if any)'
)
rack_name = forms.CharField(
required=False,
help_text='Name of parent rack'
)
face = CSVChoiceField(
choices=RACK_FACE_CHOICES,
required=False,
help_text='Mounted rack face'
)
class Meta(BaseDeviceFromCSVForm.Meta):
class Meta(BaseDeviceCSVForm.Meta):
fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
'site', 'rack_name', 'position', 'face',
'site', 'rack_group', 'rack_name', 'position', 'face',
]
def clean(self):
super(DeviceFromCSVForm, self).clean()
super(DeviceCSVForm, self).clean()
site = self.cleaned_data.get('site')
rack_group = self.cleaned_data.get('rack_group')
rack_name = self.cleaned_data.get('rack_name')
# Validate rack
if site and rack_name:
if site and rack_group and rack_name:
try:
self.instance.rack = Rack.objects.get(site=site, name=rack_name)
self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name)
except Rack.DoesNotExist:
self.add_error('rack_name', "Invalid rack ({})".format(rack_name))
def clean_face(self):
face = self.cleaned_data['face']
if not face:
return None
try:
return {
'front': 0,
'rear': 1,
}[face.lower()]
except KeyError:
raise forms.ValidationError('Invalid rack face ({}); must be "front" or "rear".'.format(face))
raise forms.ValidationError("Rack {} not found in site {} group {}".format(rack_name, site, rack_group))
elif site and rack_name:
try:
self.instance.rack = Rack.objects.get(site=site, group__isnull=True, name=rack_name)
except Rack.DoesNotExist:
raise forms.ValidationError("Rack {} not found in site {} (no group)".format(rack_name, site))
class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
class ChildDeviceCSVForm(BaseDeviceCSVForm):
parent = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
required=False,
help_text='Name or ID of parent device',
error_messages={
'invalid_choice': 'Parent device not found.'
'invalid_choice': 'Parent device not found.',
}
)
device_bay_name = forms.CharField(required=False)
device_bay_name = forms.CharField(
help_text='Name of device bay',
)
class Meta(BaseDeviceFromCSVForm.Meta):
class Meta(BaseDeviceCSVForm.Meta):
fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
'parent', 'device_bay_name',
@ -768,7 +824,7 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
def clean(self):
super(ChildDeviceFromCSVForm, self).clean()
super(ChildDeviceCSVForm, self).clean()
parent = self.cleaned_data.get('parent')
device_bay_name = self.cleaned_data.get('device_bay_name')
@ -776,22 +832,12 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
# Validate device bay
if parent and device_bay_name:
try:
device_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
if device_bay.installed_device:
self.add_error('device_bay_name',
"Device bay ({} {}) is already occupied".format(parent, device_bay_name))
else:
self.instance.parent_bay = device_bay
self.instance.parent_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
# Inherit site and rack from parent device
self.instance.site = parent.site
self.instance.rack = parent.rack
except DeviceBay.DoesNotExist:
self.add_error('device_bay_name', "Parent device/bay ({} {}) not found".format(parent, device_bay_name))
class DeviceImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=DeviceFromCSVForm)
class ChildDeviceImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=ChildDeviceFromCSVForm)
raise forms.ValidationError("Parent device/bay ({} {}) not found".format(parent, device_bay_name))
class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@ -889,75 +935,84 @@ class ConsolePortCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name')
class ConsoleConnectionCSVForm(forms.Form):
class ConsoleConnectionCSVForm(forms.ModelForm):
console_server = FlexibleModelChoiceField(
queryset=Device.objects.filter(device_type__is_console_server=True),
to_field_name='name',
help_text='Console server name or ID',
error_messages={
'invalid_choice': 'Console server not found',
}
)
cs_port = forms.CharField()
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Device not found'})
console_port = forms.CharField()
status = forms.CharField(validators=[validate_connection_status])
cs_port = forms.CharField(
help_text='Console server port name'
)
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Device name or ID',
error_messages={
'invalid_choice': 'Device not found',
}
)
console_port = forms.CharField(
help_text='Console port name'
)
connection_status = CSVChoiceField(
choices=CONNECTION_STATUS_CHOICES,
help_text='Connection status'
)
def clean(self):
class Meta:
model = ConsolePort
fields = ['console_server', 'cs_port', 'device', 'console_port', 'connection_status']
# Validate console server port
if self.cleaned_data.get('console_server'):
try:
cs_port = ConsoleServerPort.objects.get(device=self.cleaned_data['console_server'],
name=self.cleaned_data['cs_port'])
if ConsolePort.objects.filter(cs_port=cs_port):
raise forms.ValidationError("Console server port is already occupied (by {} {})"
.format(cs_port.connected_console.device, cs_port.connected_console))
except ConsoleServerPort.DoesNotExist:
raise forms.ValidationError("Invalid console server port ({} {})"
.format(self.cleaned_data['console_server'], self.cleaned_data['cs_port']))
def clean_console_port(self):
# Validate console port
if self.cleaned_data.get('device'):
try:
console_port = ConsolePort.objects.get(device=self.cleaned_data['device'],
name=self.cleaned_data['console_port'])
if console_port.cs_port:
raise forms.ValidationError("Console port is already connected (to {} {})"
.format(console_port.cs_port.device, console_port.cs_port))
except ConsolePort.DoesNotExist:
raise forms.ValidationError("Invalid console port ({} {})"
.format(self.cleaned_data['device'], self.cleaned_data['console_port']))
console_port_name = self.cleaned_data.get('console_port')
if not self.cleaned_data.get('device') or not console_port_name:
return None
try:
# Retrieve console port by name
consoleport = ConsolePort.objects.get(
device=self.cleaned_data['device'], name=console_port_name
)
# Check if the console port is already connected
if consoleport.cs_port is not None:
raise forms.ValidationError("{} {} is already connected".format(
self.cleaned_data['device'], console_port_name
))
except ConsolePort.DoesNotExist:
raise forms.ValidationError("Invalid console port ({} {})".format(
self.cleaned_data['device'], console_port_name
))
class ConsoleConnectionImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=ConsoleConnectionCSVForm)
self.instance = consoleport
return consoleport
def clean(self):
records = self.cleaned_data.get('csv')
if not records:
return
def clean_cs_port(self):
connection_list = []
cs_port_name = self.cleaned_data.get('cs_port')
if not self.cleaned_data.get('console_server') or not cs_port_name:
return None
for i, record in enumerate(records, start=1):
form = self.fields['csv'].csv_form(data=record)
if form.is_valid():
console_port = ConsolePort.objects.get(device=form.cleaned_data['device'],
name=form.cleaned_data['console_port'])
console_port.cs_port = ConsoleServerPort.objects.get(device=form.cleaned_data['console_server'],
name=form.cleaned_data['cs_port'])
if form.cleaned_data['status'] == 'planned':
console_port.connection_status = CONNECTION_STATUS_PLANNED
else:
console_port.connection_status = CONNECTION_STATUS_CONNECTED
connection_list.append(console_port)
else:
for field, errors in form.errors.items():
for e in errors:
self.add_error('csv', "Record {} {}: {}".format(i, field, e))
try:
# Retrieve console server port by name
cs_port = ConsoleServerPort.objects.get(
device=self.cleaned_data['console_server'], name=cs_port_name
)
# Check if the console server port is already connected
if ConsolePort.objects.filter(cs_port=cs_port).count():
raise forms.ValidationError("{} {} is already connected".format(
self.cleaned_data['console_server'], cs_port_name
))
except ConsoleServerPort.DoesNotExist:
raise forms.ValidationError("Invalid console server port ({} {})".format(
self.cleaned_data['console_server'], cs_port_name
))
self.cleaned_data['csv'] = connection_list
return cs_port
class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
@ -1137,76 +1192,84 @@ class PowerPortCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name')
class PowerConnectionCSVForm(forms.Form):
class PowerConnectionCSVForm(forms.ModelForm):
pdu = FlexibleModelChoiceField(
queryset=Device.objects.filter(device_type__is_pdu=True),
to_field_name='name',
help_text='PDU name or ID',
error_messages={
'invalid_choice': 'PDU not found.',
}
)
power_outlet = forms.CharField()
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Device not found'})
power_port = forms.CharField()
status = forms.CharField(validators=[validate_connection_status])
power_outlet = forms.CharField(
help_text='Power outlet name'
)
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Device name or ID',
error_messages={
'invalid_choice': 'Device not found',
}
)
power_port = forms.CharField(
help_text='Power port name'
)
connection_status = CSVChoiceField(
choices=CONNECTION_STATUS_CHOICES,
help_text='Connection status'
)
def clean(self):
class Meta:
model = PowerPort
fields = ['pdu', 'power_outlet', 'device', 'power_port', 'connection_status']
# Validate power outlet
if self.cleaned_data.get('pdu'):
try:
power_outlet = PowerOutlet.objects.get(device=self.cleaned_data['pdu'],
name=self.cleaned_data['power_outlet'])
if PowerPort.objects.filter(power_outlet=power_outlet):
raise forms.ValidationError("Power outlet is already occupied (by {} {})"
.format(power_outlet.connected_port.device,
power_outlet.connected_port))
except PowerOutlet.DoesNotExist:
raise forms.ValidationError("Invalid PDU port ({} {})"
.format(self.cleaned_data['pdu'], self.cleaned_data['power_outlet']))
def clean_power_port(self):
# Validate power port
if self.cleaned_data.get('device'):
try:
power_port = PowerPort.objects.get(device=self.cleaned_data['device'],
name=self.cleaned_data['power_port'])
if power_port.power_outlet:
raise forms.ValidationError("Power port is already connected (to {} {})"
.format(power_port.power_outlet.device, power_port.power_outlet))
except PowerPort.DoesNotExist:
raise forms.ValidationError("Invalid power port ({} {})"
.format(self.cleaned_data['device'], self.cleaned_data['power_port']))
power_port_name = self.cleaned_data.get('power_port')
if not self.cleaned_data.get('device') or not power_port_name:
return None
try:
# Retrieve power port by name
powerport = PowerPort.objects.get(
device=self.cleaned_data['device'], name=power_port_name
)
# Check if the power port is already connected
if powerport.power_outlet is not None:
raise forms.ValidationError("{} {} is already connected".format(
self.cleaned_data['device'], power_port_name
))
except PowerPort.DoesNotExist:
raise forms.ValidationError("Invalid power port ({} {})".format(
self.cleaned_data['device'], power_port_name
))
class PowerConnectionImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=PowerConnectionCSVForm)
self.instance = powerport
return powerport
def clean(self):
records = self.cleaned_data.get('csv')
if not records:
return
def clean_power_outlet(self):
connection_list = []
power_outlet_name = self.cleaned_data.get('power_outlet')
if not self.cleaned_data.get('pdu') or not power_outlet_name:
return None
for i, record in enumerate(records, start=1):
form = self.fields['csv'].csv_form(data=record)
if form.is_valid():
power_port = PowerPort.objects.get(device=form.cleaned_data['device'],
name=form.cleaned_data['power_port'])
power_port.power_outlet = PowerOutlet.objects.get(device=form.cleaned_data['pdu'],
name=form.cleaned_data['power_outlet'])
if form.cleaned_data['status'] == 'planned':
power_port.connection_status = CONNECTION_STATUS_PLANNED
else:
power_port.connection_status = CONNECTION_STATUS_CONNECTED
connection_list.append(power_port)
else:
for field, errors in form.errors.items():
for e in errors:
self.add_error('csv', "Record {} {}: {}".format(i, field, e))
try:
# Retrieve power outlet by name
power_outlet = PowerOutlet.objects.get(
device=self.cleaned_data['pdu'], name=power_outlet_name
)
# Check if the power outlet is already connected
if PowerPort.objects.filter(power_outlet=power_outlet).count():
raise forms.ValidationError("{} {} is already connected".format(
self.cleaned_data['pdu'], power_outlet_name
))
except PowerOutlet.DoesNotExist:
raise forms.ValidationError("Invalid power outlet ({} {})".format(
self.cleaned_data['pdu'], power_outlet_name
))
self.cleaned_data['csv'] = connection_list
return power_outlet
class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
@ -1536,94 +1599,79 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
]
class InterfaceConnectionCSVForm(forms.Form):
class InterfaceConnectionCSVForm(forms.ModelForm):
device_a = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Name or ID of device A',
error_messages={'invalid_choice': 'Device A not found.'}
)
interface_a = forms.CharField()
interface_a = forms.CharField(
help_text='Name of interface A'
)
device_b = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Name or ID of device B',
error_messages={'invalid_choice': 'Device B not found.'}
)
interface_b = forms.CharField()
status = forms.CharField(
validators=[validate_connection_status]
interface_b = forms.CharField(
help_text='Name of interface B'
)
connection_status = CSVChoiceField(
choices=CONNECTION_STATUS_CHOICES,
help_text='Connection status'
)
def clean(self):
class Meta:
model = InterfaceConnection
fields = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
# Validate interface A
if self.cleaned_data.get('device_a'):
try:
interface_a = Interface.objects.get(device=self.cleaned_data['device_a'],
name=self.cleaned_data['interface_a'])
except Interface.DoesNotExist:
raise forms.ValidationError("Invalid interface ({} {})"
.format(self.cleaned_data['device_a'], self.cleaned_data['interface_a']))
try:
InterfaceConnection.objects.get(Q(interface_a=interface_a) | Q(interface_b=interface_a))
raise forms.ValidationError("{} {} is already connected"
.format(self.cleaned_data['device_a'], self.cleaned_data['interface_a']))
except InterfaceConnection.DoesNotExist:
pass
def clean_interface_a(self):
# Validate interface B
if self.cleaned_data.get('device_b'):
try:
interface_b = Interface.objects.get(device=self.cleaned_data['device_b'],
name=self.cleaned_data['interface_b'])
except Interface.DoesNotExist:
raise forms.ValidationError("Invalid interface ({} {})"
.format(self.cleaned_data['device_b'], self.cleaned_data['interface_b']))
try:
InterfaceConnection.objects.get(Q(interface_a=interface_b) | Q(interface_b=interface_b))
raise forms.ValidationError("{} {} is already connected"
.format(self.cleaned_data['device_b'], self.cleaned_data['interface_b']))
except InterfaceConnection.DoesNotExist:
pass
interface_name = self.cleaned_data.get('interface_a')
if not interface_name:
return None
try:
# Retrieve interface by name
interface = Interface.objects.get(
device=self.cleaned_data['device_a'], name=interface_name
)
# Check for an existing connection to this interface
if InterfaceConnection.objects.filter(Q(interface_a=interface) | Q(interface_b=interface)).count():
raise forms.ValidationError("{} {} is already connected".format(
self.cleaned_data['device_a'], interface_name
))
except Interface.DoesNotExist:
raise forms.ValidationError("Invalid interface ({} {})".format(
self.cleaned_data['device_a'], interface_name
))
class InterfaceConnectionImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=InterfaceConnectionCSVForm)
return interface
def clean(self):
records = self.cleaned_data.get('csv')
if not records:
return
def clean_interface_b(self):
connection_list = []
occupied_interfaces = []
interface_name = self.cleaned_data.get('interface_b')
if not interface_name:
return None
for i, record in enumerate(records, start=1):
form = self.fields['csv'].csv_form(data=record)
if form.is_valid():
interface_a = Interface.objects.get(device=form.cleaned_data['device_a'],
name=form.cleaned_data['interface_a'])
if interface_a in occupied_interfaces:
raise forms.ValidationError("{} {} found in multiple connections"
.format(interface_a.device.name, interface_a.name))
interface_b = Interface.objects.get(device=form.cleaned_data['device_b'],
name=form.cleaned_data['interface_b'])
if interface_b in occupied_interfaces:
raise forms.ValidationError("{} {} found in multiple connections"
.format(interface_b.device.name, interface_b.name))
connection = InterfaceConnection(interface_a=interface_a, interface_b=interface_b)
if form.cleaned_data['status'] == 'planned':
connection.connection_status = CONNECTION_STATUS_PLANNED
else:
connection.connection_status = CONNECTION_STATUS_CONNECTED
connection_list.append(connection)
occupied_interfaces.append(interface_a)
occupied_interfaces.append(interface_b)
else:
for field, errors in form.errors.items():
for e in errors:
self.add_error('csv', "Record {} {}: {}".format(i, field, e))
try:
# Retrieve interface by name
interface = Interface.objects.get(
device=self.cleaned_data['device_b'], name=interface_name
)
# Check for an existing connection to this interface
if InterfaceConnection.objects.filter(Q(interface_a=interface) | Q(interface_b=interface)).count():
raise forms.ValidationError("{} {} is already connected".format(
self.cleaned_data['device_b'], interface_name
))
except Interface.DoesNotExist:
raise forms.ValidationError("Invalid interface ({} {})".format(
self.cleaned_data['device_b'], interface_name
))
self.cleaned_data['csv'] = connection_list
return interface
class InterfaceConnectionDeletionForm(BootstrapMixin, forms.Form):

View File

@ -346,7 +346,7 @@ class RackGroup(models.Model):
]
def __str__(self):
return '{} - {}'.format(self.site.name, self.name)
return self.name
def get_absolute_url(self):
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
@ -1393,10 +1393,13 @@ class InterfaceConnection(models.Model):
verbose_name='Status')
def clean(self):
if self.interface_a == self.interface_b:
raise ValidationError({
'interface_b': "Cannot connect an interface to itself."
})
try:
if self.interface_a == self.interface_b:
raise ValidationError({
'interface_b': "Cannot connect an interface to itself."
})
except ObjectDoesNotExist:
pass
# Used for connections export
def to_csv(self):

View File

@ -247,7 +247,7 @@ class RackImportTable(BaseTable):
class Meta(BaseTable.Meta):
model = Rack
fields = ('site', 'group', 'name', 'facility_id', 'tenant', 'u_height')
fields = ('name', 'site', 'group', 'facility_id', 'tenant', 'u_height')
#

View File

@ -29,8 +29,8 @@ from . import filters, forms, tables
from .models import (
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site,
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
RackGroup, RackReservation, RackRole, Region, Site,
)
@ -219,9 +219,8 @@ class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_site'
form = forms.SiteImportForm
model_form = forms.SiteCSVForm
table = tables.SiteTable
template_name = 'dcim/site_import.html'
default_return_url = 'dcim:site_list'
@ -390,9 +389,8 @@ class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_rack'
form = forms.RackImportForm
model_form = forms.RackCSVForm
table = tables.RackImportTable
template_name = 'dcim/rack_import.html'
default_return_url = 'dcim:rack_list'
@ -866,7 +864,7 @@ class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_device'
form = forms.DeviceImportForm
model_form = forms.DeviceCSVForm
table = tables.DeviceImportTable
template_name = 'dcim/device_import.html'
default_return_url = 'dcim:device_list'
@ -874,23 +872,22 @@ class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_device'
form = forms.ChildDeviceImportForm
model_form = forms.ChildDeviceCSVForm
table = tables.DeviceImportTable
template_name = 'dcim/device_import_child.html'
default_return_url = 'dcim:device_list'
def save_obj(self, obj):
def _save_obj(self, obj_form):
# Inherit site and rack from parent device
obj.site = obj.parent_bay.device.site
obj.rack = obj.parent_bay.device.rack
obj.save()
obj = obj_form.save()
# Save the reverse relation
# Save the reverse relation to the parent device bay
device_bay = obj.parent_bay
device_bay.installed_device = obj
device_bay.save()
return obj
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_device'
@ -1016,9 +1013,8 @@ class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.change_consoleport'
form = forms.ConsoleConnectionImportForm
model_form = forms.ConsoleConnectionCSVForm
table = tables.ConsoleConnectionTable
template_name = 'dcim/console_connections_import.html'
default_return_url = 'dcim:console_connections_list'
@ -1239,9 +1235,8 @@ class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.change_powerport'
form = forms.PowerConnectionImportForm
model_form = forms.PowerConnectionCSVForm
table = tables.PowerConnectionTable
template_name = 'dcim/power_connections_import.html'
default_return_url = 'dcim:power_connections_list'
@ -1676,9 +1671,8 @@ def interfaceconnection_delete(request, pk):
class InterfaceConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.change_interface'
form = forms.InterfaceConnectionImportForm
model_form = forms.InterfaceConnectionCSVForm
table = tables.InterfaceConnectionTable
template_name = 'dcim/interface_connections_import.html'
default_return_url = 'dcim:interface_connections_list'

View File

@ -1,7 +1,6 @@
from __future__ import unicode_literals
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Count
from dcim.models import Site, Rack, Device, Interface
@ -9,8 +8,9 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, ChainedModelChoiceField, CSVDataField,
ExpandableIPAddressField, FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice,
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, CSVChoiceField,
ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, ReturnURLForm, SlugField,
add_blank_choice,
)
from .models import (
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
@ -48,17 +48,23 @@ class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm):
}
class VRFFromCSVForm(forms.ModelForm):
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
class VRFCSVForm(forms.ModelForm):
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text='Name of assigned tenant',
error_messages={
'invalid_choice': 'Tenant not found.',
}
)
class Meta:
model = VRF
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
class VRFImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=VRFFromCSVForm)
help_texts = {
'name': 'VRF name',
}
class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@ -116,19 +122,21 @@ class AggregateForm(BootstrapMixin, CustomFieldForm):
}
class AggregateFromCSVForm(forms.ModelForm):
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'RIR not found.'})
class AggregateCSVForm(forms.ModelForm):
rir = forms.ModelChoiceField(
queryset=RIR.objects.all(),
to_field_name='name',
help_text='Name of parent RIR',
error_messages={
'invalid_choice': 'RIR not found.',
}
)
class Meta:
model = Aggregate
fields = ['prefix', 'rir', 'date_added', 'description']
class AggregateImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=AggregateFromCSVForm)
class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), required=False, label='RIR')
@ -197,69 +205,89 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
self.fields['vrf'].empty_label = 'Global'
class PrefixFromCSVForm(forms.ModelForm):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
error_messages={'invalid_choice': 'VRF not found.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Site not found.'})
vlan_group_name = forms.CharField(required=False)
vlan_vid = forms.IntegerField(required=False)
status = forms.CharField()
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid role.'})
class PrefixCSVForm(forms.ModelForm):
vrf = forms.ModelChoiceField(
queryset=VRF.objects.all(),
required=False,
to_field_name='rd',
help_text='Route distinguisher of parent VRF',
error_messages={
'invalid_choice': 'VRF not found.',
}
)
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text='Name of assigned tenant',
error_messages={
'invalid_choice': 'Tenant not found.',
}
)
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
required=False,
to_field_name='name',
help_text='Name of parent site',
error_messages={
'invalid_choice': 'Site not found.',
}
)
vlan_group = forms.CharField(
help_text='Group name of assigned VLAN',
required=False
)
vlan_vid = forms.IntegerField(
help_text='Numeric ID of assigned VLAN',
required=False
)
status = CSVChoiceField(
choices=IPADDRESS_STATUS_CHOICES,
help_text='Operational status'
)
role = forms.ModelChoiceField(
queryset=Role.objects.all(),
required=False,
to_field_name='name',
help_text='Functional role',
error_messages={
'invalid_choice': 'Invalid role.',
}
)
class Meta:
model = Prefix
fields = [
'prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status', 'role', 'is_pool',
'description',
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
]
def clean(self):
super(PrefixFromCSVForm, self).clean()
super(PrefixCSVForm, self).clean()
site = self.cleaned_data.get('site')
vlan_group_name = self.cleaned_data.get('vlan_group_name')
vlan_group = self.cleaned_data.get('vlan_group')
vlan_vid = self.cleaned_data.get('vlan_vid')
vlan_group = None
# Validate VLAN group
if vlan_group_name:
try:
vlan_group = VLANGroup.objects.get(site=site, name=vlan_group_name)
except VLANGroup.DoesNotExist:
if site:
self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name))
else:
self.add_error('vlan_group_name', "Invalid global VLAN group ({}).".format(vlan_group_name))
# Validate VLAN
if vlan_vid:
if vlan_group and vlan_vid:
try:
self.instance.vlan = VLAN.objects.get(site=site, group=vlan_group, vid=vlan_vid)
self.instance.vlan = VLAN.objects.get(site=site, group__name=vlan_group, vid=vlan_vid)
except VLAN.DoesNotExist:
if site:
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, site))
elif vlan_group:
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for group {}.".format(vlan_vid, vlan_group_name))
elif not vlan_group_name:
self.add_error('vlan_vid', "Invalid global VLAN ID ({}).".format(vlan_vid))
except VLAN.MultipleObjectsReturned:
self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
def clean_status(self):
status_choices = {s[1].lower(): s[0] for s in PREFIX_STATUS_CHOICES}
try:
return status_choices[self.cleaned_data['status'].lower()]
except KeyError:
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
class PrefixImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=PrefixFromCSVForm)
raise forms.ValidationError("VLAN {} not found in site {} group {}".format(
vlan_vid, site, vlan_group
))
else:
raise forms.ValidationError("Global VLAN {} not found in group {}".format(vlan_vid, vlan_group))
elif vlan_vid:
try:
self.instance.vlan = VLAN.objects.get(site=site, group__isnull=True, vid=vlan_vid)
except VLAN.DoesNotExist:
if site:
raise forms.ValidationError("VLAN {} not found in site {}".format(vlan_vid, site))
else:
raise forms.ValidationError("Global VLAN {} not found".format(vlan_vid))
class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@ -513,16 +541,46 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
self.fields['vrf'].empty_label = 'Global'
class IPAddressFromCSVForm(forms.ModelForm):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
error_messages={'invalid_choice': 'VRF not found.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
status = forms.CharField()
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Device not found.'})
interface_name = forms.CharField(required=False)
is_primary = forms.BooleanField(required=False)
class IPAddressCSVForm(forms.ModelForm):
vrf = forms.ModelChoiceField(
queryset=VRF.objects.all(),
required=False,
to_field_name='rd',
help_text='Route distinguisher of the assigned VRF',
error_messages={
'invalid_choice': 'VRF not found.',
}
)
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
to_field_name='name',
required=False,
help_text='Name of the assigned tenant',
error_messages={
'invalid_choice': 'Tenant not found.',
}
)
status = CSVChoiceField(
choices=PREFIX_STATUS_CHOICES,
help_text='Operational status'
)
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
required=False,
to_field_name='name',
help_text='Name or ID of assigned device',
error_messages={
'invalid_choice': 'Device not found.',
}
)
interface_name = forms.CharField(
help_text='Name of assigned interface',
required=False
)
is_primary = forms.BooleanField(
help_text='Make this the primary IP for the assigned device',
required=False
)
class Meta:
model = IPAddress
@ -530,6 +588,8 @@ class IPAddressFromCSVForm(forms.ModelForm):
def clean(self):
super(IPAddressCSVForm, self).clean()
device = self.cleaned_data.get('device')
interface_name = self.cleaned_data.get('interface_name')
is_primary = self.cleaned_data.get('is_primary')
@ -537,24 +597,17 @@ class IPAddressFromCSVForm(forms.ModelForm):
# Validate interface
if device and interface_name:
try:
Interface.objects.get(device=device, name=interface_name)
self.instance.interface = Interface.objects.get(device=device, name=interface_name)
except Interface.DoesNotExist:
self.add_error('interface_name', "Invalid interface ({}) for {}".format(interface_name, device))
raise forms.ValidationError("Invalid interface {} for device {}".format(interface_name, device))
elif device and not interface_name:
self.add_error('interface_name', "Device set ({}) but interface missing".format(device))
raise forms.ValidationError("Device set ({}) but interface missing".format(device))
elif interface_name and not device:
self.add_error('device', "Interface set ({}) but device missing or invalid".format(interface_name))
raise forms.ValidationError("Interface set ({}) but device missing or invalid".format(interface_name))
# Validate is_primary
if is_primary and not device:
self.add_error('is_primary', "No device specified; cannot set as primary IP")
def clean_status(self):
status_choices = {s[1].lower(): s[0] for s in IPADDRESS_STATUS_CHOICES}
try:
return status_choices[self.cleaned_data['status'].lower()]
except KeyError:
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
raise forms.ValidationError("No device specified; cannot set as primary IP")
def save(self, *args, **kwargs):
@ -569,11 +622,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
elif self.instance.address.version == 6:
self.instance.primary_ip6_for = self.cleaned_data['device']
return super(IPAddressFromCSVForm, self).save(*args, **kwargs)
class IPAddressImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=IPAddressFromCSVForm)
return super(IPAddressCSVForm, self).save(*args, **kwargs)
class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@ -673,60 +722,67 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
}
class VLANFromCSVForm(forms.ModelForm):
class VLANCSVForm(forms.ModelForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Site not found.'}
queryset=Site.objects.all(),
required=False,
to_field_name='name',
help_text='Name of parent site',
error_messages={
'invalid_choice': 'Site not found.',
}
)
group_name = forms.CharField(
help_text='Name of VLAN group',
required=False
)
group_name = forms.CharField(required=False)
tenant = forms.ModelChoiceField(
Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'}
queryset=Tenant.objects.all(),
to_field_name='name',
required=False,
help_text='Name of assigned tenant',
error_messages={
'invalid_choice': 'Tenant not found.',
}
)
status = CSVChoiceField(
choices=VLAN_STATUS_CHOICES,
help_text='Operational status'
)
status = forms.CharField()
role = forms.ModelChoiceField(
queryset=Role.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid role.'}
queryset=Role.objects.all(),
required=False,
to_field_name='name',
help_text='Functional role',
error_messages={
'invalid_choice': 'Invalid role.',
}
)
class Meta:
model = VLAN
fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
help_texts = {
'vid': 'Numeric VLAN ID (1-4095)',
'name': 'VLAN name',
}
def clean(self):
super(VLANFromCSVForm, self).clean()
super(VLANCSVForm, self).clean()
# Validate VLANGroup
site = self.cleaned_data.get('site')
group_name = self.cleaned_data.get('group_name')
# Validate VLAN group
if group_name:
try:
VLANGroup.objects.get(site=self.cleaned_data.get('site'), name=group_name)
self.instance.group = VLANGroup.objects.get(site=site, name=group_name)
except VLANGroup.DoesNotExist:
self.add_error('group_name', "Invalid VLAN group {}.".format(group_name))
def clean_status(self):
status_choices = {s[1].lower(): s[0] for s in VLAN_STATUS_CHOICES}
try:
return status_choices[self.cleaned_data['status'].lower()]
except KeyError:
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
def save(self, *args, **kwargs):
vlan = super(VLANFromCSVForm, self).save(commit=False)
# Assign VLANGroup by site and name
if self.cleaned_data['group_name']:
vlan.group = VLANGroup.objects.get(site=self.cleaned_data['site'], name=self.cleaned_data['group_name'])
if kwargs.get('commit'):
vlan.save()
return vlan
class VLANImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=VLANFromCSVForm)
if site:
raise forms.ValidationError("VLAN group {} not found for site {}".format(group_name, site))
else:
raise forms.ValidationError("Global VLAN group {} not found".format(group_name))
class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):

View File

@ -498,9 +498,7 @@ class VLANGroup(models.Model):
verbose_name_plural = 'VLAN groups'
def __str__(self):
if self.site is None:
return self.name
return '{} - {}'.format(self.site.name, self.name)
return self.name
def get_absolute_url(self):
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)

View File

@ -130,9 +130,8 @@ class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'ipam.add_vrf'
form = forms.VRFImportForm
model_form = forms.VRFCSVForm
table = tables.VRFTable
template_name = 'ipam/vrf_import.html'
default_return_url = 'ipam:vrf_list'
@ -341,9 +340,8 @@ class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'ipam.add_aggregate'
form = forms.AggregateImportForm
model_form = forms.AggregateCSVForm
table = tables.AggregateTable
template_name = 'ipam/aggregate_import.html'
default_return_url = 'ipam:aggregate_list'
@ -538,9 +536,8 @@ class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'ipam.add_prefix'
form = forms.PrefixImportForm
model_form = forms.PrefixCSVForm
table = tables.PrefixTable
template_name = 'ipam/prefix_import.html'
default_return_url = 'ipam:prefix_list'
@ -640,9 +637,8 @@ class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'ipam.add_ipaddress'
form = forms.IPAddressImportForm
model_form = forms.IPAddressCSVForm
table = tables.IPAddressTable
template_name = 'ipam/ipaddress_import.html'
default_return_url = 'ipam:ipaddress_list'
def save_obj(self, obj):
@ -748,9 +744,8 @@ class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'ipam.add_vlan'
form = forms.VLANImportForm
model_form = forms.VLANCSVForm
table = tables.VLANTable
template_name = 'ipam/vlan_import.html'
default_return_url = 'ipam:vlan_list'

View File

@ -7,7 +7,7 @@ from django import forms
from django.db.models import Count
from dcim.models import Device
from utilities.forms import BootstrapMixin, BulkEditForm, BulkImportForm, CSVDataField, FilterChoiceField, SlugField
from utilities.forms import BootstrapMixin, BulkEditForm, FilterChoiceField, FlexibleModelChoiceField, SlugField
from .models import Secret, SecretRole, UserKey
@ -65,27 +65,40 @@ class SecretForm(BootstrapMixin, forms.ModelForm):
})
class SecretFromCSVForm(forms.ModelForm):
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Device not found.'})
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Invalid secret role.'})
plaintext = forms.CharField()
class SecretCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Device name or ID',
error_messages={
'invalid_choice': 'Device not found.',
}
)
role = forms.ModelChoiceField(
queryset=SecretRole.objects.all(),
to_field_name='name',
help_text='Name of assigned role',
error_messages={
'invalid_choice': 'Invalid secret role.',
}
)
plaintext = forms.CharField(
help_text='Plaintext secret data'
)
class Meta:
model = Secret
fields = ['device', 'role', 'name', 'plaintext']
help_texts = {
'name': 'Name or username',
}
def save(self, *args, **kwargs):
s = super(SecretFromCSVForm, self).save(*args, **kwargs)
s = super(SecretCSVForm, self).save(*args, **kwargs)
s.plaintext = str(self.cleaned_data['plaintext'])
return s
class SecretImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=SecretFromCSVForm, widget=forms.Textarea(attrs={'class': 'requires-session-key'}))
class SecretBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), required=False)

View File

@ -16,7 +16,7 @@ urlpatterns = [
# Secrets
url(r'^secrets/$', views.SecretListView.as_view(), name='secret_list'),
url(r'^secrets/import/$', views.secret_import, name='secret_import'),
url(r'^secrets/import/$', views.SecretBulkImportView.as_view(), name='secret_import'),
url(r'^secrets/edit/$', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'),
url(r'^secrets/delete/$', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'),
url(r'^secrets/(?P<pk>\d+)/$', views.SecretView.as_view(), name='secret'),

View File

@ -12,7 +12,9 @@ from django.utils.decorators import method_decorator
from django.views.generic import View
from dcim.models import Device
from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
from . import filters, forms, tables
from .decorators import userkey_required
from .models import SecretRole, Secret, SessionKey
@ -185,58 +187,50 @@ class SecretDeleteView(PermissionRequiredMixin, ObjectDeleteView):
default_return_url = 'secrets:secret_list'
@permission_required('secrets.add_secret')
@userkey_required()
def secret_import(request):
class SecretBulkImportView(BulkImportView):
permission_required = 'ipam.add_vlan'
model_form = forms.SecretCSVForm
table = tables.SecretTable
default_return_url = 'secrets:secret_list'
session_key = request.COOKIES.get('session_key', None)
master_key = None
if request.method == 'POST':
form = forms.SecretImportForm(request.POST)
def _save_obj(self, obj_form):
"""
Encrypt each object before saving it to the database.
"""
obj = obj_form.save(commit=False)
obj.encrypt(self.master_key)
obj.save()
return obj
if session_key is None:
form.add_error(None, "No session key was provided with the request. Unable to encrypt secret data.")
def post(self, request):
if form.is_valid():
# Grab the session key from cookies.
session_key = request.COOKIES.get('session_key')
if session_key:
new_secrets = []
session_key = base64.b64decode(session_key)
master_key = None
# Attempt to derive the master key using the provided session key.
try:
sk = SessionKey.objects.get(userkey__user=request.user)
master_key = sk.get_master_key(session_key)
self.master_key = sk.get_master_key(base64.b64decode(session_key))
except SessionKey.DoesNotExist:
form.add_error(None, "No session key found for this user.")
messages.error(request, "No session key found for this user.")
if master_key is None:
form.add_error(None, "Invalid private key! Unable to encrypt secret data.")
if self.master_key is not None:
return super(SecretBulkImportView, self).post(request)
else:
try:
with transaction.atomic():
for secret in form.cleaned_data['csv']:
secret.encrypt(master_key)
secret.save()
new_secrets.append(secret)
messages.error(request, "Invalid private key! Unable to encrypt secret data.")
table = tables.SecretTable(new_secrets)
messages.success(request, "Imported {} new secrets.".format(len(new_secrets)))
else:
messages.error(request, "No session key was provided with the request. Unable to encrypt secret data.")
return render(request, 'import_success.html', {
'table': table,
'return_url': 'secrets:secret_list',
})
except IntegrityError as e:
form.add_error('csv', "Record {}: {}".format(len(new_secrets) + 1, e.__cause__))
else:
form = forms.SecretImportForm()
return render(request, 'secrets/secret_import.html', {
'form': form,
'return_url': 'secrets:secret_list',
})
return render(request, self.template_name, {
'form': self._import_form(request.POST),
'fields': self.model_form().fields,
'obj_type': self.model_form._meta.model._meta.verbose_name,
'return_url': self.default_return_url,
})
class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):

View File

@ -1,55 +0,0 @@
{% extends 'utilities/obj_import.html' %}
{% block title %}Circuit Import{% endblock %}
{% block instructions %}
<h4>CSV Format</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Circuit ID</td>
<td>Alphanumeric circuit identifier</td>
<td>IC-603122</td>
</tr>
<tr>
<td>Provider</td>
<td>Name of circuit provider</td>
<td>TeliaSonera</td>
</tr>
<tr>
<td>Type</td>
<td>Circuit type</td>
<td>Transit</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Strickland Propane</td>
</tr>
<tr>
<td>Install Date</td>
<td>Date in YYYY-MM-DD format (optional)</td>
<td>2016-02-23</td>
</tr>
<tr>
<td>Commit rate</td>
<td>Commited rate in Kbps (optional)</td>
<td>2000</td>
</tr>
<tr>
<td>Description</td>
<td>Short description (optional)</td>
<td>Primary for voice</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>IC-603122,TeliaSonera,Transit,Strickland Propane,2016-02-23,2000,Primary for voice</pre>
{% endblock %}

View File

@ -1,45 +0,0 @@
{% extends 'utilities/obj_import.html' %}
{% block title %}Provider Import{% endblock %}
{% block instructions %}
<h4>CSV Format</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Name</td>
<td>Provider's proper name</td>
<td>Level 3</td>
</tr>
<tr>
<td>Slug</td>
<td>URL-friendly name</td>
<td>level3</td>
</tr>
<tr>
<td>ASN</td>
<td>Autonomous system number (optional)</td>
<td>3356</td>
</tr>
<tr>
<td>Account</td>
<td>Account number (optional)</td>
<td>08931544</td>
</tr>
<tr>
<td>Portal URL</td>
<td>Customer service portal URL (optional)</td>
<td>https://mylevel3.net</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>Level 3,level3,3356,08931544,https://mylevel3.net</pre>
{% endblock %}

View File

@ -1,45 +0,0 @@
{% extends 'utilities/obj_import.html' %}
{% block title %}Console Connections Import{% endblock %}
{% block instructions %}
<h4>CSV Format</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Console server</td>
<td>Device name or {ID}</td>
<td>abc1-cs3</td>
</tr>
<tr>
<td>Console server port</td>
<td>Full CS port name</td>
<td>Port 35</td>
</tr>
<tr>
<td>Device</td>
<td>Device name or {ID}</td>
<td>abc1-switch7</td>
</tr>
<tr>
<td>Console Port</td>
<td>Console port name</td>
<td>Console</td>
</tr>
<tr>
<td>Connection Status</td>
<td>"planned" or "connected"</td>
<td>planned</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>abc1-cs3,Port 35,abc1-switch7,Console,planned</pre>
{% endblock %}

View File

@ -1,103 +1,5 @@
{% extends '_base.html' %}
{% load form_helpers %}
{% extends 'utilities/obj_import.html' %}
{% block title %}Device Import{% endblock %}
{% block content %}
{% include 'dcim/inc/device_import_header.html' %}
<div class="row">
<div class="col-md-12">
<form action="." method="post" class="form">
{% csrf_token %}
{% render_form form %}
<div class="form-group">
<div class="col-md-12 text-right">
<button type="submit" class="btn btn-primary">Submit</button>
{% if return_url %}
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
{% endif %}
</div>
</div>
</form>
<h4>CSV Format</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Name</td>
<td>Device name (optional)</td>
<td>rack101_sw1</td>
</tr>
<tr>
<td>Device role</td>
<td>Functional role of device</td>
<td>ToR Switch</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Pied Piper</td>
</tr>
<tr>
<td>Device manufacturer</td>
<td>Hardware manufacturer</td>
<td>Juniper</td>
</tr>
<tr>
<td>Device model</td>
<td>Hardware model</td>
<td>EX4300-48T</td>
</tr>
<tr>
<td>Platform</td>
<td>Software running on device (optional)</td>
<td>Juniper Junos</td>
</tr>
<tr>
<td>Serial number</td>
<td>Physical serial number (optional)</td>
<td>CAB00577291</td>
</tr>
<tr>
<td>Asset tag</td>
<td>Unique alphanumeric tag (optional)</td>
<td>ABC123456</td>
</tr>
<tr>
<td>Status</td>
<td>Current status</td>
<td>Active</td>
</tr>
<tr>
<td>Site</td>
<td>Site name</td>
<td>Ashburn-VA</td>
</tr>
<tr>
<td>Rack</td>
<td>Rack name (optional)</td>
<td>R101</td>
</tr>
<tr>
<td>Position (U)</td>
<td>Lowest-numbered rack unit occupied by the device (optional)</td>
<td>21</td>
</tr>
<tr>
<td>Face</td>
<td>Rack face; front or rear (required if position is set)</td>
<td>Rear</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>rack101_sw1,ToR Switch,Pied Piper,Juniper,EX4300-48T,Juniper Junos,CAB00577291,ABC123456,Active,Ashburn-VA,R101,21,Rear</pre>
</div>
</div>
{% block tabs %}
{% include 'dcim/inc/device_import_header.html' %}
{% endblock %}

View File

@ -1,93 +1,5 @@
{% extends '_base.html' %}
{% load form_helpers %}
{% extends 'utilities/obj_import.html' %}
{% block title %}Device Import{% endblock %}
{% block content %}
{% include 'dcim/inc/device_import_header.html' with active_tab='child_import' %}
<div class="row">
<div class="col-md-12">
<form action="." method="post" class="form">
{% csrf_token %}
{% render_form form %}
<div class="form-group">
<div class="col-md-12 text-right">
<button type="submit" class="btn btn-primary">Submit</button>
{% if return_url %}
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
{% endif %}
</div>
</div>
</form>
<h4>CSV Format</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Name</td>
<td>Device name (optional)</td>
<td>Blade12</td>
</tr>
<tr>
<td>Device role</td>
<td>Functional role of device</td>
<td>Blade Server</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Pied Piper</td>
</tr>
<tr>
<td>Device manufacturer</td>
<td>Hardware manufacturer</td>
<td>Dell</td>
</tr>
<tr>
<td>Device model</td>
<td>Hardware model</td>
<td>BS2000T</td>
</tr>
<tr>
<td>Platform</td>
<td>Software running on device (optional)</td>
<td>Linux</td>
</tr>
<tr>
<td>Serial number</td>
<td>Physical serial number (optional)</td>
<td>CAB00577291</td>
</tr>
<tr>
<td>Asset tag</td>
<td>Unique alphanumeric tag (optional)</td>
<td>ABC123456</td>
</tr>
<tr>
<td>Status</td>
<td>Current status</td>
<td>Active</td>
</tr>
<tr>
<td>Parent device</td>
<td>Parent device</td>
<td>Server101</td>
</tr>
<tr>
<td>Device bay</td>
<td>Device bay name</td>
<td>Slot 4</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>Blade12,Blade Server,Pied Piper,Dell,BS2000T,Linux,CAB00577291,ABC123456,Active,Server101,Slot4</pre>
</div>
</div>
{% block tabs %}
{% include 'dcim/inc/device_import_header.html' with active_tab='child_import' %}
{% endblock %}

View File

@ -1,4 +1,3 @@
<h1>Device Import</h1>
<ul class="nav nav-tabs" style="margin-bottom: 20px">
<li role="presentation"{% if not active_tab %} class="active"{% endif %}><a href="{% url 'dcim:device_import' %}">Racked Devices</a></li>
<li role="presentation"{% if active_tab == 'child_import' %} class="active"{% endif %}><a href="{% url 'dcim:device_import_child' %}">Child Devices</a></li>

View File

@ -1,45 +0,0 @@
{% extends 'utilities/obj_import.html' %}
{% block title %}Interface Connections Import{% endblock %}
{% block instructions %}
<h4>CSV Format</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Device A</td>
<td>Device name or {ID}</td>
<td>abc1-core1</td>
</tr>
<tr>
<td>Interface A</td>
<td>Interface name</td>
<td>xe-0/0/6</td>
</tr>
<tr>
<td>Device B</td>
<td>Device name or {ID}</td>
<td>abc1-switch7</td>
</tr>
<tr>
<td>Interface B</td>
<td>Interface name</td>
<td>xe-0/0/0</td>
</tr>
<tr>
<td>Connection Status</td>
<td>"planned" or "connected"</td>
<td>planned</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>abc1-core1,xe-0/0/6,abc1-switch7,xe-0/0/0,planned</pre>
{% endblock %}

View File

@ -1,45 +0,0 @@
{% extends 'utilities/obj_import.html' %}
{% block title %}Power Connections Import{% endblock %}
{% block instructions %}
<h4>CSV Format</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>PDU</td>
<td>Device name or {ID}</td>
<td>abc1-pdu1</td>
</tr>
<tr>
<td>Power Outlet</td>
<td>Power outlet name</td>
<td>AC4</td>
</tr>
<tr>
<td>Device</td>
<td>Device name or {ID}</td>
<td>abc1-switch7</td>
</tr>
<tr>
<td>Power Port</td>
<td>Power port name</td>
<td>PSU0</td>
</tr>
<tr>
<td>Connection Status</td>
<td>"planned" or "connected"</td>
<td>connected</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>abc1-pdu1,AC4,abc1-switch7,PSU0,connected</pre>
{% endblock %}

View File

@ -1,70 +0,0 @@
{% extends 'utilities/obj_import.html' %}
{% block title %}Rack Import{% endblock %}
{% block instructions %}
<h4>CSV Format</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Site</td>
<td>Name of the assigned site</td>
<td>DC-4</td>
</tr>
<tr>
<td>Group</td>
<td>Rack group name (optional)</td>
<td>Cage 1400</td>
</tr>
<tr>
<td>Name</td>
<td>Internal rack name</td>
<td>R101</td>
</tr>
<tr>
<td>Facility ID</td>
<td>Rack ID assigned by the facility (optional)</td>
<td>J12.100</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Pied Piper</td>
</tr>
<tr>
<td>Role</td>
<td>Functional role (optional)</td>
<td>Compute</td>
</tr>
<tr>
<td>Type</td>
<td>Rack type (optional)</td>
<td>4-post cabinet</td>
</tr>
<tr>
<td>Width</td>
<td>Rail-to-rail width (19 or 23 inches)</td>
<td>19</td>
</tr>
<tr>
<td>Height</td>
<td>Height in rack units</td>
<td>42</td>
</tr>
<tr>
<td>Descending units</td>
<td>Units are numbered top-to-bottom</td>
<td>False</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>DC-4,Cage 1400,R101,J12.100,Pied Piper,Compute,4-post cabinet,19,42,False</pre>
{% endblock %}

View File

@ -1,81 +0,0 @@
{% extends '_base.html' %}
{% load form_helpers %}
{% block title %}Site Import{% endblock %}
{% block content %}
<h1>Site Import</h1>
<div class="row">
<div class="col-md-6">
<form action="." method="post" class="form">
{% csrf_token %}
{% render_form form %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div>
</form>
</div>
<div class="col-md-6">
<h4>CSV Format</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Name</td>
<td>Site's proper name</td>
<td>ASH-4 South</td>
</tr>
<tr>
<td>Slug</td>
<td>URL-friendly name</td>
<td>ash4-south</td>
</tr>
<tr>
<td>Region</td>
<td>Name of region (optional)</td>
<td>North America</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Pied Piper</td>
</tr>
<tr>
<td>Facility</td>
<td>Name of the hosting facility (optional)</td>
<td>Equinix DC6</td>
</tr>
<tr>
<td>ASN</td>
<td>Autonomous system number (optional)</td>
<td>65000</td>
</tr>
<tr>
<td>Contact Name</td>
<td>Name of administrative contact (optional)</td>
<td>Hank Hill</td>
</tr>
<tr>
<td>Contact Phone</td>
<td>Phone number (optional)</td>
<td>+1-214-555-1234</td>
</tr>
<tr>
<td>Contact E-mail</td>
<td>E-mail address (optional)</td>
<td>hhill@example.com</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>ASH-4 South,ash4-south,North America,Pied Piper,Equinix DC6,65000,Hank Hill,+1-214-555-1234,hhill@example.com</pre>
</div>
</div>
{% endblock %}

View File

@ -1,40 +0,0 @@
{% extends 'utilities/obj_import.html' %}
{% block title %}Aggregate Import{% endblock %}
{% block instructions %}
<h4>CSV Format</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Prefix</td>
<td>IPv4 or IPv6 network</td>
<td>172.16.0.0/12</td>
</tr>
<tr>
<td>RIR</td>
<td>Name of RIR</td>
<td>RFC 1918</td>
</tr>
<tr>
<td>Date Added</td>
<td>Date in YYYY-MM-DD format (optional)</td>
<td>2016-02-23</td>
</tr>
<tr>
<td>Description</td>
<td>Short description (optional)</td>
<td>Private IPv4 space</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>172.16.0.0/12,RFC 1918,2016-02-23,Private IPv4 space</pre>
{% endblock %}

View File

@ -1,60 +0,0 @@
{% extends 'utilities/obj_import.html' %}
{% block title %}IP Address Import{% endblock %}
{% block instructions %}
<h4>CSV Format</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Address</td>
<td>IPv4 or IPv6 address</td>
<td>192.0.2.42/24</td>
</tr>
<tr>
<td>VRF</td>
<td>VRF route distinguisher (optional)</td>
<td>65000:123</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>ABC01</td>
</tr>
<tr>
<td>Status</td>
<td>Current status</td>
<td>Active</td>
</tr>
<tr>
<td>Device</td>
<td>Device name (optional)</td>
<td>switch12</td>
</tr>
<tr>
<td>Interface</td>
<td>Interface name (optional)</td>
<td>ge-0/0/31</td>
</tr>
<tr>
<td>Is Primary</td>
<td>If "true", IP will be primary for device (optional)</td>
<td>True</td>
</tr>
<tr>
<td>Description</td>
<td>Short description (optional)</td>
<td>Management IP</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>192.0.2.42/24,65000:123,ABC01,Active,switch12,ge-0/0/31,True,Management IP</pre>
{% endblock %}

View File

@ -1,70 +0,0 @@
{% extends 'utilities/obj_import.html' %}
{% block title %}Prefix Import{% endblock %}
{% block instructions %}
<h4>CSV Format</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Prefix</td>
<td>IPv4 or IPv6 network</td>
<td>192.168.42.0/24</td>
</tr>
<tr>
<td>VRF</td>
<td>VRF route distinguisher (optional)</td>
<td>65000:123</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>ABC01</td>
</tr>
<tr>
<td>Site</td>
<td>Name of assigned site (optional)</td>
<td>HQ</td>
</tr>
<tr>
<td>VLAN Group</td>
<td>Name of group for VLAN selection (optional)</td>
<td>Customers</td>
</tr>
<tr>
<td>VLAN ID</td>
<td>Numeric VLAN ID (optional)</td>
<td>801</td>
</tr>
<tr>
<td>Status</td>
<td>Current status</td>
<td>Active</td>
</tr>
<tr>
<td>Role</td>
<td>Functional role (optional)</td>
<td>Customer</td>
</tr>
<tr>
<td>Is a pool</td>
<td>True if all IPs are considered usable</td>
<td>False</td>
</tr>
<tr>
<td>Description</td>
<td>Short description (optional)</td>
<td>7th floor WiFi</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,False,7th floor WiFi</pre>
{% endblock %}

View File

@ -1,60 +0,0 @@
{% extends 'utilities/obj_import.html' %}
{% block title %}VLAN Import{% endblock %}
{% block instructions %}
<h4>CSV Format</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Site</td>
<td>Name of assigned site (optional)</td>
<td>LAS2</td>
</tr>
<tr>
<td>Group</td>
<td>Name of VLAN group (optional)</td>
<td>Backend Network</td>
</tr>
<tr>
<td>ID</td>
<td>Configured VLAN ID</td>
<td>1400</td>
</tr>
<tr>
<td>Name</td>
<td>Configured VLAN name</td>
<td>Cameras</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Internal</td>
</tr>
<tr>
<td>Status</td>
<td>Current status</td>
<td>Active</td>
</tr>
<tr>
<td>Role</td>
<td>Functional role (optional)</td>
<td>Security</td>
</tr>
<tr>
<td>Description</td>
<td>Short description (optional)</td>
<td>Security team only</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>LAS2,Backend Network,1400,Cameras,Internal,Active,Security,Security team only</pre>
{% endblock %}

View File

@ -1,45 +0,0 @@
{% extends 'utilities/obj_import.html' %}
{% block title %}VRF Import{% endblock %}
{% block instructions %}
<h4>CSV Format</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Name</td>
<td>Name of VRF</td>
<td>Customer_ABC</td>
</tr>
<tr>
<td>RD</td>
<td>Route distinguisher</td>
<td>65000:123456</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>ABC01</td>
</tr>
<tr>
<td>Enforce uniqueness</td>
<td>Prevent duplicate prefixes/IP addresses</td>
<td>True</td>
</tr>
<tr>
<td>Description</td>
<td>Short description (optional)</td>
<td>Native VRF for customer ABC</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>Customer_ABC,65000:123456,ABC01,True,Native VRF for customer ABC</pre>
{% endblock %}

View File

@ -1,40 +0,0 @@
{% extends 'utilities/obj_import.html' %}
{% block title %}Tenant Import{% endblock %}
{% block instructions %}
<h4>CSV Format</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Name</td>
<td>Tenant name</td>
<td>WIDG01</td>
</tr>
<tr>
<td>Slug</td>
<td>URL-friendly name</td>
<td>widg01</td>
</tr>
<tr>
<td>Group</td>
<td>Tenant group (optional)</td>
<td>Customers</td>
</tr>
<tr>
<td>Description</td>
<td>Long-form name or other text (optional)</td>
<td>Widgets Inc.</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<pre>WIDG01,widg01,Customers,Widgets Inc.</pre>
{% endblock %}

View File

@ -1,10 +1,12 @@
{% extends '_base.html' %}
{% load helpers %}
{% load form_helpers %}
{% block content %}
<h1>{% block title %}{% endblock %}</h1>
<h1>{% block title %}{{ obj_type|bettertitle }} Import{% endblock %}</h1>
{% block tabs %}{% endblock %}
<div class="row">
<div class="col-md-6">
<div class="col-md-7">
{% if form.non_field_errors %}
<div class="panel panel-danger">
<div class="panel-heading"><strong>Errors</strong></div>
@ -26,8 +28,33 @@
</div>
</form>
</div>
<div class="col-md-6">
{% block instructions %}{% endblock %}
<div class="col-md-5">
{% if fields %}
<h4 class="text-center">CSV Format</h4>
<table class="table">
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
{% for name, field in fields.items %}
<tr>
<td><code>{{ name }}</code></td>
<td>{% if field.required %}<i class="glyphicon glyphicon-ok" title="Required"></i>{% endif %}</td>
<td>
{{ field.help_text|default:field.label }}
{% if field.choices %}
<br /><small class="text-muted">Choices: {{ field.choices|example_choices }}</small>
{% elif field|widget_type == 'dateinput' %}
<br /><small class="text-muted">Format: YYYY-MM-DD</small>
{% elif field|widget_type == 'checkboxinput' %}
<br /><small class="text-muted">Specify "true" or "false"</small>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -5,8 +5,7 @@ from django.db.models import Count
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from utilities.forms import (
APISelect, BootstrapMixin, BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField,
FilterChoiceField, SlugField,
APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField, SlugField,
)
from .models import Tenant, TenantGroup
@ -36,17 +35,25 @@ class TenantForm(BootstrapMixin, CustomFieldForm):
fields = ['name', 'slug', 'group', 'description', 'comments']
class TenantFromCSVForm(forms.ModelForm):
group = forms.ModelChoiceField(TenantGroup.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Group not found.'})
class TenantCSVForm(forms.ModelForm):
slug = SlugField()
group = forms.ModelChoiceField(
queryset=TenantGroup.objects.all(),
required=False,
to_field_name='name',
help_text='Name of parent group',
error_messages={
'invalid_choice': 'Group not found.'
}
)
class Meta:
model = Tenant
fields = ['name', 'slug', 'group', 'description']
class TenantImportForm(BootstrapMixin, BulkImportForm):
csv = CSVDataField(csv_form=TenantFromCSVForm)
fields = ['name', 'slug', 'group', 'description', 'comments']
help_texts = {
'name': 'Tenant name',
'comments': 'Free-form comments'
}
class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):

View File

@ -97,9 +97,8 @@ class TenantDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class TenantBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'tenancy.add_tenant'
form = forms.TenantImportForm
model_form = forms.TenantCSVForm
table = tables.TenantTable
template_name = 'tenancy/tenant_import.html'
default_return_url = 'tenancy:tenant_list'

View File

@ -217,45 +217,79 @@ class Livesearch(forms.TextInput):
class CSVDataField(forms.CharField):
"""
A field for comma-separated values (CSV). Values containing commas should be encased within double quotes. Example:
'"New York, NY",new-york-ny,Other stuff' => ['New York, NY', 'new-york-ny', 'Other stuff']
A CharField (rendered as a Textarea) which accepts CSV-formatted data. It returns a list of dictionaries mapping
column headers to values. Each dictionary represents an individual record.
"""
csv_form = None
widget = forms.Textarea
def __init__(self, csv_form, *args, **kwargs):
self.csv_form = csv_form
self.columns = self.csv_form().fields.keys()
def __init__(self, fields, required_fields=[], *args, **kwargs):
self.fields = fields
self.required_fields = required_fields
super(CSVDataField, self).__init__(*args, **kwargs)
self.strip = False
if not self.label:
self.label = 'CSV Data'
if not self.initial:
self.initial = ','.join(required_fields) + '\n'
if not self.help_text:
self.help_text = 'Enter one line per record in CSV format.'
self.help_text = 'Enter the list of column headers followed by one line per record to be imported, using ' \
'commas to separate values. Multi-line data and values containing commas may be wrapped ' \
'in double quotes.'
def to_python(self, value):
"""
Return a list of dictionaries, each representing an individual record
"""
# Python 2's csv module has problems with Unicode
if not isinstance(value, str):
value = value.encode('utf-8')
records = []
reader = csv.reader(value.splitlines())
# Consume and valdiate the first line of CSV data as column headers
headers = reader.next()
for f in self.required_fields:
if f not in headers:
raise forms.ValidationError('Required column header "{}" not found.'.format(f))
for f in headers:
if f not in self.fields:
raise forms.ValidationError('Unexpected column header "{}" found.'.format(f))
# Parse CSV data
for i, row in enumerate(reader, start=1):
if row:
if len(row) < len(self.columns):
raise forms.ValidationError("Line {}: Field(s) missing (found {}; expected {})"
.format(i, len(row), len(self.columns)))
elif len(row) > len(self.columns):
raise forms.ValidationError("Line {}: Too many fields (found {}; expected {})"
.format(i, len(row), len(self.columns)))
if len(row) != len(headers):
raise forms.ValidationError(
"Row {}: Expected {} columns but found {}".format(i, len(headers), len(row))
)
row = [col.strip() for col in row]
record = dict(zip(self.columns, row))
record = dict(zip(headers, row))
records.append(record)
return records
class CSVChoiceField(forms.ChoiceField):
"""
Invert the provided set of choices to take the human-friendly label as input, and return the database value.
"""
def __init__(self, choices, *args, **kwargs):
super(CSVChoiceField, self).__init__(choices, *args, **kwargs)
self.choices = [(label, label) for value, label in choices]
self.choice_values = {label: value for value, label in choices}
def clean(self, value):
value = super(CSVChoiceField, self).clean(value)
if not value:
return None
if value not in self.choice_values:
raise forms.ValidationError("Invalid choice: {}".format(value))
return self.choice_values[value]
class ExpandableNameField(forms.CharField):
"""
A field which allows for numeric range expansion
@ -483,28 +517,3 @@ class BulkEditForm(forms.Form):
self.nullable_fields = [field for field in self.Meta.nullable_fields]
else:
self.nullable_fields = []
class BulkImportForm(forms.Form):
def clean(self):
records = self.cleaned_data.get('csv')
if not records:
return
obj_list = []
for i, record in enumerate(records, start=1):
obj_form = self.fields['csv'].csv_form(data=record)
if obj_form.is_valid():
obj = obj_form.save(commit=False)
obj_list.append(obj)
else:
for field, errors in obj_form.errors.items():
for e in errors:
if field == '__all__':
self.add_error('csv', "Record {}: {}".format(i, e))
else:
self.add_error('csv', "Record {} ({}): {}".format(i, field, e))
self.cleaned_data['csv'] = obj_list

View File

@ -40,7 +40,9 @@ def widget_type(field):
"""
Return the widget type
"""
try:
if hasattr(field, 'widget'):
return field.widget.__class__.__name__.lower()
elif hasattr(field, 'field'):
return field.field.widget.__class__.__name__.lower()
except AttributeError:
else:
return None

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from markdown import markdown
from django import template
@ -60,6 +62,22 @@ def bettertitle(value):
return ' '.join([w[0].upper() + w[1:] for w in value.split()])
@register.filter()
def example_choices(value, arg=3):
"""
Returns a number (default: 3) of example choices for a ChoiceFiled (useful for CSV import forms).
"""
choices = []
for id, label in value:
if len(choices) == arg:
choices.append('etc.')
break
if not id:
continue
choices.append(label)
return ', '.join(choices) or 'None'
#
# Tags
#

View File

@ -6,9 +6,10 @@ from django_tables2 import RequestConfig
from django.conf import settings
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import transaction, IntegrityError
from django.db.models import ProtectedError
from django.forms import CharField, ModelMultipleChoiceField, MultipleHiddenInput, TypedChoiceField
from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, TypedChoiceField
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.template import TemplateSyntaxError
@ -19,6 +20,7 @@ from django.utils.safestring import mark_safe
from django.views.generic import View
from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction
from utilities.forms import BootstrapMixin, CSVDataField
from .error_handlers import handle_protectederror
from .forms import ConfirmationForm
from .paginator import EnhancedPaginator
@ -371,56 +373,85 @@ class BulkImportView(View):
"""
Import objects in bulk (CSV format).
form: Form class
model_form: The form used to create each imported object
table: The django-tables2 Table used to render the list of imported objects
template_name: The name of the template
default_return_url: The name of the URL to use for the cancel button
"""
form = None
model_form = None
table = None
template_name = None
default_return_url = None
template_name = 'utilities/obj_import.html'
def _import_form(self, *args, **kwargs):
fields = self.model_form().fields.keys()
required_fields = [name for name, field in self.model_form().fields.items() if field.required]
class ImportForm(BootstrapMixin, Form):
csv = CSVDataField(fields=fields, required_fields=required_fields)
return ImportForm(*args, **kwargs)
def _save_obj(self, obj_form):
"""
Provide a hook to modify the object immediately before saving it (e.g. to encrypt secret data).
"""
return obj_form.save()
def get(self, request):
return render(request, self.template_name, {
'form': self.form(),
'form': self._import_form(),
'fields': self.model_form().fields,
'obj_type': self.model_form._meta.model._meta.verbose_name,
'return_url': self.default_return_url,
})
def post(self, request):
form = self.form(request.POST)
if form.is_valid():
new_objs = []
try:
with transaction.atomic():
for obj in form.cleaned_data['csv']:
self.save_obj(obj)
new_objs.append(obj)
new_objs = []
form = self._import_form(request.POST)
if form.is_valid():
try:
# Iterate through CSV data and bind each row to a new model form instance.
with transaction.atomic():
for row, data in enumerate(form.cleaned_data['csv'], start=1):
obj_form = self.model_form(data)
if obj_form.is_valid():
obj = self._save_obj(obj_form)
new_objs.append(obj)
else:
for field, err in obj_form.errors.items():
form.add_error('csv', "Row {} {}: {}".format(row, field, err[0]))
raise ValidationError("")
# Compile a table containing the imported objects
obj_table = self.table(new_objs)
if new_objs:
msg = 'Imported {} {}'.format(len(new_objs), new_objs[0]._meta.verbose_name_plural)
messages.success(request, msg)
UserAction.objects.log_import(request.user, ContentType.objects.get_for_model(new_objs[0]), msg)
return render(request, "import_success.html", {
'table': obj_table,
'return_url': self.default_return_url,
})
return render(request, "import_success.html", {
'table': obj_table,
'return_url': self.default_return_url,
})
except IntegrityError as e:
form.add_error('csv', "Record {}: {}".format(len(new_objs) + 1, e.__cause__))
except ValidationError:
pass
return render(request, self.template_name, {
'form': form,
'fields': self.model_form().fields,
'obj_type': self.model_form._meta.model._meta.verbose_name,
'return_url': self.default_return_url,
})
def save_obj(self, obj):
obj.save()
class BulkEditView(View):
"""