diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md
index 02dbb878f..02a08716b 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 6d0892f67..0c8ea3716 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 02c87c122..8a8fb8d4c 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 37a3585ec..63f3a492d 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 62375c7f2..4d75cc65b 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 aed27d62a..673f51388 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 e6e4d3e47..4e17e3d36 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 783a56460..aa0a9cbd5 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 b50765271..21de1014e 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 306977207..f3c855ea7 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 555d6d3ee..32e7f20fd 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 9dccdcc9d..8471d0e00 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