Attachment 'text_plain.py.patch'

Download

   1 diff -r 63016f784d88 MoinMoin/formatter/text_plain.py
   2 --- a/MoinMoin/formatter/text_plain.py	Mon Apr 05 23:37:52 2010 +0200
   3 +++ b/MoinMoin/formatter/text_plain.py	Tue Apr 06 20:40:15 2010 +0000
   4 @@ -3,11 +3,13 @@
   5      MoinMoin - "text/plain" Formatter
   6  
   7      @copyright: 2000-2002 Juergen Hermann <jh@web.de>
   8 +                2007 by Timo Sirainen <tss@iki.fi>
   9      @license: GNU GPL, see COPYING for details.
  10  """
  11  
  12  from MoinMoin.formatter import FormatterBase
  13  
  14 +
  15  class Formatter(FormatterBase):
  16      """
  17          Send plain text data.
  18 @@ -20,168 +22,252 @@
  19          self._in_code_area = 0
  20          self._in_code_line = 0
  21          self._code_area_state = [0, -1, -1, 0]
  22 -        self._in_list = 0
  23 -        self._did_para = 0
  24 +        self._lists = []
  25          self._url = None
  26 -        self._text = None # XXX does not work with links in headings!!!!!
  27 +        self._text = None  # XXX does not work with links in headings!!!!!
  28 +        self._text_stack = []
  29 +        self._skip_text = False
  30 +        self._wrap_skip_text = False
  31 +        self._textbuf = ''
  32 +        self._indent = 0
  33 +        self._listitem_on = []
  34 +        self._empty_line_count = 2
  35 +        self._paragraph_ended = False
  36 +        self._paragraph_skip_begin = True
  37  
  38      def startDocument(self, pagename):
  39 -        line = u"*" * (len(pagename) + 2) + u'\n'
  40 -        return u"%s %s \n%s" % (line, pagename, line)
  41 +        line = u'\n'.rjust(len(pagename) + 2, u'*')
  42 +        return self.wrap(u"%s %s \n%s" % (line, pagename, line))
  43  
  44 -    def endDocument(self):
  45 -        return u'\n'
  46 +    def endContent(self):
  47 +        return self.flush(True)
  48  
  49      def sysmsg(self, on, **kw):
  50 -        return (u'\n\n*** ', u' ***\n\n')[not on]
  51 +        return self.wrap((u'\n\n*** ', u' ***\n\n')[not on])
  52  
  53      def pagelink(self, on, pagename='', page=None, **kw):
  54          FormatterBase.pagelink(self, on, pagename, page, **kw)
  55 -        return (u">>", u"<<") [not on]
  56 +        if on:
  57 +            if not self._textbuf or self._textbuf[-1] in ('\n', ' '):
  58 +                result = self.wrap(u'<')
  59 +            else:
  60 +                result = self.wrap(u' <')
  61 +            self.text_on(True)
  62 +            self.add_missing_space()
  63 +            return result
  64 +        else:
  65 +            linktext = self._text
  66 +            self.text_off()
  67 +            orig_pagename = pagename
  68 +            if pagename.find('/'):
  69 +                pagename = pagename.replace('/', '.')
  70 +            pagename += '.txt'
  71 +            if linktext == orig_pagename:
  72 +                return self.wrap(u'%s>' % pagename)
  73 +            else:
  74 +                return self.wrap(u'%s> [%s]' % (linktext, pagename))
  75  
  76      def interwikilink(self, on, interwiki='', pagename='', **kw):
  77          if on:
  78 +            self.add_missing_space()
  79              self._url = u"%s:%s" % (interwiki, pagename)
  80 -            self._text = []
  81 +            self.text_on()
  82              return u''
  83          else:
  84 -            if "".join(self._text) == self._url:
  85 -                self._url = None
  86 -                self._text = None
  87 -                return ''
  88 +            text = self._text
  89 +            self.text_off()
  90 +            if text == self._url:
  91 +                result = ''
  92              else:
  93 -                self._url = None
  94 -                self._text = None
  95 -                return u' [%s]' % (self._url)
  96 +                result = self.wrap(u' [%s]' % (self._url))
  97 +            self._url = None
  98 +            return result
  99  
 100      def url(self, on, url='', css=None, **kw):
 101          if on:
 102 +            self.add_missing_space()
 103              self._url = url
 104 -            self._text = []
 105 +            self.text_on()
 106              return u''
 107          else:
 108 -            if "".join(self._text) == self._url:
 109 -                self._url = None
 110 -                self._text = None
 111 -                return ''
 112 +            text = self._text
 113 +            self.text_off()
 114 +            if text == self._url or 'mailto:' + text == self._url:
 115 +                result = ''
 116              else:
 117 -                self._url = None
 118 -                self._text = None
 119 -                return u' [%s]' % (self._url)
 120 +                result = self.wrap(u' [%s]' % (self._url))
 121 +            self._url = None
 122 +            return result
 123  
 124      def attachment_link(self, on, url=None, **kw):
 125          if on:
 126 -            return "["
 127 -        else:
 128 -            return "]"
 129 +            if 'title' in kw and kw['title']:
 130 +                if kw['title'] != url:
 131 +                    return self.wrap(u'[attachment:%s ' % url)
 132 +            return self.wrap(u'[attachment:')
 133 +        return self.wrap(']')
 134  
 135      def attachment_image(self, url, **kw):
 136          title = ''
 137          for a in (u'title', u'html__title', u'alt', u'html_alt'):
 138              if a in kw:
 139                  title = ':' + kw[a]
 140 -        return "[image:%s%s]" % (url, title)
 141 +        return self.wrap("[image:%s%s]" % (url, title))
 142  
 143      def attachment_drawing(self, url, text, **kw):
 144 -        return "[drawing:%s]" % text
 145 +        return self.wrap("[drawing:%s]" % text)
 146  
 147      def text(self, text, **kw):
 148 -        self._did_para = 0
 149          if self._text is not None:
 150 -            self._text.append(text)
 151 -        return text
 152 +            self._text += text
 153 +        if self._wrap_skip_text:
 154 +            return ''
 155 +        return self.wrap(text)
 156  
 157      def rule(self, size=0, **kw):
 158          size = min(size, 10)
 159          ch = u"---~=*+#####"[size]
 160 -        return (ch * 79) + u'\n'
 161 +        self.paragraph_begin()
 162 +        result = self.wrap((ch * (79 - self._indent)))
 163 +        self.paragraph_end()
 164 +        return result
 165  
 166      def strong(self, on, **kw):
 167 -        return u'*'
 168 +        if on:
 169 +            self.add_missing_space()
 170 +        return self.wrap(u'*')
 171  
 172      def emphasis(self, on, **kw):
 173 -        return u'/'
 174 +        if on:
 175 +            self.add_missing_space()
 176 +        return self.wrap(u'/')
 177  
 178      def highlight(self, on, **kw):
 179          return u''
 180  
 181      def number_list(self, on, type=None, start=None, **kw):
 182          if on:
 183 -            self._in_list = 1
 184 -            return [u'\n', u'\n\n'][not self._did_para]
 185 -        else:
 186 -            self._in_list = 0
 187 -            if not self._did_para:
 188 -                self._did_para = 1
 189 -                return u'\n'
 190 -        return u''
 191 +            if self._lists:
 192 +                # No empty lines between sublists
 193 +                self._paragraph_ended = False
 194 +            self.paragraph_begin()
 195 +            self._lists.append(0)
 196 +            self._listitem_on.append(False)
 197 +        elif self._lists:
 198 +            self.paragraph_end()
 199 +            num = self._lists.pop()
 200 +            listitem_on = self._listitem_on.pop()
 201 +            if listitem_on:
 202 +                prefix = ' %d. ' % (num)
 203 +                self._indent -= len(prefix)
 204 +        return ''
 205  
 206      def bullet_list(self, on, **kw):
 207          if on:
 208 -            self._in_list = -1
 209 -            return [u'\n', u'\n\n'][not self._did_para]
 210 +            if self._lists:
 211 +                # No empty lines between sublists
 212 +                self._paragraph_ended = False
 213 +            self.paragraph_begin()
 214 +            self._lists.append(-1)
 215 +            self._listitem_on.append(False)
 216          else:
 217 -            self._in_list = 0
 218 -            if not self._did_para:
 219 -                self._did_para = 1
 220 -                return u'\n'
 221 -        return u''
 222 +            self.paragraph_end()
 223 +            self._lists.pop()
 224 +            listitem_on = self._listitem_on.pop()
 225 +            if listitem_on:
 226 +                self._indent -= 3
 227 +        return ''
 228  
 229      def listitem(self, on, **kw):
 230 -        if on:
 231 -            if self._in_list > 0:
 232 -                self._in_list += 1
 233 -                self._did_para = 1
 234 -                return ' %d. ' % (self._in_list-1, )
 235 -            elif self._in_list < 0:
 236 -                self._did_para = 1
 237 -                return u' * '
 238 +        self._paragraph_ended = False
 239 +        if not on:
 240 +            # we can't rely on this
 241 +            self.paragraph_end()
 242 +            return ''
 243 +
 244 +        result = ''
 245 +        num = self._lists.pop()
 246 +        listitem_on = self._listitem_on.pop()
 247 +        if listitem_on and on:
 248 +            # we didn't receive on=False for previous listitem
 249 +            self.paragraph_end()
 250 +            if num >= 0:
 251 +                prefix = ' %d. ' % (num)
 252 +                self._indent -= len(prefix)
 253              else:
 254 -                return u' * '
 255 +                self._indent -= 3
 256 +
 257 +        if num >= 0:
 258 +            num += 1
 259 +            prefix = ' %d. ' % (num)
 260          else:
 261 -            self._did_para = 1
 262 -            return u'\n'
 263 +            # FIXME: also before tables, at leat in LDA.Sieve.txt
 264 +            prefix = ' * '
 265 +        self._lists.append(num)
 266 +        self._listitem_on.append(on)
 267 +
 268 +        result += self.wrap(prefix)
 269 +        self._indent += len(prefix)
 270 +        self._paragraph_skip_begin = True
 271 +        return result
 272  
 273      def sup(self, on, **kw):
 274 -        return u'^'
 275 +        if on:
 276 +            return self.wrap(u'^')
 277 +        else:
 278 +            return ''
 279  
 280      def sub(self, on, **kw):
 281 -        return u'_'
 282 +        return self.wrap(u'_')
 283  
 284      def strike(self, on, **kw):
 285 -        return u'__'
 286 +        if on:
 287 +            self.add_missing_space()
 288 +        return self.wrap(u'__')
 289  
 290      def code(self, on, **kw):
 291 -        #return [unichr(0x60), unichr(0xb4)][not on]
 292 -        return u"'" # avoid high-ascii
 293 +        if on:
 294 +            self.add_missing_space()
 295 +        return self.wrap(u"'")
 296  
 297      def preformatted(self, on, **kw):
 298          FormatterBase.preformatted(self, on)
 299 -        snip = u'---%<'
 300 -        snip = snip + (u'-' * (78 - len(snip)))
 301 +        snip = u'%s\n' % u'---%<'.ljust(78 - self._indent, u'-')
 302          if on:
 303 -            return u'\n' + snip + u'\n'
 304 +            self.paragraph_begin()
 305 +            return self.wrap(snip)
 306          else:
 307 -            return snip + u'\n'
 308 +            if self._textbuf and not self._textbuf.endswith('\n'):
 309 +                self._textbuf += '\n'
 310 +            result = self.wrap(snip)
 311 +            self.paragraph_end()
 312 +            return result
 313  
 314      def small(self, on, **kw):
 315 +        if on:
 316 +            self.add_missing_space()
 317          return u''
 318  
 319      def big(self, on, **kw):
 320 +        if on:
 321 +            self.add_missing_space()
 322          return u''
 323  
 324 -    def code_area(self, on, code_id, code_type='code', show=0, start=-1, step=-1, msg=None):
 325 -        snip = u'---CodeArea'
 326 -        snip = snip + (u'-' * (78 - len(snip)))
 327 +    def code_area(self, on, code_id, code_type='code', show=0, start=-1,
 328 +                  step=-1, msg=None):
 329 +        snip = u'%s\n' % u'---CodeArea'.ljust(78 - self._indent, u'-')
 330          if on:
 331 +            self.paragraph_begin()
 332              self._in_code_area = 1
 333              self._in_code_line = 0
 334              self._code_area_state = [show, start, step, start]
 335 -            return u'\n' + snip + u'\n'
 336 +            return self.wrap(snip)
 337          else:
 338              if self._in_code_line:
 339 -                return self.code_line(0) + snip + u'\n'
 340 -            return snip + u'\n'
 341 +                return self.wrap(self.code_line(0) + snip)
 342 +            result = self.wrap(snip)
 343 +            self.paragraph_end()
 344 +            return result
 345  
 346      def code_line(self, on):
 347          res = u''
 348 @@ -192,70 +278,302 @@
 349                  res += u' %4d  ' % self._code_area_state[3]
 350                  self._code_area_state[3] += self._code_area_state[2]
 351          self._in_code_line = on != 0
 352 -        return res
 353 +        return self.wrap(res)
 354  
 355      def code_token(self, on, tok_type):
 356          return ""
 357  
 358 +    def add_missing_space(self):
 359 +        if self._textbuf and self._textbuf[-1].isalnum():
 360 +            self._textbuf += ' '
 361 +
 362      def paragraph(self, on, **kw):
 363          FormatterBase.paragraph(self, on)
 364 -        if self._did_para:
 365 -            on = 0
 366 -        return [u'\n', u''][not on]
 367 +        if on:
 368 +            self.paragraph_begin()
 369 +        else:
 370 +            self.paragraph_end()
 371 +        return ''
 372  
 373      def linebreak(self, preformatted=1):
 374 -        return u'\n'
 375 +        return self.wrap(u'\n')
 376  
 377      def smiley(self, text):
 378 -        return text
 379 +        return self.wrap(text)
 380  
 381      def heading(self, on, depth, **kw):
 382          if on:
 383 -            self._text = []
 384 -            return '\n\n'
 385 +            self.paragraph_begin()
 386 +            self.text_on()
 387 +            result = ''
 388          else:
 389 -            result = u'\n%s\n\n' % (u'=' * len("".join(self._text)))
 390 -            self._text = None
 391 -            return result
 392 +            if depth == 1:
 393 +                ch = u'='
 394 +            else:
 395 +                ch = u'-'
 396 +
 397 +            result = u'\n%s\n' % (ch * len(self._text))
 398 +            self.text_off()
 399 +            result = self.wrap(result)
 400 +            self.paragraph_end()
 401 +        return result
 402 +
 403 +    def get_table_sep(self, col_widths):
 404 +        result = ''
 405 +        for width in col_widths:
 406 +            result += '+' + ('-' * width)
 407 +        return result + '+\n'
 408 +
 409 +    def fix_col_widths(self):
 410 +        min_widths = self._table_column_min_len
 411 +        max_widths = self._table_column_max_len
 412 +        max_len = sum(max_widths)
 413 +        # take the needed space equally from all columns
 414 +        count = len(max_widths)
 415 +        idx, skip = 0, 0
 416 +        available_len = 79 - count - 1
 417 +        while max_len > available_len:
 418 +            if max_widths[idx] > min_widths[idx]:
 419 +                max_widths[idx] -= 1
 420 +                max_len -= 1
 421 +                skip = 0
 422 +            else:
 423 +                skip += 1
 424 +                if skip == count:
 425 +                    # there are only too wide columns
 426 +                    break
 427 +            if idx == count - 1:
 428 +                idx = 0
 429 +            else:
 430 +                idx += 1
 431 +        return max_widths
 432  
 433      def table(self, on, attrs={}, **kw):
 434 -        return u''
 435 +        if on:
 436 +            self._table = []
 437 +            self._table_column_min_len = []
 438 +            self._table_column_max_len = []
 439 +            result = self.flush(True)
 440 +        else:
 441 +            result = u''
 442 +            col_widths = self.fix_col_widths()
 443 +            for row in self._table:
 444 +                result += self.get_table_sep(col_widths)
 445 +                more = True
 446 +                while more:
 447 +                    more = False
 448 +                    num = 0
 449 +                    result += '|'
 450 +                    for col in row:
 451 +                        # break at next LF
 452 +                        lf_idx = col.find('\n')
 453 +                        if lf_idx != -1:
 454 +                            more = True
 455 +                            col_len = lf_idx
 456 +                            next_idx = lf_idx + 1
 457 +                        else:
 458 +                            col_len = len(col)
 459 +                            next_idx = col_len
 460 +                        # possibly break earlier if we need to wrap
 461 +                        if col_len > col_widths[num]:
 462 +                            idx = col.rfind(' ', 0, col_widths[num])
 463 +                            if idx == -1:
 464 +                                idx = col.find(' ', col_widths[num])
 465 +                            if idx != -1:
 466 +                                col_len = idx
 467 +                                next_idx = idx + 1
 468 +                            more = True
 469 +                        result += ' ' + col[:col_len]
 470 +                        result += (' ' * (col_widths[num] - col_len - 1)) + '|'
 471 +                        row[num] = col[next_idx:]
 472 +                        num += 1
 473 +                    result += '\n'
 474 +            result += self.get_table_sep(col_widths)
 475 +            self._table = None
 476 +            self._table_column_min_len = None
 477 +            self._table_column_max_len = None
 478 +            self._empty_line_count = 0
 479 +            self.paragraph_end()
 480 +        return result
 481  
 482      def table_row(self, on, attrs={}, **kw):
 483 +        if on:
 484 +            self._table.append([])
 485          return u''
 486  
 487      def table_cell(self, on, attrs={}, **kw):
 488 +        if on:
 489 +            self.text_on()
 490 +            self._wrap_skip_text = True
 491 +        else:
 492 +            # keep track of the longest word and the longest line in the cell
 493 +            self._text = self._text.strip()
 494 +            max_line_len = 0
 495 +            max_word_len = 0
 496 +            for line in self._text.split('\n'):
 497 +                if len(line) > max_line_len:
 498 +                    max_line_len = len(line)
 499 +            for word in self._text.split(' '):
 500 +                if len(word) > max_word_len:
 501 +                    max_word_len = len(word)
 502 +            # one preceding and trailing cell whitespace
 503 +            max_word_len += 2
 504 +            max_line_len += 2
 505 +
 506 +            rownum = len(self._table) - 1
 507 +            colnum = len(self._table[rownum])
 508 +            if len(self._table_column_max_len) <= colnum:
 509 +                self._table_column_min_len.append(max_word_len)
 510 +                self._table_column_max_len.append(max_line_len)
 511 +            else:
 512 +                if max_word_len > self._table_column_min_len[colnum]:
 513 +                    self._table_column_min_len[colnum] = max_word_len
 514 +                if self._table_column_max_len[colnum] < max_line_len:
 515 +                    self._table_column_max_len[colnum] = max_line_len
 516 +            self._table[rownum].append(self._text)
 517 +            self.text_off()
 518          return u''
 519  
 520      def underline(self, on, **kw):
 521 -        return u'_'
 522 +        return self.wrap(u'_')
 523  
 524      def definition_list(self, on, **kw):
 525 +        if on:
 526 +            self.paragraph_begin()
 527 +        else:
 528 +            self.paragraph_end()
 529          return u''
 530  
 531      def definition_term(self, on, compact=0, **kw):
 532          result = u''
 533 -        if not compact:
 534 -            result = result + u'\n'
 535 +        #if not compact:
 536 +        #    result = result + u'\n'
 537          if not on:
 538 -            result = result + u':\n'
 539 -        return result
 540 +            result = result + u':'
 541 +        return self.wrap(result)
 542  
 543      def definition_desc(self, on, **kw):
 544 -        return [u'    ', u'\n'][not on]
 545 +        if on:
 546 +            self._indent += 2
 547 +            self.paragraph_begin()
 548 +        else:
 549 +            self.paragraph_end()
 550 +            self._textbuf += '\n'
 551 +            self._indent -= 2
 552 +        return ''
 553  
 554      def image(self, src=None, **kw):
 555          for a in (u'title', u'html__title', u'alt', u'html_alt'):
 556              if a in kw:
 557 -                return kw[a]
 558 -        return u''
 559 -
 560 -    def transclusion(self, on, **kw):
 561 -        return u''
 562 -
 563 -    def transclusion_param(self, **kw):
 564 -        return u''
 565 +                return self.wrap(kw[a] + ' [' + src + ']')
 566 +        return self.wrap('[' + src + ']')
 567  
 568      def lang(self, on, lang_name):
 569          return ''
 570  
 571 +    def paragraph_begin(self):
 572 +        if self._paragraph_ended:
 573 +            self._textbuf += '\n'
 574 +        elif not self._paragraph_skip_begin:
 575 +            if self._textbuf and not self._textbuf.endswith('\n'):
 576 +                self._textbuf += '\n'
 577 +        self._paragraph_ended = False
 578 +        self._paragraph_skip_begin = False
 579 +
 580 +    def paragraph_end(self):
 581 +        if self._textbuf and not self._textbuf.endswith('\n'):
 582 +            self._textbuf += '\n'
 583 +        self._paragraph_ended = True
 584 +
 585 +    def wrap(self, text):
 586 +        if not text:
 587 +            return ''
 588 +        if self._wrap_skip_text:
 589 +            # we're inside table
 590 +            #self._text += 'w{' + text + '}'
 591 +            self._text += text
 592 +            return ''
 593 +
 594 +        self._paragraph_ended = False
 595 +        self._paragraph_skip_begin = False
 596 +
 597 +        # add indents after all LFs. kind of dirty to split twice though..
 598 +        lines = text.split('\n')
 599 +        text = lines.pop(0)
 600 +        while lines:
 601 +            text += '\n%s%s' % (' ' * self._indent, lines.pop(0))
 602 +
 603 +        if not self._textbuf or self._textbuf.endswith('\n'):
 604 +            self._textbuf += ' ' * self._indent
 605 +        self._textbuf += text
 606 +
 607 +        lines = self._textbuf.split('\n')
 608 +        self._textbuf = ''
 609 +        text = ''
 610 +        while lines:
 611 +            self._textbuf += lines.pop(0)
 612 +            if lines:
 613 +                # LFs found
 614 +                text += self.flush(True)
 615 +            if len(self._textbuf) > 80 and \
 616 +                    self._textbuf.find(' ', self._indent) != -1:
 617 +                # wrap time
 618 +                text += self.flush(False)
 619 +        return text
 620 +
 621 +    def flush(self, addlf):
 622 +        result = ''
 623 +
 624 +        while len(self._textbuf) >= 80:
 625 +            # need to wrap
 626 +            last_space = self._textbuf.rfind(' ', self._indent, 80)
 627 +            if last_space == -1:
 628 +                # a long line. split at the next possible space
 629 +                last_space = self._textbuf.find(' ', self._indent)
 630 +                if last_space == -1:
 631 +                    break
 632 +            result += self._textbuf[:last_space] + '\n'
 633 +            self._empty_line_count = 0
 634 +            self._textbuf = ' ' * self._indent + self._textbuf[last_space + 1:]
 635 +        self._textbuf = self._textbuf.rstrip()
 636 +
 637 +        if not self._textbuf:
 638 +            if not addlf:
 639 +                return result
 640 +            self._empty_line_count += 1
 641 +            if self._empty_line_count >= 2:
 642 +                return result
 643 +        else:
 644 +            self._empty_line_count = 0
 645 +
 646 +        if addlf:
 647 +            result += self._textbuf + '\n'
 648 +            self._textbuf = ''
 649 +        return result
 650 +
 651 +    def text_on(self, skip_text=False):
 652 +        if self._text is None:
 653 +            self._text_stack.append(None)
 654 +        else:
 655 +            self._text_stack.append(self._text)
 656 +            #self._text_stack.append('[' + self._text + ']')
 657 +        self._text_stack.append(self._skip_text)
 658 +        self._text_stack.append(self._wrap_skip_text)
 659 +        self._text = ""
 660 +        self._skip_text = skip_text
 661 +        if skip_text:
 662 +            self._wrap_skip_text = True
 663 +
 664 +    def text_off(self):
 665 +        prev_skip_text = self._skip_text
 666 +        self._wrap_skip_text = self._text_stack.pop()
 667 +        self._skip_text = self._text_stack.pop()
 668 +        old_text = self._text_stack.pop()
 669 +        if old_text is None:
 670 +            self._text = None
 671 +        else:
 672 +            if not prev_skip_text:
 673 +                #self._text = 'o#' + old_text + '#|#' + self._text + '#'
 674 +                self._text = old_text + self._text
 675 +            else:
 676 +                self._text = old_text
 677 

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.
  • [get | view] (2010-04-25 11:38:40, 21.4 KB) [[attachment:HelpOnMoinWikiSyntax.txt]]
  • [get | view] (2010-04-06 20:40:40, 21.9 KB) [[attachment:text_plain.py.patch]]
 All files | Selected Files: delete move to page copy to page

You are not allowed to attach a file to this page.