Merge pull request #774 from digitalocean/develop

Release v1.8.1
This commit is contained in:
Jeremy Stretch 2017-01-04 15:30:54 -05:00 committed by GitHub
commit 23f6832d9c
11 changed files with 93 additions and 81 deletions

View File

@ -96,6 +96,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
def search(self, queryset, value): def search(self, queryset, value):
return queryset.filter( return queryset.filter(
Q(cid__icontains=value) | Q(cid__icontains=value) |
Q(xconnect_id__icontains=value) | Q(terminations__xconnect_id__icontains=value) |
Q(terminations__pp_info__icontains=value) |
Q(comments__icontains=value) Q(comments__icontains=value)
) )

View File

@ -5,6 +5,7 @@ from django.db import models
from dcim.fields import ASNField from dcim.fields import ASNField
from extras.models import CustomFieldModel, CustomFieldValue from extras.models import CustomFieldModel, CustomFieldValue
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.utils import csv_format
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
@ -57,10 +58,10 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
return reverse('circuits:provider', args=[self.slug]) return reverse('circuits:provider', args=[self.slug])
def to_csv(self): def to_csv(self):
return ','.join([ return csv_format([
self.name, self.name,
self.slug, self.slug,
str(self.asn) if self.asn else '', self.asn,
self.account, self.account,
self.portal_url, self.portal_url,
]) ])
@ -68,7 +69,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
class CircuitType(models.Model): class CircuitType(models.Model):
""" """
Circuits can be orgnanized by their functional role. For example, a user might wish to define CircuitTypes named Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
"Long Haul," "Metro," or "Out-of-Band". "Long Haul," "Metro," or "Out-of-Band".
""" """
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
@ -110,13 +111,13 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
return reverse('circuits:circuit', args=[self.pk]) return reverse('circuits:circuit', args=[self.pk])
def to_csv(self): def to_csv(self):
return ','.join([ return csv_format([
self.cid, self.cid,
self.provider.name, self.provider.name,
self.type.name, self.type.name,
self.tenant.name if self.tenant else '', self.tenant.name if self.tenant else None,
self.install_date.isoformat() if self.install_date else '', self.install_date.isoformat() if self.install_date else None,
str(self.commit_rate) if self.commit_rate else '', self.commit_rate,
]) ])
def _get_termination(self, side): def _get_termination(self, side):

View File

@ -118,11 +118,13 @@ class RackUnitListView(APIView):
rack = get_object_or_404(Rack, pk=pk) rack = get_object_or_404(Rack, pk=pk)
face = request.GET.get('face', 0) face = request.GET.get('face', 0)
try: exclude_pk = request.GET.get('exclude', None)
exclude = int(request.GET.get('exclude', None)) if exclude_pk is not None:
except ValueError: try:
exclude = None exclude_pk = int(exclude_pk)
elevation = rack.get_rack_units(face, exclude) except ValueError:
exclude_pk = None
elevation = rack.get_rack_units(face, exclude_pk)
# Serialize Devices within the rack elevation # Serialize Devices within the rack elevation
for u in elevation: for u in elevation:

View File

@ -16,6 +16,7 @@ from tenancy.models import Tenant
from utilities.fields import ColorField, NullableCharField from utilities.fields import ColorField, NullableCharField
from utilities.managers import NaturalOrderByManager from utilities.managers import NaturalOrderByManager
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format
from .fields import ASNField, MACAddressField from .fields import ASNField, MACAddressField
@ -263,12 +264,12 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
return reverse('dcim:site', args=[self.slug]) return reverse('dcim:site', args=[self.slug])
def to_csv(self): def to_csv(self):
return ','.join([ return csv_format([
self.name, self.name,
self.slug, self.slug,
self.tenant.name if self.tenant else '', self.tenant.name if self.tenant else None,
self.facility, self.facility,
str(self.asn) if self.asn else '', self.asn,
self.contact_name, self.contact_name,
self.contact_phone, self.contact_phone,
self.contact_email, self.contact_email,
@ -398,17 +399,17 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
}) })
def to_csv(self): def to_csv(self):
return ','.join([ return csv_format([
self.site.name, self.site.name,
self.group.name if self.group else '', self.group.name if self.group else None,
self.name, self.name,
self.facility_id or '', self.facility_id,
self.tenant.name if self.tenant else '', self.tenant.name if self.tenant else None,
self.role.name if self.role else '', self.role.name if self.role else None,
self.get_type_display() if self.type else '', self.get_type_display() if self.type else None,
str(self.width), self.width,
str(self.u_height), self.u_height,
'True' if self.desc_units else '', self.desc_units,
]) ])
@property @property
@ -910,19 +911,19 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
Device.objects.filter(parent_bay__device=self).update(rack=self.rack) Device.objects.filter(parent_bay__device=self).update(rack=self.rack)
def to_csv(self): def to_csv(self):
return ','.join([ return csv_format([
self.name or '', self.name or '',
self.device_role.name, self.device_role.name,
self.tenant.name if self.tenant else '', self.tenant.name if self.tenant else None,
self.device_type.manufacturer.name, self.device_type.manufacturer.name,
self.device_type.model, self.device_type.model,
self.platform.name if self.platform else '', self.platform.name if self.platform else None,
self.serial, self.serial,
self.asset_tag if self.asset_tag else '', self.asset_tag,
self.rack.site.name, self.rack.site.name,
self.rack.name, self.rack.name,
str(self.position) if self.position else '', self.position,
self.get_face_display() or '', self.get_face_display(),
]) ])
@property @property
@ -991,9 +992,9 @@ class ConsolePort(models.Model):
# Used for connections export # Used for connections export
def to_csv(self): def to_csv(self):
return ','.join([ return csv_format([
self.cs_port.device.identifier if self.cs_port else '', self.cs_port.device.identifier if self.cs_port else None,
self.cs_port.name if self.cs_port else '', self.cs_port.name if self.cs_port else None,
self.device.identifier, self.device.identifier,
self.name, self.name,
self.get_connection_status_display(), self.get_connection_status_display(),
@ -1055,10 +1056,10 @@ class PowerPort(models.Model):
return self.device.get_absolute_url() return self.device.get_absolute_url()
# Used for connections export # Used for connections export
def to_csv(self): def csv_format(self):
return ','.join([ return ','.join([
self.power_outlet.device.identifier if self.power_outlet else '', self.power_outlet.device.identifier if self.power_outlet else None,
self.power_outlet.name if self.power_outlet else '', self.power_outlet.name if self.power_outlet else None,
self.device.identifier, self.device.identifier,
self.name, self.name,
self.get_connection_status_display(), self.get_connection_status_display(),
@ -1196,7 +1197,7 @@ class InterfaceConnection(models.Model):
# Used for connections export # Used for connections export
def to_csv(self): def to_csv(self):
return ','.join([ return csv_format([
self.interface_a.device.identifier, self.interface_a.device.identifier,
self.interface_a.name, self.interface_a.name,
self.interface_b.device.identifier, self.interface_b.device.identifier,

View File

@ -34,9 +34,9 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
(0, 'False'), (0, 'False'),
) )
if cf.default.lower() in ['true', 'yes', '1']: if cf.default.lower() in ['true', 'yes', '1']:
initial = True initial = 1
elif cf.default.lower() in ['false', 'no', '0']: elif cf.default.lower() in ['false', 'no', '0']:
initial = False initial = 0
else: else:
initial = None initial = None
field = forms.NullBooleanField(required=cf.required, initial=initial, field = forms.NullBooleanField(required=cf.required, initial=initial,

View File

@ -126,7 +126,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
vlan_id = django_filters.ModelMultipleChoiceFilter( vlan_id = NullableModelMultipleChoiceFilter(
name='vlan', name='vlan',
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
label='VLAN (ID)', label='VLAN (ID)',

View File

@ -13,6 +13,7 @@ from extras.models import CustomFieldModel, CustomFieldValue
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from utilities.sql import NullsFirstQuerySet from utilities.sql import NullsFirstQuerySet
from utilities.utils import csv_format
from .fields import IPNetworkField, IPAddressField from .fields import IPNetworkField, IPAddressField
@ -95,11 +96,11 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
return reverse('ipam:vrf', args=[self.pk]) return reverse('ipam:vrf', args=[self.pk])
def to_csv(self): def to_csv(self):
return ','.join([ return csv_format([
self.name, self.name,
self.rd, self.rd,
self.tenant.name if self.tenant else '', self.tenant.name if self.tenant else None,
'True' if self.enforce_unique else '', self.enforce_unique,
self.description, self.description,
]) ])
@ -183,10 +184,10 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
super(Aggregate, self).save(*args, **kwargs) super(Aggregate, self).save(*args, **kwargs)
def to_csv(self): def to_csv(self):
return ','.join([ return csv_format([
str(self.prefix), self.prefix,
self.rir.name, self.rir.name,
self.date_added.isoformat() if self.date_added else '', self.date_added.isoformat() if self.date_added else None,
self.description, self.description,
]) ])
@ -319,16 +320,16 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
super(Prefix, self).save(*args, **kwargs) super(Prefix, self).save(*args, **kwargs)
def to_csv(self): def to_csv(self):
return ','.join([ return csv_format([
str(self.prefix), self.prefix,
self.vrf.rd if self.vrf else '', self.vrf.rd if self.vrf else None,
self.tenant.name if self.tenant else '', self.tenant.name if self.tenant else None,
self.site.name if self.site else '', self.site.name if self.site else None,
self.vlan.group.name if self.vlan and self.vlan.group else '', self.vlan.group.name if self.vlan and self.vlan.group else None,
str(self.vlan.vid) if self.vlan else '', self.vlan.vid if self.vlan else None,
self.get_status_display(), self.get_status_display(),
self.role.name if self.role else '', self.role.name if self.role else None,
'True' if self.is_pool else '', self.is_pool,
self.description, self.description,
]) ])
@ -432,14 +433,14 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
elif self.family == 6 and getattr(self, 'primary_ip6_for', False): elif self.family == 6 and getattr(self, 'primary_ip6_for', False):
is_primary = True is_primary = True
return ','.join([ return csv_format([
str(self.address), self.address,
self.vrf.rd if self.vrf else '', self.vrf.rd if self.vrf else None,
self.tenant.name if self.tenant else '', self.tenant.name if self.tenant else None,
self.get_status_display(), self.get_status_display(),
self.device.identifier if self.device else '', self.device.identifier if self.device else None,
self.interface.name if self.interface else '', self.interface.name if self.interface else None,
'True' if is_primary else '', is_primary,
self.description, self.description,
]) ])
@ -523,14 +524,14 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
}) })
def to_csv(self): def to_csv(self):
return ','.join([ return csv_format([
self.site.name, self.site.name,
self.group.name if self.group else '', self.group.name if self.group else None,
str(self.vid), self.vid,
self.name, self.name,
self.tenant.name if self.tenant else '', self.tenant.name if self.tenant else None,
self.get_status_display(), self.get_status_display(),
self.role.name if self.role else '', self.role.name if self.role else None,
self.description, self.description,
]) ])

View File

@ -12,7 +12,7 @@ except ImportError:
"the documentation.") "the documentation.")
VERSION = '1.8.0' VERSION = '1.8.1'
# Import local configuration # Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:

View File

@ -4,6 +4,7 @@ from django.db import models
from extras.models import CustomFieldModel, CustomFieldValue from extras.models import CustomFieldModel, CustomFieldValue
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format
class TenantGroup(models.Model): class TenantGroup(models.Model):
@ -45,9 +46,9 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
return reverse('tenancy:tenant', args=[self.slug]) return reverse('tenancy:tenant', args=[self.slug])
def to_csv(self): def to_csv(self):
return ','.join([ return csv_format([
self.name, self.name,
self.slug, self.slug,
self.group.name if self.group else '', self.group.name if self.group else None,
self.description, self.description,
]) ])

15
netbox/utilities/utils.py Normal file
View File

@ -0,0 +1,15 @@
def csv_format(data):
"""
Encapsulate any data which contains a comma within double quotes.
"""
csv = []
for d in data:
if d in [None, False]:
csv.append(u'')
elif type(d) not in (str, unicode):
csv.append(u'{}'.format(d))
elif u',' in d:
csv.append(u'"{}"'.format(d))
else:
csv.append(d)
return u','.join(csv)

View File

@ -48,8 +48,6 @@ class ObjectListView(View):
table: The django-tables2 Table used to render the objects list table: The django-tables2 Table used to render the objects list
edit_permissions: Editing controls are displayed only if the user has these permissions edit_permissions: Editing controls are displayed only if the user has these permissions
template_name: The name of the template template_name: The name of the template
redirect_on_single_result: If True and the queryset returns only a single object, the user is automatically
redirected to that object
""" """
queryset = None queryset = None
filter = None filter = None
@ -57,7 +55,6 @@ class ObjectListView(View):
table = None table = None
edit_permissions = [] edit_permissions = []
template_name = None template_name = None
redirect_on_single_result = True
def get(self, request): def get(self, request):
@ -95,13 +92,6 @@ class ObjectListView(View):
.format(self.queryset.model._meta.verbose_name_plural) .format(self.queryset.model._meta.verbose_name_plural)
return response return response
# Attempt to redirect automatically if the search query returns a single result
if self.redirect_on_single_result and self.queryset.count() == 1 and request.GET:
try:
return HttpResponseRedirect(self.queryset[0].get_absolute_url())
except AttributeError:
pass
# Provide a hook to tweak the queryset based on the request immediately prior to rendering the object list # Provide a hook to tweak the queryset based on the request immediately prior to rendering the object list
self.queryset = self.alter_queryset(request) self.queryset = self.alter_queryset(request)