mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop' into bug/1921
This commit is contained in:
@ -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
|
# 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:
|
If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
|
@ -1679,6 +1679,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm, ChainedFieldsMixin):
|
|||||||
label='Untagged VLAN',
|
label='Untagged VLAN',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',
|
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',
|
||||||
|
display_field='display_name'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
tagged_vlans = ChainedModelMultipleChoiceField(
|
tagged_vlans = ChainedModelMultipleChoiceField(
|
||||||
@ -1691,6 +1692,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm, ChainedFieldsMixin):
|
|||||||
label='Tagged VLANs',
|
label='Tagged VLANs',
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',
|
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)
|
super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Initialize interface A choices
|
# 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'
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
)
|
)
|
||||||
self.fields['interface_a'].choices = [
|
self.fields['interface_a'].choices = [
|
||||||
@ -2076,9 +2078,11 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
|
|||||||
|
|
||||||
# Mark connected interfaces as disabled
|
# Mark connected interfaces as disabled
|
||||||
if self.data.get('device_b'):
|
if self.data.get('device_b'):
|
||||||
self.fields['interface_b'].choices = [
|
self.fields['interface_b'].choices = []
|
||||||
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface_b'].queryset
|
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):
|
class InterfaceConnectionCSVForm(forms.ModelForm):
|
||||||
|
@ -7,7 +7,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
|
|||||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Count, Q
|
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.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
|
||||||
@ -2082,14 +2082,13 @@ class VirtualChassisCreateView(PermissionRequiredMixin, View):
|
|||||||
# Get the list of devices being added to a VirtualChassis
|
# Get the list of devices being added to a VirtualChassis
|
||||||
pk_form = forms.DeviceSelectionForm(request.POST)
|
pk_form = forms.DeviceSelectionForm(request.POST)
|
||||||
pk_form.full_clean()
|
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(
|
device_queryset = Device.objects.filter(
|
||||||
pk__in=pk_form.cleaned_data.get('pk')
|
pk__in=pk_form.cleaned_data.get('pk')
|
||||||
).select_related('rack').order_by('vc_position')
|
).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(
|
VCMemberFormSet = modelformset_factory(
|
||||||
model=Device,
|
model=Device,
|
||||||
formset=forms.BaseVCMemberFormSet,
|
formset=forms.BaseVCMemberFormSet,
|
||||||
|
@ -22,7 +22,7 @@ if sys.version_info[0] < 3:
|
|||||||
DeprecationWarning
|
DeprecationWarning
|
||||||
)
|
)
|
||||||
|
|
||||||
VERSION = '2.3.0'
|
VERSION = '2.3.1-dev'
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'dcim:consoleport_delete' pk=cp.pk %}" title="Delete port" class="btn btn-danger btn-xs">
|
<a href="{% url 'dcim:consoleport_delete' pk=cp.pk %}?return_url={{ device.get_absolute_url }}" title="Delete port" class="btn btn-danger btn-xs">
|
||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'dcim:consoleserverport_delete' pk=csp.pk %}" title="Delete port" class="btn btn-danger btn-xs">
|
<a href="{% url 'dcim:consoleserverport_delete' pk=csp.pk %}?return_url={{ device.get_absolute_url }}" title="Delete port" class="btn btn-danger btn-xs">
|
||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'dcim:devicebay_delete' pk=devicebay.pk %}" class="btn btn-danger btn-xs">
|
<a href="{% url 'dcim:devicebay_delete' pk=devicebay.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete device bay"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete device bay"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -124,7 +124,7 @@
|
|||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% if iface.device_id %}{% url 'dcim:interface_delete' pk=iface.pk %}{% else %}{% url 'virtualization:interface_delete' pk=iface.pk %}{% endif %}" class="btn btn-danger btn-xs" title="Delete interface">
|
<a href="{% if iface.device_id %}{% url 'dcim:interface_delete' pk=iface.pk %}{% else %}{% url 'virtualization:interface_delete' pk=iface.pk %}{% endif %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs" title="Delete interface">
|
||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
|
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_inventoryitem %}
|
{% if perms.dcim.delete_inventoryitem %}
|
||||||
<a href="{% url 'dcim:inventoryitem_delete' pk=item.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
|
<a href="{% url 'dcim:inventoryitem_delete' pk=item.pk %}?return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'dcim:poweroutlet_delete' pk=po.pk %}" title="Delete outlet" class="btn btn-danger btn-xs">
|
<a href="{% url 'dcim:poweroutlet_delete' pk=po.pk %}?return_url={{ device.get_absolute_url }}" title="Delete outlet" class="btn btn-danger btn-xs">
|
||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'dcim:powerport_delete' pk=pp.pk %}" title="Delete port" class="btn btn-danger btn-xs">
|
<a href="{% url 'dcim:powerport_delete' pk=pp.pk %}?return_url={{ device.get_absolute_url }}" title="Delete port" class="btn btn-danger btn-xs">
|
||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -5,6 +5,7 @@ import pytz
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db.models import ManyToManyField
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.exceptions import APIException
|
from rest_framework.exceptions import APIException
|
||||||
@ -51,6 +52,11 @@ class ValidatedModelSerializer(ModelSerializer):
|
|||||||
|
|
||||||
# Run clean() on an instance of the model
|
# Run clean() on an instance of the model
|
||||||
if self.instance is None:
|
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)
|
instance = self.Meta.model(**attrs)
|
||||||
else:
|
else:
|
||||||
instance = self.instance
|
instance = self.instance
|
||||||
|
Reference in New Issue
Block a user