Compare commits

...

16 Commits

Author SHA1 Message Date
Jeremy Stretch
328958876a Merge pull request #2041 from digitalocean/develop
Release v2.3.3
2018-04-19 11:15:48 -04:00
Jeremy Stretch
a7389de109 Release v2.3.3 2018-04-19 11:07:19 -04:00
Jeremy Stretch
b911ab01d2 Merge pull request #2038 from DirtyCajunRice/develop
stop force value split w ArrayFieldSelectMultiple. Fixes #2037
2018-04-19 10:55:25 -04:00
Nicholas St. Germain
9153c71cbf stop force value split w ArrayFieldSelectMultiple 2018-04-18 14:02:40 -05:00
Jeremy Stretch
b44aa9d32e Fixes #2014: Allow assignment of VLANs to VM interfaces via the API 2018-04-18 12:37:20 -04:00
Jeremy Stretch
bcb1d9af0b Fixes #2012: Fixed deselection of an IP address as the primary IP for its parent device/VM 2018-04-12 13:03:20 -04:00
Jeremy Stretch
ef84889a57 Fixes #2022: Show 0 for zero-value fields on CSV export 2018-04-12 12:54:21 -04:00
Jeremy Stretch
81c027e7cf Fixes #2023: Manufacturer should not be a required field when importing platforms 2018-04-12 12:45:25 -04:00
Jeremy Stretch
fd62a248ee Merge pull request #2020 from Wikia/intfix
#2019 : avoid illegal casts on large integers
2018-04-12 12:06:44 -04:00
frankfarmer
2c8bea1b59 avoid illegal casts on large integers
A similar fix was applied in e5e32d82d00e454ba5edf25316828c1cdcd7673e
2018-04-09 17:42:54 -07:00
Jeremy Stretch
07364abf9e Fixes #1988: Order interfaces naturally when bulk renaming 2018-03-29 15:15:13 -04:00
Jeremy Stretch
20cb13e1bb Fixes #1975: Correct filtering logic for custom boolean fields 2018-03-29 14:47:35 -04:00
Jeremy Stretch
3f3b385de7 Fixes #1999: Added missing description field to site edit form 2018-03-29 13:49:50 -04:00
Jeremy Stretch
94b12e506e Fixes #1993: Corrected status choices in site CSV import form 2018-03-29 09:50:29 -04:00
Jeremy Stretch
4ec6e52e73 Closes #1990: Improved search function when assigning an IP address to an interface 2018-03-29 09:45:17 -04:00
Jeremy Stretch
88adc5ca86 Post-release version bump 2018-03-22 15:06:59 -04:00
13 changed files with 64 additions and 40 deletions

View File

@@ -112,9 +112,8 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class Meta:
model = Site
fields = [
'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'description',
'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'time_zone',
'comments',
'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
]
widgets = {
'physical_address': SmallTextarea(attrs={'rows': 3}),
@@ -124,6 +123,8 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
'name': "Full name of the site",
'facility': "Data center provider and facility (e.g. Equinix NY7)",
'asn': "BGP autonomous system number",
'time_zone': "Local time zone",
'description': "Short description (will appear in sites list)",
'physical_address': "Physical location of the building (e.g. for GPS)",
'shipping_address': "If different from the physical address"
}
@@ -131,7 +132,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class SiteCSVForm(forms.ModelForm):
status = CSVChoiceField(
choices=DEVICE_STATUS_CHOICES,
choices=SITE_STATUS_CHOICES,
required=False,
help_text='Operational status'
)
@@ -705,7 +706,7 @@ class PlatformCSVForm(forms.ModelForm):
slug = SlugField()
manufacturer = forms.ModelChoiceField(
queryset=Manufacturer.objects.all(),
required=True,
required=False,
to_field_name='name',
help_text='Manufacturer name',
error_messages={

View File

@@ -43,13 +43,13 @@ class InterfaceQuerySet(QuerySet):
}[method]
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)"
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?([0-9]+)\/') AS integer)"
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/)([0-9]+)') AS integer), 0)"
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)"
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)"
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)"
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)"
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(\d{{1,9}})$') AS integer)"
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(\d{{1,9}})\/') AS integer)"
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/)(\d{{1,9}})') AS integer), 0)"
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/){{2}}(\d{{1,9}})') AS integer), 0)"
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/){{3}}(\d{{1,9}})') AS integer), 0)"
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':(\d{{1,9}})(\.\d{{1,9}})?$') AS integer), 0)"
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.(\d{{1,9}})$') AS integer), 0)"
fields = {
'_type': RawSQL(TYPE_RE.format(sql_col), []),

View File

@@ -41,19 +41,21 @@ class BulkRenameView(View):
"""
An extendable view for renaming device components in bulk.
"""
model = None
queryset = None
form = None
template_name = 'dcim/bulk_rename.html'
def post(self, request):
model = self.queryset.model
return_url = request.GET.get('return_url')
if not return_url or not is_safe_url(url=return_url, host=request.get_host()):
return_url = 'home'
if '_preview' in request.POST or '_apply' in request.POST:
form = self.form(request.POST, initial={'pk': request.POST.getlist('pk')})
selected_objects = self.model.objects.filter(pk__in=form.initial['pk'])
selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
if form.is_valid():
for obj in selected_objects:
@@ -65,17 +67,17 @@ class BulkRenameView(View):
obj.save()
messages.success(request, "Renamed {} {}".format(
len(selected_objects),
self.model._meta.verbose_name_plural
model._meta.verbose_name_plural
))
return redirect(return_url)
else:
form = self.form(initial={'pk': request.POST.getlist('pk')})
selected_objects = self.model.objects.filter(pk__in=form.initial['pk'])
selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
return render(request, self.template_name, {
'form': form,
'obj_type_plural': self.model._meta.verbose_name_plural,
'obj_type_plural': model._meta.verbose_name_plural,
'selected_objects': selected_objects,
'return_url': return_url,
})
@@ -1316,7 +1318,7 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class ConsoleServerPortBulkRenameView(PermissionRequiredMixin, BulkRenameView):
permission_required = 'dcim.change_consoleserverport'
model = ConsoleServerPort
queryset = ConsoleServerPort.objects.all()
form = forms.ConsoleServerPortBulkRenameForm
@@ -1600,7 +1602,7 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class PowerOutletBulkRenameView(PermissionRequiredMixin, BulkRenameView):
permission_required = 'dcim.change_poweroutlet'
model = PowerOutlet
queryset = PowerOutlet.objects.all()
form = forms.PowerOutletBulkRenameForm
@@ -1676,7 +1678,7 @@ class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
class InterfaceBulkRenameView(PermissionRequiredMixin, BulkRenameView):
permission_required = 'dcim.change_interface'
model = Interface
queryset = Interface.objects.order_naturally()
form = forms.InterfaceBulkRenameForm
@@ -1783,7 +1785,7 @@ class DeviceBayDepopulateView(PermissionRequiredMixin, View):
class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
permission_required = 'dcim.change_devicebay'
model = DeviceBay
queryset = DeviceBay.objects.all()
form = forms.DeviceBayBulkRenameForm

View File

@@ -43,11 +43,18 @@ class CustomFieldFilter(django_filters.Filter):
return queryset.none()
# Apply the assigned filter logic (exact or loose)
queryset = queryset.filter(custom_field_values__field__name=self.name)
if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT:
return queryset.filter(custom_field_values__serialized_value=value)
queryset = queryset.filter(
custom_field_values__field__name=self.name,
custom_field_values__serialized_value=value
)
else:
return queryset.filter(custom_field_values__serialized_value__icontains=value)
queryset = queryset.filter(
custom_field_values__field__name=self.name,
custom_field_values__serialized_value__icontains=value
)
return queryset
class CustomFieldFilterSet(django_filters.FilterSet):

View File

@@ -508,7 +508,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
ipaddress = super(IPAddressForm, self).save(*args, **kwargs)
# Assign this IPAddress as the primary for the associated Device.
# Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
if self.cleaned_data['primary_for_parent']:
parent = self.cleaned_data['interface'].parent
if ipaddress.address.version == 4:
@@ -516,14 +516,12 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
else:
parent.primary_ip6 = ipaddress
parent.save()
# Clear assignment as primary for device if set.
elif self.cleaned_data['interface']:
parent = self.cleaned_data['interface'].parent
if ipaddress.address.version == 4 and parent.primary_ip4 == self:
if ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress:
parent.primary_ip4 = None
parent.save()
elif ipaddress.address.version == 6 and parent.primary_ip6 == self:
elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress:
parent.primary_ip6 = None
parent.save()

View File

@@ -329,7 +329,7 @@ class IPAddressAssignTable(BaseTable):
class Meta(BaseTable.Meta):
model = IPAddress
fields = ('address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface')
fields = ('address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'description')
orderable = False

View File

@@ -729,8 +729,8 @@ class IPAddressAssignView(PermissionRequiredMixin, View):
'vrf', 'tenant', 'interface__device', 'interface__virtual_machine'
).filter(
vrf=form.cleaned_data['vrf'],
address__net_host=form.cleaned_data['address'],
)
address__istartswith=form.cleaned_data['address'],
)[:100] # Limit to 100 results
table = tables.IPAddressAssignTable(queryset)
return render(request, 'ipam/ipaddress_assign.html', {

View File

@@ -22,7 +22,7 @@ if sys.version_info[0] < 3:
DeprecationWarning
)
VERSION = '2.3.2'
VERSION = '2.3.3'
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

View File

@@ -12,6 +12,7 @@
{% render_field form.facility %}
{% render_field form.asn %}
{% render_field form.time_zone %}
{% render_field form.description %}
</div>
</div>
<div class="panel panel-default">

View File

@@ -39,7 +39,7 @@
</form>
{% if table %}
<div class="row">
<div class="col-md-10 col-md-offset-1" style="margin-top: 20px">
<div class="col-md-12" style="margin-top: 20px">
<h3>Search Results</h3>
{% include 'utilities/obj_table.html' with table_template='panel_table.html' %}
</div>

View File

@@ -205,7 +205,8 @@ class ArrayFieldSelectMultiple(SelectWithDisabled, forms.SelectMultiple):
def optgroups(self, name, value, attrs=None):
# Split the delimited string of values into a list
value = value[0].split(self.delimiter)
if value:
value = value[0].split(self.delimiter)
return super(ArrayFieldSelectMultiple, self).optgroups(name, value, attrs)
def value_from_datadict(self, data, files, name):

View File

@@ -14,7 +14,7 @@ def csv_format(data):
for value in data:
# Represent None or False with empty string
if value in [None, False]:
if value is None or value is False:
csv.append('')
continue

View File

@@ -3,10 +3,10 @@ from __future__ import unicode_literals
from rest_framework import serializers
from dcim.api.serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
from dcim.constants import IFACE_FF_VIRTUAL
from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_CHOICES
from dcim.models import Interface
from extras.api.customfields import CustomFieldModelSerializer
from ipam.models import IPAddress
from ipam.models import IPAddress, VLAN
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
from virtualization.constants import VM_STATUS_CHOICES
@@ -133,13 +133,26 @@ class WritableVirtualMachineSerializer(CustomFieldModelSerializer):
# VM interfaces
#
# Cannot import ipam.api.serializers.NestedVLANSerializer due to circular dependency
class InterfaceVLANSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
class Meta:
model = VLAN
fields = ['id', 'url', 'vid', 'name', 'display_name']
class InterfaceSerializer(serializers.ModelSerializer):
virtual_machine = NestedVirtualMachineSerializer()
mode = ChoiceFieldSerializer(choices=IFACE_MODE_CHOICES)
untagged_vlan = InterfaceVLANSerializer()
tagged_vlans = InterfaceVLANSerializer(many=True)
class Meta:
model = Interface
fields = [
'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'description',
'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'untagged_vlan', 'tagged_vlans',
'description',
]
@@ -157,5 +170,6 @@ class WritableInterfaceSerializer(ValidatedModelSerializer):
class Meta:
model = Interface
fields = [
'id', 'name', 'virtual_machine', 'form_factor', 'enabled', 'mac_address', 'mtu', 'description',
'id', 'name', 'virtual_machine', 'form_factor', 'enabled', 'mac_address', 'mtu', 'mode', 'untagged_vlan',
'tagged_vlans', 'description',
]