Attachment 'hg-export-1.diff'
Download 1 # HG changeset patch
2 # User Sam Morris <sam@robots.org.uk>
3 # Date 1215362207 -3600
4 # Node ID d961ac66ccb331b2ae2751cdb70c2048aa5eb7d7
5 # Parent df0218925ef8a74b5a6887c35da1c71d3b92bf9e
6 Add sorting to DataBrowserWidget by using sorttable.js.
7
8 diff -r df0218925ef8 -r d961ac66ccb3 MoinMoin/widget/browser.py
9 --- a/MoinMoin/widget/browser.py Thu Jul 03 20:49:52 2008 +0200
10 +++ b/MoinMoin/widget/browser.py Sun Jul 06 17:36:47 2008 +0100
11 @@ -7,6 +7,7 @@
12 """
13
14 from MoinMoin.widget import base
15 +from MoinMoin import config
16 from MoinMoin import wikiutil
17
18 class DataBrowserWidget(base.Widget):
19 @@ -108,7 +109,18 @@
20 if havefilters:
21 result.append(fmt.rawHTML('<input type="submit" value="%s" %s>' % (self._filter, self._name('submit'))))
22
23 - result.append(fmt.table(1, id='%stable' % self.data_id))
24 + havesort = False
25 + for col in self.data.columns:
26 + if col.sortable:
27 + havesort = True
28 + break
29 + if havesort:
30 + result.append(fmt.rawHTML('<script type="text/javascript" src="%s/common/js/sorttable.js"></script>' % (config.url_prefix_static)))
31 +
32 + attrs = {'id': '%stable' % (self.data_id)}
33 + if havesort:
34 + attrs['css_class'] = 'sortable'
35 + result.append(fmt.table(1, **attrs))
36
37 # add header line
38 if self._show_header:
39 @@ -117,7 +129,10 @@
40 col = self.data.columns[idx]
41 if col.hidden:
42 continue
43 - result.append(fmt.table_cell(1))
44 + if havesort and not col.sortable:
45 + result.append(fmt.table_cell(1, css_class='sorttable_nosort'))
46 + else:
47 + result.append(fmt.table_cell(1))
48 result.append(fmt.strong(1))
49 result.append(col.label or col.name)
50 result.append(fmt.strong(0))
51 diff -r df0218925ef8 -r d961ac66ccb3 wiki/htdocs/common/js/sorttable.js
52 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
53 +++ b/wiki/htdocs/common/js/sorttable.js Sun Jul 06 17:36:47 2008 +0100
54 @@ -0,0 +1,493 @@
55 +/*
56 + SortTable
57 + version 2
58 + 7th April 2007
59 + Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/
60 +
61 + Instructions:
62 + Download this file
63 + Add <script src="sorttable.js"></script> to your HTML
64 + Add class="sortable" to any table you'd like to make sortable
65 + Click on the headers to sort
66 +
67 + Thanks to many, many people for contributions and suggestions.
68 + Licenced as X11: http://www.kryogenix.org/code/browser/licence.html
69 + This basically means: do what you want with it.
70 +*/
71 +
72 +
73 +var stIsIE = /*@cc_on!@*/false;
74 +
75 +sorttable = {
76 + init: function() {
77 + // quit if this function has already been called
78 + if (arguments.callee.done) return;
79 + // flag this function so we don't do the same thing twice
80 + arguments.callee.done = true;
81 + // kill the timer
82 + if (_timer) clearInterval(_timer);
83 +
84 + if (!document.createElement || !document.getElementsByTagName) return;
85 +
86 + sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/;
87 +
88 + forEach(document.getElementsByTagName('table'), function(table) {
89 + if (table.className.search(/\bsortable\b/) != -1) {
90 + sorttable.makeSortable(table);
91 + }
92 + });
93 +
94 + },
95 +
96 + makeSortable: function(table) {
97 + if (table.getElementsByTagName('thead').length == 0) {
98 + // table doesn't have a tHead. Since it should have, create one and
99 + // put the first table row in it.
100 + the = document.createElement('thead');
101 + the.appendChild(table.rows[0]);
102 + table.insertBefore(the,table.firstChild);
103 + }
104 + // Safari doesn't support table.tHead, sigh
105 + if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0];
106 +
107 + if (table.tHead.rows.length != 1) return; // can't cope with two header rows
108 +
109 + // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as
110 + // "total" rows, for example). This is B&R, since what you're supposed
111 + // to do is put them in a tfoot. So, if there are sortbottom rows,
112 + // for backwards compatibility, move them to tfoot (creating it if needed).
113 + sortbottomrows = [];
114 + for (var i=0; i<table.rows.length; i++) {
115 + if (table.rows[i].className.search(/\bsortbottom\b/) != -1) {
116 + sortbottomrows[sortbottomrows.length] = table.rows[i];
117 + }
118 + }
119 + if (sortbottomrows) {
120 + if (table.tFoot == null) {
121 + // table doesn't have a tfoot. Create one.
122 + tfo = document.createElement('tfoot');
123 + table.appendChild(tfo);
124 + }
125 + for (var i=0; i<sortbottomrows.length; i++) {
126 + tfo.appendChild(sortbottomrows[i]);
127 + }
128 + delete sortbottomrows;
129 + }
130 +
131 + // work through each column and calculate its type
132 + headrow = table.tHead.rows[0].cells;
133 + for (var i=0; i<headrow.length; i++) {
134 + // manually override the type with a sorttable_type attribute
135 + if (!headrow[i].className.match(/\bsorttable_nosort\b/)) { // skip this col
136 + mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/);
137 + if (mtch) { override = mtch[1]; }
138 + if (mtch && typeof sorttable["sort_"+override] == 'function') {
139 + headrow[i].sorttable_sortfunction = sorttable["sort_"+override];
140 + } else {
141 + headrow[i].sorttable_sortfunction = sorttable.guessType(table,i);
142 + }
143 + // make it clickable to sort
144 + headrow[i].sorttable_columnindex = i;
145 + headrow[i].sorttable_tbody = table.tBodies[0];
146 + dean_addEvent(headrow[i],"click", function(e) {
147 +
148 + if (this.className.search(/\bsorttable_sorted\b/) != -1) {
149 + // if we're already sorted by this column, just
150 + // reverse the table, which is quicker
151 + sorttable.reverse(this.sorttable_tbody);
152 + this.className = this.className.replace('sorttable_sorted',
153 + 'sorttable_sorted_reverse');
154 + this.removeChild(document.getElementById('sorttable_sortfwdind'));
155 + sortrevind = document.createElement('span');
156 + sortrevind.id = "sorttable_sortrevind";
157 + sortrevind.innerHTML = stIsIE ? ' <font face="webdings">5</font>' : ' ▴';
158 + this.appendChild(sortrevind);
159 + return;
160 + }
161 + if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) {
162 + // if we're already sorted by this column in reverse, just
163 + // re-reverse the table, which is quicker
164 + sorttable.reverse(this.sorttable_tbody);
165 + this.className = this.className.replace('sorttable_sorted_reverse',
166 + 'sorttable_sorted');
167 + this.removeChild(document.getElementById('sorttable_sortrevind'));
168 + sortfwdind = document.createElement('span');
169 + sortfwdind.id = "sorttable_sortfwdind";
170 + sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾';
171 + this.appendChild(sortfwdind);
172 + return;
173 + }
174 +
175 + // remove sorttable_sorted classes
176 + theadrow = this.parentNode;
177 + forEach(theadrow.childNodes, function(cell) {
178 + if (cell.nodeType == 1) { // an element
179 + cell.className = cell.className.replace('sorttable_sorted_reverse','');
180 + cell.className = cell.className.replace('sorttable_sorted','');
181 + }
182 + });
183 + sortfwdind = document.getElementById('sorttable_sortfwdind');
184 + if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); }
185 + sortrevind = document.getElementById('sorttable_sortrevind');
186 + if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); }
187 +
188 + this.className += ' sorttable_sorted';
189 + sortfwdind = document.createElement('span');
190 + sortfwdind.id = "sorttable_sortfwdind";
191 + sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾';
192 + this.appendChild(sortfwdind);
193 +
194 + // build an array to sort. This is a Schwartzian transform thing,
195 + // i.e., we "decorate" each row with the actual sort key,
196 + // sort based on the sort keys, and then put the rows back in order
197 + // which is a lot faster because you only do getInnerText once per row
198 + row_array = [];
199 + col = this.sorttable_columnindex;
200 + rows = this.sorttable_tbody.rows;
201 + for (var j=0; j<rows.length; j++) {
202 + row_array[row_array.length] = [sorttable.getInnerText(rows[j].cells[col]), rows[j]];
203 + }
204 + /* If you want a stable sort, uncomment the following line */
205 + //sorttable.shaker_sort(row_array, this.sorttable_sortfunction);
206 + /* and comment out this one */
207 + row_array.sort(this.sorttable_sortfunction);
208 +
209 + tb = this.sorttable_tbody;
210 + for (var j=0; j<row_array.length; j++) {
211 + tb.appendChild(row_array[j][1]);
212 + }
213 +
214 + delete row_array;
215 + });
216 + }
217 + }
218 + },
219 +
220 + guessType: function(table, column) {
221 + // guess the type of a column based on its first non-blank row
222 + sortfn = sorttable.sort_alpha;
223 + for (var i=0; i<table.tBodies[0].rows.length; i++) {
224 + text = sorttable.getInnerText(table.tBodies[0].rows[i].cells[column]);
225 + if (text != '') {
226 + if (text.match(/^-?[£$¤]?[\d,.]+%?$/)) {
227 + return sorttable.sort_numeric;
228 + }
229 + // check for a date: dd/mm/yyyy or dd/mm/yy
230 + // can have / or . or - as separator
231 + // can be mm/dd as well
232 + possdate = text.match(sorttable.DATE_RE)
233 + if (possdate) {
234 + // looks like a date
235 + first = parseInt(possdate[1]);
236 + second = parseInt(possdate[2]);
237 + if (first > 12) {
238 + // definitely dd/mm
239 + return sorttable.sort_ddmm;
240 + } else if (second > 12) {
241 + return sorttable.sort_mmdd;
242 + } else {
243 + // looks like a date, but we can't tell which, so assume
244 + // that it's dd/mm (English imperialism!) and keep looking
245 + sortfn = sorttable.sort_ddmm;
246 + }
247 + }
248 + }
249 + }
250 + return sortfn;
251 + },
252 +
253 + getInnerText: function(node) {
254 + // gets the text we want to use for sorting for a cell.
255 + // strips leading and trailing whitespace.
256 + // this is *not* a generic getInnerText function; it's special to sorttable.
257 + // for example, you can override the cell text with a customkey attribute.
258 + // it also gets .value for <input> fields.
259 +
260 + hasInputs = (typeof node.getElementsByTagName == 'function') &&
261 + node.getElementsByTagName('input').length;
262 +
263 + if (node.getAttribute("sorttable_customkey") != null) {
264 + return node.getAttribute("sorttable_customkey");
265 + }
266 + else if (typeof node.textContent != 'undefined' && !hasInputs) {
267 + return node.textContent.replace(/^\s+|\s+$/g, '');
268 + }
269 + else if (typeof node.innerText != 'undefined' && !hasInputs) {
270 + return node.innerText.replace(/^\s+|\s+$/g, '');
271 + }
272 + else if (typeof node.text != 'undefined' && !hasInputs) {
273 + return node.text.replace(/^\s+|\s+$/g, '');
274 + }
275 + else {
276 + switch (node.nodeType) {
277 + case 3:
278 + if (node.nodeName.toLowerCase() == 'input') {
279 + return node.value.replace(/^\s+|\s+$/g, '');
280 + }
281 + case 4:
282 + return node.nodeValue.replace(/^\s+|\s+$/g, '');
283 + break;
284 + case 1:
285 + case 11:
286 + var innerText = '';
287 + for (var i = 0; i < node.childNodes.length; i++) {
288 + innerText += sorttable.getInnerText(node.childNodes[i]);
289 + }
290 + return innerText.replace(/^\s+|\s+$/g, '');
291 + break;
292 + default:
293 + return '';
294 + }
295 + }
296 + },
297 +
298 + reverse: function(tbody) {
299 + // reverse the rows in a tbody
300 + newrows = [];
301 + for (var i=0; i<tbody.rows.length; i++) {
302 + newrows[newrows.length] = tbody.rows[i];
303 + }
304 + for (var i=newrows.length-1; i>=0; i--) {
305 + tbody.appendChild(newrows[i]);
306 + }
307 + delete newrows;
308 + },
309 +
310 + /* sort functions
311 + each sort function takes two parameters, a and b
312 + you are comparing a[0] and b[0] */
313 + sort_numeric: function(a,b) {
314 + aa = parseFloat(a[0].replace(/[^0-9.-]/g,''));
315 + if (isNaN(aa)) aa = 0;
316 + bb = parseFloat(b[0].replace(/[^0-9.-]/g,''));
317 + if (isNaN(bb)) bb = 0;
318 + return aa-bb;
319 + },
320 + sort_alpha: function(a,b) {
321 + if (a[0]==b[0]) return 0;
322 + if (a[0]<b[0]) return -1;
323 + return 1;
324 + },
325 + sort_ddmm: function(a,b) {
326 + mtch = a[0].match(sorttable.DATE_RE);
327 + y = mtch[3]; m = mtch[2]; d = mtch[1];
328 + if (m.length == 1) m = '0'+m;
329 + if (d.length == 1) d = '0'+d;
330 + dt1 = y+m+d;
331 + mtch = b[0].match(sorttable.DATE_RE);
332 + y = mtch[3]; m = mtch[2]; d = mtch[1];
333 + if (m.length == 1) m = '0'+m;
334 + if (d.length == 1) d = '0'+d;
335 + dt2 = y+m+d;
336 + if (dt1==dt2) return 0;
337 + if (dt1<dt2) return -1;
338 + return 1;
339 + },
340 + sort_mmdd: function(a,b) {
341 + mtch = a[0].match(sorttable.DATE_RE);
342 + y = mtch[3]; d = mtch[2]; m = mtch[1];
343 + if (m.length == 1) m = '0'+m;
344 + if (d.length == 1) d = '0'+d;
345 + dt1 = y+m+d;
346 + mtch = b[0].match(sorttable.DATE_RE);
347 + y = mtch[3]; d = mtch[2]; m = mtch[1];
348 + if (m.length == 1) m = '0'+m;
349 + if (d.length == 1) d = '0'+d;
350 + dt2 = y+m+d;
351 + if (dt1==dt2) return 0;
352 + if (dt1<dt2) return -1;
353 + return 1;
354 + },
355 +
356 + shaker_sort: function(list, comp_func) {
357 + // A stable sort function to allow multi-level sorting of data
358 + // see: http://en.wikipedia.org/wiki/Cocktail_sort
359 + // thanks to Joseph Nahmias
360 + var b = 0;
361 + var t = list.length - 1;
362 + var swap = true;
363 +
364 + while(swap) {
365 + swap = false;
366 + for(var i = b; i < t; ++i) {
367 + if ( comp_func(list[i], list[i+1]) > 0 ) {
368 + var q = list[i]; list[i] = list[i+1]; list[i+1] = q;
369 + swap = true;
370 + }
371 + } // for
372 + t--;
373 +
374 + if (!swap) break;
375 +
376 + for(var i = t; i > b; --i) {
377 + if ( comp_func(list[i], list[i-1]) < 0 ) {
378 + var q = list[i]; list[i] = list[i-1]; list[i-1] = q;
379 + swap = true;
380 + }
381 + } // for
382 + b++;
383 +
384 + } // while(swap)
385 + }
386 +}
387 +
388 +/* ******************************************************************
389 + Supporting functions: bundled here to avoid depending on a library
390 + ****************************************************************** */
391 +
392 +// Dean Edwards/Matthias Miller/John Resig
393 +
394 +/* for Mozilla/Opera9 */
395 +if (document.addEventListener) {
396 + document.addEventListener("DOMContentLoaded", sorttable.init, false);
397 +}
398 +
399 +/* for Internet Explorer */
400 +/*@cc_on @*/
401 +/*@if (@_win32)
402 + document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>");
403 + var script = document.getElementById("__ie_onload");
404 + script.onreadystatechange = function() {
405 + if (this.readyState == "complete") {
406 + sorttable.init(); // call the onload handler
407 + }
408 + };
409 +/*@end @*/
410 +
411 +/* for Safari */
412 +if (/WebKit/i.test(navigator.userAgent)) { // sniff
413 + var _timer = setInterval(function() {
414 + if (/loaded|complete/.test(document.readyState)) {
415 + sorttable.init(); // call the onload handler
416 + }
417 + }, 10);
418 +}
419 +
420 +/* for other browsers */
421 +window.onload = sorttable.init;
422 +
423 +// written by Dean Edwards, 2005
424 +// with input from Tino Zijdel, Matthias Miller, Diego Perini
425 +
426 +// http://dean.edwards.name/weblog/2005/10/add-event/
427 +
428 +function dean_addEvent(element, type, handler) {
429 + if (element.addEventListener) {
430 + element.addEventListener(type, handler, false);
431 + } else {
432 + // assign each event handler a unique ID
433 + if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++;
434 + // create a hash table of event types for the element
435 + if (!element.events) element.events = {};
436 + // create a hash table of event handlers for each element/event pair
437 + var handlers = element.events[type];
438 + if (!handlers) {
439 + handlers = element.events[type] = {};
440 + // store the existing event handler (if there is one)
441 + if (element["on" + type]) {
442 + handlers[0] = element["on" + type];
443 + }
444 + }
445 + // store the event handler in the hash table
446 + handlers[handler.$$guid] = handler;
447 + // assign a global event handler to do all the work
448 + element["on" + type] = handleEvent;
449 + }
450 +};
451 +// a counter used to create unique IDs
452 +dean_addEvent.guid = 1;
453 +
454 +function removeEvent(element, type, handler) {
455 + if (element.removeEventListener) {
456 + element.removeEventListener(type, handler, false);
457 + } else {
458 + // delete the event handler from the hash table
459 + if (element.events && element.events[type]) {
460 + delete element.events[type][handler.$$guid];
461 + }
462 + }
463 +};
464 +
465 +function handleEvent(event) {
466 + var returnValue = true;
467 + // grab the event object (IE uses a global event object)
468 + event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
469 + // get a reference to the hash table of event handlers
470 + var handlers = this.events[event.type];
471 + // execute each event handler
472 + for (var i in handlers) {
473 + this.$$handleEvent = handlers[i];
474 + if (this.$$handleEvent(event) === false) {
475 + returnValue = false;
476 + }
477 + }
478 + return returnValue;
479 +};
480 +
481 +function fixEvent(event) {
482 + // add W3C standard event methods
483 + event.preventDefault = fixEvent.preventDefault;
484 + event.stopPropagation = fixEvent.stopPropagation;
485 + return event;
486 +};
487 +fixEvent.preventDefault = function() {
488 + this.returnValue = false;
489 +};
490 +fixEvent.stopPropagation = function() {
491 + this.cancelBubble = true;
492 +}
493 +
494 +// Dean's forEach: http://dean.edwards.name/base/forEach.js
495 +/*
496 + forEach, version 1.0
497 + Copyright 2006, Dean Edwards
498 + License: http://www.opensource.org/licenses/mit-license.php
499 +*/
500 +
501 +// array-like enumeration
502 +if (!Array.forEach) { // mozilla already supports this
503 + Array.forEach = function(array, block, context) {
504 + for (var i = 0; i < array.length; i++) {
505 + block.call(context, array[i], i, array);
506 + }
507 + };
508 +}
509 +
510 +// generic enumeration
511 +Function.prototype.forEach = function(object, block, context) {
512 + for (var key in object) {
513 + if (typeof this.prototype[key] == "undefined") {
514 + block.call(context, object[key], key, object);
515 + }
516 + }
517 +};
518 +
519 +// character enumeration
520 +String.forEach = function(string, block, context) {
521 + Array.forEach(string.split(""), function(chr, index) {
522 + block.call(context, chr, index, string);
523 + });
524 +};
525 +
526 +// globally resolve forEach enumeration
527 +var forEach = function(object, block, context) {
528 + if (object) {
529 + var resolve = Object; // default
530 + if (object instanceof Function) {
531 + // functions have a "length" property
532 + resolve = Function;
533 + } else if (object.forEach instanceof Function) {
534 + // the object implements a custom forEach method so use that
535 + object.forEach(block, context);
536 + return;
537 + } else if (typeof object == "string") {
538 + // the object is a string
539 + resolve = String;
540 + } else if (typeof object.length == "number") {
541 + // the object is array-like
542 + resolve = Array;
543 + }
544 + resolve.forEach(object, block, context);
545 + }
546 +};
547 +
548 # HG changeset patch
549 # User Sam Morris <sam@robots.org.uk>
550 # Date 1215362603 -3600
551 # Node ID cf6f92d98e613e9fa8479dcf8f38b5941d348f85
552 # Parent d961ac66ccb331b2ae2751cdb70c2048aa5eb7d7
553 Replace sorttable's method of hooking into the 'document loaded' event with
554 MoinMoin's addLoadEvent function.
555
556 diff -r d961ac66ccb3 -r cf6f92d98e61 wiki/htdocs/common/js/sorttable.js
557 --- a/wiki/htdocs/common/js/sorttable.js Sun Jul 06 17:36:47 2008 +0100
558 +++ b/wiki/htdocs/common/js/sorttable.js Sun Jul 06 17:43:23 2008 +0100
559 @@ -24,8 +24,6 @@
560 if (arguments.callee.done) return;
561 // flag this function so we don't do the same thing twice
562 arguments.callee.done = true;
563 - // kill the timer
564 - if (_timer) clearInterval(_timer);
565
566 if (!document.createElement || !document.getElementsByTagName) return;
567
568 @@ -335,36 +333,8 @@
569 Supporting functions: bundled here to avoid depending on a library
570 ****************************************************************** */
571
572 -// Dean Edwards/Matthias Miller/John Resig
573 -
574 -/* for Mozilla/Opera9 */
575 -if (document.addEventListener) {
576 - document.addEventListener("DOMContentLoaded", sorttable.init, false);
577 -}
578 -
579 -/* for Internet Explorer */
580 -/*@cc_on @*/
581 -/*@if (@_win32)
582 - document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>");
583 - var script = document.getElementById("__ie_onload");
584 - script.onreadystatechange = function() {
585 - if (this.readyState == "complete") {
586 - sorttable.init(); // call the onload handler
587 - }
588 - };
589 -/*@end @*/
590 -
591 -/* for Safari */
592 -if (/WebKit/i.test(navigator.userAgent)) { // sniff
593 - var _timer = setInterval(function() {
594 - if (/loaded|complete/.test(document.readyState)) {
595 - sorttable.init(); // call the onload handler
596 - }
597 - }, 10);
598 -}
599 -
600 -/* for other browsers */
601 -window.onload = sorttable.init;
602 +// Hook into MoinMoin's method of calling functions when a page loads
603 +addLoadEvent (sorttable.init);
604
605 // written by Dean Edwards, 2005
606 // with input from Tino Zijdel, Matthias Miller, Diego Perini
607 # HG changeset patch
608 # User Sam Morris <sam@robots.org.uk>
609 # Date 1215363020 -3600
610 # Node ID 687aed38de8693827418d63f5ffbac45ec24c323
611 # Parent cf6f92d98e613e9fa8479dcf8f38b5941d348f85
612 Note that a way is needed to prevent sorttable.js from being executed more than
613 once by the browser (say two DataBrowserWidgets are present on a single page).
614
615 diff -r cf6f92d98e61 -r 687aed38de86 MoinMoin/widget/browser.py
616 --- a/MoinMoin/widget/browser.py Sun Jul 06 17:43:23 2008 +0100
617 +++ b/MoinMoin/widget/browser.py Sun Jul 06 17:50:20 2008 +0100
618 @@ -115,6 +115,7 @@
619 havesort = True
620 break
621 if havesort:
622 + # How can we make sure this only happens once per page?
623 result.append(fmt.rawHTML('<script type="text/javascript" src="%s/common/js/sorttable.js"></script>' % (config.url_prefix_static)))
624
625 attrs = {'id': '%stable' % (self.data_id)}
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.