diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 45cbed3a4..e2ec5f55b 100644 --- a/netbox/circuits/filters.py +++ b/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) ) diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index c4bf86a28..d43778657 100644 --- a/netbox/circuits/models.py +++ b/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): diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 53bda6208..13e69392f 100644 --- a/netbox/dcim/api/views.py +++ b/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: diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index e0f467d17..c95bca426 100644 --- a/netbox/dcim/models.py +++ b/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, diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 497d204a6..b4549fcf1 100644 --- a/netbox/extras/forms.py +++ b/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, diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 7b7b15eec..a0dc9f633 100644 --- a/netbox/ipam/filters.py +++ b/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)', diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index ecc62bf5f..fa42d68f1 100644 --- a/netbox/ipam/models.py +++ b/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, ]) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 005126e39..9ea64e6b7 100644 --- a/netbox/netbox/settings.py +++ b/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']: diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index b80a40382..4317af196 100644 --- a/netbox/tenancy/models.py +++ b/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, ]) diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py new file mode 100644 index 000000000..7002cafc0 --- /dev/null +++ b/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) diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index a263ed3ff..3422b2d9d 100644 --- a/netbox/utilities/views.py +++ b/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)