fix: distinguish kinds of relationship with orgs

Refactor node/edge style method to improve performance
This commit is contained in:
James Graham
2021-04-25 14:00:37 +01:00
parent bd13bb29e8
commit 7d14fed90f
3 changed files with 71 additions and 54 deletions

View File

@@ -41,6 +41,7 @@ class RelationshipSerializer(serializers.ModelSerializer):
class OrganisationRelationshipSerializer(serializers.ModelSerializer): class OrganisationRelationshipSerializer(serializers.ModelSerializer):
source = PersonSerializer() source = PersonSerializer()
target = OrganisationSerializer() target = OrganisationSerializer()
kind = serializers.ReadOnlyField(default='organisation-relationship')
class Meta: class Meta:
model = models.OrganisationRelationship model = models.OrganisationRelationship
@@ -48,4 +49,5 @@ class OrganisationRelationshipSerializer(serializers.ModelSerializer):
'pk', 'pk',
'source', 'source',
'target', 'target',
'kind',
] ]

View File

@@ -7,33 +7,24 @@ var network_style = [
selector: 'node[name]', selector: 'node[name]',
style: { style: {
label: 'data(name)', label: 'data(name)',
width: '50px',
height: '50px',
'text-halign': 'center', 'text-halign': 'center',
'text-valign': 'center', 'text-valign': 'center',
'font-size': 8, 'text-wrap': 'wrap',
'background-color': function (ele) { 'text-max-width': '100px',
switch (ele.data('kind')) { 'font-size': 12,
case 'person': 'background-color': 'data(nodeColor)',
return '#0099cc' 'shape': 'data(nodeShape)'
default:
return '#669933'
}
},
'shape': function (ele) {
switch (ele.data('kind')) {
case 'person':
return 'ellipse'
default:
return 'rectangle'
}
}
} }
}, },
{ {
selector: 'edge', selector: 'edge',
style: { style: {
'mid-target-arrow-shape': 'triangle', 'mid-target-arrow-shape': 'data(lineArrowShape)',
'curve-style': 'straight', 'curve-style': 'straight',
'width': 1, 'width': 1,
'line-color': 'data(lineColor)'
} }
} }
] ]
@@ -65,7 +56,9 @@ function get_network() {
data: { data: {
id: 'person-' + person.pk.toString(), id: 'person-' + person.pk.toString(),
name: person.name, name: person.name,
kind: 'person' kind: 'person',
nodeColor: '#0099cc',
nodeShape: 'elipse'
} }
}) })
} }
@@ -79,7 +72,8 @@ function get_network() {
data: { data: {
id: 'organisation-' + item.pk.toString(), id: 'organisation-' + item.pk.toString(),
name: item.name, name: item.name,
kind: 'organisation' nodeColor: '#669933',
nodeShape: 'rectangle'
} }
}) })
} }
@@ -94,10 +88,11 @@ function get_network() {
data: { data: {
id: 'relationship-' + relationship.pk.toString(), id: 'relationship-' + relationship.pk.toString(),
source: 'person-' + relationship.source.pk.toString(), source: 'person-' + relationship.source.pk.toString(),
target: 'person-' + relationship.target.pk.toString() target: 'person-' + relationship.target.pk.toString(),
lineArrowShape: 'triangle'
} }
}) })
} catch { } catch (exc) {
// Exception thrown if a node in the relationship does not exist // Exception thrown if a node in the relationship does not exist
// This is probably because it's been filtered out // This is probably because it's been filtered out
} }
@@ -107,17 +102,20 @@ function get_network() {
relationship_set = JSON.parse(document.getElementById('organisation-relationship-set-data').textContent); relationship_set = JSON.parse(document.getElementById('organisation-relationship-set-data').textContent);
for (var relationship of relationship_set) { for (var relationship of relationship_set) {
console.log(relationship)
try { try {
cy.add({ cy.add({
group: 'edges', group: 'edges',
data: { data: {
id: 'organisation-relationship-' + relationship.pk.toString(), id: 'organisation-relationship-' + relationship.pk.toString(),
source: 'person-' + relationship.source.pk.toString(), source: 'person-' + relationship.source.pk.toString(),
target: 'organisation-' + relationship.target.pk.toString() target: 'organisation-' + relationship.target.pk.toString(),
lineColor: {
'organisation-membership': '#669933'
}[relationship.kind] || 'black',
lineArrowShape: 'none'
} }
}) })
} catch { } catch (exc) {
// Exception thrown if a node in the relationship does not exist // Exception thrown if a node in the relationship does not exist
// This is probably because it's been filtered out // This is probably because it's been filtered out
} }
@@ -128,7 +126,8 @@ function get_network() {
name: 'cose', name: 'cose',
randomize: true, randomize: true,
animate: false, animate: false,
idealEdgeLength: function (edge) { return 64; } idealEdgeLength: function (edge) { return 64; },
nodeRepulsion: function (node) { return 8192; }
}); });
layout.run(); layout.run();

View File

@@ -16,37 +16,34 @@ from people import forms, models, serializers
logger = logging.getLogger(__name__) # pylint: disable=invalid-name logger = logging.getLogger(__name__) # pylint: disable=invalid-name
def filter_by_form_answers(model: typing.Type, answerset_model: typing.Type, def filter_by_form_answers(model: typing.Type, answerset_model: typing.Type, relationship_key: str):
relationship_key: str):
"""Build a filter to select based on form responses.""" """Build a filter to select based on form responses."""
def inner(form, at_date): def inner(form, at_date):
answerset_set = answerset_model.objects.filter( answerset_set = answerset_model.objects.filter(
Q(replaced_timestamp__gte=at_date) Q(replaced_timestamp__gte=at_date)
| Q(replaced_timestamp__isnull=True), | Q(replaced_timestamp__isnull=True),
timestamp__lte=at_date) timestamp__lte=at_date
)
# Filter answers to relationship questions # Filter answers to relationship questions
for field, values in form.cleaned_data.items(): for field, values in form.cleaned_data.items():
if field.startswith(f'{form.question_prefix}question_') and values: if field.startswith(f'{form.question_prefix}question_') and values:
answerset_set = answerset_set.filter( answerset_set = answerset_set.filter(question_answers__in=values)
question_answers__in=values)
return model.objects.filter( return model.objects.filter(pk__in=answerset_set.values_list(relationship_key, flat=True))
pk__in=answerset_set.values_list(relationship_key, flat=True))
return inner return inner
filter_relationships = filter_by_form_answers(models.Relationship, filter_relationships = filter_by_form_answers(
models.RelationshipAnswerSet, models.Relationship, models.RelationshipAnswerSet, 'relationship'
'relationship') )
filter_organisations = filter_by_form_answers(models.Organisation, filter_organisations = filter_by_form_answers(
models.OrganisationAnswerSet, models.Organisation, models.OrganisationAnswerSet, 'organisation'
'organisation') )
filter_people = filter_by_form_answers(models.Person, models.PersonAnswerSet, filter_people = filter_by_form_answers(models.Person, models.PersonAnswerSet, 'person')
'person')
class NetworkView(LoginRequiredMixin, TemplateView): class NetworkView(LoginRequiredMixin, TemplateView):
@@ -95,41 +92,60 @@ class NetworkView(LoginRequiredMixin, TemplateView):
if not all(map(lambda f: f.is_valid(), all_forms.values())): if not all(map(lambda f: f.is_valid(), all_forms.values())):
return context return context
# Filter on timestamp__date doesn't seem to work on MySQL
# To compare datetimes we need at_date to be midnight at
# the *end* of the day in question - so add one day to each
relationship_at_date = all_forms['relationship'].cleaned_data['date'] relationship_at_date = all_forms['relationship'].cleaned_data['date']
if not relationship_at_date: if not relationship_at_date:
relationship_at_date = timezone.now().date() relationship_at_date = timezone.now().date()
relationship_at_date += timezone.timedelta(days=1)
person_at_date = all_forms['person'].cleaned_data['date'] person_at_date = all_forms['person'].cleaned_data['date']
if not person_at_date: if not person_at_date:
person_at_date = timezone.now().date() person_at_date = timezone.now().date()
person_at_date += timezone.timedelta(days=1)
organisation_at_date = all_forms['organisation'].cleaned_data['date'] organisation_at_date = all_forms['organisation'].cleaned_data['date']
if not organisation_at_date: if not organisation_at_date:
organisation_at_date = timezone.now().date() organisation_at_date = timezone.now().date()
organisation_at_date += timezone.timedelta(days=1)
# Filter on timestamp__date doesn't seem to work on MySQL
# To compare datetimes we need at_date to be midnight at
# the *end* of the day in question - so add one day here
relationship_at_date += timezone.timedelta(days=1)
context['person_set'] = serializers.PersonSerializer( context['person_set'] = serializers.PersonSerializer(
filter_people(all_forms['person'], person_at_date), filter_people(all_forms['person'], person_at_date), many=True
many=True).data ).data
context['organisation_set'] = serializers.OrganisationSerializer( context['organisation_set'] = serializers.OrganisationSerializer(
filter_organisations(all_forms['organisation'], organisation_at_date), filter_organisations(all_forms['organisation'], organisation_at_date), many=True
many=True).data ).data
context['relationship_set'] = serializers.RelationshipSerializer( context['relationship_set'] = serializers.RelationshipSerializer(
filter_relationships(all_forms['relationship'], relationship_at_date), filter_relationships(all_forms['relationship'], relationship_at_date), many=True
many=True).data ).data
context['organisation_relationship_set'] = serializers.OrganisationRelationshipSerializer( context['organisation_relationship_set'] = serializers.OrganisationRelationshipSerializer(
models.OrganisationRelationship.objects.all(), many=True models.OrganisationRelationship.objects.all(), many=True
).data ).data
logger.info('Found %d distinct relationships matching filters', for person in models.Person.objects.all():
len(context['relationship_set'])) try:
context['organisation_relationship_set'].append(
{
'pk': f'membership-{person.pk}',
'source': serializers.PersonSerializer(person).data,
'target': serializers.OrganisationSerializer(
person.current_answers.organisation
).data,
'kind': 'organisation-membership'
}
)
except AttributeError:
pass
logger.info(
'Found %d distinct relationships matching filters', len(context['relationship_set'])
)
return context return context