From 7d14fed90f5f14eacf8346e5a7e6392c53b1533c Mon Sep 17 00:00:00 2001 From: James Graham Date: Sun, 25 Apr 2021 14:00:37 +0100 Subject: [PATCH] fix: distinguish kinds of relationship with orgs Refactor node/edge style method to improve performance --- people/serializers.py | 2 ++ people/static/js/network.js | 51 +++++++++++++------------- people/views/network.py | 72 ++++++++++++++++++++++--------------- 3 files changed, 71 insertions(+), 54 deletions(-) diff --git a/people/serializers.py b/people/serializers.py index 39f3578..684380d 100644 --- a/people/serializers.py +++ b/people/serializers.py @@ -41,6 +41,7 @@ class RelationshipSerializer(serializers.ModelSerializer): class OrganisationRelationshipSerializer(serializers.ModelSerializer): source = PersonSerializer() target = OrganisationSerializer() + kind = serializers.ReadOnlyField(default='organisation-relationship') class Meta: model = models.OrganisationRelationship @@ -48,4 +49,5 @@ class OrganisationRelationshipSerializer(serializers.ModelSerializer): 'pk', 'source', 'target', + 'kind', ] diff --git a/people/static/js/network.js b/people/static/js/network.js index 09714ed..0cb4991 100644 --- a/people/static/js/network.js +++ b/people/static/js/network.js @@ -7,33 +7,24 @@ var network_style = [ selector: 'node[name]', style: { label: 'data(name)', + width: '50px', + height: '50px', 'text-halign': 'center', 'text-valign': 'center', - 'font-size': 8, - 'background-color': function (ele) { - switch (ele.data('kind')) { - case 'person': - return '#0099cc' - default: - return '#669933' - } - }, - 'shape': function (ele) { - switch (ele.data('kind')) { - case 'person': - return 'ellipse' - default: - return 'rectangle' - } - } + 'text-wrap': 'wrap', + 'text-max-width': '100px', + 'font-size': 12, + 'background-color': 'data(nodeColor)', + 'shape': 'data(nodeShape)' } }, { selector: 'edge', style: { - 'mid-target-arrow-shape': 'triangle', + 'mid-target-arrow-shape': 'data(lineArrowShape)', 'curve-style': 'straight', 'width': 1, + 'line-color': 'data(lineColor)' } } ] @@ -65,7 +56,9 @@ function get_network() { data: { id: 'person-' + person.pk.toString(), name: person.name, - kind: 'person' + kind: 'person', + nodeColor: '#0099cc', + nodeShape: 'elipse' } }) } @@ -79,7 +72,8 @@ function get_network() { data: { id: 'organisation-' + item.pk.toString(), name: item.name, - kind: 'organisation' + nodeColor: '#669933', + nodeShape: 'rectangle' } }) } @@ -94,10 +88,11 @@ function get_network() { data: { id: 'relationship-' + relationship.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 // 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); for (var relationship of relationship_set) { - console.log(relationship) try { cy.add({ group: 'edges', data: { id: 'organisation-relationship-' + relationship.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 // This is probably because it's been filtered out } @@ -128,7 +126,8 @@ function get_network() { name: 'cose', randomize: true, animate: false, - idealEdgeLength: function (edge) { return 64; } + idealEdgeLength: function (edge) { return 64; }, + nodeRepulsion: function (node) { return 8192; } }); layout.run(); diff --git a/people/views/network.py b/people/views/network.py index 74640e0..af35359 100644 --- a/people/views/network.py +++ b/people/views/network.py @@ -16,37 +16,34 @@ from people import forms, models, serializers logger = logging.getLogger(__name__) # pylint: disable=invalid-name -def filter_by_form_answers(model: typing.Type, answerset_model: typing.Type, - relationship_key: str): +def filter_by_form_answers(model: typing.Type, answerset_model: typing.Type, relationship_key: str): """Build a filter to select based on form responses.""" def inner(form, at_date): answerset_set = answerset_model.objects.filter( Q(replaced_timestamp__gte=at_date) | Q(replaced_timestamp__isnull=True), - timestamp__lte=at_date) + 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: - answerset_set = answerset_set.filter( - question_answers__in=values) + answerset_set = answerset_set.filter(question_answers__in=values) - return model.objects.filter( - pk__in=answerset_set.values_list(relationship_key, flat=True)) + return model.objects.filter(pk__in=answerset_set.values_list(relationship_key, flat=True)) return inner -filter_relationships = filter_by_form_answers(models.Relationship, - models.RelationshipAnswerSet, - 'relationship') +filter_relationships = filter_by_form_answers( + models.Relationship, models.RelationshipAnswerSet, 'relationship' +) -filter_organisations = filter_by_form_answers(models.Organisation, - models.OrganisationAnswerSet, - 'organisation') +filter_organisations = filter_by_form_answers( + models.Organisation, models.OrganisationAnswerSet, 'organisation' +) -filter_people = filter_by_form_answers(models.Person, models.PersonAnswerSet, - 'person') +filter_people = filter_by_form_answers(models.Person, models.PersonAnswerSet, 'person') class NetworkView(LoginRequiredMixin, TemplateView): @@ -95,41 +92,60 @@ class NetworkView(LoginRequiredMixin, TemplateView): if not all(map(lambda f: f.is_valid(), all_forms.values())): 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'] if not relationship_at_date: relationship_at_date = timezone.now().date() + relationship_at_date += timezone.timedelta(days=1) person_at_date = all_forms['person'].cleaned_data['date'] if not person_at_date: person_at_date = timezone.now().date() + person_at_date += timezone.timedelta(days=1) organisation_at_date = all_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 - # 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) + organisation_at_date += timezone.timedelta(days=1) context['person_set'] = serializers.PersonSerializer( - filter_people(all_forms['person'], person_at_date), - many=True).data + filter_people(all_forms['person'], person_at_date), many=True + ).data context['organisation_set'] = serializers.OrganisationSerializer( - filter_organisations(all_forms['organisation'], organisation_at_date), - many=True).data + filter_organisations(all_forms['organisation'], organisation_at_date), many=True + ).data context['relationship_set'] = serializers.RelationshipSerializer( - filter_relationships(all_forms['relationship'], relationship_at_date), - many=True).data + filter_relationships(all_forms['relationship'], relationship_at_date), many=True + ).data context['organisation_relationship_set'] = serializers.OrganisationRelationshipSerializer( models.OrganisationRelationship.objects.all(), many=True ).data - logger.info('Found %d distinct relationships matching filters', - len(context['relationship_set'])) + for person in models.Person.objects.all(): + 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