Attachment 'VisualSiteMap_1.10.py'
Download 1 """
2 MoinMoin - VisualSiteMap action
3
4 Idea is based on the webdot.py action.
5
6 More or less redid it from scratch. Differs from the webdot action in several ways:
7 * Uses the dot executable, not webdot, since webdot is not available on windows.
8 * All links up to the search depth are displayed.
9 * There's no maximal limit to the displayed nodes.
10 * Nodes are marked during depth first visit, so each node is visited only once.
11 * The visit method in class LocalSiteMap gets the whole tree as parameter.
12 That way additional treenode information may be shown in the graph.
13 * All edges between nodes contained in the graph are displayed, even if MAX_DEPTH is exceeded that way.
14 * Optional depth controls
15 * Nodes linked more then STRONG_LINK_NR times are highlighted using the STRONG_COLOR.
16 * Search depth is configurable.
17
18 Add the following line to your (apache?) webserver configuration:
19 Alias /moin_cache/ /var/cache/moin/
20 See CACHE_URL and CACHE_DIR below.
21
22 Add this to your stylesheet:
23 img.sitemap
24 {
25 border-width: 1;
26 border-color: #000000;
27 }
28
29 The following settings may be worth a change:
30 * DEPTH_CONTROL
31 * OUTPUT_FORMAT
32 * DEFAULT_DEPTH
33 * MAX_DEPTH
34 * LINK_TO_SITEMAP
35
36 07.10.2004
37 * Maximum image size can be configured
38 * Output image format is configurable
39 * David Linke changed the output code (print() -> request.write())
40 * Changed link counting algorithm to get the depth controls right.
41
42 08.10.2004
43 * IE caching problem with depth controls resolved. Now the current search depth is part of the file names.
44 * Problems with pagenames containing non ASCII characters fixed.
45
46 14.03.2005
47 * cleanup & adapted to moin 1.3.4 -- ThomasWaldmann
48 * Fixed for utf-8 and sub pages
49
50 16.3.2005
51 * included patch from David Linke for Windows compatibility
52 * FONTNAME and FONTSIZE
53 * removed invalid print debug statements
54 * use config.charset
55
56 19.08.2014
57 * Cleanup & adapted for moin 1.9.4
58 * Changed default output type from PNG to SVG.
59 * Use configured category regex instead of separate setting.
60 * Configurable: node links point to the VisualSiteMap of the target node (instead of its wiki page).
61 * Fixed FS/URL escaping inconsistency for pagenames with special characters.
62 * Enabled DEPTH_CONTROL by default.
63
64 01.09.2015 - v1.10
65 * escape xml-Entities in URLs (this broke svg output)
66 * fixed a python3-incompatibility (old octal number format)
67 * tested with moin 1.9.8
68 """
69
70 ##################################################################
71 # Be warned that calculating large graphs may block your server! #
72 # So be careful with the parameter settings. #
73 ##################################################################
74
75 # This should be a public path on your web server. The dot files, images and map files are created in this directory and
76 # served from there.
77 #CACHE_DIR = "C:/DocumentRoot/cache"
78 #CACHE_URL = "http://my-server/cache"
79 CACHE_DIR = "/var/cache/moin/"
80 CACHE_URL = "/moin_cache"
81
82 # Absolute location of the dot (or neato) executable.
83 #DOT_EXE = "C:/Programme/ATT/GraphViz/bin/dot.exe"
84 #DOT_EXE = "/usr/bin/dot"
85 DOT_EXE = "/usr/bin/neato"
86
87 # Graph controls.
88 DEFAULT_DEPTH = 2
89 STRONG_LINK_NR = 4
90
91 # nodes are linked their sitemap (instead of their wiki page)
92 LINK_TO_SITEMAP = True
93
94 # Optional controls for interactive modification of the search depth.
95 DEPTH_CONTROL = True
96 MAX_DEPTH = 4
97
98 # Desired image format (eg. png, jpg, gif - see the dot documentation)
99 OUTPUT_FORMAT = "svg"
100
101 # Maximum output size in inches. Set to None to disable size limitation,
102 # then the graph is made as big as needed (best for readability).
103 # OUTPUT_SIZE="8,12" sets maximum width to 8, maximum height to 12 inches.
104 OUTPUT_SIZE = None
105
106 # Name and Size of the font use
107 # Times, Helvetica, Courier, Symbol are supported on any platform.
108 # Others may NOT be supported.
109 # When selecting a font, make sure it support unicode chars (at least the
110 # ones you use, e.g. german umlauts or french accented chars).
111 FONTNAME = "Times"
112 FONTSIZE = "10"
113
114 # Colors of boxes and edges.
115 BOX_COLOR = "#E0F0FF"
116 ROOT_COLOR = "#FFE0E0"
117 STRONG_COLOR = "#E0FFE0"
118 EDGE_COLOR = "#888888"
119
120
121 import re
122 import os
123 import subprocess
124
125 from MoinMoin import config, wikiutil
126 from MoinMoin.Page import Page
127
128 # escape special characters for XML output
129 try:
130 # python 3
131 from xml import escape as xml_escape
132 except ImportError:
133 # python 2
134 from cgi import escape as xml_escape
135
136
137 class LocalSiteMap:
138 def __init__(self, name, maxdepth):
139 self.name = name
140 self.maxdepth = maxdepth
141 self.result = []
142
143 def output(self, request):
144 pagebuilder = GraphBuilder(request, self.maxdepth)
145 root = pagebuilder.build_graph(self.name)
146 # count links
147 for edge in pagebuilder.all_edges:
148 edge[0].linkedfrom += 1
149 edge[1].linkedto += 1
150 # write nodes
151 for node in pagebuilder.all_nodes:
152 self.append(' "%s"'% node.name)
153 if node.depth > 0:
154 if node.linkedto >= STRONG_LINK_NR:
155 self.append(' [label="%s",color="%s"];\n' % (node.name, STRONG_COLOR))
156 else:
157 self.append(' [label="%s"];\n' % (node.name))
158 else:
159 self.append('[label="%s",shape=box,style=filled,color="%s"];\n' % (node.name, ROOT_COLOR))
160 # write edges
161 for edge in pagebuilder.all_edges:
162 self.append(' "%s"->"%s";\n' % (edge[0].name, edge[1].name))
163
164 return ''.join(self.result)
165
166 def append(self, text):
167 self.result.append(text)
168
169
170 class GraphBuilder:
171
172 def __init__(self, request, maxdepth):
173 self.request = request
174 self.maxdepth = maxdepth
175 self.all_nodes = []
176 self.all_edges = []
177
178 def is_ok(self, child):
179 if not self.request.user.may.read(child):
180 return 0
181 if Page(self.request, child).exists() and not re.search(self.request.cfg.page_category_regex, child):
182 return 1
183 return 0
184
185 def build_graph(self, name):
186 # Reuse generated trees
187 nodesMap = {}
188 root = Node(name)
189 nodesMap[name] = root
190 root.visited = 1
191 self.all_nodes.append(root)
192 self.recurse_build([root], 1, nodesMap)
193 return root
194
195 def recurse_build(self, nodes, depth, nodesMap):
196 # collect all nodes of the current search depth here for the next recursion step
197 child_nodes = []
198 # iterate over the nodes
199 for node in nodes:
200 for child in Page(self.request, node.name).getPageLinks(self.request):
201 if self.is_ok(child):
202 # Create the node with the given name
203 if not nodesMap.has_key(child):
204 # create the new node and store it
205 newNode = Node(child)
206 newNode.depth = depth
207 else:
208 newNode = nodesMap[child]
209 # If the current depth doesn't exceed the maximum depth, add newNode to recursion step
210 if depth <= self.maxdepth:
211 # The node is appended to the nodes list for the next recursion step.
212 nodesMap[child] = newNode
213 self.all_nodes.append(newNode)
214 child_nodes.append(newNode)
215 node.append(newNode)
216 # Draw an edge.
217 edge = (node, newNode)
218 if not edge in self.all_edges:
219 self.all_edges.append(edge)
220 # recurse, if the current recursion step yields children
221 if len(child_nodes):
222 self.recurse_build(child_nodes, depth+1, nodesMap)
223
224
225 class Node:
226 def __init__(self, name):
227 self.name = name
228 self.children = []
229 self.visited = 0
230 self.linkedfrom = 0
231 self.linkedto = 0
232 self.depth = 0
233
234 def append(self, node):
235 self.children.append(node)
236
237
238 def execute(pagename, request):
239 _ = request.getText
240
241 maxdepth = DEFAULT_DEPTH
242 if DEPTH_CONTROL and request.values.has_key('depth'):
243 maxdepth = int(request.values['depth'][0])
244
245 if maxdepth > MAX_DEPTH:
246 maxdepth = MAX_DEPTH
247
248 baseurl = request.getBaseURL().rstrip("/")
249 def get_page_link(pname, to_sitemap=LINK_TO_SITEMAP, **kwargs):
250 if pname is None:
251 pagelinkname = r'\N'
252 else:
253 pagelinkname = wikiutil.quoteWikinameURL(pname)
254 if to_sitemap:
255 link = "%s/%s?action=VisualSiteMap" % (baseurl, pagelinkname)
256 for key, value in kwargs.iteritems():
257 link += xml_escape("&%s=%s" % (key, value))
258 else:
259 link = "%s/%s" % (baseurl, pagelinkname)
260 return link
261
262 request.theme.send_title(_('Visual Map of %s') % pagename, pagename=pagename)
263
264 wikinamefs = wikiutil.quoteWikinameFS(pagename)
265 fnprefix = os.path.join(CACHE_DIR, '%s_%s' % (wikinamefs, maxdepth))
266 dotfilename = '%s.%s' % (fnprefix, 'dot')
267 imagefilename = '%s.%s' % (fnprefix, OUTPUT_FORMAT)
268 mapfilename = '%s.%s' % (fnprefix, 'cmap')
269 imageurl = '%s/%s_%s.%s' % (CACHE_URL, wikinamefs, maxdepth, OUTPUT_FORMAT)
270
271 lsm = LocalSiteMap(pagename, maxdepth).output(request).encode(config.charset)
272
273 os.umask(0o22)
274 dotfile = file(dotfilename, 'w')
275 dotfile.write('digraph G {\n')
276 if OUTPUT_SIZE:
277 dotfile.write(' size="%s"\n' % OUTPUT_SIZE)
278 dotfile.write(' ratio=compress;\n')
279 dotfile.write(' URL="%s";\n' % get_page_link(pagename, to_sitemap=False))
280 dotfile.write(' overlap=false;\n')
281 dotfile.write(' concentrate=true;\n')
282 dotfile.write(' edge [color="%s"];\n' % EDGE_COLOR)
283 dotfile.write(' node [URL="%s", ' % get_page_link(None, depth=maxdepth))
284 dotfile.write('fontcolor=black, fontname="%s", fontsize=%s, style=filled, color="%s"]\n' % (FONTNAME, FONTSIZE, BOX_COLOR))
285 dotfile.write(lsm)
286 dotfile.write('}\n')
287 dotfile.close()
288
289 subprocess.call([DOT_EXE, "-T%s" % OUTPUT_FORMAT, "-o%s" % imagefilename, dotfilename])
290 subprocess.call([DOT_EXE, "-Tcmap", "-o%s" % mapfilename, dotfilename])
291
292 # show the image
293 request.write('<center><img class="sitemap" border="1" src="%s" usemap="#map1"/></center>' % imageurl)
294
295 # image map for links ("img" does not enable svg links)
296 request.write('<map name="map1">')
297 mapfile = file(mapfilename, 'r')
298 for row in mapfile:
299 request.write(row)
300 mapfile.close()
301 request.write('</map>')
302
303 if DEPTH_CONTROL:
304 linkname = wikiutil.quoteWikinameURL(pagename)
305 links = []
306 if maxdepth > 1:
307 links.append('<a href="%s">Less</a>' % get_page_link(pagename, depth=maxdepth-1))
308 if maxdepth < MAX_DEPTH:
309 links.append('<a href="%s">More</a>' % get_page_link(pagename, depth=maxdepth+1))
310 request.write('<p align="center">%s</p>' % ' | '.join(links))
311
312 request.write('<p align="center"><small>Search depth is %s. Nodes linked more than %s times are highlighted.</small></p>' % (maxdepth, STRONG_LINK_NR))
313
314 request.theme.send_footer(pagename)
Attached Files
To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.You are not allowed to attach a file to this page.