Răsfoiți Sursa

Merge pull request #774 from digitalocean/develop

Release v1.8.1
Jeremy Stretch 9 ani în urmă
părinte
comite
23f6832d9c

+ 2 - 1
netbox/circuits/filters.py

@@ -96,6 +96,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
     def search(self, queryset, value):
         return queryset.filter(
             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)
         )

+ 8 - 7
netbox/circuits/models.py

@@ -5,6 +5,7 @@ from django.db import models
 from dcim.fields import ASNField
 from extras.models import CustomFieldModel, CustomFieldValue
 from tenancy.models import Tenant
+from utilities.utils import csv_format
 from utilities.models import CreatedUpdatedModel
 
 
@@ -57,10 +58,10 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
         return reverse('circuits:provider', args=[self.slug])
 
     def to_csv(self):
-        return ','.join([
+        return csv_format([
             self.name,
             self.slug,
-            str(self.asn) if self.asn else '',
+            self.asn,
             self.account,
             self.portal_url,
         ])
@@ -68,7 +69,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
 
 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".
     """
     name = models.CharField(max_length=50, unique=True)
@@ -110,13 +111,13 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
         return reverse('circuits:circuit', args=[self.pk])
 
     def to_csv(self):
-        return ','.join([
+        return csv_format([
             self.cid,
             self.provider.name,
             self.type.name,
-            self.tenant.name if self.tenant else '',
-            self.install_date.isoformat() if self.install_date else '',
-            str(self.commit_rate) if self.commit_rate else '',
+            self.tenant.name if self.tenant else None,
+            self.install_date.isoformat() if self.install_date else None,
+            self.commit_rate,
         ])
 
     def _get_termination(self, side):

+ 7 - 5
netbox/dcim/api/views.py

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

+ 26 - 25
netbox/dcim/models.py

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

+ 2 - 2
netbox/extras/forms.py

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

+ 1 - 1
netbox/ipam/filters.py

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

+ 28 - 27
netbox/ipam/models.py

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

+ 1 - 1
netbox/netbox/settings.py

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

+ 3 - 2
netbox/tenancy/models.py

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

+ 15 - 0
netbox/utilities/utils.py

@@ -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)

+ 0 - 10
netbox/utilities/views.py

@@ -48,8 +48,6 @@ class ObjectListView(View):
     table: The django-tables2 Table used to render the objects list
     edit_permissions: Editing controls are displayed only if the user has these permissions
     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
     filter = None
@@ -57,7 +55,6 @@ class ObjectListView(View):
     table = None
     edit_permissions = []
     template_name = None
-    redirect_on_single_result = True
 
     def get(self, request):
 
@@ -95,13 +92,6 @@ class ObjectListView(View):
                 .format(self.queryset.model._meta.verbose_name_plural)
             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
         self.queryset = self.alter_queryset(request)