refactor: Move export into separate app

This commit is contained in:
James Graham
2020-04-01 16:00:22 +01:00
parent 76270c4572
commit d02f865952
17 changed files with 188 additions and 128 deletions

View File

@@ -77,6 +77,7 @@ THIRD_PARTY_APPS = [
FIRST_PARTY_APPS = [ FIRST_PARTY_APPS = [
'people', 'people',
'activities', 'activities',
'export',
] ]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + FIRST_PARTY_APPS INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + FIRST_PARTY_APPS

View File

@@ -79,7 +79,7 @@
{% if request.user.is_superuser %} {% if request.user.is_superuser %}
<li class="nav-item"> <li class="nav-item">
<a href="{% url 'export' %}" class="nav-link">Export</a> <a href="{% url 'export:index' %}" class="nav-link">Export</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">

View File

@@ -28,9 +28,8 @@ urlpatterns = [
views.IndexView.as_view(), views.IndexView.as_view(),
name='index'), name='index'),
path('export', path('',
views.ExportListView.as_view(), include('export.urls')),
name='export'),
path('', path('',
include('people.urls')), include('people.urls')),

View File

@@ -4,13 +4,8 @@ Views belonging to the core of the project.
These views don't represent any of the models in the apps. These views don't represent any of the models in the apps.
""" """
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView from django.views.generic import TemplateView
class IndexView(TemplateView): class IndexView(TemplateView):
template_name = 'index.html' template_name = 'index.html'
class ExportListView(LoginRequiredMixin, TemplateView):
template_name = 'export.html'

0
export/__init__.py Normal file
View File

5
export/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class ExportConfig(AppConfig):
name = 'export'

View File

@@ -0,0 +1,3 @@
from . import (
people
)

View File

@@ -0,0 +1,65 @@
"""
Serialize models to and deserialize from JSON.
"""
from collections import OrderedDict
import typing
from rest_framework import serializers
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.List[str]:
"""
Get all column headers that will be output by this serializer.
"""
fields = self.flatten_data(self.fields,
sub_type=serializers.BaseSerializer,
sub_value_accessor=lambda x: x.fields.items())
return list(fields)
def to_representation(self, instance) -> typing.OrderedDict:
"""
"""
rep = super().to_representation(instance)
rep = self.flatten_data(rep)
return rep

View File

@@ -0,0 +1,66 @@
import typing
from rest_framework import serializers
from people import models
from . import base
class PersonSerializer(serializers.ModelSerializer):
class Meta:
model = models.Person
fields = [
'pk',
'name',
]
class PersonExportSerializer(serializers.ModelSerializer):
class Meta:
model = models.Person
fields = [
'pk',
'name',
'core_member',
'gender',
'age_group',
'nationality',
'country_of_residence',
]
class RelationshipSerializer(base.FlattenedModelSerializer):
source = PersonSerializer()
target = PersonSerializer()
class Meta:
model = models.Relationship
fields = [
'pk',
'source',
'target',
]
@property
def column_headers(self) -> typing.List[str]:
headers = super().column_headers
# Add relationship questions to columns
for question in models.RelationshipQuestion.objects.all():
headers.append(question.slug)
return headers
def to_representation(self, instance):
rep = super().to_representation(instance)
try:
# Add relationship question answers to data
for answer in instance.current_answers.question_answers.all():
rep[answer.question.slug] = answer.slug
except AttributeError:
pass
return rep

View File

@@ -24,7 +24,7 @@
<td></td> <td></td>
<td> <td>
<a class="btn btn-info" <a class="btn btn-info"
href="{% url 'people:person.export' %}">Export</a> href="{% url 'export:person' %}">Export</a>
</td> </td>
</tr> </tr>
@@ -33,7 +33,7 @@
<td></td> <td></td>
<td> <td>
<a class="btn btn-info" <a class="btn btn-info"
href="{% url 'people:relationship.export' %}">Export</a> href="{% url 'export:relationship' %}">Export</a>
</td> </td>
</tr> </tr>
</tbody> </tbody>

20
export/urls.py Normal file
View File

@@ -0,0 +1,20 @@
from django.urls import path
from . import views
app_name = 'export'
urlpatterns = [
path('export',
views.ExportListView.as_view(),
name='index'),
path('export/people',
views.people.PersonExportView.as_view(),
name='person'),
path('export/relationships',
views.people.RelationshipExportView.as_view(),
name='relationship'),
]

5
export/views/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from .base import ExportListView
from . import (
people
)

View File

@@ -3,10 +3,9 @@ import typing
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse from django.http import HttpResponse
from django.views.generic import TemplateView
from django.views.generic.list import BaseListView from django.views.generic.list import BaseListView
from .. import models, serializers
class CsvExportView(LoginRequiredMixin, BaseListView): class CsvExportView(LoginRequiredMixin, BaseListView):
model = None model = None
@@ -26,11 +25,5 @@ class CsvExportView(LoginRequiredMixin, BaseListView):
return response return response
class PersonExportView(CsvExportView): class ExportListView(LoginRequiredMixin, TemplateView):
model = models.Person template_name = 'export/export.html'
serializer_class = serializers.PersonExportSerializer
class RelationshipExportView(CsvExportView):
model = models.Relationship
serializer_class = serializers.RelationshipSerializer

14
export/views/people.py Normal file
View File

@@ -0,0 +1,14 @@
from . import base
from .. import serializers
from people import models
class PersonExportView(base.CsvExportView):
model = models.person.Person
serializer_class = serializers.people.PersonExportSerializer
class RelationshipExportView(base.CsvExportView):
model = models.relationship.Relationship
serializer_class = serializers.people.RelationshipSerializer

View File

@@ -2,71 +2,11 @@
Serialize models to and deserialize from JSON. Serialize models to and deserialize from JSON.
""" """
from collections import OrderedDict
import typing
from rest_framework import serializers from rest_framework import serializers
from . import models 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.List[str]:
"""
Get all column headers that will be output by this serializer.
"""
fields = self.flatten_data(self.fields,
sub_type=serializers.BaseSerializer,
sub_value_accessor=lambda x: x.fields.items())
return list(fields)
def to_representation(self, instance) -> typing.OrderedDict:
"""
"""
rep = super().to_representation(instance)
rep = self.flatten_data(rep)
return rep
class PersonSerializer(serializers.ModelSerializer): class PersonSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.Person model = models.Person
@@ -75,22 +15,8 @@ class PersonSerializer(serializers.ModelSerializer):
'name', 'name',
] ]
class PersonExportSerializer(serializers.ModelSerializer):
class Meta:
model = models.Person
fields = [
'pk',
'name',
'core_member',
'gender',
'age_group',
'nationality',
'country_of_residence',
]
class RelationshipSerializer(FlattenedModelSerializer): class RelationshipSerializer(serializers.ModelSerializer):
source = PersonSerializer() source = PersonSerializer()
target = PersonSerializer() target = PersonSerializer()
@@ -101,26 +27,3 @@ class RelationshipSerializer(FlattenedModelSerializer):
'source', 'source',
'target', 'target',
] ]
@property
def column_headers(self) -> typing.List[str]:
headers = super().column_headers
# Add relationship questions to columns
for question in models.RelationshipQuestion.objects.all():
headers.append(question.slug)
return headers
def to_representation(self, instance):
rep = super().to_representation(instance)
try:
# Add relationship question answers to data
for answer in instance.current_answers.question_answers.all():
rep[answer.question.slug] = answer.slug
except AttributeError:
pass
return rep

View File

@@ -38,14 +38,6 @@ urlpatterns = [
views.relationship.RelationshipUpdateView.as_view(), views.relationship.RelationshipUpdateView.as_view(),
name='relationship.update'), name='relationship.update'),
path('people/export',
views.export.PersonExportView.as_view(),
name='person.export'),
path('relationships/export',
views.export.RelationshipExportView.as_view(),
name='relationship.export'),
path('network', path('network',
views.network.NetworkView.as_view(), views.network.NetworkView.as_view(),
name='network'), name='network'),

View File

@@ -3,7 +3,6 @@ Views for displaying or manipulating models within the `people` app.
""" """
from . import ( from . import (
export,
network, network,
person, person,
relationship relationship