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:
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
|
||||
43
export/templates/export/export.html
Normal file
43
export/templates/export/export.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item active" aria-current="page">Export Data</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<hr>
|
||||
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Records</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>People</td>
|
||||
<td></td>
|
||||
<td>
|
||||
<a class="btn btn-info"
|
||||
href="{% url 'export:person' %}">Export</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Relationships</td>
|
||||
<td></td>
|
||||
<td>
|
||||
<a class="btn btn-info"
|
||||
href="{% url 'export:relationship' %}">Export</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
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
|
||||
)
|
||||
29
export/views/base.py
Normal file
29
export/views/base.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import csv
|
||||
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
|
||||
|
||||
|
||||
class CsvExportView(LoginRequiredMixin, BaseListView):
|
||||
model = None
|
||||
serializer_class = None
|
||||
|
||||
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"'
|
||||
|
||||
# Force ordering by PK - though this should be default anyway
|
||||
serializer = self.serializer_class(self.get_queryset().order_by('pk'), many=True)
|
||||
|
||||
writer = csv.DictWriter(response, fieldnames=serializer.child.column_headers)
|
||||
writer.writeheader()
|
||||
writer.writerows(serializer.data)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class ExportListView(LoginRequiredMixin, TemplateView):
|
||||
template_name = 'export/export.html'
|
||||
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
|
||||
Reference in New Issue
Block a user