diff --git a/README.md b/README.md index e2a8e708c..c21b140cf 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https ### 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) | | **develop** | [![Build Status](https://travis-ci.org/digitalocean/netbox.svg?branch=develop)](https://travis-ci.org/digitalocean/netbox) | diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 074f0dfa4..4fea49ca7 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -10,9 +10,9 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi from ipam.models import IPAddress from tenancy.models import Tenant from utilities.forms import ( - APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField, - CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, - SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField, + APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, + BulkImportForm, CommentField, CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, + Livesearch, SelectWithDisabled, SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField, ) 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') width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width') u_height = forms.IntegerField(required=False, label='Height (U)') + desc_units = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Descending units') comments = CommentField(widget=SmallTextarea) class Meta: @@ -374,7 +375,13 @@ class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput) manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), 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) + 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: nullable_fields = [] @@ -483,6 +490,7 @@ class InterfaceTemplateCreateForm(DeviceComponentForm): class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=InterfaceTemplate.objects.all(), widget=forms.MultipleHiddenInput) 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: nullable_fields = [] @@ -1415,6 +1423,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm): device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput) 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) + mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only') description = forms.CharField(max_length=100, required=False) class Meta: diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 5eb80005d..9b0d41065 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -12,11 +12,12 @@ from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.http import urlencode +from django.utils.safestring import mark_safe from django.views.generic import View from ipam.models import Prefix, Service, VLAN 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.paginator import EnhancedPaginator from utilities.views import ( @@ -892,12 +893,16 @@ def consoleport_connect(request, pk): form = forms.ConsolePortConnectionForm(request.POST, instance=consoleport) if form.is_valid(): consoleport = form.save() - messages.success(request, u"Connected {} {} to {} {}.".format( - consoleport.device, - consoleport.name, - consoleport.cs_port.device, - consoleport.cs_port.name, - )) + msg = u'Connected {} {} to {} {}'.format( + consoleport.device.get_absolute_url(), + escape(consoleport.device), + escape(consoleport.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) else: @@ -921,17 +926,28 @@ def consoleport_disconnect(request, pk): consoleport = get_object_or_404(ConsolePort, pk=pk) if not consoleport.cs_port: - messages.warning(request, u"Cannot disconnect console port {}: It is not connected to anything." - .format(consoleport)) + messages.warning( + request, u"Cannot disconnect console port {}: It is not connected to anything.".format(consoleport) + ) return redirect('dcim:device', pk=consoleport.device.pk) if request.method == 'POST': form = ConfirmationForm(request.POST) if form.is_valid(): + cs_port = consoleport.cs_port consoleport.cs_port = None consoleport.connection_status = None consoleport.save() - messages.success(request, u"Console port {} has been disconnected.".format(consoleport)) + msg = u'Disconnected {} {} from {} {}'.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) else: @@ -966,6 +982,7 @@ class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView): form = forms.ConsoleConnectionImportForm table = tables.ConsoleConnectionTable 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.connection_status = form.cleaned_data['connection_status'] consoleport.save() - messages.success(request, u"Connected {} {} to {} {}.".format( - consoleport.device, - consoleport.name, - consoleserverport.device, - consoleserverport.name, - )) + msg = u'Connected {} {} to {} {}'.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) else: @@ -1022,8 +1043,9 @@ def consoleserverport_disconnect(request, pk): consoleserverport = get_object_or_404(ConsoleServerPort, pk=pk) if not hasattr(consoleserverport, 'connected_console'): - messages.warning(request, u"Cannot disconnect console server port {}: Nothing is connected to it." - .format(consoleserverport)) + messages.warning( + request, u"Cannot disconnect console server port {}: Nothing is connected to it.".format(consoleserverport) + ) return redirect('dcim:device', pk=consoleserverport.device.pk) if request.method == 'POST': @@ -1033,7 +1055,16 @@ def consoleserverport_disconnect(request, pk): consoleport.cs_port = None consoleport.connection_status = None consoleport.save() - messages.success(request, u"Console server port {} has been disconnected.".format(consoleserverport)) + msg = u'Disconnected {} {} from {} {}'.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) else: @@ -1085,12 +1116,16 @@ def powerport_connect(request, pk): form = forms.PowerPortConnectionForm(request.POST, instance=powerport) if form.is_valid(): powerport = form.save() - messages.success(request, u"Connected {} {} to {} {}.".format( - powerport.device, - powerport.name, - powerport.power_outlet.device, - powerport.power_outlet.name, - )) + msg = u'Connected {} {} to {} {}'.format( + powerport.device.get_absolute_url(), + escape(powerport.device), + escape(powerport.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) else: @@ -1114,17 +1149,28 @@ def powerport_disconnect(request, pk): powerport = get_object_or_404(PowerPort, pk=pk) if not powerport.power_outlet: - messages.warning(request, u"Cannot disconnect power port {}: It is not connected to an outlet." - .format(powerport)) + messages.warning( + request, u"Cannot disconnect power port {}: It is not connected to an outlet.".format(powerport) + ) return redirect('dcim:device', pk=powerport.device.pk) if request.method == 'POST': form = ConfirmationForm(request.POST) if form.is_valid(): + power_outlet = powerport.power_outlet powerport.power_outlet = None powerport.connection_status = None powerport.save() - messages.success(request, u"Power port {} has been disconnected.".format(powerport)) + msg = u'Disconnected {} {} from {} {}'.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) else: @@ -1159,6 +1205,7 @@ class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView): form = forms.PowerConnectionImportForm table = tables.PowerConnectionTable 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.connection_status = form.cleaned_data['connection_status'] powerport.save() - messages.success(request, u"Connected {} {} to {} {}.".format( - powerport.device, - powerport.name, - poweroutlet.device, - poweroutlet.name, - )) + msg = u'Connected {} {} to {} {}'.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) else: @@ -1215,7 +1266,9 @@ def poweroutlet_disconnect(request, pk): poweroutlet = get_object_or_404(PowerOutlet, pk=pk) 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) if request.method == 'POST': @@ -1225,7 +1278,16 @@ def poweroutlet_disconnect(request, pk): powerport.power_outlet = None powerport.connection_status = None powerport.save() - messages.success(request, u"Power outlet {} has been disconnected.".format(poweroutlet)) + msg = u'Disconnected {} {} from {} {}'.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) else: @@ -1491,13 +1553,19 @@ def interfaceconnection_add(request, pk): if request.method == 'POST': form = forms.InterfaceConnectionForm(device, request.POST) if form.is_valid(): + interfaceconnection = form.save() - messages.success(request, u"Connected {} {} to {} {}.".format( - interfaceconnection.interface_a.device, - interfaceconnection.interface_a, - interfaceconnection.interface_b.device, - interfaceconnection.interface_b, - )) + msg = u'Connected {} {} to {} {}'.format( + interfaceconnection.interface_a.device.get_absolute_url(), + escape(interfaceconnection.interface_a.device), + escape(interfaceconnection.interface_a.name), + 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: base_url = reverse('dcim:interfaceconnection_add', kwargs={'pk': device.pk}) device_b = interfaceconnection.interface_b.device @@ -1535,12 +1603,16 @@ def interfaceconnection_delete(request, pk): form = forms.InterfaceConnectionDeletionForm(request.POST) if form.is_valid(): interfaceconnection.delete() - messages.success(request, u"Deleted the connection between {} {} and {} {}.".format( - interfaceconnection.interface_a.device, - interfaceconnection.interface_a, - interfaceconnection.interface_b.device, - interfaceconnection.interface_b, - )) + msg = u'Disconnected {} {} from {} {}'.format( + interfaceconnection.interface_a.device.get_absolute_url(), + escape(interfaceconnection.interface_a.device), + escape(interfaceconnection.interface_a.name), + 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']: return redirect('dcim:device', pk=form.cleaned_data['device'].pk) else: @@ -1570,6 +1642,7 @@ class InterfaceConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView form = forms.InterfaceConnectionImportForm table = tables.InterfaceConnectionTable template_name = 'dcim/interface_connections_import.html' + default_return_url = 'dcim:interface_connections_list' # diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 3c39a4308..3229ad2b8 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -9,7 +9,10 @@ from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant 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): @@ -153,10 +156,13 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Role (slug)', ) + status = django_filters.MultipleChoiceFilter( + choices=PREFIX_STATUS_CHOICES + ) class Meta: model = Prefix - fields = ['family', 'status'] + fields = ['family'] def search(self, queryset, name, value): if not value.strip(): @@ -237,10 +243,13 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): queryset=Interface.objects.all(), label='Interface (ID)', ) + status = django_filters.MultipleChoiceFilter( + choices=IPADDRESS_STATUS_CHOICES + ) class Meta: model = IPAddress - fields = ['family', 'status'] + fields = ['family'] def search(self, queryset, name, value): if not value.strip(): @@ -337,10 +346,13 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Role (slug)', ) + status = django_filters.MultipleChoiceFilter( + choices=VLAN_STATUS_CHOICES + ) class Meta: model = VLAN - fields = ['name', 'vid', 'status'] + fields = ['name', 'vid'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 8734107d8..25685c171 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -5,8 +5,8 @@ from dcim.models import Site, Rack, Device, Interface from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from tenancy.models import Tenant from utilities.forms import ( - APISelect, BootstrapMixin, BulkImportForm, CSVDataField, ExpandableIPAddressField, FilterChoiceField, Livesearch, - ReturnURLForm, SlugField, add_blank_choice, + APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, CSVDataField, ExpandableIPAddressField, + FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice, ) from .models import ( @@ -61,6 +61,9 @@ class VRFImportForm(BootstrapMixin, BulkImportForm): class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput) 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) class Meta: @@ -256,6 +259,7 @@ class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) status = forms.ChoiceField(choices=add_blank_choice(PREFIX_STATUS_CHOICES), 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) 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' ) ) + primary_for_device = forms.BooleanField(required=False, label='Make this the primary IP for the device') class Meta: model = IPAddress - fields = ['address', 'vrf', 'tenant', 'status', 'interface', 'nat_inside', 'description'] + fields = ['address', 'vrf', 'tenant', 'status', 'description', 'interface', 'primary_for_device', 'nat_inside'] widgets = { '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') @@ -384,6 +389,15 @@ class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm): else: self.fields['interface'].choices = [] + # Initialize primary_for_device if IP address is already assigned + if self.instance.interface is not None: + device = self.instance.interface.device + 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: nat_inside = self.instance.nat_inside # If the IP is assigned to an interface, populate site/device fields accordingly @@ -416,6 +430,43 @@ class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm): else: 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): address_pattern = ExpandableIPAddressField(label='Address Pattern') diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 32f1ca1cf..0a132dca3 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -221,6 +221,7 @@ def secret_import(request): return render(request, 'import_success.html', { 'table': table, + 'return_url': 'secrets:secret_list', }) except IntegrityError as e: @@ -231,7 +232,7 @@ def secret_import(request): return render(request, 'secrets/secret_import.html', { 'form': form, - 'return_url': reverse('secrets:secret_list'), + 'return_url': 'secrets:secret_list', }) diff --git a/netbox/templates/circuits/circuit_import.html b/netbox/templates/circuits/circuit_import.html index e2fc9fa36..991a99c9b 100644 --- a/netbox/templates/circuits/circuit_import.html +++ b/netbox/templates/circuits/circuit_import.html @@ -1,72 +1,57 @@ -{% extends '_base.html' %} +{% extends 'utilities/obj_import.html' %} {% load render_table from django_tables2 %} {% load form_helpers %} {% block title %}Circuit Import{% endblock %} -{% block content %} -

Circuit Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- - Cancel -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
Circuit IDAlphanumeric circuit identifierIC-603122
ProviderName of circuit providerTeliaSonera
TypeCircuit typeTransit
TenantName of tenant (optional)Strickland Propane
Install DateDate in YYYY-MM-DD format (optional)2016-02-23
Commit rateCommited rate in Kbps (optional)2000
DescriptionShort description (optional)Primary for voice
-

Example

-
IC-603122,TeliaSonera,Transit,Strickland Propane,2016-02-23,2000,Primary for voice
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
Circuit IDAlphanumeric circuit identifierIC-603122
ProviderName of circuit providerTeliaSonera
TypeCircuit typeTransit
TenantName of tenant (optional)Strickland Propane
Install DateDate in YYYY-MM-DD format (optional)2016-02-23
Commit rateCommited rate in Kbps (optional)2000
DescriptionShort description (optional)Primary for voice
+

Example

+
IC-603122,TeliaSonera,Transit,Strickland Propane,2016-02-23,2000,Primary for voice
{% endblock %} diff --git a/netbox/templates/circuits/provider_import.html b/netbox/templates/circuits/provider_import.html index a605164df..e60ee3e76 100644 --- a/netbox/templates/circuits/provider_import.html +++ b/netbox/templates/circuits/provider_import.html @@ -1,62 +1,47 @@ -{% extends '_base.html' %} +{% extends 'utilities/obj_import.html' %} {% load render_table from django_tables2 %} {% load form_helpers %} {% block title %}Provider Import{% endblock %} -{% block content %} -

Provider Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- - Cancel -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
NameProvider's proper nameLevel 3
SlugURL-friendly namelevel3
ASNAutonomous system number (optional)3356
AccountAccount number (optional)08931544
Portal URLCustomer service portal URL (optional)https://mylevel3.net
-

Example

-
Level 3,level3,3356,08931544,https://mylevel3.net
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
NameProvider's proper nameLevel 3
SlugURL-friendly namelevel3
ASNAutonomous system number (optional)3356
AccountAccount number (optional)08931544
Portal URLCustomer service portal URL (optional)https://mylevel3.net
+

Example

+
Level 3,level3,3356,08931544,https://mylevel3.net
{% endblock %} diff --git a/netbox/templates/dcim/console_connections_import.html b/netbox/templates/dcim/console_connections_import.html index 6b47ba3bb..c7308168b 100644 --- a/netbox/templates/dcim/console_connections_import.html +++ b/netbox/templates/dcim/console_connections_import.html @@ -1,61 +1,47 @@ -{% extends '_base.html' %} +{% extends 'utilities/obj_import.html' %} {% load render_table from django_tables2 %} {% load form_helpers %} {% block title %}Console Connections Import{% endblock %} -{% block content %} -

Console Connections Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
Console serverDevice name or {ID}abc1-cs3
Console server portFull CS port namePort 35
DeviceDevice name or {ID}abc1-switch7
Console PortConsole port nameConsole
Connection Status"planned" or "connected"planned
-

Example

-
abc1-cs3,Port 35,abc1-switch7,Console,planned
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
Console serverDevice name or {ID}abc1-cs3
Console server portFull CS port namePort 35
DeviceDevice name or {ID}abc1-switch7
Console PortConsole port nameConsole
Connection Status"planned" or "connected"planned
+

Example

+
abc1-cs3,Port 35,abc1-switch7,Console,planned
{% endblock %} diff --git a/netbox/templates/dcim/device_import.html b/netbox/templates/dcim/device_import.html index 50d2f81db..83d0d2195 100644 --- a/netbox/templates/dcim/device_import.html +++ b/netbox/templates/dcim/device_import.html @@ -12,8 +12,12 @@ {% csrf_token %} {% render_form form %}
- - Cancel +
+ + {% if return_url %} + Cancel + {% endif %} +

CSV Format

diff --git a/netbox/templates/dcim/device_import_child.html b/netbox/templates/dcim/device_import_child.html index ca69d7aa5..49433686f 100644 --- a/netbox/templates/dcim/device_import_child.html +++ b/netbox/templates/dcim/device_import_child.html @@ -12,8 +12,12 @@ {% csrf_token %} {% render_form form %}
- - Cancel +
+ + {% if return_url %} + Cancel + {% endif %} +

CSV Format

diff --git a/netbox/templates/dcim/inc/consoleport.html b/netbox/templates/dcim/inc/consoleport.html index 5352c949c..58f5fa7de 100644 --- a/netbox/templates/dcim/inc/consoleport.html +++ b/netbox/templates/dcim/inc/consoleport.html @@ -7,6 +7,7 @@ {{ cp.name }} + {% if cp.cs_port %} {{ cp.cs_port.device }} @@ -32,11 +33,11 @@ {% endif %} - + {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/consoleserverport.html b/netbox/templates/dcim/inc/consoleserverport.html index d317cf5a4..cfeab9212 100644 --- a/netbox/templates/dcim/inc/consoleserverport.html +++ b/netbox/templates/dcim/inc/consoleserverport.html @@ -7,6 +7,7 @@ {{ csp.name }} + {% if csp.connected_console %} {{ csp.connected_console.device }} @@ -32,11 +33,11 @@ {% endif %} - + {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index 8d8457d71..9e2f9f8a5 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -12,12 +12,13 @@ {% if iface.description %} {% endif %} - {% if iface.is_lag %} -
{{ iface.member_interfaces.all|join:", "|default:"No members" }} - {% endif %} + {{ iface.mac_address|default:"" }} {% if iface.is_lag %} - LAG interface + + LAG interface
+ {{ iface.member_interfaces.all|join:", "|default:"No members" }} + {% elif iface.is_virtual %} Virtual interface {% elif iface.connection %} diff --git a/netbox/templates/dcim/inc/poweroutlet.html b/netbox/templates/dcim/inc/poweroutlet.html index 652ac8e47..eef4874d6 100644 --- a/netbox/templates/dcim/inc/poweroutlet.html +++ b/netbox/templates/dcim/inc/poweroutlet.html @@ -7,6 +7,7 @@ {{ po.name }} + {% if po.connected_port %}
{{ po.connected_port.device }} @@ -32,11 +33,11 @@ {% endif %} - + {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/powerport.html b/netbox/templates/dcim/inc/powerport.html index 785186670..ce4ac6967 100644 --- a/netbox/templates/dcim/inc/powerport.html +++ b/netbox/templates/dcim/inc/powerport.html @@ -7,6 +7,7 @@ {{ pp.name }} + {% if pp.power_outlet %} {{ pp.power_outlet.device }} @@ -32,11 +33,11 @@ {% endif %} - + {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/interface_connections_import.html b/netbox/templates/dcim/interface_connections_import.html index 6329e0680..eab0acdba 100644 --- a/netbox/templates/dcim/interface_connections_import.html +++ b/netbox/templates/dcim/interface_connections_import.html @@ -1,69 +1,47 @@ -{% extends '_base.html' %} +{% extends 'utilities/obj_import.html' %} {% load render_table from django_tables2 %} {% load form_helpers %} {% block title %}Interface Connections Import{% endblock %} -{% block content %} -

Interface Connections Import

-
-
- {% if form.non_field_errors %} -
-
Errors
-
- {{ form.non_field_errors }} -
-
- {% endif %} -
- {% csrf_token %} - {% render_form form %} -
- -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
Device ADevice name or {ID}abc1-core1
Interface AInterface namexe-0/0/6
Device BDevice name or {ID}abc1-switch7
Interface BInterface namexe-0/0/0
Connection Status"planned" or "connected"planned
-

Example

-
abc1-core1,xe-0/0/6,abc1-switch7,xe-0/0/0,planned
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
Device ADevice name or {ID}abc1-core1
Interface AInterface namexe-0/0/6
Device BDevice name or {ID}abc1-switch7
Interface BInterface namexe-0/0/0
Connection Status"planned" or "connected"planned
+

Example

+
abc1-core1,xe-0/0/6,abc1-switch7,xe-0/0/0,planned
{% endblock %} diff --git a/netbox/templates/dcim/power_connections_import.html b/netbox/templates/dcim/power_connections_import.html index 7c436508a..56f34c456 100644 --- a/netbox/templates/dcim/power_connections_import.html +++ b/netbox/templates/dcim/power_connections_import.html @@ -1,61 +1,47 @@ -{% extends '_base.html' %} +{% extends 'utilities/obj_import.html' %} {% load render_table from django_tables2 %} {% load form_helpers %} {% block title %}Power Connections Import{% endblock %} -{% block content %} -

Power Connections Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
PDUDevice name or {ID}abc1-pdu1
Power OutletPower outlet nameAC4
DeviceDevice name or {ID}abc1-switch7
Power PortPower port namePSU0
Connection Status"planned" or "connected"connected
-

Example

-
abc1-pdu1,AC4,abc1-switch7,PSU0,connected
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
PDUDevice name or {ID}abc1-pdu1
Power OutletPower outlet nameAC4
DeviceDevice name or {ID}abc1-switch7
Power PortPower port namePSU0
Connection Status"planned" or "connected"connected
+

Example

+
abc1-pdu1,AC4,abc1-switch7,PSU0,connected
{% endblock %} diff --git a/netbox/templates/dcim/rack_import.html b/netbox/templates/dcim/rack_import.html index c462a0be9..207fcfcab 100644 --- a/netbox/templates/dcim/rack_import.html +++ b/netbox/templates/dcim/rack_import.html @@ -1,87 +1,72 @@ -{% extends '_base.html' %} +{% extends 'utilities/obj_import.html' %} {% load render_table from django_tables2 %} {% load form_helpers %} {% block title %}Rack Import{% endblock %} -{% block content %} -

Rack Import

-
-
-
- {% csrf_token %} - {% render_form form %} - -
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
SiteName of the assigned siteDC-4
GroupRack group name (optional)Cage 1400
NameInternal rack nameR101
Facility IDRack ID assigned by the facility (optional)J12.100
TenantName of tenant (optional)Pied Piper
RoleFunctional role (optional)Compute
TypeRack type (optional)4-post cabinet
WidthRail-to-rail width (19 or 23 inches)19
HeightHeight in rack units42
Descending unitsUnits are numbered top-to-bottomFalse
-

Example

-
DC-4,Cage 1400,R101,J12.100,Pied Piper,Compute,4-post cabinet,19,42,False
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
SiteName of the assigned siteDC-4
GroupRack group name (optional)Cage 1400
NameInternal rack nameR101
Facility IDRack ID assigned by the facility (optional)J12.100
TenantName of tenant (optional)Pied Piper
RoleFunctional role (optional)Compute
TypeRack type (optional)4-post cabinet
WidthRail-to-rail width (19 or 23 inches)19
HeightHeight in rack units42
Descending unitsUnits are numbered top-to-bottomFalse
+

Example

+
DC-4,Cage 1400,R101,J12.100,Pied Piper,Compute,4-post cabinet,19,42,False
{% endblock %} diff --git a/netbox/templates/import_success.html b/netbox/templates/import_success.html index 3056e39df..04c454e1d 100644 --- a/netbox/templates/import_success.html +++ b/netbox/templates/import_success.html @@ -1,13 +1,14 @@ {% extends '_base.html' %} {% load render_table from django_tables2 %} -{% block title %}Import Completed{% endblock %} - {% block content %} -

Import Completed

-{% render_table table %} - - - Import more - +

{% block title %}Import Completed{% endblock %}

+ {% render_table table %} + + + Import more + + {% if return_url %} + View All + {% endif %} {% endblock %} diff --git a/netbox/templates/ipam/aggregate_import.html b/netbox/templates/ipam/aggregate_import.html index 8075b4874..1f0a50feb 100644 --- a/netbox/templates/ipam/aggregate_import.html +++ b/netbox/templates/ipam/aggregate_import.html @@ -1,57 +1,42 @@ -{% extends '_base.html' %} +{% extends 'utilities/obj_import.html' %} {% load render_table from django_tables2 %} {% load form_helpers %} {% block title %}Aggregate Import{% endblock %} -{% block content %} -

Aggregate Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- - Cancel -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
PrefixIPv4 or IPv6 network172.16.0.0/12
RIRName of RIRRFC 1918
Date AddedDate in YYYY-MM-DD format (optional)2016-02-23
DescriptionShort description (optional)Private IPv4 space
-

Example

-
172.16.0.0/12,RFC 1918,2016-02-23,Private IPv4 space
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
PrefixIPv4 or IPv6 network172.16.0.0/12
RIRName of RIRRFC 1918
Date AddedDate in YYYY-MM-DD format (optional)2016-02-23
DescriptionShort description (optional)Private IPv4 space
+

Example

+
172.16.0.0/12,RFC 1918,2016-02-23,Private IPv4 space
{% endblock %} diff --git a/netbox/templates/ipam/ipaddress_edit.html b/netbox/templates/ipam/ipaddress_edit.html index 67fb64eea..d5c5ad1af 100644 --- a/netbox/templates/ipam/ipaddress_edit.html +++ b/netbox/templates/ipam/ipaddress_edit.html @@ -28,6 +28,7 @@ {% render_field form.interface_rack %} {% render_field form.interface_device %} {% render_field form.interface %} + {% render_field form.primary_for_device %}
diff --git a/netbox/templates/ipam/ipaddress_import.html b/netbox/templates/ipam/ipaddress_import.html index 3c01b4af0..362f64829 100644 --- a/netbox/templates/ipam/ipaddress_import.html +++ b/netbox/templates/ipam/ipaddress_import.html @@ -1,77 +1,62 @@ -{% extends '_base.html' %} +{% extends 'utilities/obj_import.html' %} {% load render_table from django_tables2 %} {% load form_helpers %} {% block title %}IP Address Import{% endblock %} -{% block content %} -

IP Address Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- - Cancel -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
AddressIPv4 or IPv6 address192.0.2.42/24
VRFVRF route distinguisher (optional)65000:123
TenantName of tenant (optional)ABC01
StatusCurrent statusActive
DeviceDevice name (optional)switch12
InterfaceInterface name (optional)ge-0/0/31
Is PrimaryIf "true", IP will be primary for device (optional)True
DescriptionShort description (optional)Management IP
-

Example

-
192.0.2.42/24,65000:123,ABC01,Active,switch12,ge-0/0/31,True,Management IP
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
AddressIPv4 or IPv6 address192.0.2.42/24
VRFVRF route distinguisher (optional)65000:123
TenantName of tenant (optional)ABC01
StatusCurrent statusActive
DeviceDevice name (optional)switch12
InterfaceInterface name (optional)ge-0/0/31
Is PrimaryIf "true", IP will be primary for device (optional)True
DescriptionShort description (optional)Management IP
+

Example

+
192.0.2.42/24,65000:123,ABC01,Active,switch12,ge-0/0/31,True,Management IP
{% endblock %} diff --git a/netbox/templates/ipam/prefix_import.html b/netbox/templates/ipam/prefix_import.html index 0a9cc8694..b9aa7ff47 100644 --- a/netbox/templates/ipam/prefix_import.html +++ b/netbox/templates/ipam/prefix_import.html @@ -1,87 +1,72 @@ -{% extends '_base.html' %} +{% extends 'utilities/obj_import.html' %} {% load render_table from django_tables2 %} {% load form_helpers %} {% block title %}Prefix Import{% endblock %} -{% block content %} -

Prefix Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- - Cancel -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
PrefixIPv4 or IPv6 network192.168.42.0/24
VRFVRF route distinguisher (optional)65000:123
TenantName of tenant (optional)ABC01
SiteName of assigned site (optional)HQ
VLAN GroupName of group for VLAN selection (optional)Customers
VLAN IDNumeric VLAN ID (optional)801
StatusCurrent statusActive
RoleFunctional role (optional)Customer
Is a poolTrue if all IPs are considered usableFalse
DescriptionShort description (optional)7th floor WiFi
-

Example

-
192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,False,7th floor WiFi
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
PrefixIPv4 or IPv6 network192.168.42.0/24
VRFVRF route distinguisher (optional)65000:123
TenantName of tenant (optional)ABC01
SiteName of assigned site (optional)HQ
VLAN GroupName of group for VLAN selection (optional)Customers
VLAN IDNumeric VLAN ID (optional)801
StatusCurrent statusActive
RoleFunctional role (optional)Customer
Is a poolTrue if all IPs are considered usableFalse
DescriptionShort description (optional)7th floor WiFi
+

Example

+
192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,False,7th floor WiFi
{% endblock %} diff --git a/netbox/templates/ipam/vlan_import.html b/netbox/templates/ipam/vlan_import.html index 16456ba01..8d1741fd4 100644 --- a/netbox/templates/ipam/vlan_import.html +++ b/netbox/templates/ipam/vlan_import.html @@ -1,77 +1,62 @@ -{% extends '_base.html' %} +{% extends 'utilities/obj_import.html' %} {% load render_table from django_tables2 %} {% load form_helpers %} {% block title %}VLAN Import{% endblock %} -{% block content %} -

VLAN Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- - Cancel -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
SiteName of assigned siteLAS2
GroupName of VLAN group (optional)Backend Network
IDConfigured VLAN ID1400
NameConfigured VLAN nameCameras
TenantName of tenant (optional)Internal
StatusCurrent statusActive
RoleFunctional role (optional)Security
DescriptionShort description (optional)Security team only
-

Example

-
LAS2,Backend Network,1400,Cameras,Internal,Active,Security,Security team only
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
SiteName of assigned siteLAS2
GroupName of VLAN group (optional)Backend Network
IDConfigured VLAN ID1400
NameConfigured VLAN nameCameras
TenantName of tenant (optional)Internal
StatusCurrent statusActive
RoleFunctional role (optional)Security
DescriptionShort description (optional)Security team only
+

Example

+
LAS2,Backend Network,1400,Cameras,Internal,Active,Security,Security team only
{% endblock %} diff --git a/netbox/templates/ipam/vrf_import.html b/netbox/templates/ipam/vrf_import.html index 9953542d2..0a1a31205 100644 --- a/netbox/templates/ipam/vrf_import.html +++ b/netbox/templates/ipam/vrf_import.html @@ -1,62 +1,47 @@ -{% extends '_base.html' %} +{% extends 'utilities/obj_import.html' %} {% load render_table from django_tables2 %} {% load form_helpers %} {% block title %}VRF Import{% endblock %} -{% block content %} -

VRF Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- - Cancel -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
NameName of VRFCustomer_ABC
RDRoute distinguisher65000:123456
TenantName of tenant (optional)ABC01
Enforce uniquenessPrevent duplicate prefixes/IP addressesTrue
DescriptionShort description (optional)Native VRF for customer ABC
-

Example

-
Customer_ABC,65000:123456,ABC01,True,Native VRF for customer ABC
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
NameName of VRFCustomer_ABC
RDRoute distinguisher65000:123456
TenantName of tenant (optional)ABC01
Enforce uniquenessPrevent duplicate prefixes/IP addressesTrue
DescriptionShort description (optional)Native VRF for customer ABC
+

Example

+
Customer_ABC,65000:123456,ABC01,True,Native VRF for customer ABC
{% endblock %} diff --git a/netbox/templates/secrets/secret_import.html b/netbox/templates/secrets/secret_import.html index 76022fac1..73daf72d9 100644 --- a/netbox/templates/secrets/secret_import.html +++ b/netbox/templates/secrets/secret_import.html @@ -20,10 +20,14 @@
{% csrf_token %} {% render_form form %} -
- - Cancel -
+
+
+ + {% if return_url %} + Cancel + {% endif %} +
+
diff --git a/netbox/templates/table.html b/netbox/templates/table.html index 31f89ee84..6da4a91e4 100644 --- a/netbox/templates/table.html +++ b/netbox/templates/table.html @@ -1,4 +1,4 @@ -{% extends 'django_tables2/table.html' %} +{% extends 'django_tables2/bootstrap-responsive.html' %} {% load django_tables2 %} {# Extends the stock django_tables2 template to provide custom formatting of the pagination controls #} diff --git a/netbox/templates/tenancy/tenant_import.html b/netbox/templates/tenancy/tenant_import.html index 81f82989f..c0e94269a 100644 --- a/netbox/templates/tenancy/tenant_import.html +++ b/netbox/templates/tenancy/tenant_import.html @@ -1,57 +1,42 @@ -{% extends '_base.html' %} +{% extends 'utilities/obj_import.html' %} {% load render_table from django_tables2 %} {% load form_helpers %} {% block title %}Tenant Import{% endblock %} -{% block content %} -

Tenant Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- - Cancel -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
NameTenant nameWIDG01
SlugURL-friendly namewidg01
GroupTenant group (optional)Customers
DescriptionLong-form name or other text (optional)Widgets Inc.
-

Example

-
WIDG01,widg01,Customers,Widgets Inc.
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
NameTenant nameWIDG01
SlugURL-friendly namewidg01
GroupTenant group (optional)Customers
DescriptionLong-form name or other text (optional)Widgets Inc.
+

Example

+
WIDG01,widg01,Customers,Widgets Inc.
{% endblock %} diff --git a/netbox/templates/utilities/obj_import.html b/netbox/templates/utilities/obj_import.html new file mode 100644 index 000000000..bea9a2319 --- /dev/null +++ b/netbox/templates/utilities/obj_import.html @@ -0,0 +1,34 @@ +{% extends '_base.html' %} +{% load render_table from django_tables2 %} +{% load form_helpers %} + +{% block content %} +

{% block title %}{% endblock %}

+
+
+ {% if form.non_field_errors %} +
+
Errors
+
+ {{ form.non_field_errors }} +
+
+ {% endif %} +
+ {% csrf_token %} + {% render_form form %} +
+
+ + {% if return_url %} + Cancel + {% endif %} +
+
+
+
+
+ {% block instructions %}{% endblock %} +
+
+{% endblock %} diff --git a/netbox/utilities/error_handlers.py b/netbox/utilities/error_handlers.py index 1691b41cd..e87b6f0e1 100644 --- a/netbox/utilities/error_handlers.py +++ b/netbox/utilities/error_handlers.py @@ -1,4 +1,6 @@ from django.contrib import messages +from django.utils.html import escape +from django.utils.safestring import mark_safe def handle_protectederror(obj, request, e): @@ -25,11 +27,11 @@ def handle_protectederror(obj, request, e): # Append dependent objects to error message dependent_objects = [] - for o in e.protected_objects: - if hasattr(o, 'get_absolute_url'): - dependent_objects.append(u'{}'.format(o.get_absolute_url(), o)) + for obj in e.protected_objects: + if hasattr(obj, 'get_absolute_url'): + dependent_objects.append(u'{}'.format(obj.get_absolute_url(), escape(obj))) else: - dependent_objects.append(str(o)) + dependent_objects.append(str(obj)) err_message += u', '.join(dependent_objects) - messages.error(request, err_message) + messages.error(request, mark_safe(err_message)) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index aa3d0634f..d14c56e7b 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -125,6 +125,19 @@ class ColorSelect(forms.Select): 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): """ Modified the stock Select widget to accept choices using a dict() for a label. The dict for each option must include diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index ef36680eb..a91055964 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -17,7 +17,6 @@ from django.utils.http import is_safe_url from django.utils.safestring import mark_safe from django.views.generic import View -from extras.forms import CustomFieldForm from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction from .error_handlers import handle_protectederror @@ -195,12 +194,8 @@ class ObjectEditView(GetReturnURLMixin, View): form = self.form_class(request.POST, request.FILES, instance=obj) if form.is_valid(): - obj = form.save(commit=False) - obj_created = not obj.pk - obj.save() - form.save_m2m() - if isinstance(form, CustomFieldForm): - form.save_custom_fields() + obj_created = not form.instance.pk + obj = form.save() msg = u'Created ' if obj_created else u'Modified ' msg += self.model._meta.verbose_name @@ -400,6 +395,7 @@ class BulkImportView(View): return render(request, "import_success.html", { 'table': obj_table, + 'return_url': self.default_return_url, }) except IntegrityError as e: @@ -423,7 +419,7 @@ class BulkEditView(View): filter: FilterSet to apply when deleting by QuerySet form: The form class used to edit objects in bulk 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) """ cls = None @@ -475,7 +471,7 @@ class BulkEditView(View): fields_to_update[field] = '' else: 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] updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update) diff --git a/requirements.txt b/requirements.txt index 48e58b12e..21382bb6c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ django-debug-toolbar>=1.7 django-filter>=1.0.2 django-mptt==0.8.7 django-rest-swagger>=2.1.0 -django-tables2>=1.4.0 +django-tables2>=1.6.0 djangorestframework>=3.6.2 graphviz>=0.6 Markdown>=2.6.7