[schema diagram] ticket #191599, make schema image look better
authorSandrine Ribeau <sandrine.ribeau@logilab.fr>
Mon, 07 Jun 2010 18:50:24 +0200
changeset 5686 bbb89ba88b69
parent 5685 17883ced01f8
child 5687 3ea39709b50e
[schema diagram] ticket #191599, make schema image look better
web/views/schema.py
--- a/web/views/schema.py	Mon Jun 07 18:16:16 2010 +0200
+++ b/web/views/schema.py	Mon Jun 07 18:50:24 2010 +0200
@@ -15,22 +15,26 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Specific views for schema related entities
+"""Specific views for schema related entities"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from itertools import cycle
 
+import tempfile
+import os, os.path as osp
+
+from logilab.common.graph import GraphGenerator, DotBackend
 from logilab.common.ureports import Section, Table
 from logilab.mtconverter import xml_escape
 from yams import BASE_TYPES, schema2dot as s2d
 from yams.buildobjs import DEFAULT_ATTRPERMS
 
-from cubicweb.selectors import (implements, yes, match_user_groups,
-                                has_related_entities, authenticated_user)
+from cubicweb.selectors import (implements, match_user_groups, match_kwargs,
+                                has_related_entities, authenticated_user, yes)
 from cubicweb.schema import (META_RTYPES, SCHEMA_TYPES, SYSTEM_RTYPES,
                              WORKFLOW_TYPES, INTERNAL_TYPES)
+from cubicweb.utils import make_uid
 from cubicweb.view import EntityView, StartupView
 from cubicweb import tags, uilib
 from cubicweb.web import action, facet, uicfg, schemaviewer
@@ -158,10 +162,7 @@
         self.w(u'<div><a href="%s">%s</a></div>' %
                (self._cw.build_url('view', vid='owl'),
                 self._cw._(u'Download schema as OWL')))
-        self.w(u'<img src="%s" alt="%s"/>\n' % (
-            xml_escape(self._cw.build_url('view', vid='schemagraph', skipmeta=1)),
-            self._cw._("graphical representation of the instance'schema")))
-
+        self.wview('schemagraph')
 
 class SchemaETypeTab(StartupView):
     __regid__ = 'schema-entity-types'
@@ -311,9 +312,7 @@
             self.wview('csv', entity.related('specializes', 'object'))
             self.w(u'</div>')
         # entity schema image
-        self.w(u'<img src="%s" alt="%s"/>' % (
-            xml_escape(entity.absolute_url(vid='schemagraph')),
-            xml_escape(_('graphical schema for %s') % entity.name)))
+        self.wview('schemagraph', etype=entity.name)
         # entity schema attributes
         self.w(u'<h2>%s</h2>' % _('CWAttribute_plural'))
         rset = self._cw.execute(
@@ -491,9 +490,7 @@
         super(CWRTypeDescriptionTab, self).render_entity_attributes(entity)
         _ = self._cw._
         if not entity.final:
-            msg = _('graphical schema for %s') % entity.name
-            self.w(tags.img(src=entity.absolute_url(vid='schemagraph'),
-                            alt=msg))
+            self.wview('schemagraph', rtype=entity.name)
         rset = self._cw.execute('Any R,C,R,R, RT WHERE '
                                 'R relation_type RT, RT eid %(x)s, '
                                 'R cardinality C', {'x': entity.eid})
@@ -644,41 +641,109 @@
                            s2d.OneHopRSchemaVisitor):
     pass
 
+class CWSchemaDotPropsHandler(s2d.SchemaDotPropsHandler):
+    def __init__(self, visitor):
+        self.visitor = visitor
+        self.nextcolor = cycle( ('#ff7700', '#000000',
+                                 '#ebbc69', '#888888') ).next
 
-class SchemaImageView(TmpFileViewMixin, StartupView):
-    __regid__ = 'schemagraph'
-    content_type = 'image/png'
+    def node_properties(self, eschema):
+        """return DOT drawing options for an entity schema include href"""
+        label = ['{',eschema.type,'|']
+        label.append(r'\l'.join('%s (%s)' % (rel.type, eschema.rdef(rel.type).object)
+                                for rel in eschema.ordered_relations()
+                                    if rel.final and self.visitor.should_display_attr(eschema, rel)))
+        label.append(r'\l}') # trailing \l ensure alignement of the last one
+        return {'label' : ''.join(label), 'shape' : "record",
+                'fontname' : "Courier", 'style' : "filled",
+                'href': 'cwetype/%s' % eschema.type,
+                'fontsize': '10px'
+                }
 
-    def _generate(self, tmpfile):
-        """display global schema information"""
-        visitor = FullSchemaVisitor(self._cw, self._cw.vreg.schema,
-                                    skiptypes=skip_types(self._cw))
-        s2d.schema2dot(outputfile=tmpfile, visitor=visitor)
+    def edge_properties(self, rschema, subjnode, objnode):
+        """return default DOT drawing options for a relation schema"""
+        # symmetric rels are handled differently, let yams decide what's best
+        if rschema.symmetric:
+            kwargs = {'label': rschema.type,
+                      'color': '#887788', 'style': 'dashed',
+                      'dir': 'both', 'arrowhead': 'normal', 'arrowtail': 'normal',
+                      'fontsize': '10px', 'href': 'cwrtype/%s' % rschema.type}
+        else:
+            kwargs = {'label': rschema.type,
+                      'color' : 'black',  'style' : 'filled', 'fontsize': '10px',
+                      'href': 'cwrtype/%s' % rschema.type}
+            rdef = rschema.rdef(subjnode, objnode)
+            composite = rdef.composite
+            if rdef.composite == 'subject':
+                kwargs['arrowhead'] = 'none'
+                kwargs['arrowtail'] = 'diamond'
+            elif rdef.composite == 'object':
+                kwargs['arrowhead'] = 'diamond'
+                kwargs['arrowtail'] = 'none'
+            else:
+                kwargs['arrowhead'] = 'open'
+                kwargs['arrowtail'] = 'none'
+            # UML like cardinalities notation, omitting 1..1
+            if rdef.cardinality[1] != '1':
+                kwargs['taillabel'] = s2d.CARD_MAP[rdef.cardinality[1]]
+            if rdef.cardinality[0] != '1':
+                kwargs['headlabel'] = s2d.CARD_MAP[rdef.cardinality[0]]
+            kwargs['color'] = self.nextcolor()
+        kwargs['fontcolor'] = kwargs['color']
+        # dot label decoration is just awful (1 line underlining the label
+        # + 1 line going to the closest edge spline point)
+        kwargs['decorate'] = 'false'
+        #kwargs['labelfloat'] = 'true'
+        return kwargs
 
 
-class CWETypeSchemaImageView(TmpFileViewMixin, EntityView):
+class SchemaGraphView(StartupView):
     __regid__ = 'schemagraph'
-    __select__ = implements('CWEType')
-    content_type = 'image/png'
 
-    def _generate(self, tmpfile):
-        """display schema information for an entity"""
-        entity = self.cw_rset.get_entity(self.cw_row, self.cw_col)
-        eschema = self._cw.vreg.schema.eschema(entity.name)
-        visitor = OneHopESchemaVisitor(self._cw, eschema,
-                                       skiptypes=skip_types(self._cw))
-        s2d.schema2dot(outputfile=tmpfile, visitor=visitor)
-
-
-class CWRTypeSchemaImageView(CWETypeSchemaImageView):
-    __select__ = implements('CWRType')
-
-    def _generate(self, tmpfile):
-        """display schema information for an entity"""
-        entity = self.cw_rset.get_entity(self.cw_row, self.cw_col)
-        rschema = self._cw.vreg.schema.rschema(entity.name)
-        visitor = OneHopRSchemaVisitor(self._cw, rschema)
-        s2d.schema2dot(outputfile=tmpfile, visitor=visitor)
+    def call(self, etype=None, rtype=None, alt=''):
+        schema = self._cw.vreg.schema
+        if etype:
+            assert rtype is None
+            visitor = OneHopESchemaVisitor(self._cw, schema.eschema(etype),
+                                           skiptypes=skip_types(self._cw))
+            alt = self._cw._('graphical representation of the %(etype)s '
+                             'entity type from %(appid)s data model')
+        elif rtype:
+            visitor = OneHopRSchemaVisitor(self._cw, schema.rschema(rtype),
+                                           skiptypes=skip_types(self._cw))
+            alt = self._cw._('graphical representation of the %(rtype)s '
+                             'relation type from %(appid)s data model')
+        else:
+            visitor = FullSchemaVisitor(self._cw, schema,
+                                        skiptypes=skip_types(self._cw))
+            alt = self._cw._('graphical representation of %(appid)s data model')
+        alt %= {'rtype': rtype, 'etype': etype,
+                'appid': self._cw.vreg.config.appid}
+        prophdlr = CWSchemaDotPropsHandler(visitor)
+        generator = GraphGenerator(DotBackend('schema', 'BT',
+                                              ratio='compress',size=None,
+                                              renderer='dot',
+                                              additionnal_param={
+                                                  'overlap':'false',
+                                                  'splines':'true',
+                                                  'sep':'0.2',
+                                              }))
+        # map file
+        pmap, mapfile = tempfile.mkstemp(".map")
+        os.close(pmap)
+        # image file
+        fd, tmpfile = tempfile.mkstemp('.png')
+        os.close(fd)
+        generator.generate(visitor, prophdlr, tmpfile, mapfile)
+        filekeyid = make_uid()
+        self._cw.session.data[filekeyid] = tmpfile
+        self.w(u'<img src="%s" alt="%s" usemap="#schema" />' % (
+            xml_escape(self._cw.build_url(vid='tmppng', tmpfile=filekeyid)),
+            xml_escape(self._cw._(alt))))
+        stream = open(mapfile, 'r').read()
+        stream = stream.decode(self._cw.encoding)
+        self.w(stream)
+        os.unlink(mapfile)
 
 # breadcrumbs ##################################################################