Attachment 'Gallery-082.py'
Download 1 # -*- coding: iso-8859-1 -*-
2 """
3 Gallery.py Version 0.82
4
5 This macro creates dynamic tabulated displays based on attachment contents
6
7 @copyright: 2004 by Simon Ryan <simon<at>smartblackbox.com> http://smartblackbox.com/simon
8 @license: GPL
9
10 Special thanks go to:
11 My beautiful wife Jenny: For keeping the kids at bay long enough for me to code it :-)
12 Adam Shand: For his GallerySoftware feature wish list, support, ideas and suggestions.
13
14 Usage: [[Gallery(key1=value1,key2=value2....)]]
15
16 where the following keys are valid:
17 thumbnailwidth = no of pixels wide to make the thumbnails
18 webnailwidth = width in pixels of the web sized images
19 numberofcolumns = no of columns used in the thumbnail table
20
21 Bugs:
22
23 All attachments are expected to be images
24 Continued rotation will degrade the tmp images (but they can be forced to regen)
25
26 Features:
27
28 Simple usage, just put [[Gallery]] on any page and upload some pictures as attachments
29 Rotate buttons
30 Annotation
31
32 Not yet implemented, but in the works:
33 Comment on this Pic button (to create a moinmoin subpage)
34 Handling of video formats
35 Support for Python Imaging Library
36
37 Speed up:
38 # When you get really sick of how slow the moinmoin image system is,
39 # you can set the following variables in your moin_config.py
40 gallerytempdir (the path to a writable directory)
41 gallerytempurl (the path in your webservers url space where this directory can be read from)
42 eg:
43 gallerytempdir='/var/www/html/nails'
44 gallerytempurl='/nails'
45 or maybe:
46 gallerytempurl=url_prefix+'/nails'
47 # There are other ways of getting speedups for attachment, but this method is the safest (IMHO)
48
49 """
50
51 __author__ = "Simon D. Ryan"
52 __version__ = "0.82"
53
54 from MoinMoin import config, wikiutil
55 import string, cStringIO, os
56 import commands, shutil
57 import moin_config
58
59 class Globs:
60 # A quick place to plonk those shared variables
61 thumbnailwidth='200'
62 webnailwidth='600'
63 numberofcolumns=4
64 adminmsg=''
65 debuglevel=0
66 originals={}
67 convertbin=''
68 annotated={}
69 attachmentdir=''
70 gallerytempdirroot=''
71 gallerytempdir=''
72 gallerytempurl=''
73 pagename=''
74 admin=''
75 bcomp=''
76 baseurl=''
77 timeout=40
78
79 def message(astring,level=1):
80 if level<=Globs.debuglevel:
81 Globs.adminmsg=Globs.adminmsg+'<font color="#FF0000"><strong>Gallery</strong></font>: '+astring+'<br>\n'
82
83 def version():
84 return(' version <b>'+Globs.version+'</b> by Simon D. Ryan.'+\
85 '<br>Copyright 2004 Simon D. Ryan<br>Gallery is a MoinMoin macro and is released under the '+\
86 '<a href="http://www.gnu.org/licenses/gpl.txt">GPL</a>\n'+\
87 '<p>Upload some images as attachments to <a href="'+Globs.baseurl+Globs.pagename+'?action=AttachFile"><b>'+Globs.pagename+'</b></a> and I will generate a gallery for you.')
88
89 # Thanks to denny<at>ece.arizona.edu
90 # This can be replaced with a static translation table to speed things up (later)
91 def mktrans():
92 # Allow only letters and digits and a few other valid file characters
93 alphanumeric=string.letters+string.digits+'.,-_\'!"'
94 source_string=""
95 destination_string=""
96 for i in range(256):
97 source_string=source_string+chr(i)
98 if chr(i) in alphanumeric:
99 destination_string=destination_string+chr(i)
100 else:
101 destination_string=destination_string+' '
102 return string.maketrans(source_string,destination_string)
103
104 def qlink(pagename, querystring, query, description=''):
105 # Returns a hyperlink constructed as a form query on pagename
106 if not description:
107 description=query
108 return '<a href="'+Globs.baseurl+pagename+'?'+querystring+'='+query+Globs.bcomp+'">'+description+'</a>'
109
110 def navibar(target,querystring):
111 # Returns a navigational bar with PREV,THUMBS,NEXT
112 positions=Globs.originals.keys()
113 positions.sort()
114 thumbs='<a href="'+Globs.pagename+'">THUMBS</a>'
115 index=positions.index(target)
116 back,forward='',''
117 if not index==0:
118 # We are not the first so we can provide a back link
119 back=qlink(Globs.pagename, querystring, positions[index-1], 'PREV')
120 if not index==len(positions)-1:
121 # We are not the last so we can provide a forward link
122 forward=qlink(Globs.pagename, querystring, positions[index+1], 'NEXT')
123 return '<table><tr><td>'+back+'</td><td>'+thumbs+'</td><td>'+forward+'</td></tr></table>'
124
125 def toolbar(target,naillevel):
126 if Globs.admin:
127 rotateleft='<input type="submit" name="rotate" value="rotate left">'
128 rotateright='<input type="submit" name="rotate" value="rotate right">'
129 htarget='<input type=hidden value="'+target+'" name="'+naillevel+'">'
130 compat='<input type=hidden value="show" name="action">'
131 return '<form METHOD=POST><table><tr><td>'+rotateleft+'</td><td>'+rotateright+'</td></tr></table>\n'+htarget+compat+'</form>'
132 else:
133 return ''
134
135 def buildnails(items):
136 # For now we use commands.getoutput to do our dirty work
137 # Later we can build a batch job and fork it off.
138
139 # Make sure our temp directory is writable and generate a message if it isn't
140 try:
141 if not os.path.isfile(Globs.gallerytempdir+'/tmp.writetest'):
142 # There is probably a less ugly was to do this using stat (later)
143 open(Globs.gallerytempdir+'/tmp.writetest','w').close()
144 except IOError:
145 message('I had some trouble writing to the temp directory. Is it owned by me and writable?',0)
146
147 # Don't go further if there is a lock in place
148 if os.path.isfile(Globs.attachmentdir+'/tmp.lock'):
149 message("I'm currently busy generating thumbnails and webnails, please try again later.",0)
150 return ''
151
152 # Find the convert binary in standard locations
153 if not os.path.isfile('/usr/bin/convert'):
154 if not os.path.isfile('/usr/X11R6/bin/convert'):
155 message('<b>Please install ImageMagick so I can build thumbnails and webnails</b><p>',0)
156 return
157 else:
158 Globs.convertbin='/usr/X11R6/bin/convert'
159 else:
160 Globs.convertbin='/usr/bin/convert'
161
162 # Create a lock file in the attachments dir so we can always remotely remove it if there is a problem
163 open(Globs.attachmentdir+'/tmp.lock','w').close()
164
165 import time
166 tstart=time.time()
167 pid,pid2='',''
168
169 # For each original file, check for the existance of a nail
170 for item in items:
171 basename,prefix,width=item
172
173 # Check to see if we tarry too long on the road
174 if tstart and (time.time()-tstart) > Globs.timeout:
175 # This is taking waaay too long let us fork and detach else the browser will time out or worse, the webserver may kill us
176 pid = os.fork()
177 if pid != 0:
178 # We are in the parent so we break out
179 message('The thumbnail generation process was taking too long so it has been backgrounded. Please try again later to see the full set of thumbnails',0)
180 break
181 else:
182 # Once we are forked we want to ignore the time
183 tstart=''
184 # Break away from the controlling terminal, so that the web server cannot kill us by killing our parent
185 os.setsid()
186 # Fork again so we can get away without a controlling terminal
187 pid2 = os.fork()
188 if (pid2 != 0):
189 os._exit(0)
190 else:
191 # Close all open file descriptors
192 try:
193 max_fd = os.sysconf("SC_OPEN_MAX")
194 except (AttributeError, ValueError):
195 max_fd = 256
196 for fd in range(0, max_fd):
197 try:
198 os.close(fd)
199 except OSError:
200 pass
201 # Redirect the standard file descriptors to /dev/null
202 os.open("/dev/null", os.O_RDONLY) # stdin
203 os.open("/dev/null", os.O_RDWR) # stdout
204 os.open("/dev/null", os.O_RDWR) # stderr
205
206 # Now we are finally free to continue the conversions as a daemon
207 # If you would like to know more about the above, see:
208 # Advanced Programming in the Unix Environment: W. Richard Stevens
209 # It is also explained in:
210 # Unix Network Programming (Volume 1): W. Richard Stevens
211
212 pathtooriginal='"'+Globs.attachmentdir+'/'+Globs.originals[basename]+'"'
213 # Warning:
214 # Take care if modifying the following line,
215 # you may inadvertantly overwrite your original images!
216 convout=commands.getoutput('%s -geometry %s %s "%s/%s.%s.jpg"' % (Globs.convertbin,width+'x'+width,pathtooriginal,Globs.gallerytempdir,prefix,basename))
217 convout=string.strip(convout)
218 if convout:
219 message(convout)
220
221 if (not pid) and (not pid2):
222 # Release the lock file when finished
223 os.unlink(Globs.attachmentdir+'/tmp.lock')
224
225 # We have built thumbnails so we can deposit an indicator file to prevent rebuilding next time
226 if not os.path.isfile(Globs.attachmentdir+'/delete.me.to.regenerate.thumbnails.and.webnails'):
227 open(Globs.attachmentdir+'/delete.me.to.regenerate.thumbnails.and.webnails','w').close()
228
229
230 def rotate(target,direction):
231 # Rotate the images
232 # Don't go further if there is a lock in place
233 if os.path.isfile(Globs.attachmentdir+'/tmp.lock'):
234 message("I'm currently busy generating thumbnails and webnails. Please try your rotate request again later.",0)
235 return ''
236
237 # Find the correct binary
238 if not os.path.isfile('/usr/bin/mogrify'):
239 if not os.path.isfile('/usr/X11R6/bin/mogrify'):
240 message('<b>Please install ImageMagick so I can build thumbnails and webnails</b><p>',0)
241 return
242 else:
243 Globs.convertbin='/usr/X11R6/bin/mogrify'
244 else:
245 Globs.convertbin='/usr/bin/mogrify'
246
247 # Do the actual rotations
248 if direction=='rotate right':
249 degs='90'
250 else:
251 degs='270'
252 convout=commands.getoutput(Globs.convertbin+' -rotate '+degs+' '+Globs.gallerytempdir+'/tmp.webnail.'+target+'.jpg')
253 convout=commands.getoutput(Globs.convertbin+' -rotate '+degs+' '+Globs.gallerytempdir+'/tmp.thumbnail.'+target+'.jpg')
254 if not os.path.isfile(Globs.gallerytempdir+'/tmp.rotated.'+target+'.jpg'):
255 # Generate from original
256 pathtooriginal=Globs.attachmentdir+'/'+Globs.originals[target]
257 shutil.copy(pathtooriginal,Globs.gallerytempdir+'/tmp.rotated.'+target+'.jpg')
258 convout=commands.getoutput(Globs.convertbin+' -rotate '+degs+' '+Globs.gallerytempdir+'/tmp.rotated.'+target+'.jpg')
259
260 def getannotation(target):
261 # Annotations are stored as a file for now (later to be stored in images)
262 atext=''
263 if Globs.annotated.has_key(target):
264 atext=open(Globs.attachmentdir+'/tmp.annotation.'+target+'.txt').readline()
265 message('was annotated')
266 else:
267 message('was not annotated')
268 # replace double quotes with the html escape so quoted annotations appear
269 return string.replace(atext,'"','"')
270
271 def execute(macro, args):
272
273 Globs.version=__version__
274
275 # Containers
276 formvals={}
277 thumbnails={}
278 webnails={}
279 rotated={}
280
281 # Class variables need to be specifically set
282 # (except for the case where a value is to be shared with another Gallery macro on the same wiki page)
283 Globs.originals={}
284 Globs.annotated={}
285 Globs.attachmentdir=''
286 Globs.admin=''
287 Globs.adminmsg=''
288 Globs.pagename=''
289
290 # process arguments
291 if args:
292 # Arguments are comma delimited key=value pairs
293 sargs=string.split(args,',')
294 for item in sargs:
295 sitem=string.split(item,'=')
296 if len(sitem)==2:
297 key,value=sitem[0],sitem[1]
298 if key=='thumbnailwidth':
299 Globs.thumbnailwidth=value
300 elif key=='webnailwidth':
301 Globs.webnailwidth=value
302 elif key=='numberofcolumns':
303 try:
304 Globs.numberofcolumns=string.atoi(value)
305 except TypeError:
306 pass
307 # Experimental, uncomment at own risk
308 #elif key=='pagename':
309 # Globs.pagename=value
310
311 transtable=mktrans()
312
313 # Useful variables
314 dontregen=''
315 annotationmessage=''
316 textdir=config.text_dir
317 Globs.baseurl=macro.request.getBaseURL()+'/'
318 if not Globs.pagename:
319 Globs.pagename = string.replace(macro.formatter.page.page_name,'/','_2f')
320 # Hmmm. A bug in moinmoin? underscores are getting escaped. These doubly escaped pagenames are even appearing in data/pages
321 pagepath = string.replace(wikiutil.getPagePath(Globs.pagename),'_5f','_')
322 Globs.attachmentdir = pagepath+'/attachments'
323 if hasattr(moin_config,'gallerytempdir') and hasattr(moin_config,'gallerytempurl'):
324 message('gallerytempdir and gallerytempurl found')
325 Globs.gallerytempdirroot=moin_config.gallerytempdir
326 Globs.gallerytempdir=moin_config.gallerytempdir+'/'+Globs.pagename+'/'
327 Globs.gallerytempurl=moin_config.gallerytempurl+'/'+Globs.pagename+'/'
328 elif hasattr(moin_config,'attachments'):
329 Globs.gallerytempdirroot=moin_config.attachments['dir']
330 Globs.gallerytempdir=moin_config.attachments['dir']+'/'+Globs.pagename+'/attachments/'
331 Globs.gallerytempurl=moin_config.attachments['url']+'/'+Globs.pagename+'/attachments/'
332 Globs.attachmentdir = Globs.gallerytempdir
333 else:
334 Globs.gallerytempdir=Globs.attachmentdir
335 Globs.gallerytempurl=Globs.pagename+'?action=AttachFile&do=get&target='
336 if args:
337 args=macro.request.getText(args)
338
339
340 # HTML Constants
341 tleft='<table><tr><td><center>'
342 tmidd='</center></td><td><center>'
343 trigh='</center></td></tr></table>\n'
344 # Add this to the end of each URL to keep some versions of moinmoin happy
345 Globs.bcomp='&action=show'
346
347 # Process any form items into a dictionary (values become unique)
348 for item in macro.form.items():
349 if not formvals.has_key(item[0]):
350 # Here is where we clean the untrusted web input
351 # (sometimes we get foreign keys from moinmoin when the page is edited)
352 try:
353 formvals[item[0]]=string.translate(item[1][0],transtable)
354 except AttributeError:
355 pass
356
357 # Figure out if we have delete privs
358 try:
359 # If a user can delete the page containing the Gallery, then they are considered a Gallery administrator
360 # This probably should be configurable via a moin_config variable eg: galleryadminreq = <admin|delete|any>
361 if macro.request.user.may.delete(macro.formatter.page.page_name):
362 Globs.admin='true'
363 except AttributeError:
364 pass
365
366 out=cStringIO.StringIO()
367
368 # Grab a list of the files in the attachment directory
369 if os.path.isdir(Globs.attachmentdir):
370 if Globs.gallerytempdir==Globs.attachmentdir:
371 afiles=os.listdir(Globs.attachmentdir)
372 else:
373 if not os.path.isdir(Globs.gallerytempdir):
374 # Try to create it if it is absent
375 spagename=string.split(Globs.pagename,'/')
376 compbit=''
377 for component in spagename:
378 compbit=compbit+'/'+component
379 os.mkdir(Globs.gallerytempdirroot+compbit)
380 #os.mkdir(Globs.gallerytempdir)
381 if os.path.isdir(Globs.gallerytempdir):
382 afiles=os.listdir(Globs.attachmentdir)+os.listdir(Globs.gallerytempdir)
383 else:
384 message('You need to create the temp dir first:'+Globs.gallerytempdir,0)
385 return macro.formatter.rawHTML(
386 Globs.adminmsg+'<p>')
387
388 # Split out the thumbnails and webnails
389 for item in afiles:
390 if item.startswith('tmp.thumbnail.'):
391 origname=item[14:-4]
392 thumbnails[origname]=''
393 elif item.startswith('tmp.webnail.'):
394 origname=item[12:-4]
395 webnails[origname]=''
396 elif item.startswith('tmp.rotated.'):
397 origname=item[12:-4]
398 rotated[origname]=''
399 elif item.startswith('tmp.annotation.'):
400 origname=item[15:-4]
401 Globs.annotated[origname]=''
402 elif item == 'delete.me.to.regenerate.thumbnails.and.webnails':
403 dontregen='true'
404 elif item == 'tmp.writetest' or item == 'tmp.lock':
405 pass
406 else:
407 # This must be one of the original images
408 lastdot=string.rfind(item,'.')
409 origname=item[:lastdot]
410 Globs.originals[origname]=item
411 else:
412 message(version(),0)
413 return macro.formatter.rawHTML( Globs.adminmsg )
414
415 if not Globs.gallerytempdir==Globs.attachmentdir and os.path.isfile(Globs.attachmentdir+'/tmp.writetest'):
416 # If we are using the new gallerytempdir and we were using the old system then make sure there are no
417 # remnant files from the old system in the attachment dir to confuse us
418 message('You have changed to using a gallerytempdir so I am cleaning old tmp files from your attachment dir.',0)
419 for item in webnails.keys():
420 try:
421 os.unlink(Globs.attachmentdir+'/tmp.webnail.'+item+'.jpg')
422 except:
423 pass
424 # Try deleting any old thumbnails which may be in the attachment directory
425 for item in thumbnails.keys():
426 try:
427 os.unlink(Globs.attachmentdir+'/tmp.thumbnail.'+item+'.jpg')
428 except:
429 pass
430 # Try deleting any old rotated originals which may be in the attachment directory
431 for item in rotated.keys():
432 try:
433 os.unlink(Globs.attachmentdir+'/tmp.rotated.'+item+'.jpg')
434 except:
435 pass
436 os.unlink(Globs.attachmentdir+'/tmp.writetest')
437
438 newnails=[]
439 # Any thumbnails need to be built?
440 for key in Globs.originals.keys():
441 if (not thumbnails.has_key(key)) or (not dontregen):
442 # Create a thumbnail for this original
443 newnails.append((key,'tmp.thumbnail',Globs.thumbnailwidth))
444 # Any webnails need to be built?
445 for key in Globs.originals.keys():
446 if (not webnails.has_key(key)) or (not dontregen):
447 # Create a webnail for this original
448 newnails.append((key,'tmp.webnail',Globs.webnailwidth))
449 # Ok, lets build them all at once
450 if not len(newnails)==0:
451 buildnails(newnails)
452
453 # If a regen of thumbnails and webnails has occurred, then we should also delete any tmp.rotated files.
454 if not dontregen:
455 for key in rotated.keys():
456 # Wrapped in a try except since child processes may try to unlink a second time
457 try:
458 os.unlink(Globs.gallerytempdir+'/tmp.rotated.'+key+'.jpg')
459 except:
460 pass
461
462 if formvals.has_key('annotate'):
463 if Globs.admin and formvals.has_key('target'):
464 target=formvals['target']
465 # Write an annotation file
466 atext=string.replace(formvals['annotate'],'"','"')
467 target=formvals['target']
468 ouf=open(Globs.attachmentdir+'/tmp.annotation.'+target+'.txt','w')
469 ouf.write(atext)
470 ouf.close()
471 message('Annotation updated to <i>'+atext+'</i>',0)
472 # Now update the annotated dictionary
473 if not Globs.annotated.has_key(target):
474 Globs.annotated[target]=''
475
476 if formvals.has_key('webnail'):
477 # Does the webnail exist?
478 message('webnail requested')
479 target=formvals['webnail']
480 if Globs.originals.has_key(target):
481 out.write(navibar(target,'webnail'))
482 if formvals.has_key('rotate'):
483 direction=formvals['rotate']
484 message(direction)
485 rotate(target,direction)
486 # Put things in a table
487 out.write(tleft)
488 # Lets build up an image tag
489 out.write('<a href="'+Globs.baseurl+Globs.pagename+'?original='+target+'&action=content"><img src="'+Globs.gallerytempurl+'tmp.webnail.'+target+'.jpg"></a>\n')
490 out.write(trigh)
491 out.write(tleft)
492
493 atext=getannotation(target)
494
495 # Are we an administrator?
496 if Globs.admin:
497 # We always provide an annotation text field
498 out.write('<form action='+Globs.pagename+' name=annotate METHOD=POST>')
499 out.write('<input maxLength=256 size=55 name=annotate value="'+atext+'">')
500 out.write('<input type=hidden value="'+target+'" name="target">')
501 out.write('<input type=hidden value="show" name="action">')
502 out.write('<input type=hidden value="'+target+'" name="webnail">')
503 out.write('</form>')
504 else:
505 out.write(atext)
506 out.write(trigh)
507 out.write(toolbar(target,'webnail'))
508
509 else:
510 message('I do not have file: '+target,0)
511 elif formvals.has_key('original'):
512 # Now we just construct a single item rather than a table
513 # Does the webnail exist?
514 message('original requested')
515 target=formvals['original']
516 if not Globs.originals.has_key(target):
517 message('I do not have file: '+target,0)
518 else:
519 if formvals.has_key('rotate'):
520 direction=formvals['rotate']
521 message(direction)
522 rotate(target,direction)
523 # Lets build up an image tag
524 out.write(navibar(target,'original'))
525 out.write(tleft)
526 originalfilename=Globs.originals[target]
527 # If there is a rotated version, show that instead
528 if rotated.has_key(target):
529 out.write('<a href="'+Globs.baseurl+Globs.pagename+'?webnail='+target+Globs.bcomp+'"><img src="'+Globs.gallerytempurl+'tmp.rotated.'+target+'.jpg"></a>\n')
530 else:
531 out.write('<a href="'+Globs.baseurl+Globs.pagename+'?webnail='+target+Globs.bcomp+'"><img src="'+Globs.pagename+'?action=AttachFile&do=get&target='+originalfilename+'"></a>\n')
532 out.write(trigh)
533 out.write(tleft)
534
535 atext=getannotation(target)
536
537 # Are we an administrator?
538 if Globs.admin:
539 # We always provide an annotation text field
540 out.write('<form action='+Globs.pagename+' name=annotate METHOD=POST>')
541 out.write('<input maxLength=256 size=55 name=annotate value="'+atext+'">')
542 out.write('<input type=hidden value="'+target+'" name="target">')
543 out.write('<input type=hidden value="show" name="action">')
544 out.write('<input type=hidden value="'+target+'" name="original">')
545 out.write('</form>')
546 else:
547 out.write(atext)
548 out.write(trigh)
549 out.write(toolbar(target,'original'))
550
551 elif formvals.has_key('rotate'):
552 # We rotate all sizes of this image to the left or right
553 message('rotate requested')
554 target=formvals['target']
555 direction=formvals['rotate']
556 if not Globs.originals.has_key(target):
557 message('I do not have file: '+target,0)
558 else:
559 # Do the rotation
560 rotate(target,direction)
561 # Display the new image in webnail mode
562 # We may need a way of forcing the browser to reload the newly rotated image here (later)
563 out.write(tleft)
564 out.write('<a href="'+Globs.baseurl+Globs.pagename+'?webnail='+target+Globs.bcomp+'"><img src="'+Globs.gallerytempurl+'tmp.webnail.'+target+'.jpg"></a>\n')
565 out.write(trigh)
566
567 else:
568 # Finally lets build a table of thumbnails
569 thumbs=Globs.originals.keys()
570 thumbs.sort()
571 thumbs.reverse()
572 # If version number is requested (append a ?version=tellme&action=show to the page request)
573 # or if there are no original images, just give help message and return
574 if formvals.has_key('version') or len(thumbs)==0:
575 message(version(),0)
576 return macro.formatter.rawHTML( Globs.adminmsg )
577 out.write('\n<table>')
578 cease=''
579 rollover=''
580 while 1:
581 out.write('<tr>')
582 for i in range(Globs.numberofcolumns):
583 try:
584 item=thumbs.pop()
585 except IndexError:
586 cease='true'
587 break
588
589 # Alt text
590 atext=getannotation(item)
591 rollover='alt="'+atext+'" title="'+atext+'"'
592
593 # Table entry for thumbnail image
594 out.write('<td><a href="'+Globs.baseurl+Globs.pagename+'?webnail='+item+Globs.bcomp+'"><center><img src="'+Globs.gallerytempurl+'tmp.thumbnail.'+item+'.jpg" '+rollover+'></a></center></td>')
595 out.write('</tr>\n')
596 if cease:
597 out.write('</table>')
598 break
599
600 out.seek(0)
601 # Finally output any administrative messages at the top followed by any generated content
602 return macro.formatter.rawHTML(
603 Globs.adminmsg+'<p>'
604 +out.read()
605 )
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.