Attachment 'PageComment2-099-rg.py'
Download 1 # -*- coding: iso-8859-1 -*-
2 """
3 PageComment2.py Version 0.98.1 April 25, 2006
4
5 This macro gives a form to post a new comment to the page and shows a list of the posted comments.
6
7 @copyright: 2005 by Seungik Lee <seungiklee<at>gmail.com> http://www.silee.net/
8 @license: GPL
9
10 Usage: [[PageComment2]]
11
12 Features:
13
14 - Simple usage, just put [[PageComment2]] on any page.
15 - Lets anonymous users post a new comment with an input form.
16 - Shows a list of the posted comments.
17 - Support for comment deletion by given password.
18 - Support for administrative action, e.g.,
19 - to delete a comment without entering a given password
20
21 Parameters:
22
23 - pagename: the page name which the comments are retrieved for. by default the page itself.
24 If the user has no 'read' ACL for that page, it does not allow to insert/view comments.
25 e.g., pagename=AnotherPage
26
27 - section: the section name of the page. The comments in different sections are managed in separated sub pages.
28 Section name should be alphanumeric format ([a-zA-Z0-9] in regular expression).
29 If not, all the non-alphanumric characters are removed.
30 e.g., section=1, section=News, section=Opinion
31
32 - inputonly: shows input form only. list of the comments are shown to admin users only.
33 - inputonly=0; default, all list is shown to all users including anonymous users
34 - inputonly=1; shown to admin users only (who has the page delete privilege)
35
36 - commentonly: shows the list of comments only.
37 - commentonly=0; default, both of the list and input form will be shown
38 - commentonly=1; only the list of comments will be shown
39
40 - countonly: returns the number of the comments posted to this page
41 - countonly=0; default, normal form (input form; list of comments)
42 - countonly=1; just return the number of comments.
43 e.g., 'There are [[PageComments(countonly=1)]] comments here'
44
45 - rows: the # of rows of the textarea. default 4. e.g., rows=4
46
47 - cols: the # of columns of the textarea. default 60. e.g., cols=60
48
49 - maxlength: limitation on # of characters for comment text. default 0 (no limit). e.g., maxlength=500
50
51 - newerfirst: order of the list of comments.
52 - newerfirst=0: default, newer ones are listed at the end
53 - newerfirst=1: newer ones are listed at the top
54
55 - commentfirst: shows comment list before the input form.
56 - commentfirst=0: default, the input form first
57 - commentfirst=1: comment list first
58
59 - articleview: shows comment list in an article view.
60 - articleview=0: default, list in table view
61 - articleview=1: list in article view
62
63 - tablewidth: the width of the table format for PageComment2, default '' (none).
64 e.g., tablewidth=600, tablewidth=100%
65
66 - smileylist: shows smiley options with drop-down list box
67 - smileylist=0: default, a part of the smiley in radio button
68 - smileylist=1: smiley in drop-down list box
69
70 - nosmiley: shows no smiley
71 - nosmiley=0: default, shows smiley selection
72 - nosmiley=1: no smiley selection
73
74 - notify: notifies to the subscribers of the page which includes the macro when a comment is added
75 - notify=0: default, notification disabled
76 - notify=1: notification enabled
77
78 - encryptpass: encrypts entered password
79 - encryptpass=0: default, the password is stored in plain text
80 - encryptpass=1: the password is stored in encrypted format
81
82 - markup: enables wiki markup in the comment text except some specified macros.
83 - markup=0: default, use of wiki markup in the text is disabled
84 - markup=1: use of wiki markup in the text is enabled and preview button is activated
85
86 - userorip:
87 - userorip=0: default, anonymous users can input their name and password
88 - userorip=1: valid users are shown under their name, anonymous users under their IP address
89
90 Change Log
91
92 - May 1, 2007 - Version 0.99-rg
93 - added userorip parameter
94 - gettextize several important messages
95
96 - April 17, 2006 - Version 0.98
97 - fixed a bug on revision history
98 - added a despam action
99
100 - Jan. 05, 2006 - Version 0.97
101 - added features:
102 - mail notification
103 - password encryption
104 - wiki markup support with preview
105 - remember author name last used
106 - administrative actions (delete without password) are allowed to those who has WRITE acl.
107
108 - Nov. 29, 2005 - Version 0.96
109 - some format parameters are added
110 - random password feature is added
111
112 - Nov. 20, 2005 - Version 0.95
113 - some minor bugs are fixed
114
115 - Nov. 20, 2005 - Version 0.94
116 - some parameters are added
117 - some minor bugs are fixed
118
119 - Nov. 19, 2005 - Version 0.92
120 - some minor bugs are fixed
121 - 'olderfirst' parameter replaced with 'newerfirst'
122
123 - Nov. 19, 2005 - Version 0.91
124 - some parameters are added
125 - validates smiley markup
126 - modified view
127
128 - Nov. 18, 2005 - Version 0.90 (Release 2)
129 - No text data file support any more: Comment is stored in the sub wiki page.
130 - (does not compatible with Release 1: PageComment.py)
131 - Custom icon (smiley) can be inserted
132 - Pre-fill the name input field with his/her login name
133 - Logs at add/remove comments
134 - Added some parameters
135
136 - Oct. 08, 2005 - Version 0.82
137 - Changed the directory the data file stored to be secured
138
139 - Oct. 07, 2005 - Version 0.81
140 - Unicode encoding related bugs in deletecomment function are patched.
141 - Instruction bugs are patched.
142
143 - Oct. 06, 2005 - Version 0.80
144 - The initial version is released.
145
146
147 Notes
148
149 - 'Gallery.py' developed by Simon Ryan has inspired this macro.
150 - Thanks to many of the MoinMoin users for valuable comments.
151 - Visit http://moinmoin.wikiwikiweb.de/MacroMarket/PageComment2 for more detail
152
153 """
154
155 from MoinMoin import config, wikiutil
156 import StringIO, time, re
157 from MoinMoin.Page import Page
158 from MoinMoin.PageEditor import PageEditor
159 from MoinMoin.parser import wiki
160
161
162 class Globs:
163 # A quick place to plonk those shared variables
164
165 adminmsg = ''
166 datapagename = ''
167 pagename = ''
168 curpagename = ''
169 cursubname = ''
170 admin = ''
171 macro = ''
172 defaultacl = ''
173 defaulticon = ''
174 formid = 0
175 smileys = []
176
177 class Params:
178
179 rows = 0
180 cols = 0
181 maxlength = 0
182 newerfirst = 0
183 tablewidth = ''
184 commentfirst = 0
185 pagename = ''
186 commentonly = 0
187 inputonly = 0
188 countonly = 0
189 section = ''
190 articleview = 0
191 notify = 0
192 encryptpass = 0
193 markup = 0
194 userorip = 0
195
196 def execute(macro, args):
197
198 # INITIALIZATION ----------------------------------------
199 getparams(args)
200 setglobalvalues(macro)
201
202 # internal variables
203 request = macro.request
204 _ = request.getText
205
206 if not Globs.pagename == Globs.curpagename:
207 if not macro.request.user.may.read(Globs.pagename):
208 return macro.formatter.rawHTML(u'PageComment: %s' % _('You are not allowed to view this page.'))
209 elif not Page(request, Globs.pagename).exists():
210 return macro.formatter.rawHTML(u'PageComment: %s' % _('This page is already deleted or was never created!'))
211
212
213 if Params.countonly:
214 html = len(fetchcomments())
215 return macro.formatter.rawHTML('%s' % html)
216
217 datapagename = Globs.datapagename
218
219 # form vals
220 comicon = Globs.defaulticon
221 comauthor = ''
222 comtext = ''
223 compasswd = ''
224 comrev = 0
225 comautopass = ''
226 commentpreview = ''
227 commarkup = ''
228
229 addcommand = u'addcomment%d' % Globs.formid
230 delcommand = u'delcomment%d' % Globs.formid
231
232 action = macro.form.get('commentaction', [''])[0]
233
234 if action == addcommand:
235
236 # process form input for comment add
237 form_fields = {'comicon': Globs.defaulticon, 'comauthor': '', 'comtext': '', 'compasswd': '', 'comrev': 0, 'autopasswd': '', 'button_save': '', 'button_preview': '', 'commarkup%d' % Globs.formid: '0'}
238 required_fields = {'comauthor': _('Name'), 'comtext': _('Text'), 'compasswd': _('Password'), 'comrev': 'Rev. #'}
239
240 formvals, missingfields = getforminput(macro.form, form_fields, required_fields)
241
242 comicon = formvals['comicon']
243 comauthor = formvals['comauthor']
244 comtext = formvals['comtext']
245 compasswd = formvals['compasswd']
246 comrev = int(formvals['comrev'])
247 comautopass = formvals['autopasswd']
248 btnsave = formvals['button_save']
249 btnpreview = formvals['button_preview']
250 commarkup = formvals['commarkup%d' % Globs.formid]
251
252 if not len(missingfields) == len(required_fields):
253 if not missingfields:
254
255 curauthor = ''
256 if macro.request.user.valid:
257 curauthor = macro.request.user.name
258 elif Params.userorip:
259 curauthor = macro.request.remote_addr
260 else:
261 curauthor = comauthor
262
263 # we should perhaps test here if curauthor is the same as comauthor from the form
264 comauthor = curauthor
265
266 # check input
267 if comicon and (not comicon in config.smileys.keys()):
268 message('Please use smiley markup only')
269
270 elif Params.maxlength and (len(comtext) > Params.maxlength):
271 message('Comment text is limited to %d characters. (%d characters now)' % (Params.maxlength, len(comtext)) )
272
273 elif not comtext.strip() or comtext == _('Add your comment'):
274 message(_('Please fill the comment text'))
275
276 ## PREVIEW
277 elif btnpreview:
278 commentpreview = previewcomment(comicon, comauthor, comtext, commarkup)
279
280 ## ADD
281 elif btnsave:
282 flag = addcomment(macro, comicon, comauthor, comtext, compasswd, comrev, comautopass, commarkup)
283
284 if flag:
285 comicon = Globs.defaulticon
286 comauthor = ''
287 comtext = ''
288 compasswd = ''
289 comrev = 0
290 commentpreview = ''
291 commarkup = ''
292
293 ## ERROR
294 else:
295 message( 'What do you want?' )
296
297 else:
298 message( _('Required attribute "%(attrname)s" missing') % { 'attrname': u', '.join(missingfields) } )
299
300 elif action == delcommand:
301
302 # process form input for comment delete
303 form_fields = {'delkey': '', 'delpasswd': ''}
304 required_fields = {'delkey': 'Comment Key', 'delpasswd': 'Password'}
305
306 formvals, missingfields = getforminput(macro.form, form_fields, required_fields)
307
308 delkey = formvals['delkey']
309 delpasswd = formvals['delpasswd']
310
311 if not len(missingfields) == len(required_fields):
312 if not missingfields:
313 deletecomment(macro, delkey, delpasswd)
314 else:
315 message( _('Required attribute "%(attrname)s" missing') % { 'attrname': u', '.join(missingfields) } )
316
317 # format output
318 html = []
319
320 html.append(u'<div id="pagecomment">')
321 html.append(u'<a name="pagecomment%d"></a>' % Globs.formid)
322
323 html.append(u'<table border="0" class="pagecomment" %s>' % Params.tablewidth)
324
325 if Globs.adminmsg:
326 html.append(u'<tr><td colspan="5" style="border-width: 0px;">')
327 html.append(u'<font color="#aa0000">%s</font>' % Globs.adminmsg)
328 html.append(u'</td></tr>')
329
330 commentlisthtml = showcommentsection()
331 commentformhtml = commentformsection(comauthor, comtext, compasswd, comicon, comrev, comautopass, commarkup)
332
333 if Params.commentfirst:
334 if commentpreview:
335 html.append(commentpreview)
336
337 html.append(commentlisthtml)
338 html.append(u'<tr><td colspan="5" class="commentblankline" style="border-width: 0px; height: 20px;"></td></tr>')
339 html.append(commentformhtml)
340 else:
341 html.append(commentformhtml)
342 html.append(u'<tr><td colspan="5" class="commentblankline" style="border-width: 0px; height: 20px;"></td></tr>')
343 if commentpreview:
344 html.append(commentpreview)
345
346 html.append(commentlisthtml)
347
348 if Globs.debugmsg:
349 html.append(u'<tr><td colspan="5" style="border-width: 0px;">')
350 html.append(u'<font color="#aa0000">%s</font>' % Globs.debugmsg)
351 html.append(u'</td></tr>')
352
353 html.append(u'</table>')
354
355 if Globs.customscript:
356 html.append(u'%s' % Globs.customscript)
357
358 html.append(u'</div>')
359
360 return macro.formatter.rawHTML(u'\n'.join(html))
361
362
363 def commentformsection(comauthor, comtext, compasswd, comicon, comrev, autopass, commarkup):
364 html = []
365
366 if not Params.commentonly:
367 html.append(u'<tr><td style="border-width: 1px; margin: 10px 0 10px 0;" colspan="5">')
368 #html.append(u'<table class="commentform"><tr><td style="border-width: 1px;">')
369 html.append(commentform(comauthor, comtext, compasswd, comicon, comrev, autopass, commarkup))
370 #html.append(u'</td></tr></table>')
371 html.append(u'</td></tr>')
372
373 return u'\n'.join(html)
374
375
376 def showcommentsection():
377 html = []
378 if (not Params.inputonly) or Globs.admin:
379 html.append(deleteform())
380 html.append(showcomment())
381 else:
382 html.append(u'<tr><td style="text-align: center; border: 0px; font-size: 0.8em; color: #aaaaaa;">(The posted comments are shown to administrators only.)</td></tr>')
383
384 return u'\n'.join(html)
385
386 def getforminput(form, inputfields, requiredfields):
387
388 formvals = {}
389 missingfields = []
390
391 for item in inputfields.keys():
392 formvals[item] = form.get(item, [inputfields[item]])[0]
393 if (not formvals[item]) and (item in requiredfields):
394 missingfields.append(requiredfields[item])
395
396 return formvals, missingfields
397
398 def getparams(args):
399 # process arguments
400
401 params = {}
402 if args:
403 # Arguments are comma delimited key=value pairs
404 sargs = args.split(',')
405
406 for item in sargs:
407 sitem = item.split('=')
408
409 if len(sitem) == 2:
410 key, value = sitem[0], sitem[1]
411 params[key.strip()] = value.strip()
412
413 Params.pagename = params.get('pagename', '')
414
415 Params.section = params.get('section', '')
416 if Params.section:
417 Params.section = getescapedsectionname(Params.section)
418
419 try:
420 Params.inputonly = int(params.get('inputonly', 0))
421 except ValueError:
422 Params.inputonly = 0
423
424 try:
425 Params.commentonly = int(params.get('commentonly', 0))
426 except ValueError:
427 Params.commentonly = 0
428
429 try:
430 Params.countonly = int(params.get('countonly', 0))
431 except ValueError:
432 Params.countonly = 0
433
434 try:
435 Params.newerfirst = int(params.get('newerfirst', 0))
436 except ValueError:
437 Params.newerfirst = 0
438
439 try:
440 Params.commentfirst = int(params.get('commentfirst', 0))
441 except ValueError:
442 Params.commentfirst = 0
443
444 try:
445 Params.articleview = int(params.get('articleview', 0))
446 except ValueError:
447 Params.articleview = 0
448
449 try:
450 Params.smileylist = int(params.get('smileylist', 0))
451 except ValueError:
452 Params.smileylist = 0
453
454 try:
455 Params.nosmiley = int(params.get('nosmiley', 0))
456 except ValueError:
457 Params.nosmiley = 0
458
459 try:
460 Params.rows = int(params.get('rows', 4))
461 except ValueError:
462 Params.rows = 4
463
464 try:
465 Params.cols = int(params.get('cols', 60))
466 except ValueError:
467 Params.cols = 60
468
469 try:
470 Params.maxlength = int(params.get('maxlength', 0))
471 except ValueError:
472 Params.maxlength = 0
473
474 try:
475 Params.notify = int(params.get('notify', 0))
476 except ValueError:
477 Params.notify = 0
478
479 try:
480 Params.encryptpass = int(params.get('encryptpass', 0))
481 except ValueError:
482 Params.encryptpass = 0
483
484 try:
485 Params.markup = int(params.get('markup', 0))
486 except ValueError:
487 Params.markup = 0
488
489 try:
490 Params.userorip = int(params.get('userorip', 0))
491 except ValueError:
492 Params.userorip = 0
493
494
495 Params.tablewidth = params.get('tablewidth', '')
496 if Params.tablewidth:
497 Params.tablewidth = ' width="%s" ' % Params.tablewidth
498
499 def setglobalvalues(macro):
500
501 # Global variables
502 Globs.macro = macro
503 Globs.defaultacl = u'#acl All:'
504 Globs.adminmsg = ''
505 Globs.debugmsg = ''
506 Globs.customscript = ''
507 Globs.defaulticon = ''
508 request = macro.request
509
510 # ADD SMILEYS HERE TO BE USED:
511 Globs.smileys = [':)', ':))', ':(', ';)', ':\\', '|)', 'X-(', 'B)']
512
513 if Params.markup:
514
515 # ADD MACROS HERE TO ALLOW TO BE USED IN THE TEXT:
516 Globs.macroallowed = [ 'BR', 'Date', 'DateTime', 'MailTo', 'Icon' ]
517
518 from MoinMoin import wikimacro
519 macronames = wikimacro.getNames(request.cfg)
520
521 for names in Globs.macroallowed:
522 macronames.remove(names)
523
524 # ADD REGEX PATTERN HERE TO MAKE IT FORBIDDEN TO USE IN MARKUP:
525 Globs.markupforbidden = {
526 #ur'(^\s*)((?P<hmarker>=+)\s.*\s(?P=hmarker))( $)': r'\1`\2`\4',
527 #ur'(?P<rule>-{4,})': r'`\1`',
528 ur'(?P<macro>\[\[(%(macronames)s)(?:\(.*?\))?\]\])' % { 'macronames': u'|'.join(macronames) } : r'`\1`'
529 }
530
531 Globs.curpagename = macro.formatter.page.page_name
532
533 if Params.pagename:
534 Globs.pagename = Params.pagename
535 else:
536 Globs.pagename = Globs.curpagename
537
538 Globs.cursubname = Globs.curpagename.split('/')[-1]
539 Globs.datapagename = u'%s/%s%s' % (Globs.pagename, 'PageCommentData', Params.section)
540
541 try:
542 #if request.user.may.delete(Globs.pagename):
543 if request.user.may.write(Globs.pagename):
544 Globs.admin = 'true'
545 else:
546 Globs.admin = ''
547 except AttributeError:
548 Globs.admin = ''
549 pass
550
551 # set form id
552
553 if not hasattr(request, 'pgformid'):
554 request.pgformid = 0
555
556 request.pgformid += 1
557 Globs.formid = request.pgformid
558
559
560 def message(astring):
561 Globs.adminmsg = u'PageComment: %s\n' % astring
562
563 def debug(astring):
564 Globs.debugmsg += u'%s\n<br>' % astring
565
566
567 def commentform(tmpauthor, tmptext, tmppasswd, tmpicon, comrev, tmpautopass, tmpmarkup):
568 # A form for posting a new comment
569 request = Globs.macro.request
570 datapagename = Globs.datapagename
571 _ = request.getText
572
573 cellstyle = u'border-width: 0px; vertical-align: middle; font-size: 0.9em;'
574
575 pg = Page( request, datapagename )
576
577 if pg.exists():
578 comrev = pg.current_rev()
579 else:
580 comrev = 0
581
582 if not Params.nosmiley:
583 if not Params.smileylist:
584 iconlist = getsmileymarkupradio(tmpicon)
585 else:
586 iconlist = getsmileymarkuplist(tmpicon)
587 else:
588 iconlist = ''
589
590 initName = tmpauthor
591 initPass = ''
592 initText = ''
593
594 if request.user.valid:
595 initName = request.user.name
596
597 if not (request.user.valid or tmpauthor):
598
599 tmpauthor = getAuthorFromCookie()
600
601 if not tmpauthor and not Params.userorip:
602
603 import socket
604 host = request.remote_addr
605
606 try:
607 hostname = socket.gethostbyaddr(host)[0]
608 except socket.error:
609 hostname = host
610
611 tmpauthor = hostname.split('.')[0]
612
613 if not tmpauthor and Params.userorip:
614 tmpauthor = request.remote_addr
615
616 initName = tmpauthor
617
618 if not tmppasswd:
619 tmppasswd = nicepass()
620 initPass = tmppasswd
621 elif tmpautopass and tmpautopass == tmppasswd:
622 tmppasswd = nicepass()
623 initPass = tmppasswd
624
625 if not tmptext:
626 tmptext = _('Add your comment')
627 initText = tmptext
628 elif tmptext and tmptext == _('Add your comment'):
629 initText = tmptext
630
631 previewbutton = ''
632 markupcheckbox = ''
633
634 if Params.markup:
635 if not (tmpmarkup == '0'):
636 markupchecked = "checked"
637 else:
638 markupchecked = ''
639
640 previewbutton = '<br><input type="submit" name="button_preview" value="%s" style="color: #ff7777; font-size: 9pt; width: 6em; ">' % _('Preview')
641 markupcheckbox = '<input type="checkbox" name="commarkup%d" value="1" %s> Markup' % (Globs.formid, markupchecked)
642
643
644 if request.user.valid or Params.userorip:
645 html1 = [
646 u'<input type="hidden" value="%s" name="comauthor">' % initName,
647 u'<input type="hidden" value="*" name="compasswd">',
648 ]
649 authorJavascriptCode = ''
650 onSubmitCode = ''
651 else:
652 html1 = [
653 u'<input type="text" style="font-size: 9pt;" size="6" maxlength="20" name="comauthor" value="%(author)s" onfocus="if (this.value==\'%(msg)s\') {this.value=\'\';};" onblur="if (this.value==\'\') {this.value=\'%(msg)s\';};">' % { 'msg': wikiutil.escape(initName), 'cellstyle': cellstyle, 'author': wikiutil.escape(tmpauthor) },
654 u'<input type="password" style="font-size: 9pt;" size="4" maxlength="10" name="compasswd" value="%(passwd)s" onfocus="if (this.value==\'%(msg)s\') {this.value=\'\';};" onblur="if (this.value==\'\') {this.value=\'%(msg)s\';};">' % { 'msg': wikiutil.escape(initPass), 'passwd': wikiutil.escape(tmppasswd) },
655 u'<input type="hidden" value="%s" name="autopasswd">' % wikiutil.escape(initPass),
656 ]
657
658 authorJavascriptCode = """
659 <script language="javascript">
660 <!--
661 function setCookie(name, value) {
662 var today = new Date();
663 var expire = new Date(today.getTime() + 60*60*24*365*1000);
664 document.cookie = name + "=" + encodeURIComponent(value) + "; expires=" + expire.toGMTString() + "; path=%s";
665 }
666 //-->
667 </script>""" % request.getScriptname()
668
669 onSubmitCode = 'onSubmit="setCookie(\'PG2AUTHOR\', this.comauthor.value);"'
670
671 html1 = u'\n'.join(html1)
672 scripthtml = u'onfocus="if (this.value==\'%(msg)s\') {this.value=\'\';};" onblur="if (this.value==\'\') {this.value=\'%(msg)s\';};"' % {'msg': wikiutil.escape(initText) }
673
674 page_url = wikiutil.quoteWikinameURL(Globs.cursubname)
675
676 html2 = [
677 u'%s' % authorJavascriptCode,
678 u'<form action="%s#pagecomment%d" name="comment" METHOD="POST" %s>' % (page_url, Globs.formid, onSubmitCode),
679 u'<table class="addcommentform">',
680 u'<tr>',
681 u'<td style="%s"><textarea name="comtext" rows="%d" cols="%d" style="font-size: 9pt;" ' % (cellstyle, Params.rows, Params.cols),
682 u'%s>%s</textarea></td>' % (scripthtml, wikiutil.escape(tmptext)),
683 u'<td style="%s vertical-align: bottom;"><input type="submit" name="button_save" value="%s" style="font-size: 9pt; width: 6em; height:3em; ">%s</td>' % (cellstyle, _('Save'), previewbutton),
684 u'</tr>',
685 u'<tr><td style="%s">' % cellstyle,
686 u'%s' % html1,
687 u'%s' % iconlist,
688 u'</td>',
689 u'<td style="%s text-align: right; font-size: 9pt;">%s</td>' % (cellstyle, markupcheckbox),
690 u'</tr>',
691 u'</table>',
692 u'<input type="hidden" name="action" value="show" >',
693 u'<input type="hidden" name="comrev" value="%s">' % comrev,
694 u'<input type="hidden" name="commentaction" value="addcomment%d">' % Globs.formid,
695 u'</form>',
696 ]
697
698
699 return u'\n'.join(html2)
700
701 def addcomment(macro, comicon, comauthor, comtext, compasswd, comrev, comautopass, commarkup):
702 # Add a comment with inputs
703
704 request = Globs.macro.request
705 cfg = request.cfg
706 _ = request.getText
707
708 datapagename = Globs.datapagename
709
710 pg = PageEditor( request, datapagename )
711 pagetext = pg.get_raw_body()
712
713 # HACK for despam
714 try:
715 if not request.user.may.save( pg, comtext, pg.current_rev()):
716 #message("No permission to save this text.")
717 #return 0
718 pass
719
720 except pg.SaveError, msg:
721 message(msg)
722 return 0
723
724 comtext = convertdelimeter(comtext)
725
726 if request.user.valid:
727 comloginuser = 'TRUE'
728 comauthor = request.user.name
729 else:
730 comloginuser = ''
731 comauthor = convertdelimeter(comauthor)
732
733 orgcompasswd = compasswd
734
735 if Params.encryptpass:
736 from MoinMoin import user
737 compasswd = user.encodePassword(compasswd)
738
739 newcomment = [
740 u'{{{',
741 u'%s,%s' % (comicon, commarkup),
742 u'%s' % comauthor,
743 u'%s' % time.strftime(cfg.datetime_fmt, time.localtime(time.time())),
744 u'',
745 u'%s' % comtext,
746 u'}}}',
747 u'##PASSWORD %s' % compasswd,
748 u'##LOGINUSER %s' % comloginuser,
749 ]
750
751 newpagetext = u'%s\n\n%s' % (pagetext, u'\n'.join(newcomment))
752
753 if not pg.exists():
754 action = 'SAVENEW'
755 defaultacl = Globs.defaultacl
756 warnmessages = '\'\'\'\'\'DO NOT EDIT THIS PAGE!!\'\'\' This page is automatically generated by Page``Comment macro.\'\'\n----'
757 newpagetext = u'%s\n%s\n%s' % (defaultacl, warnmessages, newpagetext)
758 else:
759 action = 'SAVE'
760
761 newpagetext = pg.normalizeText( newpagetext )
762
763 comment = u'PageComment modification at %s' % Globs.curpagename
764 pg._write_file(newpagetext, action, comment)
765
766 comment = u'New comment by "%s"' % comauthor
767
768 trivial = 0
769 addLogEntry(request, 'COMNEW', Globs.curpagename, comment)
770
771 #msg = _('Thank you for your changes. Your attention to detail is appreciated.')
772 msg = _('The comment is added.')
773
774 # send notification mails
775 if Params.notify:
776 msg = msg + commentNotify(comment, trivial, comtext)
777
778 if comautopass and comautopass == orgcompasswd:
779 txt = _('You did not enter a password. A random password has been generated for you:')
780 msg2 = u'<i>%s <b>%s</b></i>' % (txt, comautopass)
781 msg = u'%s %s' % (msg, msg2)
782
783 message(msg)
784 return 1
785
786
787 def previewcomment(comicon, comauthor, comtext, commarkup):
788 request = Globs.macro.request
789 _ = request.getText
790 cfg = request.cfg
791
792 # normalize text
793 lines = comtext.splitlines()
794 if not lines[-1] == u'':
795 # '' will make newline after join
796 lines.append(u'')
797
798 comtext = u'\n'.join(lines)
799
800 #comtext = convertdelimeter(comtext)
801 #comauthor = convertdelimeter(comauthor)
802
803 if Params.articleview:
804 cellstyle = u'border-width: 1px; border-bottom-width: 0px; border-color: #ff7777; background-color: #eeeeee; vertical-align: top; font-size: 9pt;'
805 htmlcomment = [
806 u'<tr><td colspan="5" class="commenttext" style="%(cellstyle)s">%(text)s</td></tr>',
807 u'<tr><td colspan="5" class="commentauthor" style="border-color: #ff7777; border-width: 1px; border-top-width: 0px; text-align: right; font-size: 8pt; color: #999999;">Posted by <b>%(author)s</b> %(icon)s at %(date)s %(delform)s</td></tr>',
808 u'<tr><td colspan="5" class="commentblankline" style="border-width: 0px; height: 20px;"></td></tr>',
809 ]
810
811 else:
812 cellstyle = u'border-width: 0px; background-color: #ffeeee; border-top-width: 1px; vertical-align: top; font-size: 9pt;'
813 htmlcomment = [
814 u'<tr><td class="commenticon" style="%(cellstyle)s">%(icon)s</td>',
815 u'<td class="commentauthor" style="%(cellstyle)s">%(author)s</td>',
816 u'<td style="%(cellstyle)s width: 10px;"> </td>',
817 u'<td class="commenttext" style="%(cellstyle)s">%(text)s</td>',
818 u'<td class="commentdate" style="%(cellstyle)s text-align: right; font-size: 8pt; " nowrap>%(date)s%(delform)s</td></tr>',
819 ]
820
821 htmlcommentitem = u'\n'.join(htmlcomment) % {
822 'cellstyle': cellstyle,
823 'icon': getsmiley(comicon),
824 'author': converttext(comauthor),
825 'text': converttext(comtext, commarkup),
826 'date': time.strftime(cfg.datetime_fmt, time.localtime(time.time())),
827 'delform': ''
828 }
829
830 return htmlcommentitem
831
832 def showcomment():
833
834 request = Globs.macro.request
835 _ = request.getText
836
837 commentlist = fetchcomments()
838
839 if Params.newerfirst:
840 commentlist.reverse()
841
842 html = []
843 cur_index = 0
844
845 if Params.articleview:
846 cellstyle = u'border-width: 0px; background-color: #eeeeee; vertical-align: top; font-size: 9pt;'
847 htmlcomment = [
848 u'<tr><td colspan="5" class="commenttext" style="%(cellstyle)s">%(text)s</td></tr>',
849 u'<tr><td colspan="5" class="commentauthor" style="text-align: right; border-width: 0px; font-size: 8pt; color: #999999;">Posted by <b>%(author)s</b> %(icon)s at %(date)s %(delform)s</td></tr>',
850 u'<tr><td colspan="5" class="commentblankline" style="border-width: 0px; height: 20px;"></td></tr>',
851 ]
852
853 else:
854 cellstyle = u'border-width: 0px; border-top-width: 1px; vertical-align: top; font-size: 9pt;'
855 htmlcomment = [
856 u'<tr><td class="commenticon" style="%(cellstyle)s">%(icon)s</td>',
857 u'<td class="commentauthor" style="%(cellstyle)s">%(author)s</td>',
858 u'<td style="%(cellstyle)s width: 10px;"> </td>',
859 u'<td class="commenttext" style="%(cellstyle)s">%(text)s</td>',
860 u'<td class="commentdate" style="%(cellstyle)s text-align: right; font-size: 8pt; " nowrap>%(date)s%(delform)s</td></tr>',
861 ]
862
863 htmlcommentdel_admin = [
864 u' <font style="font-size: 8pt;">',
865 u'<a style="color: #aa0000;" href="javascript: requesttodeleteadmin%(formid)d(document.delform%(formid)d, \'%(key)s\');" title="%(msg)s">X</a>',
866 u'</font>',
867 ]
868
869 htmlcommentdel_guest = [
870 u' <font style="font-size: 8pt;">',
871 u'<a style="color: #aa0000;" href="javascript: requesttodelete%(formid)d(document.delform%(formid)d, \'%(key)s\');" title="%(msg)s">X</a>',
872 u'</font>',
873 ]
874
875 for item in commentlist:
876 if Globs.admin or (item['loginuser'] and request.user.valid and request.user.name == item['name']):
877 htmlcommentdel = htmlcommentdel_admin
878 elif item['loginuser']:
879 htmlcommentdel = ''
880 else:
881 htmlcommentdel = htmlcommentdel_guest
882
883 htmlcommentdel = u'\n'.join(htmlcommentdel) % {
884 'formid': Globs.formid,
885 'key': item['key'],
886 'msg': _('Delete')
887 }
888
889 htmlcommentitem = u'\n'.join(htmlcomment) % {
890 'cellstyle': cellstyle,
891 'icon': getsmiley(item['icon']),
892 'author': converttext(item['name']),
893 'text': converttext(item['text'], item['markup']),
894 'date': item['date'],
895 'delform': htmlcommentdel
896 }
897
898 html.append(htmlcommentitem)
899
900 return u'\n'.join(html)
901
902 def getescapedsectionname(targettext):
903 regex = r'\W'
904 pattern = re.compile(regex, re.UNICODE)
905 sectionname = pattern.sub('', targettext)
906
907 return sectionname
908
909
910 def getsmiley(markup):
911
912 if markup in config.smileys.keys():
913 formatter = Globs.macro.formatter
914 return formatter.smiley(markup)
915 else:
916 return ''
917
918
919 def converttext(targettext, markup='0'):
920 # Converts some special characters of html to plain-text style
921 # What else to handle?
922
923 if Params.markup and markup == '1':
924 targettext = getMarkupText(targettext)
925 else:
926 # targettext = targettext.strip()
927 targettext = targettext.replace(u'&', '&')
928 targettext = targettext.replace(u'>', '>')
929 targettext = targettext.replace(u'<', '<')
930 targettext = targettext.replace(u'\n', '<br>')
931 targettext = targettext.replace(u'"', '"')
932 targettext = targettext.replace(u'\t', ' ')
933 targettext = targettext.replace(u' ', ' ')
934
935 return targettext
936
937 def convertdelimeter(targettext, reverse=0):
938 # Converts delimeter to other string to avoid a crash
939
940 if reverse:
941 targettext = targettext.replace(u'{_{_{', u'{{{')
942 targettext = targettext.replace(u'}_}_}', u'}}}')
943
944 else:
945 targettext = targettext.replace(u'{{{', u'{_{_{')
946 targettext = targettext.replace(u'}}}', u'}_}_}')
947
948 return targettext
949
950
951 def deleteform():
952 # Javascript codes for deleting or restoring a comment
953
954 request = Globs.macro.request
955 _ = request.getText
956
957 htmlresult = []
958
959 html = [
960 '<script language="javascript">',
961 '<!--',
962 ]
963 htmlresult.append(u'\n'.join(html))
964
965 html = [
966 ' function requesttodeleteadmin%d(delform, comkey) {' % Globs.formid,
967 ' if (confirm("%s")) {;' % _('Really delete this comment?'),
968 ' delform.delkey.value = comkey;',
969 ' delform.delpasswd.value = "****";',
970 ' delform.submit();',
971 ' }',
972 ' }',
973 ' function requesttodelete%d(delform, comkey) {' % Globs.formid,
974 ' var passwd = prompt("%s:", "");' % _('Please specify a password!'),
975 ' if(!(passwd == "" || passwd == null)) {',
976 ' delform.delkey.value = comkey;',
977 ' delform.delpasswd.value = passwd;',
978 ' delform.submit();',
979 ' }',
980 ' }',
981 ]
982
983 htmlresult.append(u'\n'.join(html))
984
985 page_url = wikiutil.quoteWikinameURL(Globs.cursubname)
986
987 html = [
988 '//-->',
989 '</script>',
990 '<form name="delform%d" action="%s#pagecomment%d" METHOD="post">' % (Globs.formid, page_url, Globs.formid),
991 '<input type="hidden" value="show" name="action">',
992 '<input name="delpasswd" type="hidden" value="****">',
993 '<input name="delkey" type="hidden" value="">',
994 '<input type="hidden" name="commentaction" value="delcomment%s">' % Globs.formid,
995 '</form>',
996 ]
997 htmlresult.append(u'\n'.join(html))
998
999 return u'\n'.join(htmlresult)
1000
1001
1002 def filtercomment(index='', name='', passwd=''):
1003
1004 # filter by index
1005 if index:
1006 filteredlist1 = fetchcomments(index, index)
1007 else:
1008 filteredlist1 = fetchcomments()
1009
1010 # filter by name
1011 filteredlist2 = []
1012 if name:
1013 for item in filteredlist1:
1014 if name == item['name']:
1015 filteredlist2.append(item)
1016 else:
1017 filteredlist2 = filteredlist1
1018
1019 # filter by password
1020 filteredlist3 = []
1021 if passwd:
1022 for item in filteredlist2:
1023 if passwd == item['passwd']:
1024 filteredlist3.append(item)
1025 else:
1026 filteredlist3 = filteredlist2
1027
1028 return filteredlist3
1029
1030
1031 def fetchcomments(startindex=1, endindex=9999):
1032
1033 commentlist = []
1034
1035 request = Globs.macro.request
1036 formatter = Globs.macro.formatter
1037 datapagename = Globs.datapagename
1038
1039 pg = Page( request, datapagename )
1040 pagetext = pg.get_raw_body()
1041
1042 regex = ur"""
1043 ^[\{]{3}\n
1044 ^(?P<icon>[^\n]*)\n
1045 ^(?P<name>[^\n]*)\n
1046 ^(?P<date>[^\n]*)\n\n
1047 ^(?P<text>
1048 \s*.*?
1049 (?=[\}]{3})
1050 )[\}]{3}[\n]*
1051 ^[#]{2}PASSWORD[ ](?P<passwd>[^\n]*)[\n]*
1052 ^[#]{2}LOGINUSER[ ](?P<loginuser>[^\n]*)[\n]*"""
1053
1054 pattern = re.compile(regex, re.UNICODE + re.MULTILINE + re.VERBOSE + re.DOTALL)
1055 commentitems = pattern.findall(pagetext)
1056
1057 cur_index = 0
1058
1059 for item in commentitems:
1060 comment = {}
1061 cur_index += 1
1062
1063 if cur_index < startindex:
1064 continue
1065
1066 comment['index'] = cur_index
1067
1068 custom_fields = item[0].split(',')
1069
1070 comment['icon'] = custom_fields[0]
1071
1072 if len(custom_fields) > 1:
1073 comment['markup'] = custom_fields[1].strip()
1074 else:
1075 comment['markup'] = ''
1076
1077 comment['name'] = convertdelimeter(item[1], 1)
1078 comment['date'] = item[2]
1079 comment['text'] = convertdelimeter(item[3], 1)
1080 comment['passwd'] = item[4]
1081 comment['loginuser'] = item[5]
1082
1083 # experimental
1084 comment['key'] = comment['date'].strip()
1085
1086 commentlist.append(comment)
1087
1088 if cur_index >= endindex:
1089 break
1090
1091 return commentlist
1092
1093 def deletecomment(macro, delkey, delpasswd):
1094 # Deletes a comment with given index and password
1095
1096 request = Globs.macro.request
1097 formatter = Globs.macro.formatter
1098 datapagename = Globs.datapagename
1099 _ = request.getText
1100
1101 if Params.encryptpass:
1102 from MoinMoin import user
1103 delpasswd = user.encodePassword(delpasswd)
1104
1105 pg = PageEditor( request, datapagename )
1106 pagetext = pg.get_raw_body()
1107
1108 regex = ur"""
1109 (?P<comblock>
1110 ^[\{]{3}\n
1111 ^(?P<icon>[^\n]*)\n
1112 ^(?P<name>[^\n]*)\n
1113 ^(?P<date>[^\n]*)[\n]+
1114 ^(?P<text>
1115 \s*.*?
1116 (?=[\}]{3})
1117 )[\}]{3}[\n]*
1118 ^[#]{2}PASSWORD[ ](?P<passwd>[^\n]*)[\n]*
1119 ^[#]{2}LOGINUSER[ ](?P<loginuser>[^\n]*)[\n$]*
1120 )"""
1121
1122 pattern = re.compile(regex, re.UNICODE + re.MULTILINE + re.VERBOSE + re.DOTALL)
1123 commentitems = pattern.findall(pagetext)
1124
1125 for item in commentitems:
1126
1127 if delkey == item[3].strip():
1128 comauthor = item[2]
1129 if Globs.admin or (request.user.valid and request.user.name == comauthor) or delpasswd == item[5]:
1130 newpagetext = pagetext.replace(item[0], '', 1)
1131
1132 action = 'SAVE'
1133 comment = 'Deleted comment by "%s"' % comauthor
1134 trivial = 1
1135 pg._write_file(newpagetext, action, u'PageComment modification at %s' % Globs.curpagename)
1136 addLogEntry(request, 'COMDEL', Globs.curpagename, comment)
1137
1138 msg = _('The comment is deleted.')
1139
1140 # send notification mails
1141 if Params.notify:
1142 msg = msg + commentNotify(comment, trivial)
1143
1144 message(msg)
1145
1146 return
1147 else:
1148 message(_('Sorry, wrong password.'))
1149 return
1150
1151 message(_('No such comment'))
1152
1153
1154 def getAuthorFromCookie():
1155
1156 import Cookie
1157 request = Globs.macro.request
1158 cookieauthor = ''
1159
1160 try:
1161 cookie = Cookie.SimpleCookie(request.saved_cookie)
1162 except Cookie.CookieError:
1163 # ignore invalid cookies
1164 cookie = None
1165
1166 if cookie and cookie.has_key('PG2AUTHOR'):
1167 cookieauthor = cookie['PG2AUTHOR'].value
1168
1169 cookieauthor = decodeURI(cookieauthor)
1170
1171 return cookieauthor
1172
1173
1174 def commentNotify(comment, trivial, comtext=''):
1175
1176 request = Globs.macro.request
1177
1178 if hasattr(request.cfg, 'mail_enabled'):
1179 mail_enabled = request.cfg.mail_enabled
1180 elif hasattr(request.cfg, 'mail_smarthost'):
1181 mail_enabled = request.cfg.mail_smarthost
1182 else:
1183 mail_enabled = ''
1184
1185 if not mail_enabled:
1186 return ''
1187
1188 _ = request.getText
1189 pg = PageEditor( request, Globs.curpagename )
1190
1191 subscribers = pg.getSubscribers(request, return_users=1, trivial=trivial)
1192 if subscribers:
1193 # get a list of old revisions, and append a diff
1194
1195 # send email to all subscribers
1196 results = [_('Status of sending notification mails:')]
1197 for lang in subscribers.keys():
1198 emails = map(lambda u: u.email, subscribers[lang])
1199 names = map(lambda u: u.name, subscribers[lang])
1200 mailok, status = sendNotification(pg, comtext, comment, emails, lang, trivial)
1201 recipients = ", ".join(names)
1202 results.append(_('[%(lang)s] %(recipients)s: %(status)s') % {
1203 'lang': lang, 'recipients': recipients, 'status': status})
1204
1205 # Return mail sent results. Ignore trivial - we don't have
1206 # to lie. If mail was sent, just tell about it.
1207 return '<p>\n%s\n</p> ' % '<br>'.join(results)
1208
1209 # No mail sent, no message.
1210 return ''
1211
1212 def sendNotification(pg, comtext, comment, emails, email_lang, trivial):
1213
1214 from MoinMoin import util, user
1215 request = Globs.macro.request
1216
1217 _ = lambda s, formatted=True, r=request, l=email_lang: r.getText(s, formatted=formatted, lang=l)
1218
1219 mailBody = _("Dear Wiki user,\n\n"
1220 'You have subscribed to a wiki page or wiki category on "%(sitename)s" for change notification.\n\n'
1221 "The following page has been changed by %(editor)s:\n"
1222 "%(pagelink)s\n\n", formatted=False) % {
1223 'editor': pg.uid_override or user.getUserIdentification(request),
1224 'pagelink': pg.request.getQualifiedURL(pg.url(request)),
1225 'sitename': pg.cfg.sitename or request.getBaseURL(),
1226 }
1227
1228 if comment:
1229 mailBody = mailBody + \
1230 _("The comment on the change is:\n%(comment)s\n\n", formatted=False) % {'comment': comment}
1231
1232 # append comment text
1233 if comtext:
1234 mailBody = mailBody + "%s\n%s\n" % (("-" * 78), comtext)
1235
1236 return util.mail.sendmail(request, emails,
1237 _('[%(sitename)s] %(trivial)sUpdate of "%(pagename)s" by %(username)s', formatted=False) % {
1238 'trivial' : (trivial and _("Trivial ", formatted=False)) or "",
1239 'sitename': pg.cfg.sitename or "Wiki",
1240 'pagename': pg.page_name,
1241 'username': pg.uid_override or user.getUserIdentification(request),
1242 },
1243 mailBody, mail_from=pg.cfg.mail_from)
1244
1245
1246
1247 def decodeURI(quotedstring):
1248
1249 try:
1250 unquotedstring = wikiutil.url_unquote(quotedstring)
1251 except AttributeError:
1252 # for compatibility with old versions
1253 unquotedstring = url_unquote(quotedstring)
1254
1255 return unquotedstring
1256
1257
1258 def url_unquote(s, want_unicode=True):
1259 """
1260 From moinmoin 1.5
1261
1262 Wrapper around urllib.unquote doing the encoding/decoding as usually wanted:
1263
1264 @param s: the string to unquote (can be str or unicode, if it is unicode,
1265 config.charset is used to encode it before calling urllib)
1266 @param want_unicode: for the less usual case that you want to get back
1267 str and not unicode, set this to False.
1268 Default is True.
1269 """
1270 import urllib
1271
1272 if isinstance(s, unicode):
1273 s = s.encode(config.charset) # ascii would also work
1274 s = urllib.unquote(s)
1275 if want_unicode:
1276 s = s.decode(config.charset)
1277 return s
1278
1279
1280 def addLogEntry(request, action, pagename, msg):
1281 # Add an entry to the edit log on adding comments.
1282 from MoinMoin.logfile import editlog
1283 t = wikiutil.timestamp2version(time.time())
1284 msg = unicode(msg)
1285
1286 pg = Page( request, pagename )
1287 #rev = pg.current_rev()
1288 rev = 99999999
1289
1290 # TODO: for now we simply write 2 logs, maybe better use some multilog stuff
1291 # Write to global log
1292 log = editlog.EditLog(request)
1293 log.add(request, t, rev, action, pagename, request.remote_addr, '', msg)
1294
1295 # Write to local log
1296 log = editlog.EditLog(request, rootpagename=pagename)
1297 log.add(request, t, rev, action, pagename, request.remote_addr, '', msg)
1298
1299 def getsmileymarkuplist(defaulticon):
1300
1301 html = [
1302 u'Smiley: <select name="comicon">',
1303 u' <option value=""></option>',
1304 ]
1305
1306 for smiley in config.smileys.keys():
1307 if defaulticon.strip() == smiley:
1308 html.append(u' <option selected>%s</option>' % wikiutil.escape(smiley))
1309 else:
1310 html.append(u' <option>%s</option>' % wikiutil.escape(smiley))
1311
1312 html.append(u'</select>')
1313
1314 return u'\n'.join(html)
1315
1316 def getsmileymarkupradio(defaulticon):
1317
1318 smileys = Globs.smileys
1319 html = []
1320
1321 for smiley in smileys:
1322 if defaulticon.strip() == smiley:
1323 html.append(u'<input type="radio" name="comicon" value="%s" checked>%s ' % (wikiutil.escape(smiley), getsmiley(smiley)) )
1324 else:
1325 html.append(u'<input type="radio" name="comicon" value="%s">%s ' % (wikiutil.escape(smiley), getsmiley(smiley)) )
1326
1327 html.append(u'</select>')
1328
1329 return u'\n'.join(html)
1330
1331
1332 def getMarkupText(lines):
1333 request = Globs.macro.request
1334 formatter = Globs.macro.formatter
1335
1336 markup = Globs.markupforbidden
1337
1338 for regex in markup.keys():
1339 pattern = re.compile(regex, re.UNICODE + re.VERBOSE + re.MULTILINE)
1340 lines, nchanges = pattern.subn(markup[regex], lines)
1341
1342 #if nchanges:
1343 # debug(regex)
1344
1345 out = StringIO.StringIO()
1346 request.redirect(out)
1347 wikiizer = wiki.Parser(lines, request)
1348 wikiizer.format(formatter)
1349 targettext = out.getvalue()
1350 request.redirect()
1351 del out
1352
1353 return targettext
1354
1355
1356 def nicepass(alpha=3,numeric=1):
1357 """
1358 returns a human-readble password (say rol86din instead of
1359 a difficult to remember K8Yn9muL )
1360 """
1361 import string
1362 import random
1363 vowels = ['a','e','i','o','u']
1364 consonants = [a for a in string.ascii_lowercase if a not in vowels]
1365 digits = string.digits
1366
1367 ####utility functions
1368 def a_part(slen):
1369 ret = ''
1370 for i in range(slen):
1371 if i%2 ==0:
1372 randid = random.randint(0,20) #number of consonants
1373 ret += consonants[randid]
1374 else:
1375 randid = random.randint(0,4) #number of vowels
1376 ret += vowels[randid]
1377 return ret
1378
1379 def n_part(slen):
1380 ret = ''
1381 for i in range(slen):
1382 randid = random.randint(0,9) #number of digits
1383 ret += digits[randid]
1384 return ret
1385
1386 ####
1387 fpl = alpha/2
1388 if alpha % 2 :
1389 fpl = int(alpha/2) + 1
1390 lpl = alpha - fpl
1391
1392 start = a_part(fpl)
1393 mid = n_part(numeric)
1394 end = a_part(lpl)
1395
1396 # return "%s%s%s" % (start,mid,end)
1397 return "%s%s%s" % (start,end,mid)
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.