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

Revision 299, 16.6 KB checked in by pcosquer, 8 years ago (diff)

navigate: ajax

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