forked from ScoDoc/ScoDoc
Fixed graph generation with pydot and added unit test
This commit is contained in:
parent
30f88dfd4f
commit
d93b5688ae
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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), ... ]
|
||||||
|
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
|
ICONSIZES = {} # name : (width, height) cache image sizes
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user