From 52567c4ade29b7caa3847d3fd9bdd4d1551071af Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 4 Jan 2017 10:47:00 -0500 Subject: [PATCH] Fixes #764: Encapsulate in double quotes values containing commas when exporting to CSV --- netbox/circuits/models.py | 15 ++++++----- netbox/dcim/models.py | 51 ++++++++++++++++++------------------ netbox/ipam/models.py | 55 ++++++++++++++++++++------------------- netbox/tenancy/models.py | 5 ++-- netbox/utilities/utils.py | 15 +++++++++++ 5 files changed, 80 insertions(+), 61 deletions(-) create mode 100644 netbox/utilities/utils.py 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/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/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/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)