From 9d3cac43b77e09efa5d6f90fdeb59b6879e9dc51 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 9 Jun 2021 15:00:28 -0400 Subject: [PATCH 1/5] Modify default CSV export to render from tables --- netbox/dcim/views.py | 53 ------------------ netbox/netbox/views/generic.py | 93 ++++++++++++------------------- netbox/utilities/testing/views.py | 8 +-- 3 files changed, 40 insertions(+), 114 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 182e1ab81..93e7a84a6 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2530,23 +2530,6 @@ class ConsoleConnectionsListView(generic.ObjectListView): table = tables.ConsoleConnectionTable template_name = 'dcim/connections_list.html' - def queryset_to_csv(self): - csv_data = [ - # Headers - ','.join(['console_server', 'port', 'device', 'console_port', 'reachable']) - ] - for obj in self.queryset: - csv = csv_format([ - obj._path.destination.device.identifier if obj._path.destination else None, - obj._path.destination.name if obj._path.destination else None, - obj.device.identifier, - obj.name, - obj._path.is_active - ]) - csv_data.append(csv) - - return '\n'.join(csv_data) - def extra_context(self): return { 'title': 'Console Connections' @@ -2560,23 +2543,6 @@ class PowerConnectionsListView(generic.ObjectListView): table = tables.PowerConnectionTable template_name = 'dcim/connections_list.html' - def queryset_to_csv(self): - csv_data = [ - # Headers - ','.join(['pdu', 'outlet', 'device', 'power_port', 'reachable']) - ] - for obj in self.queryset: - csv = csv_format([ - obj._path.destination.device.identifier if obj._path.destination else None, - obj._path.destination.name if obj._path.destination else None, - obj.device.identifier, - obj.name, - obj._path.is_active - ]) - csv_data.append(csv) - - return '\n'.join(csv_data) - def extra_context(self): return { 'title': 'Power Connections' @@ -2594,25 +2560,6 @@ class InterfaceConnectionsListView(generic.ObjectListView): table = tables.InterfaceConnectionTable template_name = 'dcim/connections_list.html' - def queryset_to_csv(self): - csv_data = [ - # Headers - ','.join([ - 'device_a', 'interface_a', 'device_b', 'interface_b', 'reachable' - ]) - ] - for obj in self.queryset: - csv = csv_format([ - obj._path.destination.device.identifier if obj._path.destination else None, - obj._path.destination.name if obj._path.destination else None, - obj.device.identifier, - obj.name, - obj._path.is_active - ]) - csv_data.append(csv) - - return '\n'.join(csv_data) - def extra_context(self): return { 'title': 'Interface Connections' diff --git a/netbox/netbox/views/generic.py b/netbox/netbox/views/generic.py index f6f153d71..c3bde9f9d 100644 --- a/netbox/netbox/views/generic.py +++ b/netbox/netbox/views/generic.py @@ -92,7 +92,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View): def get_required_permission(self): return get_permission_for_model(self.queryset.model, 'view') - def queryset_to_yaml(self): + def export_yaml(self): """ Export the queryset of objects as concatenated YAML documents. """ @@ -100,34 +100,32 @@ class ObjectListView(ObjectPermissionRequiredMixin, View): return '---\n'.join(yaml_data) - def queryset_to_csv(self): + def export_table(self, table, columns=None): """ - Export the queryset of objects as comma-separated value (CSV), using the model's to_csv() method. + Export all table data in CSV format. + + :param table: The Table instance to export + :param columns: A list of specific columns to include. If not specified, the default view + will be exported. """ - csv_data = [] - custom_fields = [] - - # Start with the column headers - headers = self.queryset.model.csv_headers.copy() - - # Add custom field headers, if any - if hasattr(self.queryset.model, 'custom_field_data'): - for custom_field in CustomField.objects.get_for_model(self.queryset.model): - headers.append(custom_field.name) - custom_fields.append(custom_field.name) - - csv_data.append(','.join(headers)) - - # Iterate through the queryset appending each object - for obj in self.queryset: - data = obj.to_csv() - - for custom_field in custom_fields: - data += (obj.cf.get(custom_field, ''),) - - csv_data.append(csv_format(data)) - - return '\n'.join(csv_data) + exclude_columns = {'pk'} + if columns: + all_columns = [col_name for col_name, _ in table.selected_columns + table.available_columns] + exclude_columns.update({ + col for col in all_columns if col not in columns + }) + else: + exclude_columns.update({ + name for name, _ in table.available_columns + }) + exporter = TableExport( + export_format=TableExport.CSV, + table=table, + exclude_columns=exclude_columns + ) + return exporter.response( + filename=f'netbox_{self.queryset.model._meta.verbose_name_plural}.csv' + ) def get(self, request): @@ -137,7 +135,13 @@ class ObjectListView(ObjectPermissionRequiredMixin, View): if self.filterset: self.queryset = self.filterset(request.GET, self.queryset).qs - # Check for export rendering (except for table-based) + # Compile a dictionary indicating which permissions are available to the current user for this model + permissions = {} + for action in ('add', 'change', 'delete', 'view'): + perm_name = get_permission_for_model(model, action) + permissions[action] = request.user.has_perm(perm_name) + + # Export template/YAML rendering if 'export' in request.GET and request.GET['export'] != 'table': # An export template has been specified @@ -155,44 +159,21 @@ class ObjectListView(ObjectPermissionRequiredMixin, View): # Check for YAML export support elif hasattr(model, 'to_yaml'): - response = HttpResponse(self.queryset_to_yaml(), content_type='text/yaml') + response = HttpResponse(self.export_yaml(), content_type='text/yaml') filename = 'netbox_{}.yaml'.format(self.queryset.model._meta.verbose_name_plural) response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename) return response - # Fall back to built-in CSV formatting if export requested but no template specified - elif 'export' in request.GET and hasattr(model, 'to_csv'): - response = HttpResponse(self.queryset_to_csv(), content_type='text/csv') - filename = 'netbox_{}.csv'.format(self.queryset.model._meta.verbose_name_plural) - response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename) - return response - - # Compile a dictionary indicating which permissions are available to the current user for this model - permissions = {} - for action in ('add', 'change', 'delete', 'view'): - perm_name = get_permission_for_model(model, action) - permissions[action] = request.user.has_perm(perm_name) - # Construct the objects table table = self.table(self.queryset, user=request.user) if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']): table.columns.show('pk') - # Handle table-based export + # Handle table-based exports (current view or static CSV-based) if request.GET.get('export') == 'table': - exclude_columns = {'pk'} - exclude_columns.update({ - name for name, _ in table.available_columns - }) - exporter = TableExport( - export_format=TableExport.CSV, - table=table, - exclude_columns=exclude_columns, - dataset_kwargs={}, - ) - return exporter.response( - filename=f'netbox_{self.queryset.model._meta.verbose_name_plural}.csv' - ) + return self.export_table(table) + elif 'export' in request.GET: + return self.export_table(table, model.csv_headers) # Paginate the objects table paginate_table(table, request) diff --git a/netbox/utilities/testing/views.py b/netbox/utilities/testing/views.py index 4da724c7d..cf408b9ec 100644 --- a/netbox/utilities/testing/views.py +++ b/netbox/utilities/testing/views.py @@ -452,12 +452,10 @@ class ViewTestCases: url = self._get_url('list') # Test default CSV export - response = self.client.get(f'{url}?export') - self.assertHttpStatus(response, 200) if hasattr(self.model, 'csv_headers'): - self.assertEqual(response.get('Content-Type'), 'text/csv') - content = response.content.decode('utf-8') - self.assertEqual(content.splitlines()[0], ','.join(self.model.csv_headers)) + response = self.client.get(f'{url}?export') + self.assertHttpStatus(response, 200) + self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8') # Test table-based export response = self.client.get(f'{url}?export=table') From 3ec6194a47e9a19e945f0eae46c3c50e9c73b3b5 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 9 Jun 2021 15:08:49 -0400 Subject: [PATCH 2/5] Remove obsolete to_csv() methods --- docs/development/extending-models.md | 2 +- netbox/circuits/models.py | 40 --------- netbox/dcim/models/cables.py | 14 --- netbox/dcim/models/device_components.py | 108 ------------------------ netbox/dcim/models/devices.py | 52 ------------ netbox/dcim/models/power.py | 25 ------ netbox/dcim/models/racks.py | 40 --------- netbox/dcim/models/sites.py | 47 ----------- netbox/extras/models/change_logging.py | 16 ---- netbox/extras/models/tags.py | 8 -- netbox/ipam/models/ip.py | 66 --------------- netbox/ipam/models/services.py | 10 --- netbox/ipam/models/vlans.py | 21 ----- netbox/ipam/models/vrfs.py | 16 ---- netbox/tenancy/models.py | 17 ---- netbox/virtualization/models.py | 50 ----------- 16 files changed, 1 insertion(+), 531 deletions(-) diff --git a/docs/development/extending-models.md b/docs/development/extending-models.md index 9a27b68b6..dd2884939 100644 --- a/docs/development/extending-models.md +++ b/docs/development/extending-models.md @@ -34,7 +34,7 @@ class Foo(models.Model): ## 3. Add CSV helpers -Add the name of the new field to `csv_headers` and included a CSV-friendly representation of its data in the model's `to_csv()` method. These will be used when exporting objects in CSV format. +Add the name of the new field to `csv_headers`. This will be used when exporting objects in CSV format. ## 4. Update relevant querysets diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 699ded7b0..a40455a47 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -79,18 +79,6 @@ class Provider(PrimaryModel): def get_absolute_url(self): return reverse('circuits:provider', args=[self.pk]) - def to_csv(self): - return ( - self.name, - self.slug, - self.asn, - self.account, - self.portal_url, - self.noc_contact, - self.admin_contact, - self.comments, - ) - # # Provider networks @@ -140,14 +128,6 @@ class ProviderNetwork(PrimaryModel): def get_absolute_url(self): return reverse('circuits:providernetwork', args=[self.pk]) - def to_csv(self): - return ( - self.provider.name, - self.name, - self.description, - self.comments, - ) - @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class CircuitType(OrganizationalModel): @@ -181,13 +161,6 @@ class CircuitType(OrganizationalModel): def get_absolute_url(self): return reverse('circuits:circuittype', args=[self.pk]) - def to_csv(self): - return ( - self.name, - self.slug, - self.description, - ) - @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class Circuit(PrimaryModel): @@ -276,19 +249,6 @@ class Circuit(PrimaryModel): def get_absolute_url(self): return reverse('circuits:circuit', args=[self.pk]) - def to_csv(self): - return ( - self.cid, - self.provider.name, - self.type.name, - self.get_status_display(), - self.tenant.name if self.tenant else None, - self.install_date, - self.commit_rate, - self.description, - self.comments, - ) - def get_status_class(self): return CircuitStatusChoices.CSS_CLASSES.get(self.status) diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 8dd0f2f11..302623cb0 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -289,20 +289,6 @@ class Cable(PrimaryModel): # Update the private pk used in __str__ in case this is a new object (i.e. just got its pk) self._pk = self.pk - def to_csv(self): - return ( - '{}.{}'.format(self.termination_a_type.app_label, self.termination_a_type.model), - self.termination_a_id, - '{}.{}'.format(self.termination_b_type.app_label, self.termination_b_type.model), - self.termination_b_id, - self.get_type_display(), - self.get_status_display(), - self.label, - self.color, - self.length, - self.length_unit, - ) - def get_status_class(self): return CableStatusChoices.CSS_CLASSES.get(self.status) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index bd7f4ac55..c58eb5cc9 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -238,17 +238,6 @@ class ConsolePort(ComponentModel, CableTermination, PathEndpoint): def get_absolute_url(self): return reverse('dcim:consoleport', kwargs={'pk': self.pk}) - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.label, - self.type, - self.speed, - self.mark_connected, - self.description, - ) - # # Console server ports @@ -281,17 +270,6 @@ class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint): def get_absolute_url(self): return reverse('dcim:consoleserverport', kwargs={'pk': self.pk}) - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.label, - self.type, - self.speed, - self.mark_connected, - self.description, - ) - # # Power ports @@ -332,18 +310,6 @@ class PowerPort(ComponentModel, CableTermination, PathEndpoint): def get_absolute_url(self): return reverse('dcim:powerport', kwargs={'pk': self.pk}) - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.label, - self.get_type_display(), - self.mark_connected, - self.maximum_draw, - self.allocated_draw, - self.description, - ) - def clean(self): super().clean() @@ -442,18 +408,6 @@ class PowerOutlet(ComponentModel, CableTermination, PathEndpoint): def get_absolute_url(self): return reverse('dcim:poweroutlet', kwargs={'pk': self.pk}) - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.label, - self.get_type_display(), - self.mark_connected, - self.power_port.name if self.power_port else None, - self.get_feed_leg_display(), - self.description, - ) - def clean(self): super().clean() @@ -582,23 +536,6 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint): def get_absolute_url(self): return reverse('dcim:interface', kwargs={'pk': self.pk}) - def to_csv(self): - return ( - self.device.identifier if self.device else None, - self.name, - self.label, - self.parent.name if self.parent else None, - self.lag.name if self.lag else None, - self.get_type_display(), - self.enabled, - self.mark_connected, - self.mac_address, - self.mtu, - self.mgmt_only, - self.description, - self.get_mode_display(), - ) - def clean(self): super().clean() @@ -719,18 +656,6 @@ class FrontPort(ComponentModel, CableTermination): def get_absolute_url(self): return reverse('dcim:frontport', kwargs={'pk': self.pk}) - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.label, - self.get_type_display(), - self.mark_connected, - self.rear_port.name, - self.rear_port_position, - self.description, - ) - def clean(self): super().clean() @@ -785,17 +710,6 @@ class RearPort(ComponentModel, CableTermination): f"({frontport_count})" }) - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.label, - self.get_type_display(), - self.mark_connected, - self.positions, - self.description, - ) - # # Device bays @@ -823,15 +737,6 @@ class DeviceBay(ComponentModel): def get_absolute_url(self): return reverse('dcim:devicebay', kwargs={'pk': self.pk}) - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.label, - self.installed_device.identifier if self.installed_device else None, - self.description, - ) - def clean(self): super().clean() @@ -917,16 +822,3 @@ class InventoryItem(MPTTModel, ComponentModel): def get_absolute_url(self): return reverse('dcim:inventoryitem', kwargs={'pk': self.pk}) - - def to_csv(self): - return ( - self.device.name or '{{{}}}'.format(self.device.pk), - self.name, - self.label, - self.manufacturer.name if self.manufacturer else None, - self.part_id, - self.serial, - self.asset_tag, - self.discovered, - self.description, - ) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index e41bc8769..f91b08019 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -67,13 +67,6 @@ class Manufacturer(OrganizationalModel): def get_absolute_url(self): return reverse('dcim:manufacturer', args=[self.pk]) - def to_csv(self): - return ( - self.name, - self.slug, - self.description - ) - @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class DeviceType(PrimaryModel): @@ -390,15 +383,6 @@ class DeviceRole(OrganizationalModel): def get_absolute_url(self): return reverse('dcim:devicerole', args=[self.pk]) - def to_csv(self): - return ( - self.name, - self.slug, - self.color, - self.vm_role, - self.description, - ) - @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class Platform(OrganizationalModel): @@ -453,16 +437,6 @@ class Platform(OrganizationalModel): def get_absolute_url(self): return reverse('dcim:platform', args=[self.pk]) - def to_csv(self): - return ( - self.name, - self.slug, - self.manufacturer.name if self.manufacturer else None, - self.napalm_driver, - self.napalm_args, - self.description, - ) - @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class Device(PrimaryModel, ConfigContextModel): @@ -816,25 +790,6 @@ class Device(PrimaryModel, ConfigContextModel): device.rack = self.rack device.save() - def to_csv(self): - return ( - self.name or '', - self.device_role.name, - self.tenant.name if self.tenant else None, - self.device_type.manufacturer.name, - self.device_type.model, - self.platform.name if self.platform else None, - self.serial, - self.asset_tag, - self.get_status_display(), - self.site.name, - self.rack.location.name if self.rack and self.rack.location else None, - self.rack.name if self.rack else None, - self.position, - self.get_face_display(), - self.comments, - ) - @property def identifier(self): """ @@ -967,10 +922,3 @@ class VirtualChassis(PrimaryModel): ) return super().delete(*args, **kwargs) - - def to_csv(self): - return ( - self.name, - self.domain, - self.master.name if self.master else None, - ) diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index 03e77eea9..81f305512 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -54,13 +54,6 @@ class PowerPanel(PrimaryModel): def get_absolute_url(self): return reverse('dcim:powerpanel', args=[self.pk]) - def to_csv(self): - return ( - self.site.name, - self.location.name if self.location else None, - self.name, - ) - def clean(self): super().clean() @@ -152,24 +145,6 @@ class PowerFeed(PrimaryModel, PathEndpoint, CableTermination): def get_absolute_url(self): return reverse('dcim:powerfeed', args=[self.pk]) - def to_csv(self): - return ( - self.power_panel.site.name, - self.power_panel.name, - self.rack.location.name if self.rack and self.rack.location else None, - self.rack.name if self.rack else None, - self.name, - self.get_status_display(), - self.get_type_display(), - self.mark_connected, - self.get_supply_display(), - self.get_phase_display(), - self.voltage, - self.amperage, - self.max_utilization, - self.comments, - ) - def clean(self): super().clean() diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 5270004d1..ba9509a15 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -69,14 +69,6 @@ class RackRole(OrganizationalModel): def get_absolute_url(self): return reverse('dcim:rackrole', args=[self.pk]) - def to_csv(self): - return ( - self.name, - self.slug, - self.color, - self.description, - ) - @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class Rack(PrimaryModel): @@ -251,27 +243,6 @@ class Rack(PrimaryModel): 'location': f"Location must be from the same site, {self.site}." }) - def to_csv(self): - return ( - self.site.name, - self.location.name if self.location else None, - self.name, - self.facility_id, - self.tenant.name if self.tenant else None, - self.get_status_display(), - self.role.name if self.role else None, - self.get_type_display() if self.type else None, - self.serial, - self.asset_tag, - self.width, - self.u_height, - self.desc_units, - self.outer_width, - self.outer_depth, - self.outer_unit, - self.comments, - ) - @property def units(self): if self.desc_units: @@ -531,17 +502,6 @@ class RackReservation(PrimaryModel): ) }) - def to_csv(self): - return ( - self.rack.site.name, - self.rack.location if self.rack.location else None, - self.rack.name, - ','.join([str(u) for u in self.units]), - self.tenant.name if self.tenant else None, - self.user.username, - self.description - ) - @property def unit_list(self): return array_to_string(self.units) diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index 7ab37567a..b6ff7dee4 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -59,14 +59,6 @@ class Region(NestedGroupModel): def get_absolute_url(self): return reverse('dcim:region', args=[self.pk]) - def to_csv(self): - return ( - self.name, - self.slug, - self.parent.name if self.parent else None, - self.description, - ) - def get_site_count(self): return Site.objects.filter( Q(region=self) | @@ -111,14 +103,6 @@ class SiteGroup(NestedGroupModel): def get_absolute_url(self): return reverse('dcim:sitegroup', args=[self.pk]) - def to_csv(self): - return ( - self.name, - self.slug, - self.parent.name if self.parent else None, - self.description, - ) - def get_site_count(self): return Site.objects.filter( Q(group=self) | @@ -255,28 +239,6 @@ class Site(PrimaryModel): def get_absolute_url(self): return reverse('dcim:site', args=[self.pk]) - def to_csv(self): - return ( - self.name, - self.slug, - self.get_status_display(), - self.region.name if self.region else None, - self.group.name if self.group else None, - self.tenant.name if self.tenant else None, - self.facility, - self.asn, - self.time_zone, - self.description, - self.physical_address, - self.shipping_address, - self.latitude, - self.longitude, - self.contact_name, - self.contact_phone, - self.contact_email, - self.comments, - ) - def get_status_class(self): return SiteStatusChoices.CSS_CLASSES.get(self.status) @@ -331,15 +293,6 @@ class Location(NestedGroupModel): def get_absolute_url(self): return reverse('dcim:location', args=[self.pk]) - def to_csv(self): - return ( - self.site, - self.parent.name if self.parent else '', - self.name, - self.slug, - self.description, - ) - def clean(self): super().clean() diff --git a/netbox/extras/models/change_logging.py b/netbox/extras/models/change_logging.py index fac86b641..638e7c77f 100644 --- a/netbox/extras/models/change_logging.py +++ b/netbox/extras/models/change_logging.py @@ -109,21 +109,5 @@ class ObjectChange(BigIDModel): def get_absolute_url(self): return reverse('extras:objectchange', args=[self.pk]) - def to_csv(self): - return ( - self.time, - self.user, - self.user_name, - self.request_id, - self.get_action_display(), - self.changed_object_type, - self.changed_object_id, - self.related_object_type, - self.related_object_id, - self.object_repr, - self.prechange_data, - self.postchange_data, - ) - def get_action_class(self): return ObjectChangeActionChoices.CSS_CLASSES.get(self.action) diff --git a/netbox/extras/models/tags.py b/netbox/extras/models/tags.py index 6268751b2..9b4c8c92e 100644 --- a/netbox/extras/models/tags.py +++ b/netbox/extras/models/tags.py @@ -41,14 +41,6 @@ class Tag(ChangeLoggedModel, TagBase): slug += "_%d" % i return slug - def to_csv(self): - return ( - self.name, - self.slug, - self.color, - self.description - ) - class TaggedItem(BigIDModel, GenericTaggedItemBase): tag = models.ForeignKey( diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index edfad625a..5bb5f85ee 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -68,14 +68,6 @@ class RIR(OrganizationalModel): def get_absolute_url(self): return reverse('ipam:rir', args=[self.pk]) - def to_csv(self): - return ( - self.name, - self.slug, - self.is_private, - self.description, - ) - @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class Aggregate(PrimaryModel): @@ -160,15 +152,6 @@ class Aggregate(PrimaryModel): ) }) - def to_csv(self): - return ( - self.prefix, - self.rir.name, - self.tenant.name if self.tenant else None, - self.date_added, - self.description, - ) - @property def family(self): if self.prefix: @@ -219,14 +202,6 @@ class Role(OrganizationalModel): def get_absolute_url(self): return reverse('ipam:role', args=[self.pk]) - def to_csv(self): - return ( - self.name, - self.slug, - self.weight, - self.description, - ) - @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class Prefix(PrimaryModel): @@ -375,21 +350,6 @@ class Prefix(PrimaryModel): super().save(*args, **kwargs) - def to_csv(self): - return ( - self.prefix, - self.vrf.name 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 None, - self.is_pool, - self.mark_utilized, - self.description, - ) - @property def family(self): if self.prefix: @@ -697,32 +657,6 @@ class IPAddress(PrimaryModel): # Annotate the assigned object, if any return super().to_objectchange(action, related_object=self.assigned_object) - def to_csv(self): - - # Determine if this IP is primary for a Device - is_primary = False - if self.address.version == 4 and getattr(self, 'primary_ip4_for', False): - is_primary = True - elif self.address.version == 6 and getattr(self, 'primary_ip6_for', False): - is_primary = True - - obj_type = None - if self.assigned_object_type: - obj_type = f'{self.assigned_object_type.app_label}.{self.assigned_object_type.model}' - - return ( - self.address, - self.vrf.name if self.vrf else None, - self.tenant.name if self.tenant else None, - self.get_status_display(), - self.get_role_display(), - obj_type, - self.assigned_object_id, - is_primary, - self.dns_name, - self.description, - ) - @property def family(self): if self.address: diff --git a/netbox/ipam/models/services.py b/netbox/ipam/models/services.py index 3fc9c8ec6..ebb8f2889 100644 --- a/netbox/ipam/models/services.py +++ b/netbox/ipam/models/services.py @@ -91,16 +91,6 @@ class Service(PrimaryModel): if not self.device and not self.virtual_machine: raise ValidationError("A service must be associated with either a device or a virtual machine.") - def to_csv(self): - return ( - self.device.name if self.device else None, - self.virtual_machine.name if self.virtual_machine else None, - self.name, - self.get_protocol_display(), - self.ports, - self.description, - ) - @property def port_list(self): return array_to_string(self.ports) diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index 9c7a80ade..738c8aff1 100644 --- a/netbox/ipam/models/vlans.py +++ b/netbox/ipam/models/vlans.py @@ -80,15 +80,6 @@ class VLANGroup(OrganizationalModel): if self.scope_id and not self.scope_type: raise ValidationError("Cannot set scope_id without scope_type.") - def to_csv(self): - return ( - self.name, - self.slug, - f'{self.scope_type.app_label}.{self.scope_type.model}', - self.scope_id, - self.description, - ) - def get_next_available_vid(self): """ Return the first available VLAN ID (1-4094) in the group. @@ -187,18 +178,6 @@ class VLAN(PrimaryModel): f"site {self.site}." }) - def to_csv(self): - return ( - self.site.name if self.site else None, - self.group.name if self.group else None, - self.vid, - self.name, - self.tenant.name if self.tenant else None, - self.get_status_display(), - self.role.name if self.role else None, - self.description, - ) - def get_status_class(self): return VLANStatusChoices.CSS_CLASSES.get(self.status) diff --git a/netbox/ipam/models/vrfs.py b/netbox/ipam/models/vrfs.py index c2cd1dc4a..c88ce81c1 100644 --- a/netbox/ipam/models/vrfs.py +++ b/netbox/ipam/models/vrfs.py @@ -78,15 +78,6 @@ class VRF(PrimaryModel): def get_absolute_url(self): return reverse('ipam:vrf', args=[self.pk]) - def to_csv(self): - return ( - self.name, - self.rd, - self.tenant.name if self.tenant else None, - self.enforce_unique, - self.description, - ) - @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class RouteTarget(PrimaryModel): @@ -122,10 +113,3 @@ class RouteTarget(PrimaryModel): def get_absolute_url(self): return reverse('ipam:routetarget', args=[self.pk]) - - def to_csv(self): - return ( - self.name, - self.description, - self.tenant.name if self.tenant else None, - ) diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index 63f960b0e..85087d529 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -48,14 +48,6 @@ class TenantGroup(NestedGroupModel): def get_absolute_url(self): return reverse('tenancy:tenantgroup', args=[self.pk]) - def to_csv(self): - return ( - self.name, - self.slug, - self.parent.name if self.parent else '', - self.description, - ) - @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class Tenant(PrimaryModel): @@ -101,12 +93,3 @@ class Tenant(PrimaryModel): def get_absolute_url(self): return reverse('tenancy:tenant', args=[self.pk]) - - def to_csv(self): - return ( - self.name, - self.slug, - self.group.name if self.group else None, - self.description, - self.comments, - ) diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 41638c5c7..c68fe9e01 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -61,13 +61,6 @@ class ClusterType(OrganizationalModel): def get_absolute_url(self): return reverse('virtualization:clustertype', args=[self.pk]) - def to_csv(self): - return ( - self.name, - self.slug, - self.description, - ) - # # Cluster groups @@ -104,13 +97,6 @@ class ClusterGroup(OrganizationalModel): def get_absolute_url(self): return reverse('virtualization:clustergroup', args=[self.pk]) - def to_csv(self): - return ( - self.name, - self.slug, - self.description, - ) - # # Clusters @@ -184,16 +170,6 @@ class Cluster(PrimaryModel): ) }) - def to_csv(self): - return ( - self.name, - self.type.name, - self.group.name if self.group else None, - self.site.name if self.site else None, - self.tenant.name if self.tenant else None, - self.comments, - ) - # # Virtual machines @@ -337,20 +313,6 @@ class VirtualMachine(PrimaryModel, ConfigContextModel): field: f"The specified IP address ({ip}) is not assigned to this VM.", }) - def to_csv(self): - return ( - self.name, - self.get_status_display(), - self.role.name if self.role else None, - self.cluster.name, - self.tenant.name if self.tenant else None, - self.platform.name if self.platform else None, - self.vcpus, - self.memory, - self.disk, - self.comments, - ) - def get_status_class(self): return VirtualMachineStatusChoices.CSS_CLASSES.get(self.status) @@ -440,18 +402,6 @@ class VMInterface(PrimaryModel, BaseInterface): def get_absolute_url(self): return reverse('virtualization:vminterface', kwargs={'pk': self.pk}) - def to_csv(self): - return ( - self.virtual_machine.name, - self.name, - self.enabled, - self.parent.name if self.parent else None, - self.mac_address, - self.mtu, - self.description, - self.get_mode_display(), - ) - def clean(self): super().clean() From 578885225fa6747febc7187319f1a7492ba83751 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 9 Jun 2021 15:22:58 -0400 Subject: [PATCH 3/5] Replace 'default format' export with 'all data' --- netbox/netbox/views/generic.py | 16 ++++++---------- netbox/utilities/templates/buttons/export.html | 2 +- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/netbox/netbox/views/generic.py b/netbox/netbox/views/generic.py index c3bde9f9d..23b7f9c8d 100644 --- a/netbox/netbox/views/generic.py +++ b/netbox/netbox/views/generic.py @@ -16,7 +16,7 @@ from django.utils.safestring import mark_safe from django.views.generic import View from django_tables2.export import TableExport -from extras.models import CustomField, ExportTemplate +from extras.models import ExportTemplate from utilities.error_handlers import handle_protectederror from utilities.exceptions import AbortTransaction from utilities.forms import ( @@ -24,7 +24,7 @@ from utilities.forms import ( ) from utilities.permissions import get_permission_for_model from utilities.tables import paginate_table -from utilities.utils import csv_format, normalize_querydict, prepare_cloned_fields +from utilities.utils import normalize_querydict, prepare_cloned_fields from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin @@ -105,8 +105,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View): Export all table data in CSV format. :param table: The Table instance to export - :param columns: A list of specific columns to include. If not specified, the default view - will be exported. + :param columns: A list of specific columns to include. If not specified, all columns will be exported. """ exclude_columns = {'pk'} if columns: @@ -114,10 +113,6 @@ class ObjectListView(ObjectPermissionRequiredMixin, View): exclude_columns.update({ col for col in all_columns if col not in columns }) - else: - exclude_columns.update({ - name for name, _ in table.available_columns - }) exporter = TableExport( export_format=TableExport.CSV, table=table, @@ -171,9 +166,10 @@ class ObjectListView(ObjectPermissionRequiredMixin, View): # Handle table-based exports (current view or static CSV-based) if request.GET.get('export') == 'table': - return self.export_table(table) + columns = [name for name, _ in table.selected_columns] + return self.export_table(table, columns) elif 'export' in request.GET: - return self.export_table(table, model.csv_headers) + return self.export_table(table) # Paginate the objects table paginate_table(table, request) diff --git a/netbox/utilities/templates/buttons/export.html b/netbox/utilities/templates/buttons/export.html index 4664d5e55..c13f0cfdf 100644 --- a/netbox/utilities/templates/buttons/export.html +++ b/netbox/utilities/templates/buttons/export.html @@ -4,7 +4,7 @@