mirror of
https://github.com/Southampton-RSG/breccia-mapper.git
synced 2026-03-03 03:17:07 +00:00
refactor: Move export into separate app
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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')),
|
||||||
|
|||||||
@@ -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
0
export/__init__.py
Normal file
5
export/apps.py
Normal file
5
export/apps.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ExportConfig(AppConfig):
|
||||||
|
name = 'export'
|
||||||
3
export/serializers/__init__.py
Normal file
3
export/serializers/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from . import (
|
||||||
|
people
|
||||||
|
)
|
||||||
65
export/serializers/base.py
Normal file
65
export/serializers/base.py
Normal 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
|
||||||
66
export/serializers/people.py
Normal file
66
export/serializers/people.py
Normal 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
|
||||||
@@ -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
20
export/urls.py
Normal 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
5
export/views/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from .base import ExportListView
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
people
|
||||||
|
)
|
||||||
@@ -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
14
export/views/people.py
Normal 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
|
||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user