Merge branch 'develop' into v2-develop

This commit is contained in:
Jeremy Stretch 2017-05-08 15:02:06 -04:00 committed by GitHub
commit 5ca87c0f20
34 changed files with 888 additions and 861 deletions

View File

@ -10,7 +10,9 @@ Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https
### Build Status ### Build Status
| | python 2.7 | NetBox is built against both Python 2.7 and 3.5. Python 3.5 is recommended.
| | status |
|-------------|------------| |-------------|------------|
| **master** | [![Build Status](https://travis-ci.org/digitalocean/netbox.svg?branch=master)](https://travis-ci.org/digitalocean/netbox) | | **master** | [![Build Status](https://travis-ci.org/digitalocean/netbox.svg?branch=master)](https://travis-ci.org/digitalocean/netbox) |
| **develop** | [![Build Status](https://travis-ci.org/digitalocean/netbox.svg?branch=develop)](https://travis-ci.org/digitalocean/netbox) | | **develop** | [![Build Status](https://travis-ci.org/digitalocean/netbox.svg?branch=develop)](https://travis-ci.org/digitalocean/netbox) |

View File

@ -10,9 +10,9 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
from ipam.models import IPAddress from ipam.models import IPAddress
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField, APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, BulkImportForm, CommentField, CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField,
SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField,
) )
from .formfields import MACAddressFormField from .formfields import MACAddressFormField
@ -271,6 +271,7 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type') type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type')
width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width') width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width')
u_height = forms.IntegerField(required=False, label='Height (U)') u_height = forms.IntegerField(required=False, label='Height (U)')
desc_units = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Descending units')
comments = CommentField(widget=SmallTextarea) comments = CommentField(widget=SmallTextarea)
class Meta: class Meta:
@ -374,7 +375,13 @@ class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False) manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
u_height = forms.IntegerField(min_value=1, required=False) u_height = forms.IntegerField(min_value=1, required=False)
is_full_depth = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
interface_ordering = forms.ChoiceField(choices=add_blank_choice(IFACE_ORDERING_CHOICES), required=False) interface_ordering = forms.ChoiceField(choices=add_blank_choice(IFACE_ORDERING_CHOICES), required=False)
is_console_server = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
is_pdu = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a PDU')
is_network_device = forms.NullBooleanField(
required=False, widget=BulkEditNullBooleanSelect, label='Is a network device'
)
class Meta: class Meta:
nullable_fields = [] nullable_fields = []
@ -483,6 +490,7 @@ class InterfaceTemplateCreateForm(DeviceComponentForm):
class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm): class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=InterfaceTemplate.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=InterfaceTemplate.objects.all(), widget=forms.MultipleHiddenInput)
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False) form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only')
class Meta: class Meta:
nullable_fields = [] nullable_fields = []
@ -1415,6 +1423,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput) device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput)
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG') lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False) form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only')
description = forms.CharField(max_length=100, required=False) description = forms.CharField(max_length=100, required=False)
class Meta: class Meta:

View File

@ -12,11 +12,12 @@ from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.http import urlencode from django.utils.http import urlencode
from django.utils.safestring import mark_safe
from django.views.generic import View from django.views.generic import View
from ipam.models import Prefix, Service, VLAN from ipam.models import Prefix, Service, VLAN
from circuits.models import Circuit from circuits.models import Circuit
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE, UserAction
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator from utilities.paginator import EnhancedPaginator
from utilities.views import ( from utilities.views import (
@ -892,12 +893,16 @@ def consoleport_connect(request, pk):
form = forms.ConsolePortConnectionForm(request.POST, instance=consoleport) form = forms.ConsolePortConnectionForm(request.POST, instance=consoleport)
if form.is_valid(): if form.is_valid():
consoleport = form.save() consoleport = form.save()
messages.success(request, u"Connected {} {} to {} {}.".format( msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
consoleport.device, consoleport.device.get_absolute_url(),
consoleport.name, escape(consoleport.device),
consoleport.cs_port.device, escape(consoleport.name),
consoleport.cs_port.name, consoleport.cs_port.device.get_absolute_url(),
)) escape(consoleport.cs_port.device),
escape(consoleport.cs_port.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, consoleport, msg)
return redirect('dcim:device', pk=consoleport.device.pk) return redirect('dcim:device', pk=consoleport.device.pk)
else: else:
@ -921,17 +926,28 @@ def consoleport_disconnect(request, pk):
consoleport = get_object_or_404(ConsolePort, pk=pk) consoleport = get_object_or_404(ConsolePort, pk=pk)
if not consoleport.cs_port: if not consoleport.cs_port:
messages.warning(request, u"Cannot disconnect console port {}: It is not connected to anything." messages.warning(
.format(consoleport)) request, u"Cannot disconnect console port {}: It is not connected to anything.".format(consoleport)
)
return redirect('dcim:device', pk=consoleport.device.pk) return redirect('dcim:device', pk=consoleport.device.pk)
if request.method == 'POST': if request.method == 'POST':
form = ConfirmationForm(request.POST) form = ConfirmationForm(request.POST)
if form.is_valid(): if form.is_valid():
cs_port = consoleport.cs_port
consoleport.cs_port = None consoleport.cs_port = None
consoleport.connection_status = None consoleport.connection_status = None
consoleport.save() consoleport.save()
messages.success(request, u"Console port {} has been disconnected.".format(consoleport)) msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
consoleport.device.get_absolute_url(),
escape(consoleport.device),
escape(consoleport.name),
cs_port.device.get_absolute_url(),
escape(cs_port.device),
escape(cs_port.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, consoleport, msg)
return redirect('dcim:device', pk=consoleport.device.pk) return redirect('dcim:device', pk=consoleport.device.pk)
else: else:
@ -966,6 +982,7 @@ class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.ConsoleConnectionImportForm form = forms.ConsoleConnectionImportForm
table = tables.ConsoleConnectionTable table = tables.ConsoleConnectionTable
template_name = 'dcim/console_connections_import.html' template_name = 'dcim/console_connections_import.html'
default_return_url = 'dcim:console_connections_list'
# #
@ -993,12 +1010,16 @@ def consoleserverport_connect(request, pk):
consoleport.cs_port = consoleserverport consoleport.cs_port = consoleserverport
consoleport.connection_status = form.cleaned_data['connection_status'] consoleport.connection_status = form.cleaned_data['connection_status']
consoleport.save() consoleport.save()
messages.success(request, u"Connected {} {} to {} {}.".format( msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
consoleport.device, consoleport.device.get_absolute_url(),
consoleport.name, escape(consoleport.device),
consoleserverport.device, escape(consoleport.name),
consoleserverport.name, consoleserverport.device.get_absolute_url(),
)) escape(consoleserverport.device),
escape(consoleserverport.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, consoleport, msg)
return redirect('dcim:device', pk=consoleserverport.device.pk) return redirect('dcim:device', pk=consoleserverport.device.pk)
else: else:
@ -1022,8 +1043,9 @@ def consoleserverport_disconnect(request, pk):
consoleserverport = get_object_or_404(ConsoleServerPort, pk=pk) consoleserverport = get_object_or_404(ConsoleServerPort, pk=pk)
if not hasattr(consoleserverport, 'connected_console'): if not hasattr(consoleserverport, 'connected_console'):
messages.warning(request, u"Cannot disconnect console server port {}: Nothing is connected to it." messages.warning(
.format(consoleserverport)) request, u"Cannot disconnect console server port {}: Nothing is connected to it.".format(consoleserverport)
)
return redirect('dcim:device', pk=consoleserverport.device.pk) return redirect('dcim:device', pk=consoleserverport.device.pk)
if request.method == 'POST': if request.method == 'POST':
@ -1033,7 +1055,16 @@ def consoleserverport_disconnect(request, pk):
consoleport.cs_port = None consoleport.cs_port = None
consoleport.connection_status = None consoleport.connection_status = None
consoleport.save() consoleport.save()
messages.success(request, u"Console server port {} has been disconnected.".format(consoleserverport)) msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
consoleport.device.get_absolute_url(),
escape(consoleport.device),
escape(consoleport.name),
consoleserverport.device.get_absolute_url(),
escape(consoleserverport.device),
escape(consoleserverport.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, consoleport, msg)
return redirect('dcim:device', pk=consoleserverport.device.pk) return redirect('dcim:device', pk=consoleserverport.device.pk)
else: else:
@ -1085,12 +1116,16 @@ def powerport_connect(request, pk):
form = forms.PowerPortConnectionForm(request.POST, instance=powerport) form = forms.PowerPortConnectionForm(request.POST, instance=powerport)
if form.is_valid(): if form.is_valid():
powerport = form.save() powerport = form.save()
messages.success(request, u"Connected {} {} to {} {}.".format( msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
powerport.device, powerport.device.get_absolute_url(),
powerport.name, escape(powerport.device),
powerport.power_outlet.device, escape(powerport.name),
powerport.power_outlet.name, powerport.power_outlet.device.get_absolute_url(),
)) escape(powerport.power_outlet.device),
escape(powerport.power_outlet.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, powerport, msg)
return redirect('dcim:device', pk=powerport.device.pk) return redirect('dcim:device', pk=powerport.device.pk)
else: else:
@ -1114,17 +1149,28 @@ def powerport_disconnect(request, pk):
powerport = get_object_or_404(PowerPort, pk=pk) powerport = get_object_or_404(PowerPort, pk=pk)
if not powerport.power_outlet: if not powerport.power_outlet:
messages.warning(request, u"Cannot disconnect power port {}: It is not connected to an outlet." messages.warning(
.format(powerport)) request, u"Cannot disconnect power port {}: It is not connected to an outlet.".format(powerport)
)
return redirect('dcim:device', pk=powerport.device.pk) return redirect('dcim:device', pk=powerport.device.pk)
if request.method == 'POST': if request.method == 'POST':
form = ConfirmationForm(request.POST) form = ConfirmationForm(request.POST)
if form.is_valid(): if form.is_valid():
power_outlet = powerport.power_outlet
powerport.power_outlet = None powerport.power_outlet = None
powerport.connection_status = None powerport.connection_status = None
powerport.save() powerport.save()
messages.success(request, u"Power port {} has been disconnected.".format(powerport)) msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
powerport.device.get_absolute_url(),
escape(powerport.device),
escape(powerport.name),
power_outlet.device.get_absolute_url(),
escape(power_outlet.device),
escape(power_outlet.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, powerport, msg)
return redirect('dcim:device', pk=powerport.device.pk) return redirect('dcim:device', pk=powerport.device.pk)
else: else:
@ -1159,6 +1205,7 @@ class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.PowerConnectionImportForm form = forms.PowerConnectionImportForm
table = tables.PowerConnectionTable table = tables.PowerConnectionTable
template_name = 'dcim/power_connections_import.html' template_name = 'dcim/power_connections_import.html'
default_return_url = 'dcim:power_connections_list'
# #
@ -1186,12 +1233,16 @@ def poweroutlet_connect(request, pk):
powerport.power_outlet = poweroutlet powerport.power_outlet = poweroutlet
powerport.connection_status = form.cleaned_data['connection_status'] powerport.connection_status = form.cleaned_data['connection_status']
powerport.save() powerport.save()
messages.success(request, u"Connected {} {} to {} {}.".format( msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
powerport.device, powerport.device.get_absolute_url(),
powerport.name, escape(powerport.device),
poweroutlet.device, escape(powerport.name),
poweroutlet.name, poweroutlet.device.get_absolute_url(),
)) escape(poweroutlet.device),
escape(poweroutlet.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, powerport, msg)
return redirect('dcim:device', pk=poweroutlet.device.pk) return redirect('dcim:device', pk=poweroutlet.device.pk)
else: else:
@ -1215,7 +1266,9 @@ def poweroutlet_disconnect(request, pk):
poweroutlet = get_object_or_404(PowerOutlet, pk=pk) poweroutlet = get_object_or_404(PowerOutlet, pk=pk)
if not hasattr(poweroutlet, 'connected_port'): if not hasattr(poweroutlet, 'connected_port'):
messages.warning(request, u"Cannot disconnect power outlet {}: Nothing is connected to it.".format(poweroutlet)) messages.warning(
request, u"Cannot disconnect power outlet {}: Nothing is connected to it.".format(poweroutlet)
)
return redirect('dcim:device', pk=poweroutlet.device.pk) return redirect('dcim:device', pk=poweroutlet.device.pk)
if request.method == 'POST': if request.method == 'POST':
@ -1225,7 +1278,16 @@ def poweroutlet_disconnect(request, pk):
powerport.power_outlet = None powerport.power_outlet = None
powerport.connection_status = None powerport.connection_status = None
powerport.save() powerport.save()
messages.success(request, u"Power outlet {} has been disconnected.".format(poweroutlet)) msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
powerport.device.get_absolute_url(),
escape(powerport.device),
escape(powerport.name),
poweroutlet.device.get_absolute_url(),
escape(poweroutlet.device),
escape(poweroutlet.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, powerport, msg)
return redirect('dcim:device', pk=poweroutlet.device.pk) return redirect('dcim:device', pk=poweroutlet.device.pk)
else: else:
@ -1491,13 +1553,19 @@ def interfaceconnection_add(request, pk):
if request.method == 'POST': if request.method == 'POST':
form = forms.InterfaceConnectionForm(device, request.POST) form = forms.InterfaceConnectionForm(device, request.POST)
if form.is_valid(): if form.is_valid():
interfaceconnection = form.save() interfaceconnection = form.save()
messages.success(request, u"Connected {} {} to {} {}.".format( msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
interfaceconnection.interface_a.device, interfaceconnection.interface_a.device.get_absolute_url(),
interfaceconnection.interface_a, escape(interfaceconnection.interface_a.device),
interfaceconnection.interface_b.device, escape(interfaceconnection.interface_a.name),
interfaceconnection.interface_b, interfaceconnection.interface_b.device.get_absolute_url(),
)) escape(interfaceconnection.interface_b.device),
escape(interfaceconnection.interface_b.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, interfaceconnection, msg)
if '_addanother' in request.POST: if '_addanother' in request.POST:
base_url = reverse('dcim:interfaceconnection_add', kwargs={'pk': device.pk}) base_url = reverse('dcim:interfaceconnection_add', kwargs={'pk': device.pk})
device_b = interfaceconnection.interface_b.device device_b = interfaceconnection.interface_b.device
@ -1535,12 +1603,16 @@ def interfaceconnection_delete(request, pk):
form = forms.InterfaceConnectionDeletionForm(request.POST) form = forms.InterfaceConnectionDeletionForm(request.POST)
if form.is_valid(): if form.is_valid():
interfaceconnection.delete() interfaceconnection.delete()
messages.success(request, u"Deleted the connection between {} {} and {} {}.".format( msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
interfaceconnection.interface_a.device, interfaceconnection.interface_a.device.get_absolute_url(),
interfaceconnection.interface_a, escape(interfaceconnection.interface_a.device),
interfaceconnection.interface_b.device, escape(interfaceconnection.interface_a.name),
interfaceconnection.interface_b, interfaceconnection.interface_b.device.get_absolute_url(),
)) escape(interfaceconnection.interface_b.device),
escape(interfaceconnection.interface_b.name),
)
messages.success(request, mark_safe(msg))
UserAction.objects.log_edit(request.user, interfaceconnection, msg)
if form.cleaned_data['device']: if form.cleaned_data['device']:
return redirect('dcim:device', pk=form.cleaned_data['device'].pk) return redirect('dcim:device', pk=form.cleaned_data['device'].pk)
else: else:
@ -1570,6 +1642,7 @@ class InterfaceConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView
form = forms.InterfaceConnectionImportForm form = forms.InterfaceConnectionImportForm
table = tables.InterfaceConnectionTable table = tables.InterfaceConnectionTable
template_name = 'dcim/interface_connections_import.html' template_name = 'dcim/interface_connections_import.html'
default_return_url = 'dcim:interface_connections_list'
# #

View File

@ -9,7 +9,10 @@ from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from .models import (
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
VLAN_STATUS_CHOICES, VLANGroup, VRF,
)
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet): class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
@ -153,10 +156,13 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Role (slug)', label='Role (slug)',
) )
status = django_filters.MultipleChoiceFilter(
choices=PREFIX_STATUS_CHOICES
)
class Meta: class Meta:
model = Prefix model = Prefix
fields = ['family', 'status'] fields = ['family']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -237,10 +243,13 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
label='Interface (ID)', label='Interface (ID)',
) )
status = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_STATUS_CHOICES
)
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = ['family', 'status'] fields = ['family']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -337,10 +346,13 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Role (slug)', label='Role (slug)',
) )
status = django_filters.MultipleChoiceFilter(
choices=VLAN_STATUS_CHOICES
)
class Meta: class Meta:
model = VLAN model = VLAN
fields = ['name', 'vid', 'status'] fields = ['name', 'vid']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -5,8 +5,8 @@ from dcim.models import Site, Rack, Device, Interface
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
APISelect, BootstrapMixin, BulkImportForm, CSVDataField, ExpandableIPAddressField, FilterChoiceField, Livesearch, APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, CSVDataField, ExpandableIPAddressField,
ReturnURLForm, SlugField, add_blank_choice, FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice,
) )
from .models import ( from .models import (
@ -61,6 +61,9 @@ class VRFImportForm(BootstrapMixin, BulkImportForm):
class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
enforce_unique = forms.NullBooleanField(
required=False, widget=BulkEditNullBooleanSelect, label='Enforce unique space'
)
description = forms.CharField(max_length=100, required=False) description = forms.CharField(max_length=100, required=False)
class Meta: class Meta:
@ -256,6 +259,7 @@ class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
status = forms.ChoiceField(choices=add_blank_choice(PREFIX_STATUS_CHOICES), required=False) status = forms.ChoiceField(choices=add_blank_choice(PREFIX_STATUS_CHOICES), required=False)
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False) role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
is_pool = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a pool')
description = forms.CharField(max_length=100, required=False) description = forms.CharField(max_length=100, required=False)
class Meta: class Meta:
@ -340,10 +344,11 @@ class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm):
query_key='q', query_url='ipam-api:ipaddress-list', field_to_update='nat_inside', obj_label='address' 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: class Meta:
model = IPAddress model = IPAddress
fields = ['address', 'vrf', 'tenant', 'status', 'interface', 'nat_inside', 'description'] fields = ['address', 'vrf', 'tenant', 'status', 'description', 'interface', 'primary_for_device', 'nat_inside']
widgets = { widgets = {
'interface': APISelect(api_url='/api/dcim/devices/interfaces/?device_id={{interface_device}}'), 'interface': APISelect(api_url='/api/dcim/devices/interfaces/?device_id={{interface_device}}'),
'nat_inside': APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}', display_field='address') 'nat_inside': APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}', display_field='address')
@ -384,6 +389,15 @@ class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm):
else: else:
self.fields['interface'].choices = [] 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
if (
self.instance.address.version == 4 and device.primary_ip4 == self.instance or
self.instance.address.version == 6 and device.primary_ip6 == self.instance
):
self.initial['primary_for_device'] = True
if self.instance.nat_inside: if self.instance.nat_inside:
nat_inside = self.instance.nat_inside nat_inside = self.instance.nat_inside
# If the IP is assigned to an interface, populate site/device fields accordingly # If the IP is assigned to an interface, populate site/device fields accordingly
@ -416,6 +430,43 @@ class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm):
else: else:
self.fields['nat_inside'].choices = [] self.fields['nat_inside'].choices = []
def clean(self):
super(IPAddressForm, self).clean()
# Primary IP assignment is only available if an interface has been assigned.
if self.cleaned_data.get('primary_for_device') and not self.cleaned_data.get('interface'):
self.add_error(
'primary_for_device', "Only IP addresses assigned to an interface can be designated as primary IPs."
)
def save(self, *args, **kwargs):
ipaddress = super(IPAddressForm, self).save(*args, **kwargs)
# Assign this IPAddress as the primary for the associated Device.
if self.cleaned_data['primary_for_device']:
device = self.cleaned_data['interface'].device
if ipaddress.address.version == 4:
device.primary_ip4 = ipaddress
else:
device.primary_ip6 = ipaddress
device.save()
# Clear assignment as primary for device if set.
else:
try:
if ipaddress.address.version == 4:
device = ipaddress.primary_ip4_for
device.primary_ip4 = None
else:
device = ipaddress.primary_ip6_for
device.primary_ip6 = None
device.save()
except Device.DoesNotExist:
pass
return ipaddress
class IPAddressBulkAddForm(BootstrapMixin, CustomFieldForm): class IPAddressBulkAddForm(BootstrapMixin, CustomFieldForm):
address_pattern = ExpandableIPAddressField(label='Address Pattern') address_pattern = ExpandableIPAddressField(label='Address Pattern')

View File

@ -221,6 +221,7 @@ def secret_import(request):
return render(request, 'import_success.html', { return render(request, 'import_success.html', {
'table': table, 'table': table,
'return_url': 'secrets:secret_list',
}) })
except IntegrityError as e: except IntegrityError as e:
@ -231,7 +232,7 @@ def secret_import(request):
return render(request, 'secrets/secret_import.html', { return render(request, 'secrets/secret_import.html', {
'form': form, 'form': form,
'return_url': reverse('secrets:secret_list'), 'return_url': 'secrets:secret_list',
}) })

View File

@ -1,72 +1,57 @@
{% extends '_base.html' %} {% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}Circuit Import{% endblock %} {% block title %}Circuit Import{% endblock %}
{% block content %} {% block instructions %}
<h1>Circuit Import</h1> <h4>CSV Format</h4>
<div class="row"> <table class="table">
<div class="col-md-6"> <thead>
<form action="." method="post" class="form"> <tr>
{% csrf_token %} <th>Field</th>
{% render_form form %} <th>Description</th>
<div class="form-group"> <th>Example</th>
<button type="submit" class="btn btn-primary">Submit</button> </tr>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a> </thead>
</div> <tbody>
</form> <tr>
</div> <td>Circuit ID</td>
<div class="col-md-6"> <td>Alphanumeric circuit identifier</td>
<h4>CSV Format</h4> <td>IC-603122</td>
<table class="table"> </tr>
<thead> <tr>
<tr> <td>Provider</td>
<th>Field</th> <td>Name of circuit provider</td>
<th>Description</th> <td>TeliaSonera</td>
<th>Example</th> </tr>
</tr> <tr>
</thead> <td>Type</td>
<tbody> <td>Circuit type</td>
<tr> <td>Transit</td>
<td>Circuit ID</td> </tr>
<td>Alphanumeric circuit identifier</td> <tr>
<td>IC-603122</td> <td>Tenant</td>
</tr> <td>Name of tenant (optional)</td>
<tr> <td>Strickland Propane</td>
<td>Provider</td> </tr>
<td>Name of circuit provider</td> <tr>
<td>TeliaSonera</td> <td>Install Date</td>
</tr> <td>Date in YYYY-MM-DD format (optional)</td>
<tr> <td>2016-02-23</td>
<td>Type</td> </tr>
<td>Circuit type</td> <tr>
<td>Transit</td> <td>Commit rate</td>
</tr> <td>Commited rate in Kbps (optional)</td>
<tr> <td>2000</td>
<td>Tenant</td> </tr>
<td>Name of tenant (optional)</td> <tr>
<td>Strickland Propane</td> <td>Description</td>
</tr> <td>Short description (optional)</td>
<tr> <td>Primary for voice</td>
<td>Install Date</td> </tr>
<td>Date in YYYY-MM-DD format (optional)</td> </tbody>
<td>2016-02-23</td> </table>
</tr> <h4>Example</h4>
<tr> <pre>IC-603122,TeliaSonera,Transit,Strickland Propane,2016-02-23,2000,Primary for voice</pre>
<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>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,62 +1,47 @@
{% extends '_base.html' %} {% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}Provider Import{% endblock %} {% block title %}Provider Import{% endblock %}
{% block content %} {% block instructions %}
<h1>Provider Import</h1> <h4>CSV Format</h4>
<div class="row"> <table class="table">
<div class="col-md-6"> <thead>
<form action="." method="post" class="form"> <tr>
{% csrf_token %} <th>Field</th>
{% render_form form %} <th>Description</th>
<div class="form-group"> <th>Example</th>
<button type="submit" class="btn btn-primary">Submit</button> </tr>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a> </thead>
</div> <tbody>
</form> <tr>
</div> <td>Name</td>
<div class="col-md-6"> <td>Provider's proper name</td>
<h4>CSV Format</h4> <td>Level 3</td>
<table class="table"> </tr>
<thead> <tr>
<tr> <td>Slug</td>
<th>Field</th> <td>URL-friendly name</td>
<th>Description</th> <td>level3</td>
<th>Example</th> </tr>
</tr> <tr>
</thead> <td>ASN</td>
<tbody> <td>Autonomous system number (optional)</td>
<tr> <td>3356</td>
<td>Name</td> </tr>
<td>Provider's proper name</td> <tr>
<td>Level 3</td> <td>Account</td>
</tr> <td>Account number (optional)</td>
<tr> <td>08931544</td>
<td>Slug</td> </tr>
<td>URL-friendly name</td> <tr>
<td>level3</td> <td>Portal URL</td>
</tr> <td>Customer service portal URL (optional)</td>
<tr> <td>https://mylevel3.net</td>
<td>ASN</td> </tr>
<td>Autonomous system number (optional)</td> </tbody>
<td>3356</td> </table>
</tr> <h4>Example</h4>
<tr> <pre>Level 3,level3,3356,08931544,https://mylevel3.net</pre>
<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>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,61 +1,47 @@
{% extends '_base.html' %} {% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}Console Connections Import{% endblock %} {% block title %}Console Connections Import{% endblock %}
{% block content %} {% block instructions %}
<h1>Console Connections Import</h1> <h4>CSV Format</h4>
<div class="row"> <table class="table">
<div class="col-md-6"> <thead>
<form action="." method="post" class="form"> <tr>
{% csrf_token %} <th>Field</th>
{% render_form form %} <th>Description</th>
<div class="form-group"> <th>Example</th>
<button type="submit" class="btn btn-primary">Submit</button> </tr>
</div> </thead>
</form> <tbody>
</div> <tr>
<div class="col-md-6"> <td>Console server</td>
<h4>CSV Format</h4> <td>Device name or {ID}</td>
<table class="table"> <td>abc1-cs3</td>
<thead> </tr>
<tr> <tr>
<th>Field</th> <td>Console server port</td>
<th>Description</th> <td>Full CS port name</td>
<th>Example</th> <td>Port 35</td>
</tr> </tr>
</thead> <tr>
<tbody> <td>Device</td>
<tr> <td>Device name or {ID}</td>
<td>Console server</td> <td>abc1-switch7</td>
<td>Device name or {ID}</td> </tr>
<td>abc1-cs3</td> <tr>
</tr> <td>Console Port</td>
<tr> <td>Console port name</td>
<td>Console server port</td> <td>Console</td>
<td>Full CS port name</td> </tr>
<td>Port 35</td> <tr>
</tr> <td>Connection Status</td>
<tr> <td>"planned" or "connected"</td>
<td>Device</td> <td>planned</td>
<td>Device name or {ID}</td> </tr>
<td>abc1-switch7</td> </tbody>
</tr> </table>
<tr> <h4>Example</h4>
<td>Console Port</td> <pre>abc1-cs3,Port 35,abc1-switch7,Console,planned</pre>
<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>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -12,8 +12,12 @@
{% csrf_token %} {% csrf_token %}
{% render_form form %} {% render_form form %}
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button> <div class="col-md-12 text-right">
<a href="{% url return_url %}" class="btn btn-default">Cancel</a> <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> </div>
</form> </form>
<h4>CSV Format</h4> <h4>CSV Format</h4>

View File

@ -12,8 +12,12 @@
{% csrf_token %} {% csrf_token %}
{% render_form form %} {% render_form form %}
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button> <div class="col-md-12 text-right">
<a href="{% url return_url %}" class="btn btn-default">Cancel</a> <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> </div>
</form> </form>
<h4>CSV Format</h4> <h4>CSV Format</h4>

View File

@ -7,6 +7,7 @@
<td> <td>
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }} <i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
</td> </td>
<td></td>
{% if cp.cs_port %} {% if cp.cs_port %}
<td> <td>
<a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a> <a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
@ -32,11 +33,11 @@
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'dcim:consoleport_disconnect' pk=cp.pk %}" class="btn btn-danger btn-xs"> <a href="{% url 'dcim:consoleport_disconnect' pk=cp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Delete connection"></i> <i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
</a> </a>
{% else %} {% else %}
<a href="{% url 'dcim:consoleport_connect' pk=cp.pk %}" class="btn btn-success btn-xs"> <a href="{% url 'dcim:consoleport_connect' pk=cp.pk %}" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Connect"></i> <i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}" class="btn btn-info btn-xs"> <a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}" class="btn btn-info btn-xs">

View File

@ -7,6 +7,7 @@
<td> <td>
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp.name }} <i class="fa fa-fw fa-keyboard-o"></i> {{ csp.name }}
</td> </td>
<td></td>
{% if csp.connected_console %} {% if csp.connected_console %}
<td> <td>
<a href="{% url 'dcim:device' pk=csp.connected_console.device.pk %}">{{ csp.connected_console.device }}</a> <a href="{% url 'dcim:device' pk=csp.connected_console.device.pk %}">{{ csp.connected_console.device }}</a>
@ -32,11 +33,11 @@
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'dcim:consoleserverport_disconnect' pk=csp.pk %}" class="btn btn-danger btn-xs"> <a href="{% url 'dcim:consoleserverport_disconnect' pk=csp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Delete connection"></i> <i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
</a> </a>
{% else %} {% else %}
<a href="{% url 'dcim:consoleserverport_connect' pk=csp.pk %}" class="btn btn-success btn-xs"> <a href="{% url 'dcim:consoleserverport_connect' pk=csp.pk %}" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Connect"></i> <i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'dcim:consoleserverport_edit' pk=csp.pk %}" class="btn btn-info btn-xs"> <a href="{% url 'dcim:consoleserverport_edit' pk=csp.pk %}" class="btn btn-info btn-xs">

View File

@ -12,12 +12,13 @@
{% if iface.description %} {% if iface.description %}
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i> <i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
{% endif %} {% endif %}
{% if iface.is_lag %}
<br /><small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small>
{% endif %}
</td> </td>
<td>{{ iface.mac_address|default:"" }}</td>
{% if iface.is_lag %} {% if iface.is_lag %}
<td colspan="2" class="text-muted">LAG interface</td> <td colspan="2" class="text-muted">
LAG interface<br />
<small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small>
</td>
{% elif iface.is_virtual %} {% elif iface.is_virtual %}
<td colspan="2" class="text-muted">Virtual interface</td> <td colspan="2" class="text-muted">Virtual interface</td>
{% elif iface.connection %} {% elif iface.connection %}

View File

@ -7,6 +7,7 @@
<td> <td>
<i class="fa fa-fw fa-bolt"></i> {{ po.name }} <i class="fa fa-fw fa-bolt"></i> {{ po.name }}
</td> </td>
<td></td>
{% if po.connected_port %} {% if po.connected_port %}
<td> <td>
<a href="{% url 'dcim:device' pk=po.connected_port.device.pk %}">{{ po.connected_port.device }}</a> <a href="{% url 'dcim:device' pk=po.connected_port.device.pk %}">{{ po.connected_port.device }}</a>
@ -32,11 +33,11 @@
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'dcim:poweroutlet_disconnect' pk=po.pk %}" class="btn btn-danger btn-xs"> <a href="{% url 'dcim:poweroutlet_disconnect' pk=po.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Delete connection"></i> <i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
</a> </a>
{% else %} {% else %}
<a href="{% url 'dcim:poweroutlet_connect' pk=po.pk %}" class="btn btn-success btn-xs"> <a href="{% url 'dcim:poweroutlet_connect' pk=po.pk %}" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Connect"></i> <i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'dcim:poweroutlet_edit' pk=po.pk %}" class="btn btn-info btn-xs"> <a href="{% url 'dcim:poweroutlet_edit' pk=po.pk %}" class="btn btn-info btn-xs">

View File

@ -7,6 +7,7 @@
<td> <td>
<i class="fa fa-fw fa-bolt"></i> {{ pp.name }} <i class="fa fa-fw fa-bolt"></i> {{ pp.name }}
</td> </td>
<td></td>
{% if pp.power_outlet %} {% if pp.power_outlet %}
<td> <td>
<a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a> <a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
@ -32,11 +33,11 @@
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'dcim:powerport_disconnect' pk=pp.pk %}" class="btn btn-danger btn-xs"> <a href="{% url 'dcim:powerport_disconnect' pk=pp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Delete connection"></i> <i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
</a> </a>
{% else %} {% else %}
<a href="{% url 'dcim:powerport_connect' pk=pp.pk %}" class="btn btn-success btn-xs"> <a href="{% url 'dcim:powerport_connect' pk=pp.pk %}" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Connect"></i> <i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'dcim:powerport_edit' pk=pp.pk %}" class="btn btn-info btn-xs"> <a href="{% url 'dcim:powerport_edit' pk=pp.pk %}" class="btn btn-info btn-xs">

View File

@ -1,69 +1,47 @@
{% extends '_base.html' %} {% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}Interface Connections Import{% endblock %} {% block title %}Interface Connections Import{% endblock %}
{% block content %} {% block instructions %}
<h1>Interface Connections Import</h1> <h4>CSV Format</h4>
<div class="row"> <table class="table">
<div class="col-md-6"> <thead>
{% if form.non_field_errors %} <tr>
<div class="panel panel-danger"> <th>Field</th>
<div class="panel-heading"><strong>Errors</strong></div> <th>Description</th>
<div class="panel-body"> <th>Example</th>
{{ form.non_field_errors }} </tr>
</div> </thead>
</div> <tbody>
{% endif %} <tr>
<form action="." method="post" class="form"> <td>Device A</td>
{% csrf_token %} <td>Device name or {ID}</td>
{% render_form form %} <td>abc1-core1</td>
<div class="form-group"> </tr>
<button type="submit" class="btn btn-primary">Submit</button> <tr>
</div> <td>Interface A</td>
</form> <td>Interface name</td>
</div> <td>xe-0/0/6</td>
<div class="col-md-6"> </tr>
<h4>CSV Format</h4> <tr>
<table class="table"> <td>Device B</td>
<thead> <td>Device name or {ID}</td>
<tr> <td>abc1-switch7</td>
<th>Field</th> </tr>
<th>Description</th> <tr>
<th>Example</th> <td>Interface B</td>
</tr> <td>Interface name</td>
</thead> <td>xe-0/0/0</td>
<tbody> </tr>
<tr> <tr>
<td>Device A</td> <td>Connection Status</td>
<td>Device name or {ID}</td> <td>"planned" or "connected"</td>
<td>abc1-core1</td> <td>planned</td>
</tr> </tr>
<tr> </tbody>
<td>Interface A</td> </table>
<td>Interface name</td> <h4>Example</h4>
<td>xe-0/0/6</td> <pre>abc1-core1,xe-0/0/6,abc1-switch7,xe-0/0/0,planned</pre>
</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>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,61 +1,47 @@
{% extends '_base.html' %} {% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}Power Connections Import{% endblock %} {% block title %}Power Connections Import{% endblock %}
{% block content %} {% block instructions %}
<h1>Power Connections Import</h1> <h4>CSV Format</h4>
<div class="row"> <table class="table">
<div class="col-md-6"> <thead>
<form action="." method="post" class="form"> <tr>
{% csrf_token %} <th>Field</th>
{% render_form form %} <th>Description</th>
<div class="form-group"> <th>Example</th>
<button type="submit" class="btn btn-primary">Submit</button> </tr>
</div> </thead>
</form> <tbody>
</div> <tr>
<div class="col-md-6"> <td>PDU</td>
<h4>CSV Format</h4> <td>Device name or {ID}</td>
<table class="table"> <td>abc1-pdu1</td>
<thead> </tr>
<tr> <tr>
<th>Field</th> <td>Power Outlet</td>
<th>Description</th> <td>Power outlet name</td>
<th>Example</th> <td>AC4</td>
</tr> </tr>
</thead> <tr>
<tbody> <td>Device</td>
<tr> <td>Device name or {ID}</td>
<td>PDU</td> <td>abc1-switch7</td>
<td>Device name or {ID}</td> </tr>
<td>abc1-pdu1</td> <tr>
</tr> <td>Power Port</td>
<tr> <td>Power port name</td>
<td>Power Outlet</td> <td>PSU0</td>
<td>Power outlet name</td> </tr>
<td>AC4</td> <tr>
</tr> <td>Connection Status</td>
<tr> <td>"planned" or "connected"</td>
<td>Device</td> <td>connected</td>
<td>Device name or {ID}</td> </tr>
<td>abc1-switch7</td> </tbody>
</tr> </table>
<tr> <h4>Example</h4>
<td>Power Port</td> <pre>abc1-pdu1,AC4,abc1-switch7,PSU0,connected</pre>
<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>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,87 +1,72 @@
{% extends '_base.html' %} {% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}Rack Import{% endblock %} {% block title %}Rack Import{% endblock %}
{% block content %} {% block instructions %}
<h1>Rack Import</h1> <h4>CSV Format</h4>
<div class="row"> <table class="table">
<div class="col-md-6"> <thead>
<form action="." method="post" class="form"> <tr>
{% csrf_token %} <th>Field</th>
{% render_form form %} <th>Description</th>
<div class="form-group"> <th>Example</th>
<button type="submit" class="btn btn-primary">Submit</button> </tr>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a> </thead>
</div> <tbody>
</form> <tr>
</div> <td>Site</td>
<div class="col-md-6"> <td>Name of the assigned site</td>
<h4>CSV Format</h4> <td>DC-4</td>
<table class="table"> </tr>
<thead> <tr>
<tr> <td>Group</td>
<th>Field</th> <td>Rack group name (optional)</td>
<th>Description</th> <td>Cage 1400</td>
<th>Example</th> </tr>
</tr> <tr>
</thead> <td>Name</td>
<tbody> <td>Internal rack name</td>
<tr> <td>R101</td>
<td>Site</td> </tr>
<td>Name of the assigned site</td> <tr>
<td>DC-4</td> <td>Facility ID</td>
</tr> <td>Rack ID assigned by the facility (optional)</td>
<tr> <td>J12.100</td>
<td>Group</td> </tr>
<td>Rack group name (optional)</td> <tr>
<td>Cage 1400</td> <td>Tenant</td>
</tr> <td>Name of tenant (optional)</td>
<tr> <td>Pied Piper</td>
<td>Name</td> </tr>
<td>Internal rack name</td> <tr>
<td>R101</td> <td>Role</td>
</tr> <td>Functional role (optional)</td>
<tr> <td>Compute</td>
<td>Facility ID</td> </tr>
<td>Rack ID assigned by the facility (optional)</td> <tr>
<td>J12.100</td> <td>Type</td>
</tr> <td>Rack type (optional)</td>
<tr> <td>4-post cabinet</td>
<td>Tenant</td> </tr>
<td>Name of tenant (optional)</td> <tr>
<td>Pied Piper</td> <td>Width</td>
</tr> <td>Rail-to-rail width (19 or 23 inches)</td>
<tr> <td>19</td>
<td>Role</td> </tr>
<td>Functional role (optional)</td> <tr>
<td>Compute</td> <td>Height</td>
</tr> <td>Height in rack units</td>
<tr> <td>42</td>
<td>Type</td> </tr>
<td>Rack type (optional)</td> <tr>
<td>4-post cabinet</td> <td>Descending units</td>
</tr> <td>Units are numbered top-to-bottom</td>
<tr> <td>False</td>
<td>Width</td> </tr>
<td>Rail-to-rail width (19 or 23 inches)</td> </tbody>
<td>19</td> </table>
</tr> <h4>Example</h4>
<tr> <pre>DC-4,Cage 1400,R101,J12.100,Pied Piper,Compute,4-post cabinet,19,42,False</pre>
<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>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,13 +1,14 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% block title %}Import Completed{% endblock %}
{% block content %} {% block content %}
<h1>Import Completed</h1> <h1>{% block title %}Import Completed{% endblock %}</h1>
{% render_table table %} {% render_table table %}
<a href="{{ request.path }}" class="btn btn-primary"> <a href="{{ request.path }}" class="btn btn-primary">
<span class="fa fa-download" aria-hidden="true"></span> <span class="fa fa-download" aria-hidden="true"></span>
Import more Import more
</a> </a>
{% if return_url %}
<a href="{% url return_url %}" class="btn btn-default">View All</a>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -1,57 +1,42 @@
{% extends '_base.html' %} {% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}Aggregate Import{% endblock %} {% block title %}Aggregate Import{% endblock %}
{% block content %} {% block instructions %}
<h1>Aggregate Import</h1> <h4>CSV Format</h4>
<div class="row"> <table class="table">
<div class="col-md-6"> <thead>
<form action="." method="post" class="form"> <tr>
{% csrf_token %} <th>Field</th>
{% render_form form %} <th>Description</th>
<div class="form-group"> <th>Example</th>
<button type="submit" class="btn btn-primary">Submit</button> </tr>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a> </thead>
</div> <tbody>
</form> <tr>
</div> <td>Prefix</td>
<div class="col-md-6"> <td>IPv4 or IPv6 network</td>
<h4>CSV Format</h4> <td>172.16.0.0/12</td>
<table class="table"> </tr>
<thead> <tr>
<tr> <td>RIR</td>
<th>Field</th> <td>Name of RIR</td>
<th>Description</th> <td>RFC 1918</td>
<th>Example</th> </tr>
</tr> <tr>
</thead> <td>Date Added</td>
<tbody> <td>Date in YYYY-MM-DD format (optional)</td>
<tr> <td>2016-02-23</td>
<td>Prefix</td> </tr>
<td>IPv4 or IPv6 network</td> <tr>
<td>172.16.0.0/12</td> <td>Description</td>
</tr> <td>Short description (optional)</td>
<tr> <td>Private IPv4 space</td>
<td>RIR</td> </tr>
<td>Name of RIR</td> </tbody>
<td>RFC 1918</td> </table>
</tr> <h4>Example</h4>
<tr> <pre>172.16.0.0/12,RFC 1918,2016-02-23,Private IPv4 space</pre>
<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>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -28,6 +28,7 @@
{% render_field form.interface_rack %} {% render_field form.interface_rack %}
{% render_field form.interface_device %} {% render_field form.interface_device %}
{% render_field form.interface %} {% render_field form.interface %}
{% render_field form.primary_for_device %}
</div> </div>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">

View File

@ -1,77 +1,62 @@
{% extends '_base.html' %} {% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}IP Address Import{% endblock %} {% block title %}IP Address Import{% endblock %}
{% block content %} {% block instructions %}
<h1>IP Address Import</h1> <h4>CSV Format</h4>
<div class="row"> <table class="table">
<div class="col-md-6"> <thead>
<form action="." method="post" class="form"> <tr>
{% csrf_token %} <th>Field</th>
{% render_form form %} <th>Description</th>
<div class="form-group"> <th>Example</th>
<button type="submit" class="btn btn-primary">Submit</button> </tr>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a> </thead>
</div> <tbody>
</form> <tr>
</div> <td>Address</td>
<div class="col-md-6"> <td>IPv4 or IPv6 address</td>
<h4>CSV Format</h4> <td>192.0.2.42/24</td>
<table class="table"> </tr>
<thead> <tr>
<tr> <td>VRF</td>
<th>Field</th> <td>VRF route distinguisher (optional)</td>
<th>Description</th> <td>65000:123</td>
<th>Example</th> </tr>
</tr> <tr>
</thead> <td>Tenant</td>
<tbody> <td>Name of tenant (optional)</td>
<tr> <td>ABC01</td>
<td>Address</td> </tr>
<td>IPv4 or IPv6 address</td> <tr>
<td>192.0.2.42/24</td> <td>Status</td>
</tr> <td>Current status</td>
<tr> <td>Active</td>
<td>VRF</td> </tr>
<td>VRF route distinguisher (optional)</td> <tr>
<td>65000:123</td> <td>Device</td>
</tr> <td>Device name (optional)</td>
<tr> <td>switch12</td>
<td>Tenant</td> </tr>
<td>Name of tenant (optional)</td> <tr>
<td>ABC01</td> <td>Interface</td>
</tr> <td>Interface name (optional)</td>
<tr> <td>ge-0/0/31</td>
<td>Status</td> </tr>
<td>Current status</td> <tr>
<td>Active</td> <td>Is Primary</td>
</tr> <td>If "true", IP will be primary for device (optional)</td>
<tr> <td>True</td>
<td>Device</td> </tr>
<td>Device name (optional)</td> <tr>
<td>switch12</td> <td>Description</td>
</tr> <td>Short description (optional)</td>
<tr> <td>Management IP</td>
<td>Interface</td> </tr>
<td>Interface name (optional)</td> </tbody>
<td>ge-0/0/31</td> </table>
</tr> <h4>Example</h4>
<tr> <pre>192.0.2.42/24,65000:123,ABC01,Active,switch12,ge-0/0/31,True,Management IP</pre>
<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>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,87 +1,72 @@
{% extends '_base.html' %} {% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}Prefix Import{% endblock %} {% block title %}Prefix Import{% endblock %}
{% block content %} {% block instructions %}
<h1>Prefix Import</h1> <h4>CSV Format</h4>
<div class="row"> <table class="table">
<div class="col-md-6"> <thead>
<form action="." method="post" class="form"> <tr>
{% csrf_token %} <th>Field</th>
{% render_form form %} <th>Description</th>
<div class="form-group"> <th>Example</th>
<button type="submit" class="btn btn-primary">Submit</button> </tr>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a> </thead>
</div> <tbody>
</form> <tr>
</div> <td>Prefix</td>
<div class="col-md-6"> <td>IPv4 or IPv6 network</td>
<h4>CSV Format</h4> <td>192.168.42.0/24</td>
<table class="table"> </tr>
<thead> <tr>
<tr> <td>VRF</td>
<th>Field</th> <td>VRF route distinguisher (optional)</td>
<th>Description</th> <td>65000:123</td>
<th>Example</th> </tr>
</tr> <tr>
</thead> <td>Tenant</td>
<tbody> <td>Name of tenant (optional)</td>
<tr> <td>ABC01</td>
<td>Prefix</td> </tr>
<td>IPv4 or IPv6 network</td> <tr>
<td>192.168.42.0/24</td> <td>Site</td>
</tr> <td>Name of assigned site (optional)</td>
<tr> <td>HQ</td>
<td>VRF</td> </tr>
<td>VRF route distinguisher (optional)</td> <tr>
<td>65000:123</td> <td>VLAN Group</td>
</tr> <td>Name of group for VLAN selection (optional)</td>
<tr> <td>Customers</td>
<td>Tenant</td> </tr>
<td>Name of tenant (optional)</td> <tr>
<td>ABC01</td> <td>VLAN ID</td>
</tr> <td>Numeric VLAN ID (optional)</td>
<tr> <td>801</td>
<td>Site</td> </tr>
<td>Name of assigned site (optional)</td> <tr>
<td>HQ</td> <td>Status</td>
</tr> <td>Current status</td>
<tr> <td>Active</td>
<td>VLAN Group</td> </tr>
<td>Name of group for VLAN selection (optional)</td> <tr>
<td>Customers</td> <td>Role</td>
</tr> <td>Functional role (optional)</td>
<tr> <td>Customer</td>
<td>VLAN ID</td> </tr>
<td>Numeric VLAN ID (optional)</td> <tr>
<td>801</td> <td>Is a pool</td>
</tr> <td>True if all IPs are considered usable</td>
<tr> <td>False</td>
<td>Status</td> </tr>
<td>Current status</td> <tr>
<td>Active</td> <td>Description</td>
</tr> <td>Short description (optional)</td>
<tr> <td>7th floor WiFi</td>
<td>Role</td> </tr>
<td>Functional role (optional)</td> </tbody>
<td>Customer</td> </table>
</tr> <h4>Example</h4>
<tr> <pre>192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,False,7th floor WiFi</pre>
<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>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,77 +1,62 @@
{% extends '_base.html' %} {% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}VLAN Import{% endblock %} {% block title %}VLAN Import{% endblock %}
{% block content %} {% block instructions %}
<h1>VLAN Import</h1> <h4>CSV Format</h4>
<div class="row"> <table class="table">
<div class="col-md-6"> <thead>
<form action="." method="post" class="form"> <tr>
{% csrf_token %} <th>Field</th>
{% render_form form %} <th>Description</th>
<div class="form-group"> <th>Example</th>
<button type="submit" class="btn btn-primary">Submit</button> </tr>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a> </thead>
</div> <tbody>
</form> <tr>
</div> <td>Site</td>
<div class="col-md-6"> <td>Name of assigned site</td>
<h4>CSV Format</h4> <td>LAS2</td>
<table class="table"> </tr>
<thead> <tr>
<tr> <td>Group</td>
<th>Field</th> <td>Name of VLAN group (optional)</td>
<th>Description</th> <td>Backend Network</td>
<th>Example</th> </tr>
</tr> <tr>
</thead> <td>ID</td>
<tbody> <td>Configured VLAN ID</td>
<tr> <td>1400</td>
<td>Site</td> </tr>
<td>Name of assigned site</td> <tr>
<td>LAS2</td> <td>Name</td>
</tr> <td>Configured VLAN name</td>
<tr> <td>Cameras</td>
<td>Group</td> </tr>
<td>Name of VLAN group (optional)</td> <tr>
<td>Backend Network</td> <td>Tenant</td>
</tr> <td>Name of tenant (optional)</td>
<tr> <td>Internal</td>
<td>ID</td> </tr>
<td>Configured VLAN ID</td> <tr>
<td>1400</td> <td>Status</td>
</tr> <td>Current status</td>
<tr> <td>Active</td>
<td>Name</td> </tr>
<td>Configured VLAN name</td> <tr>
<td>Cameras</td> <td>Role</td>
</tr> <td>Functional role (optional)</td>
<tr> <td>Security</td>
<td>Tenant</td> </tr>
<td>Name of tenant (optional)</td> <tr>
<td>Internal</td> <td>Description</td>
</tr> <td>Short description (optional)</td>
<tr> <td>Security team only</td>
<td>Status</td> </tr>
<td>Current status</td> </tbody>
<td>Active</td> </table>
</tr> <h4>Example</h4>
<tr> <pre>LAS2,Backend Network,1400,Cameras,Internal,Active,Security,Security team only</pre>
<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>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,62 +1,47 @@
{% extends '_base.html' %} {% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}VRF Import{% endblock %} {% block title %}VRF Import{% endblock %}
{% block content %} {% block instructions %}
<h1>VRF Import</h1> <h4>CSV Format</h4>
<div class="row"> <table class="table">
<div class="col-md-6"> <thead>
<form action="." method="post" class="form"> <tr>
{% csrf_token %} <th>Field</th>
{% render_form form %} <th>Description</th>
<div class="form-group"> <th>Example</th>
<button type="submit" class="btn btn-primary">Submit</button> </tr>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a> </thead>
</div> <tbody>
</form> <tr>
</div> <td>Name</td>
<div class="col-md-6"> <td>Name of VRF</td>
<h4>CSV Format</h4> <td>Customer_ABC</td>
<table class="table"> </tr>
<thead> <tr>
<tr> <td>RD</td>
<th>Field</th> <td>Route distinguisher</td>
<th>Description</th> <td>65000:123456</td>
<th>Example</th> </tr>
</tr> <tr>
</thead> <td>Tenant</td>
<tbody> <td>Name of tenant (optional)</td>
<tr> <td>ABC01</td>
<td>Name</td> </tr>
<td>Name of VRF</td> <tr>
<td>Customer_ABC</td> <td>Enforce uniqueness</td>
</tr> <td>Prevent duplicate prefixes/IP addresses</td>
<tr> <td>True</td>
<td>RD</td> </tr>
<td>Route distinguisher</td> <tr>
<td>65000:123456</td> <td>Description</td>
</tr> <td>Short description (optional)</td>
<tr> <td>Native VRF for customer ABC</td>
<td>Tenant</td> </tr>
<td>Name of tenant (optional)</td> </tbody>
<td>ABC01</td> </table>
</tr> <h4>Example</h4>
<tr> <pre>Customer_ABC,65000:123456,ABC01,True,Native VRF for customer ABC</pre>
<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>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -20,10 +20,14 @@
<form action="." method="post" class="form"> <form action="." method="post" class="form">
{% csrf_token %} {% csrf_token %}
{% render_form form %} {% render_form form %}
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button> <div class="col-md-12 text-right">
<a href="{{ return_url }}" class="btn btn-default">Cancel</a> <button type="submit" class="btn btn-primary">Submit</button>
</div> {% if return_url %}
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
{% endif %}
</div>
</div>
</form> </form>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">

View File

@ -1,4 +1,4 @@
{% extends 'django_tables2/table.html' %} {% extends 'django_tables2/bootstrap-responsive.html' %}
{% load django_tables2 %} {% load django_tables2 %}
{# Extends the stock django_tables2 template to provide custom formatting of the pagination controls #} {# Extends the stock django_tables2 template to provide custom formatting of the pagination controls #}

View File

@ -1,57 +1,42 @@
{% extends '_base.html' %} {% extends 'utilities/obj_import.html' %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}Tenant Import{% endblock %} {% block title %}Tenant Import{% endblock %}
{% block content %} {% block instructions %}
<h1>Tenant Import</h1> <h4>CSV Format</h4>
<div class="row"> <table class="table">
<div class="col-md-6"> <thead>
<form action="." method="post" class="form"> <tr>
{% csrf_token %} <th>Field</th>
{% render_form form %} <th>Description</th>
<div class="form-group"> <th>Example</th>
<button type="submit" class="btn btn-primary">Submit</button> </tr>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a> </thead>
</div> <tbody>
</form> <tr>
</div> <td>Name</td>
<div class="col-md-6"> <td>Tenant name</td>
<h4>CSV Format</h4> <td>WIDG01</td>
<table class="table"> </tr>
<thead> <tr>
<tr> <td>Slug</td>
<th>Field</th> <td>URL-friendly name</td>
<th>Description</th> <td>widg01</td>
<th>Example</th> </tr>
</tr> <tr>
</thead> <td>Group</td>
<tbody> <td>Tenant group (optional)</td>
<tr> <td>Customers</td>
<td>Name</td> </tr>
<td>Tenant name</td> <tr>
<td>WIDG01</td> <td>Description</td>
</tr> <td>Long-form name or other text (optional)</td>
<tr> <td>Widgets Inc.</td>
<td>Slug</td> </tr>
<td>URL-friendly name</td> </tbody>
<td>widg01</td> </table>
</tr> <h4>Example</h4>
<tr> <pre>WIDG01,widg01,Customers,Widgets Inc.</pre>
<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>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,34 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% load form_helpers %}
{% block content %}
<h1>{% block title %}{% endblock %}</h1>
<div class="row">
<div class="col-md-6">
{% if form.non_field_errors %}
<div class="panel panel-danger">
<div class="panel-heading"><strong>Errors</strong></div>
<div class="panel-body">
{{ form.non_field_errors }}
</div>
</div>
{% endif %}
<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>
</div>
<div class="col-md-6">
{% block instructions %}{% endblock %}
</div>
</div>
{% endblock %}

View File

@ -1,4 +1,6 @@
from django.contrib import messages from django.contrib import messages
from django.utils.html import escape
from django.utils.safestring import mark_safe
def handle_protectederror(obj, request, e): def handle_protectederror(obj, request, e):
@ -25,11 +27,11 @@ def handle_protectederror(obj, request, e):
# Append dependent objects to error message # Append dependent objects to error message
dependent_objects = [] dependent_objects = []
for o in e.protected_objects: for obj in e.protected_objects:
if hasattr(o, 'get_absolute_url'): if hasattr(obj, 'get_absolute_url'):
dependent_objects.append(u'<a href="{}">{}</a>'.format(o.get_absolute_url(), o)) dependent_objects.append(u'<a href="{}">{}</a>'.format(obj.get_absolute_url(), escape(obj)))
else: else:
dependent_objects.append(str(o)) dependent_objects.append(str(obj))
err_message += u', '.join(dependent_objects) err_message += u', '.join(dependent_objects)
messages.error(request, err_message) messages.error(request, mark_safe(err_message))

View File

@ -125,6 +125,19 @@ class ColorSelect(forms.Select):
super(ColorSelect, self).__init__(*args, **kwargs) super(ColorSelect, self).__init__(*args, **kwargs)
class BulkEditNullBooleanSelect(forms.NullBooleanSelect):
def __init__(self, *args, **kwargs):
super(BulkEditNullBooleanSelect, self).__init__(*args, **kwargs)
# Override the built-in choice labels
self.choices = (
('1', '---------'),
('2', 'Yes'),
('3', 'No'),
)
class SelectWithDisabled(forms.Select): class SelectWithDisabled(forms.Select):
""" """
Modified the stock Select widget to accept choices using a dict() for a label. The dict for each option must include Modified the stock Select widget to accept choices using a dict() for a label. The dict for each option must include

View File

@ -17,7 +17,6 @@ from django.utils.http import is_safe_url
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.views.generic import View from django.views.generic import View
from extras.forms import CustomFieldForm
from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction
from .error_handlers import handle_protectederror from .error_handlers import handle_protectederror
@ -195,12 +194,8 @@ class ObjectEditView(GetReturnURLMixin, View):
form = self.form_class(request.POST, request.FILES, instance=obj) form = self.form_class(request.POST, request.FILES, instance=obj)
if form.is_valid(): if form.is_valid():
obj = form.save(commit=False) obj_created = not form.instance.pk
obj_created = not obj.pk obj = form.save()
obj.save()
form.save_m2m()
if isinstance(form, CustomFieldForm):
form.save_custom_fields()
msg = u'Created ' if obj_created else u'Modified ' msg = u'Created ' if obj_created else u'Modified '
msg += self.model._meta.verbose_name msg += self.model._meta.verbose_name
@ -400,6 +395,7 @@ class BulkImportView(View):
return render(request, "import_success.html", { return render(request, "import_success.html", {
'table': obj_table, 'table': obj_table,
'return_url': self.default_return_url,
}) })
except IntegrityError as e: except IntegrityError as e:
@ -423,7 +419,7 @@ class BulkEditView(View):
filter: FilterSet to apply when deleting by QuerySet filter: FilterSet to apply when deleting by QuerySet
form: The form class used to edit objects in bulk form: The form class used to edit objects in bulk
template_name: The name of the template template_name: The name of the template
default_return_url: Name of the URL to which the user is redirected after editing the objects (can be overriden by default_return_url: Name of the URL to which the user is redirected after editing the objects (can be overridden by
POSTing return_url) POSTing return_url)
""" """
cls = None cls = None
@ -475,7 +471,7 @@ class BulkEditView(View):
fields_to_update[field] = '' fields_to_update[field] = ''
else: else:
fields_to_update[field] = None fields_to_update[field] = None
elif form.cleaned_data[field]: elif form.cleaned_data[field] not in (None, ''):
fields_to_update[field] = form.cleaned_data[field] fields_to_update[field] = form.cleaned_data[field]
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update) updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)

View File

@ -6,7 +6,7 @@ django-debug-toolbar>=1.7
django-filter>=1.0.2 django-filter>=1.0.2
django-mptt==0.8.7 django-mptt==0.8.7
django-rest-swagger>=2.1.0 django-rest-swagger>=2.1.0
django-tables2>=1.4.0 django-tables2>=1.6.0
djangorestframework>=3.6.2 djangorestframework>=3.6.2
graphviz>=0.6 graphviz>=0.6
Markdown>=2.6.7 Markdown>=2.6.7