feat: add person and org filters to network

Resolves #54
This commit is contained in:
James Graham
2021-04-22 21:16:39 +01:00
parent 5a2890ece1
commit 20812dfc40
4 changed files with 162 additions and 63 deletions

View File

@@ -53,8 +53,9 @@ class DynamicAnswerSetBase(forms.Form):
continue continue
# Placeholder question for sorting hardcoded questions # Placeholder question for sorting hardcoded questions
if question.is_hardcoded and (question.hardcoded_field if (question.is_hardcoded
in self.Meta.fields): and (as_filters or
(question.hardcoded_field in self.Meta.fields))):
field_order.append(question.hardcoded_field) field_order.append(question.hardcoded_field)
continue continue
@@ -83,7 +84,7 @@ class DynamicAnswerSetBase(forms.Form):
self.fields[field_name] = field self.fields[field_name] = field
field_order.append(field_name) field_order.append(field_name)
if question.allow_free_text: if question.allow_free_text and not as_filters:
free_field = forms.CharField(label=f'{question} free text', free_field = forms.CharField(label=f'{question} free text',
required=False) required=False)
self.fields[f'{field_name}_free'] = free_field self.fields[f'{field_name}_free'] = free_field
@@ -347,3 +348,22 @@ class NetworkPersonFilterForm(DynamicAnswerSetBase):
required=False, required=False,
widget=DatePickerInput(format='%Y-%m-%d'), widget=DatePickerInput(format='%Y-%m-%d'),
help_text='Show relationships as they were on this date') help_text='Show relationships as they were on this date')
class NetworkOrganisationFilterForm(DynamicAnswerSetBase):
"""Form to provide filtering on the network view."""
field_class = forms.ModelMultipleChoiceField
field_widget = Select2MultipleWidget
field_required = False
question_model = models.OrganisationQuestion
answer_model = models.OrganisationQuestionChoice
question_prefix = 'organisation_'
def __init__(self, *args, **kwargs):
super().__init__(*args, as_filters=True, **kwargs)
# Add date field to select relationships at a particular point in time
self.fields['date'] = forms.DateField(
required=False,
widget=DatePickerInput(format='%Y-%m-%d'),
help_text='Show relationships as they were on this date')

View File

@@ -88,14 +88,19 @@ function get_network() {
var relationship_set = JSON.parse(document.getElementById('relationship-set-data').textContent); var relationship_set = JSON.parse(document.getElementById('relationship-set-data').textContent);
for (var relationship of relationship_set) { for (var relationship of relationship_set) {
cy.add({ try {
group: 'edges', cy.add({
data: { group: 'edges',
id: 'relationship-' + relationship.pk.toString(), data: {
source: 'person-' + relationship.source.pk.toString(), id: 'relationship-' + relationship.pk.toString(),
target: 'person-' + relationship.target.pk.toString() source: 'person-' + relationship.source.pk.toString(),
} target: 'person-' + relationship.target.pk.toString()
}) }
})
} catch {
// Exception thrown if a node in the relationship does not exist
// This is probably because it's been filtered out
}
} }
// Optimise graph layout // Optimise graph layout

View File

@@ -14,22 +14,40 @@
<form class="form" <form class="form"
method="POST"> method="POST">
{% csrf_token %} {% csrf_token %}
{% load bootstrap4 %}
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-4">
<h3>Filter Relationships</h3> <h3>Filter Relationships</h3>
{% load bootstrap4 %} {% bootstrap_form relationship_form exclude='date' %}
{% bootstrap_form form exclude='date' %}
<hr>
{% bootstrap_field form.date %}
</div> </div>
<div class="col-md-6"> <div class="col-md-4">
<h3>Filter People</h3> <h3>Filter People</h3>
{% bootstrap_form person_form exclude='date' %}
</div> </div>
<div class="col-md-4">
<h3>Filter Organisations</h3>
{% bootstrap_form organisation_form exclude='date' %}
</div>
</div>
<div class="row">
<div class="col-md-4">
<hr>
{% bootstrap_field relationship_form.date %}
</div>
<div class="col-md-4">
<hr>
{% bootstrap_field person_form.date %}
</div>
<div class="col-md-4">
<hr>
{% bootstrap_field organisation_form.date %}
</div>
</div> </div>
{% buttons %} {% buttons %}

View File

@@ -8,32 +8,89 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q from django.db.models import Q
from django.forms import ValidationError from django.forms import ValidationError
from django.utils import timezone from django.utils import timezone
from django.views.generic import FormView from django.views.generic import TemplateView
from people import forms, models, serializers from people import forms, models, serializers
logger = logging.getLogger(__name__) # pylint: disable=invalid-name logger = logging.getLogger(__name__) # pylint: disable=invalid-name
class NetworkView(LoginRequiredMixin, FormView): def filter_relationships(form, at_date):
""" relationship_answerset_set = models.RelationshipAnswerSet.objects.filter(
View to display relationship network. Q(replaced_timestamp__gte=at_date)
""" | Q(replaced_timestamp__isnull=True),
timestamp__lte=at_date)
# Filter answers to relationship questions
for field, values in form.cleaned_data.items():
if field.startswith(f'{form.question_prefix}question_') and values:
relationship_answerset_set = relationship_answerset_set.filter(
question_answers__in=values)
return models.Relationship.objects.filter(
pk__in=relationship_answerset_set.values_list('relationship',
flat=True))
def filter_people(form, at_date):
answerset_set = models.PersonAnswerSet.objects.filter(
Q(replaced_timestamp__gte=at_date)
| Q(replaced_timestamp__isnull=True),
timestamp__lte=at_date)
# Filter answers to questions
for field, values in form.cleaned_data.items():
if field.startswith(f'{form.question_prefix}question_') and values:
answerset_set = answerset_set.filter(question_answers__in=values)
return models.Person.objects.filter(
pk__in=answerset_set.values_list('person', flat=True))
def filter_organisations(form, at_date):
answerset_set = models.OrganisationAnswerSet.objects.filter(
Q(replaced_timestamp__gte=at_date)
| Q(replaced_timestamp__isnull=True),
timestamp__lte=at_date)
# Filter answers to questions
for field, values in form.cleaned_data.items():
if field.startswith(f'{form.question_prefix}question_') and values:
answerset_set = answerset_set.filter(question_answers__in=values)
return models.Organisation.objects.filter(
pk__in=answerset_set.values_list('organisation', flat=True))
class NetworkView(LoginRequiredMixin, TemplateView):
"""View to display relationship network."""
template_name = 'people/network.html' template_name = 'people/network.html'
form_class = forms.NetworkRelationshipFilterForm
def post(self, request, *args, **kwargs):
forms = self.get_forms()
if all(map(lambda f: f.is_valid(), forms.values())):
return self.forms_valid(forms)
return self.forms_invalid(forms)
def get_forms(self):
form_kwargs = self.get_form_kwargs()
return {
'relationship': forms.NetworkRelationshipFilterForm(**form_kwargs),
'person': forms.NetworkPersonFilterForm(**form_kwargs),
'organisation': forms.NetworkOrganisationFilterForm(**form_kwargs),
}
def get_form_kwargs(self): def get_form_kwargs(self):
""" """Add GET params to form data."""
Add GET params to form data. kwargs = {}
"""
kwargs = super().get_form_kwargs()
if self.request.method == 'GET': if self.request.method == 'GET':
if 'data' in kwargs: kwargs['data'] = self.request.GET
kwargs['data'].update(self.request.GET)
else: if self.request.method in ('POST', 'PUT'):
kwargs['data'] = self.request.GET kwargs['data'] = self.request.POST
return kwargs return kwargs
@@ -42,46 +99,42 @@ class NetworkView(LoginRequiredMixin, FormView):
Add filtered QuerySets of :class:`Person` and :class:`Relationship` to the context. Add filtered QuerySets of :class:`Person` and :class:`Relationship` to the context.
""" """
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
form: forms.NetworkRelationshipFilterForm = context['form']
if not form.is_valid(): forms = self.get_forms()
context['relationship_form'] = forms['relationship']
context['person_form'] = forms['person']
context['organisation_form'] = forms['organisation']
if not all(map(lambda f: f.is_valid(), forms.values())):
return context return context
at_date = form.cleaned_data['date'] relationship_at_date = forms['relationship'].cleaned_data['date']
if not at_date: if not relationship_at_date:
at_date = timezone.now().date() relationship_at_date = timezone.now().date()
person_at_date = forms['person'].cleaned_data['date']
if not person_at_date:
person_at_date = timezone.now().date()
organisation_at_date = forms['organisation'].cleaned_data['date']
if not organisation_at_date:
organisation_at_date = timezone.now().date()
# Filter on timestamp__date doesn't seem to work on MySQL # Filter on timestamp__date doesn't seem to work on MySQL
# To compare datetimes we need at_date to be midnight at # To compare datetimes we need at_date to be midnight at
# the *end* of the day in question - so add one day here # the *end* of the day in question - so add one day here
at_date += timezone.timedelta(days=1) relationship_at_date += timezone.timedelta(days=1)
relationship_answerset_set = models.RelationshipAnswerSet.objects.filter(
Q(replaced_timestamp__gte=at_date)
| Q(replaced_timestamp__isnull=True),
timestamp__lte=at_date)
logger.info('Found %d relationship answer sets for %s',
relationship_answerset_set.count(), at_date)
# Filter answers to relationship questions
for field, values in form.cleaned_data.items():
if field.startswith(f'{form.question_prefix}question_') and values:
relationship_answerset_set = relationship_answerset_set.filter(
question_answers__in=values)
logger.info('Found %d relationship answer sets matching filters',
relationship_answerset_set.count())
context['person_set'] = serializers.PersonSerializer( context['person_set'] = serializers.PersonSerializer(
models.Person.objects.all(), many=True).data filter_people(forms['person'], person_at_date),
many=True).data
context['organisation_set'] = serializers.OrganisationSerializer( context['organisation_set'] = serializers.OrganisationSerializer(
models.Organisation.objects.all(), many=True).data filter_organisations(forms['organisation'], organisation_at_date),
many=True).data
context['relationship_set'] = serializers.RelationshipSerializer( context['relationship_set'] = serializers.RelationshipSerializer(
models.Relationship.objects.filter( filter_relationships(forms['relationship'], relationship_at_date),
pk__in=relationship_answerset_set.values_list('relationship',
flat=True)),
many=True).data many=True).data
logger.info('Found %d distinct relationships matching filters', logger.info('Found %d distinct relationships matching filters',
@@ -89,9 +142,12 @@ class NetworkView(LoginRequiredMixin, FormView):
return context return context
def form_valid(self, form): def forms_valid(self, forms):
try: try:
return self.render_to_response(self.get_context_data()) return self.render_to_response(self.get_context_data())
except ValidationError: except ValidationError:
return self.form_invalid(form) return self.forms_invalid(forms)
def forms_invalid(self, forms):
return self.render_to_response(self.get_context_data())