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