diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index 02dbb878f2b..02a08716b87 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -21,6 +21,12 @@ Copy the 'configuration.py' you created when first installing to the new version # cp /opt/netbox-X.Y.Z/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/configuration.py ``` +Be sure to replicate your uploaded media as well. (The exact action necessary will depend on where you choose to store your media, but in general moving or copying the media directory will suffice.) + +```no-highlight +# cp -pr /opt/netbox-X.Y.Z/netbox/media/ /opt/netbox/netbox/ +``` + If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well: ```no-highlight diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 6d0892f6794..0c8ea371674 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1679,6 +1679,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm, ChainedFieldsMixin): label='Untagged VLAN', widget=APISelect( api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}', + display_field='display_name' ) ) tagged_vlans = ChainedModelMultipleChoiceField( @@ -1691,6 +1692,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm, ChainedFieldsMixin): label='Tagged VLANs', widget=APISelectMultiple( api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}', + display_field='display_name' ) ) @@ -2067,7 +2069,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor super(InterfaceConnectionForm, self).__init__(*args, **kwargs) # Initialize interface A choices - device_a_interfaces = Interface.objects.connectable().order_naturally().filter(device=device_a).select_related( + device_a_interfaces = device_a.vc_interfaces.connectable().order_naturally().select_related( 'circuit_termination', 'connected_as_a', 'connected_as_b' ) self.fields['interface_a'].choices = [ @@ -2076,9 +2078,11 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor # Mark connected interfaces as disabled if self.data.get('device_b'): - self.fields['interface_b'].choices = [ - (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface_b'].queryset - ] + self.fields['interface_b'].choices = [] + for iface in self.fields['interface_b'].queryset: + self.fields['interface_b'].choices.append( + (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) + ) class InterfaceConnectionCSVForm(forms.ModelForm): diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 02c87c122a9..8a8fb8d4c3c 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -7,7 +7,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from django.core.paginator import EmptyPage, PageNotAnInteger from django.db import transaction from django.db.models import Count, Q -from django.forms import ModelChoiceField, ModelForm, modelformset_factory +from django.forms import modelformset_factory from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse @@ -2082,14 +2082,13 @@ class VirtualChassisCreateView(PermissionRequiredMixin, View): # Get the list of devices being added to a VirtualChassis pk_form = forms.DeviceSelectionForm(request.POST) pk_form.full_clean() + if not pk_form.cleaned_data.get('pk'): + messages.warning(request, "No devices were selected.") + return redirect('dcim:device_list') device_queryset = Device.objects.filter( pk__in=pk_form.cleaned_data.get('pk') ).select_related('rack').order_by('vc_position') - if not device_queryset: - messages.warning(request, "No devices were selected.") - return redirect('dcim:device_list') - VCMemberFormSet = modelformset_factory( model=Device, formset=forms.BaseVCMemberFormSet, diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 37a3585ec85..63f3a492dcb 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ if sys.version_info[0] < 3: DeprecationWarning ) -VERSION = '2.3.0' +VERSION = '2.3.1-dev' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) diff --git a/netbox/templates/dcim/inc/consoleport.html b/netbox/templates/dcim/inc/consoleport.html index 62375c7f2a2..4d75cc65b15 100644 --- a/netbox/templates/dcim/inc/consoleport.html +++ b/netbox/templates/dcim/inc/consoleport.html @@ -44,7 +44,7 @@ {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/consoleserverport.html b/netbox/templates/dcim/inc/consoleserverport.html index aed27d62a85..673f51388da 100644 --- a/netbox/templates/dcim/inc/consoleserverport.html +++ b/netbox/templates/dcim/inc/consoleserverport.html @@ -49,7 +49,7 @@ {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/devicebay.html b/netbox/templates/dcim/inc/devicebay.html index e6e4d3e47fa..4e17e3d36df 100644 --- a/netbox/templates/dcim/inc/devicebay.html +++ b/netbox/templates/dcim/inc/devicebay.html @@ -40,7 +40,7 @@ {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index 783a564600a..aa0a9cbd5f7 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -124,7 +124,7 @@ {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/inventoryitem.html b/netbox/templates/dcim/inc/inventoryitem.html index b5076527180..21de1014ead 100644 --- a/netbox/templates/dcim/inc/inventoryitem.html +++ b/netbox/templates/dcim/inc/inventoryitem.html @@ -11,7 +11,7 @@ {% endif %} {% if perms.dcim.delete_inventoryitem %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/poweroutlet.html b/netbox/templates/dcim/inc/poweroutlet.html index 30697720716..f3c855ea795 100644 --- a/netbox/templates/dcim/inc/poweroutlet.html +++ b/netbox/templates/dcim/inc/poweroutlet.html @@ -49,7 +49,7 @@ {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/powerport.html b/netbox/templates/dcim/inc/powerport.html index 555d6d3eefc..32e7f20fd4a 100644 --- a/netbox/templates/dcim/inc/powerport.html +++ b/netbox/templates/dcim/inc/powerport.html @@ -44,7 +44,7 @@ {% else %} - + {% endif %} diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 9dccdcc9db1..8471d0e00c7 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -5,6 +5,7 @@ import pytz from django.conf import settings from django.contrib.contenttypes.models import ContentType +from django.db.models import ManyToManyField from django.http import Http404 from rest_framework import mixins from rest_framework.exceptions import APIException @@ -51,6 +52,11 @@ class ValidatedModelSerializer(ModelSerializer): # Run clean() on an instance of the model if self.instance is None: + model = self.Meta.model + # Ignore ManyToManyFields for new instances (a PK is needed for validation) + for field in model._meta.get_fields(): + if isinstance(field, ManyToManyField): + attrs.pop(field.name) instance = self.Meta.model(**attrs) else: instance = self.instance