source: main/trunk/openPLM/plmapp/navigate.py @ 858

Revision 858, 23.0 KB checked in by pcosquer, 10 years ago (diff)

navigate: tests if results are groups

Line 
1############################################################################
2# openPLM - open source PLM
3# Copyright 2010 Philippe Joulaud, Pierre Cosquer
4#
5# This file is part of openPLM.
6#
7#    openPLM is free software: you can redistribute it and/or modify
8#    it under the terms of the GNU General Public License as published by
9#    the Free Software Foundation, either version 3 of the License, or
10#    (at your option) any later version.
11#
12#    openPLM is distributed in the hope that it will be useful,
13#    but WITHOUT ANY WARRANTY; without even the implied warranty of
14#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15#    GNU General Public License for more details.
16#
17#    You should have received a copy of the GNU General Public License
18#    along with Foobar.  If not, see <http://www.gnu.org/licenses/>.
19#
20# Contact :
21#    Philippe Joulaud : ninoo.fr@gmail.com
22#    Pierre Cosquer : pierre.cosquer@insa-rennes.fr
23################################################################################
24
25"""
26This module provides :class:`NavigationGraph` which is used to generate
27the navigation's graph in :func:`~plmapp.views.navigate`.
28"""
29
30import re
31import warnings
32import cStringIO as StringIO
33import xml.etree.cElementTree as ET
34from collections import defaultdict
35
36from django.contrib.auth.models import User, Group
37from django.template.loader import render_to_string
38from django.utils.html import linebreaks
39
40import pygraphviz as pgv
41
42from openPLM.plmapp import models
43from openPLM.plmapp.controllers import PLMObjectController, PartController,\
44                                       GroupController
45from openPLM.plmapp.controllers.user import UserController
46
47# just a shortcut
48OSR = "only_search_results"
49class FrozenAGraph(pgv.AGraph):
50    '''
51    A frozen AGraph
52
53    :param data: representation of the graph in dot format
54    '''
55
56    def __init__(self, data):
57        pgv.AGraph.__init__(self, data)
58        self.data = data
59
60    def write(self, path):
61        if hasattr(path, "write"):
62            path.write(self.data.encode("utf-8"))
63        else:
64            with file(path, "w") as f:
65                f.write(self.data)
66
67def get_path(obj):
68    if hasattr(obj, "type"):
69        return u"/".join((obj.type, obj.reference, obj.revision))
70    elif hasattr(obj, 'name'):
71        return u"Group/%s/-/" % obj.name
72    else:
73        return u"User/%s/-/" % obj.username
74
75_attrs = ("id", "type", "reference", "revision", "name")
76_plmobjects_attrs = ["plmobject__" + x for x in _attrs]
77_parts_attrs = ["part__" + x for x in _attrs]
78_documents_attrs = ["document__" + x for x in _attrs]
79
80class NavigationGraph(object):
81    """
82    This object can be used to generate a naviation's graph from an
83    object.
84
85    By default, the graph contains one node: the object given as argument.
86    You can change this behaviour with :meth`set_options`
87
88    Usage::
89
90        graph = NavigationGraph(a_part_controller)
91        graph.set_options({'child' : True, "parents" : True })
92        graph.create_edges()
93        map_str, picture_url = graph.render()
94
95    :param obj: root of the graph
96    :type obj: :class:`.PLMObjectController` or :class:`.UserController`
97    :param results: if the option *only_search_results* is set, only objects in
98                    results are displayed
99
100    .. warning::
101        *results* must not be a QuerySet if it contains users.
102    """
103
104    GRAPH_ATTRIBUTES = dict(dpi='96.0',
105                            mindist=".5",
106                            center='true',
107                            pad='0.1',
108                            mode="ipsep",
109                            overlap="false",
110                            splines="false",
111                            sep="+.1,.1",
112                            outputorder="edgesfirst",
113                            bgcolor="transparent")
114    NODE_ATTRIBUTES = dict(shape='none',
115                           fixedsize='true',
116                           bgcolor="transparent",
117                           width=100./96,
118                           height=70./96)
119    EDGE_ATTRIBUTES = dict(color='#373434',
120                           minlen="1.5",
121                           len="1.5",
122                           arrowhead='normal',
123                           fontname="Sans bold",
124                           fontcolor="transparent",
125                           fontsize="9")
126
127    def __init__(self, obj, results=()):
128        self.object = obj
129        self.results = [r.id for r in results]
130        # a PLMObject and an user may have the same id, so we add a variable
131        # which tells if results contains users
132        self.users_result = self.groups_result = False
133        if results:
134            self.users_result = hasattr(results[0], "username")
135            self.groups_result = isinstance(results[0], Group)
136        self.plmobjects_result = not (self.groups_result or self.users_result)
137        options = ("child", "parents", "doc", "owner", "signer",
138                   "notified", "part", "owned", "to_sign",
139                   "request_notification_from", OSR)
140        self.options = dict.fromkeys(options, False)
141        self.options["prog"] = "dot"
142        self.options["doc_parts"] = []
143        self.nodes = defaultdict(dict)
144        self.edges = set()
145        self.graph = pgv.AGraph(directed=True)
146        self.graph.graph_attr.update(self.GRAPH_ATTRIBUTES)
147        self.graph.node_attr.update(self.NODE_ATTRIBUTES)
148        self.graph.edge_attr.update(self.EDGE_ATTRIBUTES)
149        self._title_to_node = {}
150        self._part_to_node = {}
151
152    def set_options(self, options):
153        """
154        Sets which kind of edges should be inserted.
155
156        Options is a dictionary(*option_name* -> boolean)
157
158        The option *only_search_results* enables results filtering.
159
160        If the root is a :class:`.PartController`, valid options are:
161
162            ========== ======================================================
163             Name       Description
164            ========== ======================================================
165             child      If True, adds recursively all children of the root
166             parents    If True, adds recursively all parents of the root
167             doc        If True, adds documents attached to the parts
168             owner      If True, adds the owner of the root
169             signer     If True, adds the signers of the root
170             notified   If True, adds the notified of the root
171            ========== ======================================================
172
173        If the root is a :class:`.DocumentController`, valid options are:
174
175            ========== ======================================================
176             Name       Description
177            ========== ======================================================
178             parts      If True, adds parts attached to the root
179             owner      If True, adds the owner of the root
180             signer     If True, adds the signers of the root
181             notified   If True, adds the notified of the root
182            ========== ======================================================
183
184        If the root is a :class:`.UserController`, valid options are:
185
186            ========================== ======================================
187             Name                          Description
188            ========================== ======================================
189             owned                     If True, adds all plmobjects owned by
190                                       the root
191             to_sign                   If True, adds all plmobjects signed by
192                                       the root
193             request_notification_from If True, adds all plmobjects which
194                                       notifies the root
195            ========================== ======================================
196
197        """
198        self.options.update(options)
199        if self.options["prog"] == "twopi":
200            self.graph.graph_attr["ranksep"] = "1.2"
201       
202    def _create_child_edges(self, obj, *args):
203        if self.options[OSR] and not self.plmobjects_result:
204            return
205        for child_l in obj.get_children(max_level=-1, related=("child",)):
206            link = child_l.link
207            if self.options[OSR] and link.child.id not in self.results:
208                continue
209            if link.parent_id not in self._part_to_node:
210                continue
211            child = link.child
212            label = "Qty: %.2f %s\\nOrder: %d" % (link.quantity,
213                    link.get_shortened_unit(), link.order)
214            self.edges.add((link.parent_id, child.id, label))
215            self._set_node_attributes(link.child)
216   
217    def _create_parents_edges(self, obj, *args):
218        if self.options[OSR] and not self.plmobjects_result:
219            return
220        for parent_l in obj.get_parents(max_level=-1, related=("parent",)):
221            link = parent_l.link
222            if self.options[OSR] and link.parent.id not in self.results:
223                continue
224            if link.child_id not in self._part_to_node:
225                continue
226            parent = link.parent
227            label = "Qty: %.2f %s\\nOrder: %d" % (link.quantity,
228                    link.get_shortened_unit(), link.order)
229            self.edges.add((parent.id, link.child_id, label))
230            self._set_node_attributes(parent)
231   
232    def _create_part_edges(self, obj, *args):
233        if self.options[OSR] and not self.plmobjects_result:
234            return
235        if isinstance(obj, GroupController):
236            node = "Group%d" % obj.id
237            parts = obj.get_attached_parts().only("type", "reference", "revision", "name")
238            for part in parts:
239                if self.options[OSR] and part.id not in self.results:
240                    continue
241                self.edges.add((node, part.id, " "))
242                self._set_node_attributes(part)
243        else:
244            for link in obj.get_attached_parts().select_related("part").only(*_parts_attrs):
245                if self.options[OSR] and link.part.id not in self.results:
246                    continue
247                # create a link part -> document:
248                # if layout is dot, the part is on top of the document
249                # cf. tickets #82 and #83
250                self.edges.add((link.part_id, obj.id, " "))
251                self._set_node_attributes(link.part)
252   
253    def _create_doc_edges(self, obj, obj_id=None, *args):
254        if self.options[OSR] and not self.plmobjects_result:
255            return
256        if isinstance(obj, GroupController):
257            node = "Group%d" % obj.id
258            docs = obj.get_attached_documents().only("type", "reference", "revision", "name")
259            for doc in docs:
260                if self.options[OSR] and doc.id not in self.results:
261                    continue
262                self.edges.add((node, doc.id, " "))
263                self._set_node_attributes(doc)
264        else:
265            links = obj.get_attached_documents().select_related("document")
266            for link in links.only(*_documents_attrs):
267                if self.options[OSR] and link.document_id not in self.results:
268                    continue
269                self.edges.add((obj_id or obj.id, link.document_id, " "))
270                self._set_node_attributes(link.document)
271
272    def _create_user_edges(self, obj, role):
273        if self.options[OSR] and not self.users_result:
274            return
275        if hasattr(obj, 'user_set'):
276            if role == "owner":
277                users = ((obj.owner, role),)
278            else:
279                users = ((u, role) for u in obj.user_set.all())
280        else:
281            users = obj.plmobjectuserlink_plmobject.filter(role__istartswith=role)
282            users = ((u.user, u.role) for u in users.all())
283        node = "Group%d" % obj.id if isinstance(obj, GroupController) else obj.id
284        for user, role in users:
285            if self.options[OSR] and user.id not in self.results:
286                continue
287            user.plmobject_url = "/user/%s/" % user.username
288            user_id = role + str(user.id)
289            self.edges.add((user_id, node, role.replace("_", "\\n")))
290            self._set_node_attributes(user, user_id, role)
291
292    def _create_object_edges(self, obj, role):
293        if self.options[OSR] and not self.plmobjects_result:
294            return
295        node = "User%d" % obj.id
296        if role in ("owner", "notified"):
297            if role == "owner":
298                qs = obj.plmobject_owner
299            else:
300                qs = obj.plmobjectuserlink_user.filter(role=role)
301                qs = qs.values_list("plmobject_id", flat=True).order_by()
302                qs = models.PLMObject.objects.filter(id__in=qs)
303            links = qs.only("id", "type", "reference", "revision", "name")
304            for plmobject in links:
305                if self.options[OSR] and plmobject.id not in self.results:
306                    continue
307                part_doc_id = role + str(plmobject.id)
308                self.edges.add((node, part_doc_id, role))
309                if plmobject.is_part:
310                    if plmobject.id in self.options["doc_parts"]:
311                        plmobject = PartController(plmobject.part, None, True, True)
312                        self._create_doc_edges(plmobject, part_doc_id)
313                self._set_node_attributes(plmobject, part_doc_id)
314
315        else:
316            # signer roles
317            qs = obj.plmobjectuserlink_user.filter(role__istartswith=role)
318            for link in qs.select_related("plmobject").only("role", *_plmobjects_attrs):
319                if self.options[OSR] and link.plmobject_id not in self.results:
320                    continue
321                part_doc_id = link.role + str(link.plmobject_id)
322                self.edges.add((node, part_doc_id, link.role.replace("_", "\\n")))
323                part_doc = link.plmobject
324                if part_doc.is_part:
325                    if part_doc.id in self.options["doc_parts"]:
326                        part_doc = PartController(part_doc.part, None, True, True)
327                        self._create_doc_edges(part_doc, part_doc_id)
328                self._set_node_attributes(part_doc, part_doc_id)
329
330    def create_edges(self):
331        """
332        Builds the graph (adds all edges and nodes that respected the options)
333        """
334        self.options["doc_parts"] = frozenset(self.options["doc_parts"])
335        self.doc_parts = "#".join(str(o) for o in self.options["doc_parts"])
336        if isinstance(self.object, UserController):
337            id_ = "User%d" % self.object.id
338        elif isinstance(self.object, GroupController):
339            id_ = "Group%d" % self.object.id
340        else:
341            id_ = self.object.id
342        node = self.nodes[id_]
343        self._set_node_attributes(self.object, id_)
344        self.main_node = node["id"]
345        node["width"] = 110. / 96
346        node["height"] = 80. / 96
347        opt_to_meth = {
348            'child' : (self._create_child_edges, None),
349            'parents' : (self._create_parents_edges, None),
350            'owner' : (self._create_user_edges, 'owner'),
351            'signer' : (self._create_user_edges, 'sign'),
352            'notified' : (self._create_user_edges, 'notified'),
353            'user' : (self._create_user_edges, 'member'),
354            'part' : (self._create_part_edges, None),
355            'owned' : (self._create_object_edges, 'owner'),
356            'to_sign' : (self._create_object_edges, 'sign'),
357            'request_notification_from' : (self._create_object_edges, 'notified'),
358        }
359        for field, value in self.options.iteritems():
360            if value and field in opt_to_meth:
361                function, argument = opt_to_meth[field]
362                function(self.object, argument)
363        # now that all parts have been added, we can add the documents
364        if self.options["doc"]:
365            if not (self.options[OSR] and not self.plmobjects_result):
366                if isinstance(self.object, GroupController):
367                    self._create_doc_edges(self.object, None)
368                links = models.DocumentPartLink.objects.\
369                        filter(part__in=self._part_to_node.keys())
370                for link in links.select_related("document"):
371                    if self.options[OSR] and link.document_id not in self.results:
372                        continue
373                   
374                    self.edges.add((link.part_id, link.document_id, " "))
375                    self._set_node_attributes(link.document)
376
377        elif not isinstance(self.object, UserController):
378            if not (self.options[OSR] and not self.plmobjects_result):
379                ids = self.options["doc_parts"].intersection(self._part_to_node.keys())
380                links = models.DocumentPartLink.objects.filter(part__in=ids)
381                for link in links.select_related("document"):
382                    if self.options[OSR] and link.document_id not in self.results:
383                        continue
384                   
385                    self.edges.add((link.part_id, link.document_id, " "))
386                    self._set_node_attributes(link.document)
387
388        # treats the parts to see if they have an attached document
389        if not self.options["doc"]:
390            parts = models.DocumentPartLink.objects.\
391                    filter(part__in=self._part_to_node.keys()).\
392                    values_list("part_id", flat=True)
393            for id_ in parts:
394                data = self._part_to_node[id_]
395                data["show_documents"] = True
396                if id_ not in self.options["doc_parts"]:
397                    data["parts"] = self.doc_parts + "#" + str(id_)
398                    data["doc_img_add"] = True
399                else:
400                    data["doc_img_add"] = False
401                    data["parts"] = "#".join(str(x) for x in self.options["doc_parts"] if x != id_)
402
403    def _set_node_attributes(self, obj, obj_id=None, extra_label=""):
404        obj_id = obj_id or obj.id
405       
406        if "id" in self.nodes[obj_id]:
407            # already treated
408            return
409        # data and _title_to_node are used to retrieve usefull data (url, tooltip)
410        # in _convert_map
411        data = {}
412       
413        # set node attributes according to its type
414        if isinstance(obj, (PLMObjectController, models.PLMObject)):
415            # display the object's name if it is not empty
416            ref = (obj.type, obj.reference, obj.revision)
417            label = obj.name.strip() or u"\n".join(ref)
418            data["title_"] = u" - ".join(ref)
419            # add data to show/hide thumbnails and attached documents
420            if obj.is_document:
421                data["path"] = get_path(obj)
422                data["thumbnails"] = True
423                type_ = "document"
424            else:
425                type_ = "part"
426                # this will be used later to see if it has an attached document
427                self._part_to_node[obj.id] = data
428        elif isinstance(obj, (User, UserController)):
429            full_name = u'%s\n%s' % (obj.first_name, obj.last_name)
430            label = full_name.strip() or obj.username
431            data["title_"] = obj.username
432            type_ = "user"
433        else:
434            label = obj.name
435            type_ = "group"
436        id_ = "%s_%s_%d" % (obj_id, type_.capitalize(), obj.id)
437
438        data["label"] = label + "\n" + extra_label if extra_label else label
439        data["type"] = type_
440        self.nodes[obj_id].update(
441                URL=obj.plmobject_url + "navigate/",
442                id=id_,
443                )
444        self._title_to_node[id_] = data
445
446    def _convert_map(self, map_string):
447        elements = []
448        ajax_navigate = "/ajax/navigate/" + get_path(self.object)
449        for area in ET.fromstring(map_string).findall("area"):
450            if area.get("href") == ".":
451                if area.get("shape") == "rect":
452                    title = area.get("title")
453                    if title:
454                        left, top, x2, y2 = map(int, area.get("coords").split(","))
455                        s = "position:absolute;z-index:3;top:%dpx;left:%dpx;" % (top, left)
456                        title = linebreaks(title.replace("\\n", "\n"))
457                        div = "<div class='edge' style='%s'>%s</div>" % (s, title)
458                        elements.append(div)
459                continue
460
461            data = self._title_to_node.get(area.get("id"), {})
462           
463            # compute css position of the div
464            left, top, x2, y2 = map(int, area.get("coords").split(","))
465            width = x2 - left
466            height = y2 - top
467            style = "position:absolute;z-index:5;top:%dpx;left:%dpx;width:%dpx;height:%dpx;" % (top, left, width, height)
468
469            # create a div with a title, and an <a> element
470            id_ = "Nav-%s" % area.get("id")
471            ctx = data.copy()
472            ctx["style"] = style
473            ctx["id"] = id_
474            main = self.main_node == area.get("id")
475            ctx["main"] = main
476            ctx["href"] = area.get("href")
477            ctx["documents_url"] = ajax_navigate
478            div = render_to_string("navigate/node.html", ctx)
479            if main:
480                # the main node must be the first item, since it is
481                # used to center the graph
482                elements.insert(0, div)
483            else:
484                elements.append(div)
485        return u"\n".join(elements)
486
487    def _parse_svg(self, svg):
488        # TODO: optimize this function
489        edges = []
490        arrows = []
491        root = ET.fromstring(svg)
492        transform = root.getchildren()[0].get("transform")
493       
494        m = re.search(r"scale\((.*?)\)", transform)
495        if m:
496            scale = map(float, m.group(1).split(" ", 1))
497        else:
498            scale = (1, 1)
499       
500        m = re.search(r"translate\((.*?)\)", transform)
501        if m:
502            translate = map(float, m.group(1).split(" ", 1))
503        else:
504            translate = (0, 0)
505
506        width = root.get("width").replace("pt", "")
507        height = root.get("height").replace("pt", "")
508        for path in root.findall(".//{http://www.w3.org/2000/svg}path"):
509            edges.append(path.get("d"))
510        for poly in root.findall(".//{http://www.w3.org/2000/svg}polygon"):
511            arrows.append(poly.get("points"))
512        return dict(width=width, height=height, scale=scale, translate=translate,
513                edges=edges, arrows=arrows)
514
515    def render(self):
516        """
517        Renders an image of the graph.
518
519        :returns: a tuple (html content, javascript content)
520        """
521        warnings.simplefilter('ignore', RuntimeWarning)
522        # builds the graph
523        for key, attrs in self.nodes.iteritems():
524            self.graph.add_node(key, label="", **attrs)
525        s = unicode(self.graph)
526        s = s[:s.rfind("}")]
527        s += "\n".join(u'%s -> %s [label="%s", href="."];' % edge for edge in sorted(self.edges)) + "}\n"
528        self.graph.close()
529        self.graph = FrozenAGraph(s)
530
531        prog = self.options.get("prog", "dot")
532        self.graph.layout(prog=prog)
533        s = StringIO.StringIO()
534        svg = StringIO.StringIO()
535        self.graph.draw(svg, format='svg', prog=prog)
536        svg.seek(0)
537        self.graph.draw(s, format='cmapx', prog=prog)
538        s.seek(0)
539        map_string = s.read()
540        self.graph.clear()
541        warnings.simplefilter('default', RuntimeWarning)
542        return self._convert_map(map_string), self._parse_svg(svg.read())
543
Note: See TracBrowser for help on using the repository browser.