From d93b5688aea5706c19e5f7481fe1a7be74ed934f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 25 Jul 2021 17:42:47 +0300 Subject: [PATCH] Fixed graph generation with pydot and added unit test --- app/scodoc/sco_formsemestre_status.py | 2 +- app/scodoc/sco_report.py | 46 +++++++++++++-------------- app/scodoc/sco_utils.py | 45 ++++++++++---------------- misc/testpydot.py | 37 --------------------- requirements-3.7.txt | 1 + 5 files changed, 42 insertions(+), 89 deletions(-) delete mode 100644 misc/testpydot.py diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 1b0c2f430..651bfe9dc 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -98,7 +98,7 @@ def defMenuStats(context, formsemestre_id): "title": "Graphe des parcours", "endpoint": "notes.formsemestre_graph_parcours", "args": {"formsemestre_id": formsemestre_id}, - "enabled": scu.WITH_PYDOT, + "enabled": True, }, { "title": "Codes des parcours", diff --git a/app/scodoc/sco_report.py b/app/scodoc/sco_report.py index f538f66e6..726d42a8b 100644 --- a/app/scodoc/sco_report.py +++ b/app/scodoc/sco_report.py @@ -38,6 +38,7 @@ import datetime from operator import itemgetter from flask import url_for, g +import pydot import app.scodoc.sco_utils as scu from app.scodoc import notesdb as ndb @@ -1255,8 +1256,6 @@ def graph_parcours( statut="", ): """""" - if not scu.WITH_PYDOT: - raise ScoValueError("pydot module is not installed") etuds, bacs, bacspecialites, annee_bacs, civilites, statuts = tsp_etud_list( context, formsemestre_id, @@ -1342,10 +1341,10 @@ def graph_parcours( edges[(s["formsemestre_id"], nid)].add(etudid) diploma_nodes.append(nid) # - g = scu.pydot.graph_from_edges(list(edges.keys())) + g = scu.graph_from_edges(list(edges.keys())) for fid in isolated_nodes: if not fid in connected_nodes: - n = scu.pydot.Node(name=fid) + n = pydot.Node(name=fid) g.add_node(n) g.set("rankdir", "LR") # left to right g.set_fontname("Helvetica") @@ -1353,7 +1352,7 @@ def graph_parcours( g.set_bgcolor("#fffff0") # ou 'transparent' # titres des semestres: for s in sems.values(): - n = scu.pydot_get_node(g, s["formsemestre_id"]) + n = g.get_node(s["formsemestre_id"])[0] log("s['formsemestre_id'] = %s" % s["formsemestre_id"]) log("n=%s" % n) log("get=%s" % g.get_node(s["formsemestre_id"])) @@ -1378,31 +1377,31 @@ def graph_parcours( n.set_shape("box") n.set_URL("formsemestre_status?formsemestre_id=" + s["formsemestre_id"]) # semestre de depart en vert - n = scu.pydot_get_node(g, formsemestre_id) + n = g.get_node(formsemestre_id)[0] n.set_color("green") # demissions en rouge, octagonal for nid in dem_nodes.values(): - n = scu.pydot_get_node(g, nid) + n = g.get_node(nid)[0] n.set_color("red") n.set_shape("octagon") n.set("label", "Dem.") # NAR en rouge, Mcircle for nid in nar_nodes.values(): - n = scu.pydot_get_node(g, nid) + n = g.get_node(nid)[0] n.set_color("red") n.set_shape("Mcircle") n.set("label", sco_codes_parcours.NAR) # diplomes: for nid in diploma_nodes: - n = scu.pydot_get_node(g, nid) + n = g.get_node(nid)[0] n.set_color("red") n.set_shape("ellipse") n.set("label", "Diplome") # bug si accent (pas compris pourquoi) # Arètes: bubbles = {} # substitue titres pour bulle aides: src_id:dst_id : etud_descr for (src_id, dst_id) in edges.keys(): - e = g.get_edge(src_id, dst_id) + e = g.get_edge(src_id, dst_id)[0] e.set("arrowhead", "normal") e.set("arrowsize", 1) e.set_label(len(edges[(src_id, dst_id)])) @@ -1416,7 +1415,7 @@ def graph_parcours( # Genere graphe _, path = tempfile.mkstemp(".gr") g.write(path=path, format=format) - data = open(path, "r").read() + data = open(path, "rb").read() log("dot generated %d bytes in %s format" % (len(data), format)) if not data: log("graph.to_string=%s" % g.to_string()) @@ -1528,14 +1527,16 @@ def formsemestre_graph_parcours( REQUEST.RESPONSE.setHeader("content-type", "image/png") return doc elif format == "html": + url_kw = { + "scodoc_dept": g.scodoc_dept, + "formsemestre_id": formsemestre_id, + "bac": bac, + "specialite": bacspecialite, + "civilite": civilite, + "statut": statut, + } if only_primo: - op = "only_primo=on&" - else: - op = "" - url = six.moves.urllib.parse.quote( - "formsemestre_graph_parcours?formsemestre_id=%s&%sbac=%s&bacspecialite=%s&civilite=%s&statut=%s&format=" - % (formsemestre_id, op, bac, bacspecialite, civilite, statut) - ) + url_kw["only_primo"] = "on" ( doc, etuds, @@ -1583,12 +1584,11 @@ def formsemestre_graph_parcours( ), """

Origine et devenir des étudiants inscrits dans %(titreannee)s""" % sem, - # En Debian 4, dot ne genere pas du pdf, et epstopdf ne marche pas sur le .ps ou ps2 générés par dot - # mais c'est OK en Debian 5 - """(version pdf""" % url, - """, image PNG)""" % url, + """(version pdf""" + % url_for("notes.formsemestre_graph_parcours", format="pdf", **url_kw), + """, image PNG)""" + % url_for("notes.formsemestre_graph_parcours", format="png", **url_kw), """

""", - """

Cette page ne s'affiche correctement que sur les navigateurs récents.

""", """

Le graphe permet de suivre les étudiants inscrits dans le semestre sélectionné (dessiné en vert). Chaque rectangle représente un semestre (cliquez dedans pour afficher son tableau de bord). Les flèches indiquent le nombre d'étudiants passant diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 038cc4821..f738252bd 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -36,6 +36,7 @@ import json from hashlib import md5 import numbers import os +import pydot import re import six import six.moves._thread @@ -673,39 +674,27 @@ def sem_decale_str(sem): return "" -# Graphes (optionnel pour ne pas accroitre les dependances de ScoDoc) -try: - import pydot - - WITH_PYDOT = True -except: - WITH_PYDOT = False - -if WITH_PYDOT: - # check API (incompatible change after pydot version 0.9.10: scodoc install may use old or new version) - junk_graph = pydot.Dot("junk") - junk_graph.add_node(pydot.Node("a")) - n = junk_graph.get_node("a") - if type(n) == type([]): # "modern" pydot - - def pydot_get_node(g, name): - r = g.get_node(name) - if not r: - return r - else: - return r[0] - - else: # very old pydot - - def pydot_get_node(g, name): - return g.get_node(name) - - def is_valid_mail(email): """True if well-formed email address""" return re.match(r"^.+@.+\..{2,3}$", email) +def graph_from_edges(edges, graph_name="mygraph"): + """Crée un graph pydot + à partir d'une liste d'arêtes [ (n1, n2), (n2, n3), ... ] + où n1, n2, ... sont des chaînes donnant l'id des nœuds. + + Fonction remplaçant celle de pydot qui est buggée. + """ + nodes = set([it for tup in edges for it in tup]) + graph = pydot.Dot(graph_name) + for n in nodes: + graph.add_node(pydot.Node(n)) + for e in edges: + graph.add_edge(pydot.Edge(src=e[0], dst=e[1])) + return graph + + ICONSIZES = {} # name : (width, height) cache image sizes diff --git a/misc/testpydot.py b/misc/testpydot.py deleted file mode 100644 index 1f2a3f6f7..000000000 --- a/misc/testpydot.py +++ /dev/null @@ -1,37 +0,0 @@ -# essai pydot (bug ?) -# EV, sept 2011 - -import pydot - -print 'pydot version:', pydot.__version__ - -g = pydot.Dot('graphname') -g.add_node(pydot.Node('a')) -g.add_node(pydot.Node('b')) - - -n = g.get_node('a') - -print n -print 'nodes names = %s' % [ x.get_name() for x in g.get_node_list() ] - -edges = [ ('a','b'), ('b','c'), ('c','d') ] -g = pydot.graph_from_edges(edges) -print 'nodes names = %s' % [ x.get_name() for x in g.get_node_list() ] - -if not len(g.get_node_list()): - print 'bug: empty node list !' # incompatibility versions python / pydot - -# Les fleches ? -for (src_id, dst_id) in edges: - e = g.get_edge(src_id, dst_id) - e.set('arrowhead', 'normal') - e.set( 'arrowsize', 2 ) - e.set_label( str( (src_id, dst_id) ) ) - e.set_fontname('Helvetica') - e.set_fontsize(8.0) - -g.write_jpeg('/tmp/graph_from_edges_dot.jpg', prog='dot') # ok sur ScoDoc / Debian 5, pas de fleches en Debian 6 -# cf https://www-lipn.univ-paris13.fr/projects/scodoc/ticket/190 - - diff --git a/requirements-3.7.txt b/requirements-3.7.txt index 9331820d9..ecdddab69 100755 --- a/requirements-3.7.txt +++ b/requirements-3.7.txt @@ -39,6 +39,7 @@ pluggy==0.13.1 psycopg2==2.9.1 py==1.10.0 pycparser==2.20 +pydot==1.4.2 pylibmc==1.6.1 pyOpenSSL==20.0.1 pyparsing==2.4.7