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 = [
'people',
'activities',
'export',
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + FIRST_PARTY_APPS

View File

@@ -79,7 +79,7 @@
{% if request.user.is_superuser %}
<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 class="nav-item">

View File

@@ -28,9 +28,8 @@ urlpatterns = [
views.IndexView.as_view(),
name='index'),
path('export',
views.ExportListView.as_view(),
name='export'),
path('',
include('export.urls')),
path('',
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.
"""
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView
class IndexView(TemplateView):
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>
<a class="btn btn-info"
href="{% url 'people:person.export' %}">Export</a>
href="{% url 'export:person' %}">Export</a>
</td>
</tr>
@@ -33,7 +33,7 @@
<td></td>
<td>
<a class="btn btn-info"
href="{% url 'people:relationship.export' %}">Export</a>
href="{% url 'export:relationship' %}">Export</a>
</td>
</tr>
</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.http import HttpResponse
from django.views.generic import TemplateView
from django.views.generic.list import BaseListView
from .. import models, serializers
class CsvExportView(LoginRequiredMixin, BaseListView):
model = None
@@ -26,11 +25,5 @@ class CsvExportView(LoginRequiredMixin, BaseListView):
return response
class PersonExportView(CsvExportView):
model = models.Person
serializer_class = serializers.PersonExportSerializer
class RelationshipExportView(CsvExportView):
model = models.Relationship
serializer_class = serializers.RelationshipSerializer
class ExportListView(LoginRequiredMixin, TemplateView):
template_name = 'export/export.html'

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.
"""
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.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 Meta:
model = models.Person
@@ -75,22 +15,8 @@ class PersonSerializer(serializers.ModelSerializer):
'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()
target = PersonSerializer()
@@ -101,26 +27,3 @@ class RelationshipSerializer(FlattenedModelSerializer):
'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

@@ -38,14 +38,6 @@ urlpatterns = [
views.relationship.RelationshipUpdateView.as_view(),
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',
views.network.NetworkView.as_view(),
name='network'),

View File

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