Attachment 'SortBy-1.7.py'
Download 1 """
2 MoinMoin - SortBy Macro
3 Copyright (c) 2001 by Fred Bremmer <Fred.Bremmer@ubc.ca>
4 Modified (c) 2007 by Lane Rettig <Lane.Rettig@deshaw.com>
5
6 Based on an earlier SortBy Macro
7 Copyright (c) 2001 by Tim Bird <tbird@lineo.com>
8 All rights reserved, see COPYING for details.
9
10 Updated June 24, 2008 for compatibility with Moin 1.7.
11 (code commented below).
12
13 DESCRIPTION: Sort a table by one or more columns and sort types/orders
14
15 SortBy(page,num_headers,column,sort_type[,othercolumn,sort_type]...)
16
17 Examples:
18
19 [[SortBy(TablePage,1,3,alpha)]]
20
21 This would return the first table on TablePage sorted by column 3
22 (column 1 is the leftmost column). The rows would be sorted using a
23 lexicographic (alpha) sort. It would preserve 1 header row of the
24 table, without sorting it.
25
26 [[SortBy(TablePage,0,2,reversenumber,1,alpha)]]
27
28 This would return the first table on TablePage sorted using a reverse
29 numeric sort on column 2, but rows with identical values in column 2
30 would be sorted in alpha order of their values in column 1. It would
31 not preserve the first row of the table. The first row of the table
32 would be sorted also (zero header rows left unsorted).
33
34 Supported sort types are: alpha, number, nocase, nosort, reversealpha,
35 reversenumber, reversenocase, reversenosort
36
37 'nosort' can be used to make the SortBy macro behave like an Include
38 that only loads the first table from the page, not the whole page.
39 Thus you can have comments on the table page that are not shown in the
40 SortBy rendering on the including page.
41
42 See SortByTest below for a more comprehensive list of examples.
43
44 Utility functions:
45 error() - convert an error message to HTML
46 read_table() - read a file and return its first table's data
47
48 Functions to convert a string into an appropriate sortable value:
49 strip_fmt() - remove wiki formatting and commas
50 to_number() - convert to a number if possible
51 to_nocase() - convert everything to the same case
52 to_nosort() - return ascending numbers regardless of arg
53
54 Main loop functions:
55 process_args() - parse args string and do some error checking
56 sort_table() - do the actual sorting of table rows
57 format() - generate the HTML output
58 execute() - the macro's main loop, called by MoinMoin
59
60 """
61
62 # After installing this macro, you can test it by creating a wiki page
63 # named "SortByTest" which contains the following text:
64
65 SortByTest = """
66 [[TableOfContents]]
67
68 == Original Table ==
69 ||'''First''' ||'''Second''' ||'''Third''' ||
70 ||A ||2,000,000 ||zebra ||
71 ||C ||1 ||123 ||
72 ||b ||2 ||word ||
73 ||d ||30 ||{{{word}}} ||
74 ||A ||2 ||Wordy ||
75 ||B ||4.2 ||23 ||
76 ||A ||2 ||'''word''' ||
77
78 == Column 1 No Sort ==
79 {{{[[SortBy(SortByTest, 1, 1, nosort)]]}}}
80
81 [[SortBy(SortByTest, 1, 1, nosort)]]
82
83 This simply includes the first table on the source page at this point in the
84 page containing the macro. Rows are in the same order as the original table.
85
86 == Column 1 Reverse No Sort ==
87 {{{[[SortBy(SortByTest, 1, 1, reversenosort)]]}}}
88
89 [[SortBy(SortByTest, 1, 1, reversenosort)]]
90
91 Similar to the previous example, but now all rows except the one header row
92 are in reverse order.
93
94 == Column 1 Alphabetical ==
95 {{{[[SortBy(SortByTest, 1, 1, alpha)]]}}}
96
97 [[SortBy(SortByTest, 1, 1, alpha)]]
98
99 In alphabetical order all uppercase letters are sorted before lowercase
100 letters.
101
102 == Column 1 Reverse Alphabetical ==
103 {{{[[SortBy(SortByTest, 1, 1, reversealpha)]]}}}
104
105 [[SortBy(SortByTest, 1, 1, reversealpha)]]
106
107 Prepending 'reverse' to the sort-type changes the order from ascending to
108 descending.
109
110 == Column 1 Case-Insensitive ==
111 {{{[[SortBy(SortByTest, 1, 1, nocase)]]}}}
112
113 [[SortBy(SortByTest, 1, 1, nocase)]]
114
115 Now 'B' and 'b' are considered equal, and are presented in the order they
116 appear in the original table.
117
118 == Column 2 Numerical ==
119 {{{[[SortBy(SortByTest, 1, 2, number)]]}}}
120
121 [[SortBy(SortByTest, 1, 2, number)]]
122
123 As you would expect, 30 comes before 2,000,000. Commas are ignored for
124 sorting. Integers and floating point can both be used, and they sort
125 correctly.
126
127 == Column 2 Alphabetical ==
128 {{{[[SortBy(SortByTest, 1, 2, alpha)]]}}}
129
130 [[SortBy(SortByTest, 1, 2, alpha)]]
131
132 In 'alphabetical' order, 2 comes before 3, so 2,000,000 sorts before 30.
133
134 == Column 2 Reverse Numerical ==
135 {{{[[SortBy(SortByTest, 1, 2, reversenumber)]]}}}
136
137 [[SortBy(SortByTest, 1, 2, reversenumber)]]
138
139 Again, prepending 'reverse' to the sort-type changes the order from ascending
140 to descending.
141
142 == Column 3 Alphabetical ==
143 {{{[[SortBy(SortByTest, 1, 3, alpha)]]}}}
144
145 [[SortBy(SortByTest, 1, 3, alpha)]]
146
147 Wiki formatting is ignored, and the sort is stable. The variations of 'word'
148 are simply listed in the same order as they appear in the original table.
149
150 == Column 3 Numerical ==
151 {{{[[SortBy(SortByTest, 1, 3, number)]]}}}
152
153 [[SortBy(SortByTest, 1, 3, number)]]
154
155 Now 23 comes before 123, and non-numeric values are sorted in alphabetical
156 order as above.
157
158 == Column 1 Alphabetical, Column 2 Numerical, Column 3 Alphabetical ==
159 {{{[[SortBy(SortByTest, 1, 1, alpha, 2, number, 3, alpha)]]}}}
160
161 [[SortBy(SortByTest, 1, 1, alpha, 2, number, 3, alpha)]]
162
163 The main sort is on column 1, but within the duplicate 'A's the 2's and
164 2,000,000 are sorted, and within the 2's the values in column 3 are in
165 case-sensitive alphabetical order.
166
167 == Column 1 Case-Insensitive, Column 2 Numerical, Column 3 Case-Insensitive ==
168 {{{[[SortBy(SortByTest, 1, 1, nocase, 2, number, 3, nocase)]]}}}
169
170 [[SortBy(SortByTest, 1, 1, nocase, 2, number, 3, nocase)]]
171
172 The main sort is still on column 1, but within the case-insensitive duplicate
173 'b's the values in column 2 are in numerical order. Also, within the duplicate
174 A-2 rows, column 3 is case-insensitive, so now Wordy follows word.
175
176 == Zero Header Rows, Case-Insensitive Sort on Column 1 ==
177 {{{[[SortBy(SortByTest, 0, 1, nocase)]]}}}
178
179 [[SortBy(SortByTest, 0, 1, nocase)]]
180
181 The first row is sorted along with all the other rows.
182
183 == Three Header Rows, Case-Insensitive Sort on Column 1 ==
184 {{{[[SortBy(SortByTest, 3, 1, nocase)]]}}}
185
186 [[SortBy(SortByTest, 3, 1, nocase)]]
187
188 The first three rows are not sorted along with all the other rows.
189
190 == Invalid Argument Examples ==
191
192 [[SortBy(NonExistentPage, 1, 1, nocase)]]
193
194 [[SortBy(SortByTest, 1, 1)]]
195
196 [[SortBy(SortByTest, x, 1, alpha)]]
197
198 [[SortBy(SortByTest, 1, x, alpha)]]
199
200 [[SortBy(SortByTest, -1, 1, alpha)]]
201
202 [[SortBy(SortByTest, 10, 1, alpha)]]
203
204 [[SortBy(SortByTest, 1, 0, alpha)]]
205
206 [[SortBy(SortByTest, 1, 10, alpha)]]
207
208 [[SortBy(SortByTest, 1, 1, x)]]
209
210 [[SortBy(SortByTest, 1, 1, alpha, 1)]]
211
212 """
213
214 import sys, re, StringIO, cgi
215 from MoinMoin.Page import Page
216
217
218 class SortByError(Exception):
219 """Raised anywhere in the macro, caught outside the main loop."""
220
221 def __init__(self, msg=''):
222 """msg -- a string to be displayed to the user"""
223 self.msg = msg
224
225
226 def error(msg, args):
227 """Return a message followed by usage information, all in HTML.
228
229 msg -- a string describing the error
230 args -- the macro's original argument string
231
232 """
233 html = """
234 <tt class="wiki">[[SortBy(%s)]]</tt><br>
235 <strong class="error">SortBy: %s</strong><br>
236 <small>%s<br>
237 Valid sort types are: %s</small>"""
238 msg = cgi.escape(msg)
239 usage = cgi.escape("""Usage: SortBy(TablePage, <number of header rows>,
240 <primary sort column number>, <sort type>
241 [, <secondary sort column>, <sort type>] ... )""")
242 sorts = ' '.join(valid_sorts)
243 return html % (args, msg, usage, sorts)
244
245
246 def read_table(page_file):
247 """Read page_file and convert its first table's lines into row data"""
248 file = open(page_file)
249 intable = 0
250 table_rows = []
251 for line in file.xreadlines():
252 if len(line) < 2 or line[0:2] != '||':
253 if intable: break # We're done
254 else: continue # Skip non-table lines until we hit the table
255 else:
256 intable = 1
257 table_rows.append(line[:-1]) # strip the \n while we're at it
258 return table_rows
259
260
261 def strip_fmt(arg):
262 """Remove formatting characters and leading/trailing whitespace.
263
264 Commas (such as in 1,000,000) are considered formatting chars."""
265 for fmt_string in ["'''", "''", '{{{', '}}}', ',']:
266 arg = arg.replace(fmt_string, '')
267 return arg.strip()
268
269
270 def to_number(arg):
271 """Convert arg to int or float if possible, else return a string."""
272 arg = strip_fmt(arg)
273 try: return int(arg)
274 except ValueError:
275 try: return float(arg)
276 except ValueError: return arg
277
278
279 def to_nocase(arg):
280 """Return arg in lowercase with no formatting characters."""
281 return strip_fmt(arg).lower()
282
283
284 def to_nosort(arg, count=[0]):
285 """Return a higher integer each time so rows don't move when sorted."""
286 count[0] += 1 # count is a default arg, so references the same list and
287 return count[0] # incr's the same [0] every time the function is called.
288
289
290 decorate_functions = {'alpha': strip_fmt,
291 'number': to_number,
292 'nocase': to_nocase,
293 'nosort': to_nosort}
294 valid_sorts = decorate_functions.keys()
295 valid_sorts.sort()
296 valid_sorts.extend(['reverse'+sort_name for sort_name in valid_sorts])
297
298
299 def process_args(args, request):
300 """Parse args string, return (sort_page, table, num_headers, sort_list)."""
301 arglist = re.split(r'\s*,\s*', args.strip())
302 if len(arglist) < 4:
303 msg = """Not enough arguments (%s).
304 At least 4 are required.""" % len(arglist)
305 raise SortByError, msg
306 table_page, num_headers = arglist[0:2]
307 try: num_headers = int(num_headers)
308 except ValueError:
309 msg = """Number of header rows (%s)
310 must be an integer""" % num_headers
311 raise SortByError, msg
312 if num_headers < 0:
313 msg = """Number of header rows (%s)
314 must be a positive integer""" % num_headers
315 raise SortByError, msg
316 arglist[:2] = [] # Make arglist contain only column & sort_type pairs
317 if len(arglist)%2 != 0:
318 raise SortByError, 'Odd number of arguments (%s)' % (len(arglist)+2)
319 sort_list = []
320 while arglist:
321
322 # Pop the sort_type and column from the end of the arglist.
323 # They get stored in the sort_list in the opposite order
324 # they were requested so the primary sort is done last.
325
326 sort_type = arglist.pop().lower()
327 sort_column = arglist.pop()
328 try: sort_column = int(sort_column)
329 except ValueError:
330 msg = 'Column number (%s) must be an integer' % sort_column
331 raise SortByError, msg
332 if sort_column < 1:
333 msg = """Column number (%s) must be 1 or higher.
334 1 = leftmost column""" % sort_column
335 raise SortByError, msg
336 sort_column -= 1 # Use zero-based column indexing internally
337 if sort_type[:7] == 'reverse': # If the sort_type begins with 'reverse'
338 reverse = 1 # set the reverse flag
339 sort_type = sort_type[7:] # and strip 'reverse' from sort_type
340 else: reverse = 0
341 sort_list.append((sort_column, sort_type, reverse)) # Append a 3-tuple
342 sort_page = Page(request, table_page)
343 try: table_rows = read_table(sort_page._text_filename())
344 except IOError:
345 raise SortByError, 'Unable to open the table page "%s"' % table_page
346 table = [row.split('||')[1:-1] for row in table_rows]
347 if num_headers > len(table):
348 msg = """Number of header rows (%s) is more than
349 the table length (%s)""" % (num_headers, len(table))
350 raise SortByError, msg
351 return (sort_page, table, num_headers, sort_list)
352
353
354 def sort_table(table, num_headers, sort_column, sort_type, reverse):
355 """Sort of the table (in-place), preserving num_headers rows at the top.
356
357 Arguments:
358 table -- a list of lists representing rows of column entries
359 num_headers -- an integer number of rows to keep at the top of the table
360 sort_column -- the column number (zero-based) containing the sort values
361 sort_type -- which kind of sort to perform on the values being sorted
362 reverse -- 0 or 1, meaning an ascending or descending (reverse) sort
363
364 """
365 header = table[:num_headers]
366 table[:num_headers] = []
367
368 if table and sort_column > min([len(row) for row in table]):
369 msg = """Column number (%s) is higher than
370 the length of one or more rows""" % (sort_column+1)
371 raise SortByError, msg
372 if sort_type not in valid_sorts:
373 raise SortByError, 'Invalid sort type "%s"' % sort_type
374 decorate = decorate_functions[sort_type]
375
376 # Use the 'decorate, sort, undecorate' pattern with ascending or
377 # descending indices to ensure that the sort is stable.
378
379 decorations = [decorate(row[sort_column]) for row in table]
380 if reverse: indices = xrange(len(table), 0, -1)
381 else: indices = xrange(len(table))
382 decorated = zip(decorations, indices, table)
383 decorated.sort()
384 table[:] = [row for (decoration, index, row) in decorated]
385
386 if reverse: table.reverse()
387 table[:0] = header
388 return
389
390
391 def format(sort_page, macro, table):
392 """Format the sorted table and return it as an HTML fragment.
393
394 Arguments:
395 sort_page -- a MoinMoin Page object representing the table page
396 macro -- the macro argument that was provided by MoinMoin
397 table -- a list of lists representing rows of column entries
398
399 """
400 ret = ''
401 table_rows = ['||%s||' % '||'.join(row) for row in table]
402 sort_page.set_raw_body('\n'.join(table_rows), 1) # format the table
403
404 # Here's some tricky stuff copied from Richard Jones' Include macro.
405 stdout = sys.stdout
406 sys.stdout = StringIO.StringIO()
407 sort_page.send_page(content_only=1) ## Removed macro.request argument for compatibility with Moin 1.7 (no longer needed).
408 ret += sys.stdout.getvalue()
409 sys.stdout = stdout
410
411 # Output a helper link to get to the page with the table.
412 name = sort_page.page_name
413 ret += '<small>' + macro.formatter.url(1, name) + \
414 macro.formatter.text('[goto %s]' % name) + macro.formatter.url(0) + \
415 '</small>'
416 return ret
417
418
419 def execute(macro, args):
420 """Parse args, sort_table() repeatedly, return formatted table as HTML.
421
422 Sort the table once for each column & sort_type in the sort_list
423 beginning with the last (and least significant) sort in args
424 because they were 'popped' from the end of the args. Since
425 sort_table() uses a stable sort algorithm, the ordering of the
426 earlier sorts is preserved in the later sorts when there are
427 duplicates of the sort key.
428
429 """
430 try:
431 sort_page, table, num_headers, sort_list = process_args(args, macro.request)
432 for sort_column, sort_type, reverse in sort_list:
433 sort_table(table, num_headers, sort_column, sort_type, reverse)
434 return format(sort_page, macro, table)
435 except SortByError, e: return error(e.msg, args)
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.