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

Revision 595, 18.6 KB checked in by pcosquer, 9 years ago (diff)

3D branch: merge changes from trunk rev 594

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
154    def set_options(self, options):
155        """
156        Sets which kind of edges should be inserted.
157
158        Options is a dictionary(*option_name* -> boolean)
159
160        The option *only_search_results* enables results filtering.
161
162        If the root is a :class:`.PartController`, valid options are:
163
164            ========== ======================================================
165             Name       Description
166            ========== ======================================================
167             child      If True, adds recursively all children of the root
168             parents    If True, adds recursively all parents of the root
169             doc        If True, adds documents attached to the parts
170             cad        Not yet implemented
171             owner      If True, adds the owner of the root
172             signer     If True, adds the signers of the root
173             notified   If True, adds the notified of the root
174            ========== ======================================================
175
176        If the root is a :class:`.DocumentController`, valid options are:
177
178            ========== ======================================================
179             Name       Description
180            ========== ======================================================
181             parts      If True, adds parts attached to the root
182             owner      If True, adds the owner of the root
183             signer     If True, adds the signers of the root
184             notified   If True, adds the notified of the root
185            ========== ======================================================
186
187        If the root is a :class:`.UserController`, valid options are:
188
189            ========================== ======================================
190             Name                          Description
191            ========================== ======================================
192             owned                     If True, adds all plmobjects owned by
193                                       the root
194             to_sign                   If True, adds all plmobjects signed by
195                                       the root
196             request_notification_from If True, adds all plmobjects which
197                                       notifies the root
198            ========================== ======================================
199
200        """
201        self.options.update(options)
202        if self.options["prog"] == "twopi":
203            self.graph.graph_attr["ranksep"] = "1.2"
204       
205    def _create_child_edges(self, obj, *args):
206        if self.options[OSR] and self.users_result:
207            return
208        for child_l in obj.get_children():
209            link = child_l.link
210            if self.options[OSR] and link.child.id not in self.results:
211                continue
212            child = PartController(link.child, None)
213            label = "Qty: %.2f\\nOrder: %d" % (link.quantity, link.order)
214            self.graph.add_edge(obj.id, child.id, label)
215            self._set_node_attributes(child)
216            if self.options['doc'] or child.id in self.options["doc_parts"]:
217               self._create_doc_edges(child)
218            self._create_child_edges(child)
219   
220    def _create_parents_edges(self, obj, *args):
221        if self.options[OSR] and self.users_result:
222            return
223        for parent_l in obj.get_parents():
224            link = parent_l.link
225            if self.options[OSR] and link.parent.id not in self.results:
226                continue
227            parent = PartController(link.parent, None)
228            label = "Qty: %.2f\\nOrder: %d" % (link.quantity, link.order)
229            self.graph.add_edge(parent.id, obj.id, label)
230            self._set_node_attributes(parent)
231            if self.options['doc'] or parent.id in self.options["doc_parts"]:
232                self._create_doc_edges(parent)
233            self._create_parents_edges(parent)
234   
235    def _create_part_edges(self, obj, *args):
236        if self.options[OSR] and self.users_result:
237            return
238        for link in obj.get_attached_parts():
239            if self.options[OSR] and link.part.id not in self.results:
240                continue
241            part = PartController(link.part, None)
242            self.graph.add_edge(obj.id, part.id, " ")
243            self._set_node_attributes(part)
244   
245    def _create_doc_edges(self, obj, obj_id=None, *args):
246        if self.options[OSR] and self.users_result:
247            return
248        for document_item in obj.get_attached_documents():
249            if self.options[OSR] and document_item.document.id not in self.results:
250                continue
251            document = DocumentController(document_item.document, None)
252            self.graph.add_edge(obj_id or obj.id, document.id, " ")
253            self._set_node_attributes(document)
254
255    def _create_user_edges(self, obj, role):
256        if self.options[OSR] and not self.users_result:
257            return
258        if hasattr(obj, 'user_set'):
259            if role == "owner":
260                users = ((obj.owner, role),)
261            else:
262                users = ((u, role) for u in obj.user_set.all())
263        else:
264            users = obj.plmobjectuserlink_plmobject.filter(role__istartswith=role)
265            users = ((u.user, u.role) for u in users.all())
266        for user, role in users:
267            if self.options[OSR] and user.id not in self.results:
268                continue
269            user = UserController(user, None)
270            user_id = role + str(user.id)
271            self.graph.add_edge(user_id, obj.id, role.replace("_", "\\n"))
272            self._set_node_attributes(user, user_id, role)
273
274    def _create_object_edges(self, obj, role):
275        if self.options[OSR] and self.users_result:
276            return
277        part_doc_list = obj.plmobjectuserlink_user.filter(role__istartswith=role)
278        for part_doc_item in part_doc_list:
279            if self.options[OSR] and part_doc_item.plmobject.id not in self.results:
280                continue
281            part_doc_id = str(part_doc_item.role) + str(part_doc_item.plmobject_id)
282            self.graph.add_edge("User%d" % obj.id, part_doc_id,
283                    part_doc_item.role.replace("_", "\\n"))
284            if part_doc_item.plmobject.is_document:
285                part_doc = DocumentController(part_doc_item.plmobject.document, None)
286            else:
287                part_doc = PartController(part_doc_item.plmobject.part, None)
288                if part_doc.id in self.options["doc_parts"]:
289                    self._create_doc_edges(part_doc, part_doc_id)
290               
291            self._set_node_attributes(part_doc, part_doc_id)
292
293    def create_edges(self):
294        """
295        Builds the graph (adds all edges and nodes that respected the options)
296        """
297        if isinstance(self.object, UserController):
298            id_ = "User%d" % self.object.id
299        else:
300            id_ = self.object.id
301        self.graph.add_node(id_)
302        node = self.graph.get_node(id_)
303        self._set_node_attributes(self.object, id_)
304        self.main_node = node.attr["id"]
305        node.attr["image"] = node.attr["image"].replace(".png", "_main.png")
306        node.attr["width"] = 110. / 96
307        node.attr["height"] = 80. / 96
308        #color = node.attr["color"]
309        #node.attr.update(color="#444444", fillcolor=color)
310        functions_dic = {'child':(self._create_child_edges, None),
311                         'parents':(self._create_parents_edges, None),
312                         'doc':(self._create_doc_edges, None),
313                         'cad':(self._create_doc_edges, None),
314                         'owner':(self._create_user_edges, 'owner'),
315                         'signer':(self._create_user_edges, 'sign'),
316                         'notified':(self._create_user_edges, 'notified'),
317                         'user':(self._create_user_edges, 'member'),
318                         'part': (self._create_part_edges, None),
319                         'owned':(self._create_object_edges, 'owner'),
320                         'to_sign':(self._create_object_edges, 'sign'),
321                         'request_notification_from':(self._create_object_edges, 'notified'),
322                         }
323        for field, value in self.options.items():
324            if value and field in functions_dic:
325                function, argument = functions_dic[field]
326                function(self.object, argument)
327        if not self.options["doc"] and self.object.id in self.options["doc_parts"]:
328            if isinstance(self.object, PartController):
329                self._create_doc_edges(self.object, None)
330
331    def _set_node_attributes(self, obj, obj_id=None, extra_label=""):
332        node = self.graph.get_node(obj_id or obj.id)
333        type_ = type(obj)
334        if issubclass(type_, PartController):
335            type_ = PartController
336        elif issubclass(type_, DocumentController):
337            type_ = DocumentController
338        node.attr.update(self.TYPE_TO_ATTRIBUTES[type_])
339        node.attr["URL"] = obj.plmobject_url + "navigate/"
340        node.attr["tooltip"] = "None"
341        if isinstance(obj, PLMObjectController):
342            node.attr['label'] = get_path(obj).replace("/", "\\n")
343            if type_ == DocumentController:
344                node.attr["tooltip"] = "/ajax/thumbnails/" + get_path(obj)
345            elif type_ == PartController and not self.options["doc"]:
346                if obj.get_attached_documents():
347                    s = "+" if obj.id not in self.options["doc_parts"] else "-"
348                    node.attr["tooltip"] = s + str(obj.id)
349        elif isinstance(obj, UserController):
350            node.attr["label"] = obj.username
351        else:
352            node.attr["label"] = obj.name
353        node.attr["label"] += "\\n" + extra_label
354        t = type_.__name__.replace("Controller", "")
355        node.attr["id"] = "_".join((str(obj_id or obj.id), t, str(obj.id)))
356
357    def convert_map(self, map_string):
358        elements = []
359        doc_parts = "#".join(str(o) for o in self.options["doc_parts"])
360        ajax_navigate = "/ajax/navigate/" + get_path(self.object)
361        for area in ET.fromstring(map_string).findall("area"):
362            left, top, x2, y2 = map(int, area.get("coords").split(","))
363            width = x2 - left
364            height = y2 - top
365            style = "position:absolute;z-index:5;top:%dpx;left:%dpx;width:%dpx;height:%dpx;" % (top, left, width, height)
366            id_ = "Nav-%s" % area.get("id")
367            div = ET.Element("div", id=id_, style=style)
368            div.set("class", "node" + " main_node" * (self.main_node == area.get("id")))
369            url = area.get("title")
370            if url.startswith("/ajax/thumbnails/"):
371                thumbnails = ET.SubElement(div, "img", src="/media/img/search.png",
372                        title="Display thumbnails")
373                thumbnails.set("class", "node_thumbnails" + self.BUTTON_CLASS)
374                thumbnails.set("onclick", "display_thumbnails('%s', '%s');" % (id_, url))
375            elif url != "None":
376                if url[0] == "+":
377                    parts = doc_parts + "#" + url[1:]
378                    img = "/media/img/add.png"
379                else:
380                    s = set(self.options["doc_parts"])
381                    img = "/media/img/remove.png"
382                    s.remove(int(url[1:]))
383                    parts = "#".join(str(o) for o in s)
384                show_doc = ET.SubElement(div, "img", src=img,
385                        title="Show related documents")
386                show_doc.set("class", "node_show_docs" + self.BUTTON_CLASS)
387                show_doc.set("onclick", "display_docs('%s', '%s', '%s');" % (id_, ajax_navigate, parts))
388            a = ET.SubElement(div, "a", href=area.get("href"))
389            span = ET.SubElement(a, "span")
390            span.text = " "
391            elements.append(div)
392
393        s = "\n".join(ET.tostring(div) for div in elements)
394        return s
395
396    def render(self):
397        """
398        Renders an image of the graph
399
400        :returns: a tuple (image map data, url of the image, path of the image)
401        """
402        warnings.simplefilter('ignore', RuntimeWarning)
403        # rebuild a frozen graph with sorted edges to avoid random output
404        edges = self.graph.edges(keys=True)
405        self.graph.remove_edges_from((a, b) for a, b, k in edges)
406        s = unicode(self.graph)
407        s = s[:s.rfind("}")]
408        edges.sort()
409        s += "\n".join(u'%s -> %s [label="%s"];' % edge for edge in edges) + "}\n"
410        self.graph.close()
411        self.graph = FrozenAGraph(s)
412
413        rand = "".join(random.choice(string.ascii_lowercase) for x in xrange(40))
414        picture_path = "media/navigate/" + rand
415        prog = self.options.get("prog", "dot")
416        self.graph.layout(prog=prog)
417        picture_path2 = os.path.join(basedir, "..", "..", picture_path)
418        map_path= picture_path2 + ".map"
419        picture_path += ".png"
420        picture_path2 += ".png"
421        s = StringIO.StringIO()
422        self.graph.draw(picture_path2, format='png', prog=prog)
423        self.graph.draw(s, format='cmapx', prog=prog)
424        s.seek(0)
425        map_string = s.read()
426        self.graph.clear()
427        warnings.simplefilter('default', RuntimeWarning)
428        return self.convert_map(map_string), picture_path, picture_path2
429
Note: See TracBrowser for help on using the repository browser.