source: main/branches/3D/openPLM/plmapp/navigate.py @ 662

Revision 662, 19.9 KB checked in by pcosquer, 8 years ago (diff)

3D branch: merge changes from trunk (rev [661])

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 os
31import string
32import random
33import warnings
34import cStringIO as StringIO
35import xml.etree.cElementTree as ET
36
37import pygraphviz as pgv
38
39from openPLM.plmapp.controllers import PLMObjectController, PartController,\
40                                       DocumentController
41from openPLM.plmapp.controllers.user import UserController
42from openPLM.plmapp.controllers.group import GroupController
43
44basedir = os.path.join(os.path.dirname(__file__), "..", "media", "img")
45
46icondir = os.path.join(basedir, "navigate")
47
48# just a shortcut
49OSR = "only_search_results"
50
51def get_path(obj):
52    if hasattr(obj, "type"):
53        return u"/".join((obj.type, obj.reference, obj.revision))
54    elif hasattr(obj, 'name'):
55        return u"Group/%s/-/" % obj.name
56    else:
57        return u"User/%s/-/" % obj.username
58
59
60class FrozenAGraph(pgv.AGraph):
61    '''
62    A frozen AGraph
63
64    :param data: representation of the graph in dot format
65    '''
66
67    def __init__(self, data):
68        pgv.AGraph.__init__(self, data)
69        self.data = data
70
71    def write(self, path):
72        if hasattr(path, "write"):
73            path.write(self.data.encode("utf-8"))
74        else:
75            with file(path, "w") as f:
76                f.write(self.data)
77
78class NavigationGraph(object):
79    """
80    This object can be used to generate a naviation's graph from an
81    object.
82
83    By default, the graph contains one node: the object given as argument.
84    You can change this behaviour with :meth`set_options`
85
86    Usage::
87
88        graph = NavigationGraph(a_part_controller)
89        graph.set_options({'child' : True, "parents" : True })
90        graph.create_edges()
91        map_str, picture_url = graph.render()
92
93    :param obj: root of the graph
94    :type obj: :class:`.PLMObjectController` or :class:`.UserController`
95    :param results: if the option *only_search_results* is set, only objects in
96                    results are displayed
97
98    .. warning::
99        *results* must not be a QuerySet if it contains users.
100    """
101
102    GRAPH_ATTRIBUTES = dict(dpi='96.0',
103                            mindist=".5",
104                            center='true',
105                            pad='0.1',
106                            mode="ipsep",
107                            overlap="false",
108                            splines="false",
109                            sep="+.1,.1",
110                            outputorder="edgesfirst",
111                            bgcolor="transparent")
112    NODE_ATTRIBUTES = dict(shape='none', fixedsize='true', fontsize='10',
113                           bgcolor="transparent", color="transparent",
114                           fontname="Sans bold",
115                           fontcolor="#ffffff",
116                           style='filled', width=100./96, height=70./96)
117    EDGE_ATTRIBUTES = dict(color='#aaaaaa',
118                           minlen="1.5",
119                           len="1.5",
120                           arrowhead='normal',
121                           fontname="Sans bold",
122                           fontcolor="#aaaaaa",
123                           fontsize="9")
124    TYPE_TO_ATTRIBUTES = {UserController : dict(
125                            image=os.path.join(icondir, "user.png")),
126                          GroupController : dict(
127                            image=os.path.join(icondir, "user.png")),
128                          PartController : dict(
129                            image=os.path.join(icondir, "part.png")),
130                          DocumentController : dict(
131                            image=os.path.join(icondir, "document.png"))}
132   
133    BUTTON_CLASS = " ui-button ui-widget ui-state-default ui-corner-all "
134
135    def __init__(self, obj, results=()):
136        self.object = obj
137        self.results = [r.id for r in results]
138        # a PLMObject and an user may have the same id, so we add a variable
139        # which tell if results contains users
140        self.users_result = False
141        if results:
142            self.users_result = hasattr(results[0], "username")
143        self.options_list = ("child", "parents", "doc", "cad", "owner", "signer",
144                             "notified", "part", "owned", "to_sign",
145                             "request_notification_from", OSR)
146        self.options = dict.fromkeys(self.options_list, False)
147        self.options["prog"] = "dot"
148        self.options["doc_parts"] = []
149        self.graph = pgv.AGraph(directed=True)
150        self.graph.graph_attr.update(self.GRAPH_ATTRIBUTES)
151        self.graph.node_attr.update(self.NODE_ATTRIBUTES)
152        self.graph.edge_attr.update(self.EDGE_ATTRIBUTES)
153        self.title_to_nodes = {}
154
155    def set_options(self, options):
156        """
157        Sets which kind of edges should be inserted.
158
159        Options is a dictionary(*option_name* -> boolean)
160
161        The option *only_search_results* enables results filtering.
162
163        If the root is a :class:`.PartController`, valid options are:
164
165            ========== ======================================================
166             Name       Description
167            ========== ======================================================
168             child      If True, adds recursively all children of the root
169             parents    If True, adds recursively all parents of the root
170             doc        If True, adds documents attached to the parts
171             cad        Not yet implemented
172             owner      If True, adds the owner of the root
173             signer     If True, adds the signers of the root
174             notified   If True, adds the notified of the root
175            ========== ======================================================
176
177        If the root is a :class:`.DocumentController`, valid options are:
178
179            ========== ======================================================
180             Name       Description
181            ========== ======================================================
182             parts      If True, adds parts attached to the root
183             owner      If True, adds the owner of the root
184             signer     If True, adds the signers of the root
185             notified   If True, adds the notified of the root
186            ========== ======================================================
187
188        If the root is a :class:`.UserController`, valid options are:
189
190            ========================== ======================================
191             Name                          Description
192            ========================== ======================================
193             owned                     If True, adds all plmobjects owned by
194                                       the root
195             to_sign                   If True, adds all plmobjects signed by
196                                       the root
197             request_notification_from If True, adds all plmobjects which
198                                       notifies the root
199            ========================== ======================================
200
201        """
202        self.options.update(options)
203        if self.options["prog"] == "twopi":
204            self.graph.graph_attr["ranksep"] = "1.2"
205       
206    def _create_child_edges(self, obj, *args):
207        if self.options[OSR] and self.users_result:
208            return
209        for child_l in obj.get_children():
210            link = child_l.link
211            if self.options[OSR] and link.child.id not in self.results:
212                continue
213            child = PartController(link.child, None)
214            label = "Qty: %.2f %s\\nOrder: %d" % (link.quantity,
215                    link.get_shortened_unit(), link.order)
216            self.graph.add_edge(obj.id, child.id, label)
217            self._set_node_attributes(child)
218            if self.options['doc'] or child.id in self.options["doc_parts"]:
219               self._create_doc_edges(child)
220            self._create_child_edges(child)
221   
222    def _create_parents_edges(self, obj, *args):
223        if self.options[OSR] and self.users_result:
224            return
225        for parent_l in obj.get_parents():
226            link = parent_l.link
227            if self.options[OSR] and link.parent.id not in self.results:
228                continue
229            parent = PartController(link.parent, None)
230            label = "Qty: %.2f %s\\nOrder: %d" % (link.quantity,
231                    link.get_shortened_unit(), link.order)
232            self.graph.add_edge(parent.id, obj.id, label)
233            self._set_node_attributes(parent)
234            if self.options['doc'] or parent.id in self.options["doc_parts"]:
235                self._create_doc_edges(parent)
236            self._create_parents_edges(parent)
237   
238    def _create_part_edges(self, obj, *args):
239        if self.options[OSR] and self.users_result:
240            return
241        for link in obj.get_attached_parts():
242            if self.options[OSR] and link.part.id not in self.results:
243                continue
244            part = PartController(link.part, None)
245            # create a link part -> document:
246            # if layout is dot, the part is on top of the document
247            # cf. tickets #82 and #83
248            self.graph.add_edge(part.id, obj.id, " ")
249            self._set_node_attributes(part)
250   
251    def _create_doc_edges(self, obj, obj_id=None, *args):
252        if self.options[OSR] and self.users_result:
253            return
254        for document_item in obj.get_attached_documents():
255            if self.options[OSR] and document_item.document.id not in self.results:
256                continue
257            document = DocumentController(document_item.document, None)
258            self.graph.add_edge(obj_id or obj.id, document.id, " ")
259            self._set_node_attributes(document)
260
261    def _create_user_edges(self, obj, role):
262        if self.options[OSR] and not self.users_result:
263            return
264        if hasattr(obj, 'user_set'):
265            if role == "owner":
266                users = ((obj.owner, role),)
267            else:
268                users = ((u, role) for u in obj.user_set.all())
269        else:
270            users = obj.plmobjectuserlink_plmobject.filter(role__istartswith=role)
271            users = ((u.user, u.role) for u in users.all())
272        for user, role in users:
273            if self.options[OSR] and user.id not in self.results:
274                continue
275            user = UserController(user, None)
276            user_id = role + str(user.id)
277            self.graph.add_edge(user_id, obj.id, role.replace("_", "\\n"))
278            self._set_node_attributes(user, user_id, role)
279
280    def _create_object_edges(self, obj, role):
281        if self.options[OSR] and self.users_result:
282            return
283        part_doc_list = obj.plmobjectuserlink_user.filter(role__istartswith=role)
284        for part_doc_item in part_doc_list:
285            if self.options[OSR] and part_doc_item.plmobject.id not in self.results:
286                continue
287            part_doc_id = str(part_doc_item.role) + str(part_doc_item.plmobject_id)
288            self.graph.add_edge("User%d" % obj.id, part_doc_id,
289                    part_doc_item.role.replace("_", "\\n"))
290            if part_doc_item.plmobject.is_document:
291                part_doc = DocumentController(part_doc_item.plmobject.document, None)
292            else:
293                part_doc = PartController(part_doc_item.plmobject.part, None)
294                if part_doc.id in self.options["doc_parts"]:
295                    self._create_doc_edges(part_doc, part_doc_id)
296               
297            self._set_node_attributes(part_doc, part_doc_id)
298
299    def create_edges(self):
300        """
301        Builds the graph (adds all edges and nodes that respected the options)
302        """
303        if isinstance(self.object, UserController):
304            id_ = "User%d" % self.object.id
305        else:
306            id_ = self.object.id
307        self.graph.add_node(id_)
308        node = self.graph.get_node(id_)
309        self._set_node_attributes(self.object, id_)
310        self.main_node = node.attr["id"]
311        node.attr["image"] = node.attr["image"].replace(".png", "_main.png")
312        node.attr["width"] = 110. / 96
313        node.attr["height"] = 80. / 96
314        #color = node.attr["color"]
315        #node.attr.update(color="#444444", fillcolor=color)
316        functions_dic = {'child':(self._create_child_edges, None),
317                         'parents':(self._create_parents_edges, None),
318                         'doc':(self._create_doc_edges, None),
319                         'cad':(self._create_doc_edges, None),
320                         'owner':(self._create_user_edges, 'owner'),
321                         'signer':(self._create_user_edges, 'sign'),
322                         'notified':(self._create_user_edges, 'notified'),
323                         'user':(self._create_user_edges, 'member'),
324                         'part': (self._create_part_edges, None),
325                         'owned':(self._create_object_edges, 'owner'),
326                         'to_sign':(self._create_object_edges, 'sign'),
327                         'request_notification_from':(self._create_object_edges, 'notified'),
328                         }
329        for field, value in self.options.items():
330            if value and field in functions_dic:
331                function, argument = functions_dic[field]
332                function(self.object, argument)
333        if not self.options["doc"] and self.object.id in self.options["doc_parts"]:
334            if isinstance(self.object, PartController):
335                self._create_doc_edges(self.object, None)
336
337    def _set_node_attributes(self, obj, obj_id=None, extra_label=""):
338        obj_id = obj_id or obj.id
339       
340        # data and title_to_nodes are used to retrieve usefull data (url, tooltip)
341        # in convert_map
342        data = {}
343        node = self.graph.get_node(obj_id)
344        node.attr["tooltip"] = str(obj_id)
345        node.attr["URL"] = obj.plmobject_url + "navigate/"
346       
347        # set node attributes according to its type
348        type_ = type(obj)
349        if issubclass(type_, PartController):
350            type_ = PartController
351        elif issubclass(type_, DocumentController):
352            type_ = DocumentController
353        node.attr.update(self.TYPE_TO_ATTRIBUTES[type_])
354
355        if isinstance(obj, PLMObjectController):
356            # display the object's name if it is not empty
357            path = get_path(obj)
358            node.attr['label'] = obj.name.strip() or path.replace("/", "\\n")
359            data["title"] = path.replace("/", " - ")
360           
361            # add urls to show/hide thumbnails and attached documents
362            if type_ == DocumentController:
363                data["url"] = "/ajax/thumbnails/" + get_path(obj)
364            elif type_ == PartController and not self.options["doc"]:
365                if obj.get_attached_documents():
366                    s = "+" if obj.id not in self.options["doc_parts"] else "-"
367                    data["url"] = s + str(obj.id)
368        elif isinstance(obj, UserController):
369            full_name =  u'%s\\n%s' % (obj.first_name, obj.last_name)
370            node.attr["label"] = full_name.strip() or obj.username
371            data["title"] = obj.username
372        else:
373            node.attr["label"] = obj.name
374        node.attr["label"] += "\\n" + extra_label
375        # id is used by the javascript
376        t = type_.__name__.replace("Controller", "")
377        node.attr["id"] = "_".join((str(obj_id), t, str(obj.id)))
378        self.title_to_nodes[node.attr["id"]] = data
379
380    def convert_map(self, map_string):
381        elements = []
382        doc_parts = "#".join(str(o) for o in self.options["doc_parts"])
383        ajax_navigate = "/ajax/navigate/" + get_path(self.object)
384        for area in ET.fromstring(map_string).findall("area"):
385            data = self.title_to_nodes.get(area.get("id"), {})
386            # compute css position of the div
387            left, top, x2, y2 = map(int, area.get("coords").split(","))
388            width = x2 - left
389            height = y2 - top
390            style = "position:absolute;z-index:5;top:%dpx;left:%dpx;width:%dpx;height:%dpx;" % (top, left, width, height)
391            # create a div with a title, and an <a> element
392            id_ = "Nav-%s" % area.get("id")
393            div = ET.Element("div", id=id_, style=style)
394            div.set("class", "node" + " main_node" * (self.main_node == area.get("id")))
395            title = data.get("title")
396            if title:
397                div.set("title", title)
398            # add thumbnails and attached documents buttons
399            url = data.get("url", "None")
400            if url.startswith("/ajax/thumbnails/"):
401                thumbnails = ET.SubElement(div, "img", src="/media/img/search.png",
402                        title="Display thumbnails")
403                thumbnails.set("class", "node_thumbnails" + self.BUTTON_CLASS)
404                thumbnails.set("onclick", "display_thumbnails('%s', '%s');" % (id_, url))
405            elif url != "None":
406                if url[0] == "+":
407                    parts = doc_parts + "#" + url[1:]
408                    img = "/media/img/add.png"
409                else:
410                    s = set(self.options["doc_parts"])
411                    img = "/media/img/remove.png"
412                    s.remove(int(url[1:]))
413                    parts = "#".join(str(o) for o in s)
414                show_doc = ET.SubElement(div, "img", src=img,
415                        title="Show related documents")
416                show_doc.set("class", "node_show_docs" + self.BUTTON_CLASS)
417                show_doc.set("onclick", "display_docs('%s', '%s', '%s');" % (id_, ajax_navigate, parts))
418            # add the link
419            a = ET.SubElement(div, "a", href=area.get("href"))
420            span = ET.SubElement(a, "span")
421            span.text = " "
422            elements.append(div)
423
424        s = "\n".join(ET.tostring(div) for div in elements)
425        return s
426
427    def render(self):
428        """
429        Renders an image of the graph
430
431        :returns: a tuple (image map data, url of the image, path of the image)
432        """
433        warnings.simplefilter('ignore', RuntimeWarning)
434        # rebuild a frozen graph with sorted edges to avoid random output
435        edges = self.graph.edges(keys=True)
436        self.graph.remove_edges_from((a, b) for a, b, k in edges)
437        s = unicode(self.graph)
438        s = s[:s.rfind("}")]
439        edges.sort()
440        s += "\n".join(u'%s -> %s [label="%s"];' % edge for edge in edges) + "}\n"
441        self.graph.close()
442        self.graph = FrozenAGraph(s)
443
444        rand = "".join(random.choice(string.ascii_lowercase) for x in xrange(40))
445        picture_path = "media/navigate/" + rand
446        prog = self.options.get("prog", "dot")
447        self.graph.layout(prog=prog)
448        picture_path2 = os.path.join(basedir, "..", "..", picture_path)
449        map_path= picture_path2 + ".map"
450        picture_path += ".png"
451        picture_path2 += ".png"
452        s = StringIO.StringIO()
453        self.graph.draw(picture_path2, format='png', prog=prog)
454        self.graph.draw(s, format='cmapx', prog=prog)
455        s.seek(0)
456        map_string = s.read()
457        self.graph.clear()
458        warnings.simplefilter('default', RuntimeWarning)
459        return self.convert_map(map_string), picture_path, picture_path2
460
Note: See TracBrowser for help on using the repository browser.