From 76ae447cc60d329102a98581d60cf6a7f85bca23 Mon Sep 17 00:00:00 2001 From: James Graham Date: Wed, 1 Apr 2020 09:57:28 +0100 Subject: [PATCH] refactor: Move flattening to serializer base class --- people/serializers.py | 72 +++++++++++++++++++++++++++++++++++++++++- people/views/export.py | 47 ++------------------------- 2 files changed, 74 insertions(+), 45 deletions(-) diff --git a/people/serializers.py b/people/serializers.py index bd501d5..3763756 100644 --- a/people/serializers.py +++ b/people/serializers.py @@ -2,11 +2,69 @@ Serialize models to and deserialize from JSON. """ +from collections import OrderedDict +import typing + from rest_framework import serializers from . import models +class FlattenedModelSerializer(serializers.ModelSerializer): + @classmethod + def flatten_data(cls, data, + sub_type: typing.Type = dict, + sub_value_accessor: typing.Callable = lambda x: x.items()) -> typing.OrderedDict: + """ + Flatten a dictionary so that subdictionaries 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 = OrderedDict() + + 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 + + @property + def column_headers(self) -> typing.Collection: + """ + Get all column headers that will be output by this serializer. + """ + return self.flatten_data(self.fields, + sub_type=serializers.BaseSerializer, + sub_value_accessor=lambda x: x.fields.items()) + + def to_representation(self, instance) -> typing.OrderedDict: + """ + + """ + rep = super().to_representation(instance) + + rep = self.flatten_data(rep) + + return rep + + class PersonSerializer(serializers.ModelSerializer): class Meta: model = models.Person @@ -30,7 +88,7 @@ class PersonExportSerializer(serializers.ModelSerializer): ] -class RelationshipSerializer(serializers.ModelSerializer): +class RelationshipSerializer(FlattenedModelSerializer): source = PersonSerializer() target = PersonSerializer() @@ -41,3 +99,15 @@ class RelationshipSerializer(serializers.ModelSerializer): 'source', 'target', ] + + def to_representation(self, instance): + rep = super().to_representation(instance) + + # try: + # for answer in instance.current_answers.question_answers.all(): + # rep[answer.question.text] = answer.text + # + # except AttributeError: + # pass + + return rep diff --git a/people/views/export.py b/people/views/export.py index 8e58ea9..c4bdc88 100644 --- a/people/views/export.py +++ b/people/views/export.py @@ -5,8 +5,6 @@ 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 @@ -14,55 +12,16 @@ 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) - 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 = csv.DictWriter(response, fieldnames=serializer.child.column_headers) writer.writeheader() + writer.writerows(serializer.data) - for row in serializer.data: - writer.writerow(self.flatten_data(row)) - return response