Fixed graph generation with pydot and added unit test

This commit is contained in:
Emmanuel Viennet 2021-07-25 17:42:47 +03:00
parent 30f88dfd4f
commit d93b5688ae
5 changed files with 42 additions and 89 deletions

View File

@ -98,7 +98,7 @@ def defMenuStats(context, formsemestre_id):
"title": "Graphe des parcours", "title": "Graphe des parcours",
"endpoint": "notes.formsemestre_graph_parcours", "endpoint": "notes.formsemestre_graph_parcours",
"args": {"formsemestre_id": formsemestre_id}, "args": {"formsemestre_id": formsemestre_id},
"enabled": scu.WITH_PYDOT, "enabled": True,
}, },
{ {
"title": "Codes des parcours", "title": "Codes des parcours",

View File

@ -38,6 +38,7 @@ import datetime
from operator import itemgetter from operator import itemgetter
from flask import url_for, g from flask import url_for, g
import pydot
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
@ -1255,8 +1256,6 @@ def graph_parcours(
statut="", statut="",
): ):
"""""" """"""
if not scu.WITH_PYDOT:
raise ScoValueError("pydot module is not installed")
etuds, bacs, bacspecialites, annee_bacs, civilites, statuts = tsp_etud_list( etuds, bacs, bacspecialites, annee_bacs, civilites, statuts = tsp_etud_list(
context, context,
formsemestre_id, formsemestre_id,
@ -1342,10 +1341,10 @@ def graph_parcours(
edges[(s["formsemestre_id"], nid)].add(etudid) edges[(s["formsemestre_id"], nid)].add(etudid)
diploma_nodes.append(nid) 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: for fid in isolated_nodes:
if not fid in connected_nodes: if not fid in connected_nodes:
n = scu.pydot.Node(name=fid) n = pydot.Node(name=fid)
g.add_node(n) g.add_node(n)
g.set("rankdir", "LR") # left to right g.set("rankdir", "LR") # left to right
g.set_fontname("Helvetica") g.set_fontname("Helvetica")
@ -1353,7 +1352,7 @@ def graph_parcours(
g.set_bgcolor("#fffff0") # ou 'transparent' g.set_bgcolor("#fffff0") # ou 'transparent'
# titres des semestres: # titres des semestres:
for s in sems.values(): 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("s['formsemestre_id'] = %s" % s["formsemestre_id"])
log("n=%s" % n) log("n=%s" % n)
log("get=%s" % g.get_node(s["formsemestre_id"])) log("get=%s" % g.get_node(s["formsemestre_id"]))
@ -1378,31 +1377,31 @@ def graph_parcours(
n.set_shape("box") n.set_shape("box")
n.set_URL("formsemestre_status?formsemestre_id=" + s["formsemestre_id"]) n.set_URL("formsemestre_status?formsemestre_id=" + s["formsemestre_id"])
# semestre de depart en vert # semestre de depart en vert
n = scu.pydot_get_node(g, formsemestre_id) n = g.get_node(formsemestre_id)[0]
n.set_color("green") n.set_color("green")
# demissions en rouge, octagonal # demissions en rouge, octagonal
for nid in dem_nodes.values(): 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_color("red")
n.set_shape("octagon") n.set_shape("octagon")
n.set("label", "Dem.") n.set("label", "Dem.")
# NAR en rouge, Mcircle # NAR en rouge, Mcircle
for nid in nar_nodes.values(): 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_color("red")
n.set_shape("Mcircle") n.set_shape("Mcircle")
n.set("label", sco_codes_parcours.NAR) n.set("label", sco_codes_parcours.NAR)
# diplomes: # diplomes:
for nid in diploma_nodes: for nid in diploma_nodes:
n = scu.pydot_get_node(g, nid) n = g.get_node(nid)[0]
n.set_color("red") n.set_color("red")
n.set_shape("ellipse") n.set_shape("ellipse")
n.set("label", "Diplome") # bug si accent (pas compris pourquoi) n.set("label", "Diplome") # bug si accent (pas compris pourquoi)
# Arètes: # Arètes:
bubbles = {} # substitue titres pour bulle aides: src_id:dst_id : etud_descr bubbles = {} # substitue titres pour bulle aides: src_id:dst_id : etud_descr
for (src_id, dst_id) in edges.keys(): 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("arrowhead", "normal")
e.set("arrowsize", 1) e.set("arrowsize", 1)
e.set_label(len(edges[(src_id, dst_id)])) e.set_label(len(edges[(src_id, dst_id)]))
@ -1416,7 +1415,7 @@ def graph_parcours(
# Genere graphe # Genere graphe
_, path = tempfile.mkstemp(".gr") _, path = tempfile.mkstemp(".gr")
g.write(path=path, format=format) 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)) log("dot generated %d bytes in %s format" % (len(data), format))
if not data: if not data:
log("graph.to_string=%s" % g.to_string()) log("graph.to_string=%s" % g.to_string())
@ -1528,14 +1527,16 @@ def formsemestre_graph_parcours(
REQUEST.RESPONSE.setHeader("content-type", "image/png") REQUEST.RESPONSE.setHeader("content-type", "image/png")
return doc return doc
elif format == "html": 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: if only_primo:
op = "only_primo=on&" url_kw["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)
)
( (
doc, doc,
etuds, etuds,
@ -1583,12 +1584,11 @@ def formsemestre_graph_parcours(
), ),
"""<p>Origine et devenir des étudiants inscrits dans %(titreannee)s""" """<p>Origine et devenir des étudiants inscrits dans %(titreannee)s"""
% sem, % 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 """(<a href="%s">version pdf</a>"""
# mais c'est OK en Debian 5 % url_for("notes.formsemestre_graph_parcours", format="pdf", **url_kw),
"""(<a href="%spdf">version pdf</a>""" % url, """, <a href="%s">image PNG</a>)"""
""", <a href="%spng">image PNG</a>)""" % url, % url_for("notes.formsemestre_graph_parcours", format="png", **url_kw),
"""</p>""", """</p>""",
"""<p class="help">Cette page ne s'affiche correctement que sur les navigateurs récents.</p>""",
"""<p class="help">Le graphe permet de suivre les étudiants inscrits dans le semestre """<p class="help">Le graphe permet de suivre les étudiants inscrits dans le semestre
sélectionné (dessiné en vert). Chaque rectangle représente un semestre (cliquez dedans 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 pour afficher son tableau de bord). Les flèches indiquent le nombre d'étudiants passant

View File

@ -36,6 +36,7 @@ import json
from hashlib import md5 from hashlib import md5
import numbers import numbers
import os import os
import pydot
import re import re
import six import six
import six.moves._thread import six.moves._thread
@ -673,39 +674,27 @@ def sem_decale_str(sem):
return "" 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): def is_valid_mail(email):
"""True if well-formed email address""" """True if well-formed email address"""
return re.match(r"^.+@.+\..{2,3}$", email) 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), ... ]
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 ICONSIZES = {} # name : (width, height) cache image sizes

View File

@ -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

View File

@ -39,6 +39,7 @@ pluggy==0.13.1
psycopg2==2.9.1 psycopg2==2.9.1
py==1.10.0 py==1.10.0
pycparser==2.20 pycparser==2.20
pydot==1.4.2
pylibmc==1.6.1 pylibmc==1.6.1
pyOpenSSL==20.0.1 pyOpenSSL==20.0.1
pyparsing==2.4.7 pyparsing==2.4.7