From 04ae8cb4f655204cfbb09f1c68cc0e3d6c71d108 Mon Sep 17 00:00:00 2001 From: James Graham Date: Tue, 31 Mar 2020 15:43:33 +0100 Subject: [PATCH] feat: Recursively flatten serializers for CSV export --- people/serializers.py | 3 +++ people/views/export.py | 46 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/people/serializers.py b/people/serializers.py index 85e1c27..bd501d5 100644 --- a/people/serializers.py +++ b/people/serializers.py @@ -31,6 +31,9 @@ class PersonExportSerializer(serializers.ModelSerializer): class RelationshipSerializer(serializers.ModelSerializer): + source = PersonSerializer() + target = PersonSerializer() + class Meta: model = models.Relationship fields = [ diff --git a/people/views/export.py b/people/views/export.py index 6e62b26..8e58ea9 100644 --- a/people/views/export.py +++ b/people/views/export.py @@ -5,6 +5,8 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpResponse from django.views.generic.list import BaseListView +from rest_framework.serializers import BaseSerializer + from .. import models, serializers @@ -12,14 +14,54 @@ class CsvExportView(LoginRequiredMixin, BaseListView): model = None serializer_class = None + @classmethod + def flatten_data(cls, data, + sub_type: typing.Type = dict, + sub_value_accessor: typing.Callable = lambda x: x.items()) -> typing.Dict: + """ + Flatten a dictionary so that subdictionaryies become a series of `key[.subkey[.subsubkey ...]]` entries + in the top level dictionary. + + Works for other data structures (e.g. DRF Serializers) by providing suitable values for the + `sub_type` and `sub_value_accessor` parameters. + + :param data: Dictionary or other data structure to flatten + :param sub_type: Type to recursively flatten + :param sub_value_accessor: Function to access keys and values contained within sub_type. + """ + data_out = {} + + for key, value in sub_value_accessor(data): + if isinstance(value, sub_type): + # Recursively flatten nested structures of type `sub_type` + sub_flattened = cls.flatten_data(value, + sub_type=sub_type, + sub_value_accessor=sub_value_accessor).items() + + # Enter recursively flattened values into result dictionary + for sub_key, sub_value in sub_flattened: + # Keys in result dictionary are of format `key[.subkey[.subsubkey ...]]` + data_out[f'{key}.{sub_key}'] = sub_value + + else: + data_out[key] = value + + return data_out + def render_to_response(self, context: typing.Dict) -> HttpResponse: response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = f'attachment; filename="{self.get_context_object_name(self.object_list)}.csv"' serializer = self.serializer_class(self.get_queryset(), many=True) - writer = csv.DictWriter(response, fieldnames=self.serializer_class.Meta.fields) + columns = self.flatten_data(serializer.child.fields, + sub_type=BaseSerializer, + sub_value_accessor=lambda x: x.fields.items()) + + writer = csv.DictWriter(response, fieldnames=columns) writer.writeheader() - writer.writerows(serializer.data) + + for row in serializer.data: + writer.writerow(self.flatten_data(row)) return response