Skip to content

Commit

Permalink
Merge pull request netbox-community#2041 from digitalocean/develop
Browse files Browse the repository at this point in the history
Release v2.3.3
  • Loading branch information
jeremystretch authored Apr 19, 2018
2 parents 68f73c7 + a7389de commit 3289588
Show file tree
Hide file tree
Showing 13 changed files with 64 additions and 40 deletions.
11 changes: 6 additions & 5 deletions netbox/dcim/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}),
Expand All @@ -124,14 +123,16 @@ class Meta:
'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"
}


class SiteCSVForm(forms.ModelForm):
status = CSVChoiceField(
choices=DEVICE_STATUS_CHOICES,
choices=SITE_STATUS_CHOICES,
required=False,
help_text='Operational status'
)
Expand Down Expand Up @@ -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={
Expand Down
14 changes: 7 additions & 7 deletions netbox/dcim/querysets.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ def order_naturally(self, method=IFACE_ORDERING_POSITION):
}[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), []),
Expand Down
20 changes: 11 additions & 9 deletions netbox/dcim/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -65,17 +67,17 @@ def post(self, request):
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,
})
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -1783,7 +1785,7 @@ def post(self, request, pk):

class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
permission_required = 'dcim.change_devicebay'
model = DeviceBay
queryset = DeviceBay.objects.all()
form = forms.DeviceBayBulkRenameForm


Expand Down
13 changes: 10 additions & 3 deletions netbox/extras/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,18 @@ def filter(self, queryset, value):
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):
Expand Down
8 changes: 3 additions & 5 deletions netbox/ipam/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,22 +508,20 @@ def save(self, *args, **kwargs):

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:
parent.primary_ip4 = ipaddress
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()

Expand Down
2 changes: 1 addition & 1 deletion netbox/ipam/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
4 changes: 2 additions & 2 deletions netbox/ipam/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -729,8 +729,8 @@ def post(self, request):
'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', {
Expand Down
2 changes: 1 addition & 1 deletion netbox/netbox/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
DeprecationWarning
)

VERSION = '2.3.2'
VERSION = '2.3.3'

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

Expand Down
1 change: 1 addition & 0 deletions netbox/templates/dcim/site_edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/ipam/ipaddress_assign.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ <h3>Assign an IP Address</h3>
</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>
Expand Down
3 changes: 2 additions & 1 deletion netbox/utilities/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ def __init__(self, *args, **kwargs):

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):
Expand Down
2 changes: 1 addition & 1 deletion netbox/utilities/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 18 additions & 4 deletions netbox/virtualization/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
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
Expand Down Expand Up @@ -133,13 +133,26 @@ class Meta:
# 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',
]


Expand All @@ -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',
]

0 comments on commit 3289588

Please sign in to comment.