diff options
author | Étienne Loks <etienne.loks@iggdrasil.net> | 2018-04-30 15:36:20 +0200 |
---|---|---|
committer | Étienne Loks <etienne.loks@iggdrasil.net> | 2018-06-12 08:43:58 +0200 |
commit | 16fa62f8fd560d09eb339df372416653d6eb916d (patch) | |
tree | 3a9af0df0ec10457521b51c05b0ecb96783937a6 | |
parent | 962b8fd05606c8187b33e9c2bf1e88985f548802 (diff) | |
download | Ishtar-16fa62f8fd560d09eb339df372416653d6eb916d.tar.bz2 Ishtar-16fa62f8fd560d09eb339df372416653d6eb916d.zip |
Generate an SVG file displaying relations using DOT file format and "dot" program
-rw-r--r-- | example_project/settings.py | 3 | ||||
-rw-r--r-- | ishtar_common/utils.py | 134 |
2 files changed, 137 insertions, 0 deletions
diff --git a/example_project/settings.py b/example_project/settings.py index 46fee04d8..d5efd2891 100644 --- a/example_project/settings.py +++ b/example_project/settings.py @@ -253,6 +253,9 @@ ISHTAR_DPTS = [] MAX_ATTEMPTS = 1 # django background tasks +# if you want to generate relation graph provide the path to the "dot" program +DOT_BINARY = "" + TEST_RUNNER = 'ishtar_common.tests.ManagedModelTestRunner' try: diff --git a/ishtar_common/utils.py b/ishtar_common/utils.py index 01b35dcef..1613b389d 100644 --- a/ishtar_common/utils.py +++ b/ishtar_common/utils.py @@ -21,7 +21,11 @@ import datetime from functools import wraps from itertools import chain import hashlib +import os import random +import shutil +import subprocess +import tempfile from django import forms from django.conf import settings @@ -29,6 +33,7 @@ from django.contrib.gis.geos import GEOSGeometry from django.contrib.sessions.backends.db import SessionStore from django.core.cache import cache from django.core.exceptions import FieldDoesNotExist +from django.core.files import File from django.core.urlresolvers import reverse from django.utils.datastructures import MultiValueDict as BaseMultiValueDict from django.utils.safestring import mark_safe @@ -480,3 +485,132 @@ def create_default_areas(models=None): idx += 1 print("* {} town associated to department area".format(idx)) + + +def get_relations_for_graph(rel_model, obj_pk, above_relations=None, + equal_relations=None, treated=None): + """ + Get all above and equal relations of an object (get all child and parent + relations) + :param rel_model: the relation model concerned + :param obj_pk: id of an object with relations + :param above_relations: list of current above_relations + :param equal_relations: list of current equal_relations + :param treated: treated relation list to prevent circular call + :return: above and equal relations list (each containing lists of two + members) + """ + if not above_relations: + above_relations = [] + if not equal_relations: + equal_relations = [] + if not treated: + treated = [] + if obj_pk in treated: + return above_relations, equal_relations + + treated.append(obj_pk) + q = rel_model.objects.filter( + left_record_id=obj_pk, + relation_type__logical_relation__isnull=False + ).values('right_record_id', 'relation_type__logical_relation') + if not q.count(): + return [], [] + for relation in q.all(): + logical_relation = relation['relation_type__logical_relation'] + right_record = relation['right_record_id'] + if not logical_relation: + continue + elif logical_relation == 'above'and \ + (obj_pk, right_record) not in above_relations: + above_relations.append((obj_pk, right_record)) + elif logical_relation == 'bellow' and \ + (right_record, obj_pk) not in above_relations: + above_relations.append((right_record, obj_pk)) + elif logical_relation == 'equal' and \ + (right_record, obj_pk) not in equal_relations and \ + (obj_pk, right_record) not in equal_relations: + equal_relations.append((obj_pk, right_record)) + else: + continue + ar, er = get_relations_for_graph( + rel_model, right_record, above_relations, equal_relations, treated) + for r in ar: + if r not in above_relations: + above_relations.append(r) + for r in er: + if r not in equal_relations: + equal_relations.append(r) + return above_relations, equal_relations + + +def generate_relation_graph(obj, debug=False): + if not settings.DOT_BINARY: + return + + model = obj.__class__ + rel_model = model._meta.get_field('right_relations').related_model + + # get relations + above_relations, equal_relations = get_relations_for_graph(rel_model, + obj.pk) + if not above_relations and not equal_relations: + obj.relation_image = None + obj.save() + return + + # generate dotfile + dot_str = "digraph relations {\nnode [shape=box];\n" + rel_str = "" + described = [] + for list, directed in ((above_relations, True), + (equal_relations, False)): + if directed: + rel_str += "subgraph Dir {\n" + else: + rel_str += "subgraph NoDir {\nedge [dir=none,style=dashed];\n" + for left_pk, right_pk in list: + if left_pk not in described: + described.append(left_pk) + left = model.objects.get(pk=left_pk) + style = 'label="{}"'.format(left.relation_label) + if left.pk == obj.pk: + style += ',style=filled,fillcolor="#C6C0C0"' + dot_str += u'item{}[{}];\n'.format(left.pk, style) + if right_pk not in described: + described.append(right_pk) + right = model.objects.get(pk=right_pk) + style = 'label="{}"'.format(right.relation_label) + if right.pk == obj.pk: + style += ',style=filled,fillcolor="#C6C0C0"' + dot_str += u'item{}[{}];\n'.format(right.pk, style) + if not directed: # on sthe same level + rel_str += u"{{rank = same; item{}; item{};}}\n".format( + left_pk, right_pk) + rel_str += u'item{} -> item{};\n'.format(left_pk, right_pk) + rel_str += "}\n" + dot_str += rel_str + "\n}" + + tempdir = tempfile.mkdtemp("-ishtardot") + dot_name = tempdir + os.path.sep + "relations.dot" + with open(dot_name, 'w') as dot_file: + dot_file.write(dot_str) + + # execute dot program + args = (settings.DOT_BINARY, "-Tsvg", dot_name) + + svg_tmp_name = tempdir + os.path.sep + "relations.svg" + + with open(svg_tmp_name, "w") as svg_file: + popen = subprocess.Popen(args, stdout=svg_file) + popen.wait() + + with open(svg_tmp_name, "r") as svg_file: + django_file = File(svg_file) + obj.relation_image.save("relations.svg", django_file, save=True) + + if debug: + print(u"DOT file: {}. Tmp SVG file: {}.".format(dot_name, svg_tmp_name)) + return + shutil.rmtree(tempdir) + |