Apply 67_update_handle_old_versions.patch
[mspang/vmailman.git] / Mailman / htmlformat.py
1 # Copyright (C) 1998-2006 by the Free Software Foundation, Inc.
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
16 # USA.
17
18
19 """Library for program-based construction of an HTML documents.
20
21 Encapsulate HTML formatting directives in classes that act as containers
22 for python and, recursively, for nested HTML formatting objects.
23 """
24
25
26 # Eventually could abstract down to HtmlItem, which outputs an arbitrary html
27 # object given start / end tags, valid options, and a value.  Ug, objects
28 # shouldn't be adding their own newlines.  The next object should.
29
30
31 import types
32
33 from Mailman import mm_cfg
34 from Mailman import Utils
35 from Mailman.i18n import _
36
37 SPACE = ' '
38 EMPTYSTRING = ''
39 NL = '\n'
40
41
42 \f
43 # Format an arbitrary object.
44 def HTMLFormatObject(item, indent):
45     "Return a presentation of an object, invoking their Format method if any."
46     if type(item) == type(''):
47         return item
48     elif not hasattr(item, "Format"):
49         return `item`
50     else:
51         return item.Format(indent)
52
53 def CaseInsensitiveKeyedDict(d):
54     result = {}
55     for (k,v) in d.items():
56         result[k.lower()] = v
57     return result
58
59 # Given references to two dictionaries, copy the second dictionary into the
60 # first one.
61 def DictMerge(destination, fresh_dict):
62     for (key, value) in fresh_dict.items():
63         destination[key] = value
64
65 class Table:
66     def __init__(self, **table_opts):
67         self.cells = []
68         self.cell_info = {}
69         self.row_info = {}
70         self.opts = table_opts
71
72     def AddOptions(self, opts):
73         DictMerge(self.opts, opts)
74
75     # Sets all of the cells.  It writes over whatever cells you had there
76     # previously.
77
78     def SetAllCells(self, cells):
79         self.cells = cells
80
81     # Add a new blank row at the end
82     def NewRow(self):
83         self.cells.append([])
84
85     # Add a new blank cell at the end
86     def NewCell(self):
87         self.cells[-1].append('')
88
89     def AddRow(self, row):
90         self.cells.append(row)
91
92     def AddCell(self, cell):
93         self.cells[-1].append(cell)
94
95     def AddCellInfo(self, row, col, **kws):
96         kws = CaseInsensitiveKeyedDict(kws)
97         if not self.cell_info.has_key(row):
98             self.cell_info[row] = { col : kws }
99         elif self.cell_info[row].has_key(col):
100             DictMerge(self.cell_info[row], kws)
101         else:
102             self.cell_info[row][col] = kws
103
104     def AddRowInfo(self, row, **kws):
105         kws = CaseInsensitiveKeyedDict(kws)
106         if not self.row_info.has_key(row):
107             self.row_info[row] = kws
108         else:
109             DictMerge(self.row_info[row], kws)
110
111     # What's the index for the row we just put in?
112     def GetCurrentRowIndex(self):
113         return len(self.cells)-1
114
115     # What's the index for the col we just put in?
116     def GetCurrentCellIndex(self):
117         return len(self.cells[-1])-1
118
119     def ExtractCellInfo(self, info):
120         valid_mods = ['align', 'valign', 'nowrap', 'rowspan', 'colspan',
121                       'bgcolor']
122         output = ''
123
124         for (key, val) in info.items():
125             if not key in valid_mods:
126                 continue
127             if key == 'nowrap':
128                 output = output + ' NOWRAP'
129                 continue
130             else:
131                 output = output + ' %s="%s"' % (key.upper(), val)
132
133         return output
134
135     def ExtractRowInfo(self, info):
136         valid_mods = ['align', 'valign', 'bgcolor']
137         output = ''
138
139         for (key, val) in info.items():
140             if not key in valid_mods:
141                 continue
142             output = output + ' %s="%s"' % (key.upper(), val)
143
144         return output
145
146     def ExtractTableInfo(self, info):
147         valid_mods = ['align', 'width', 'border', 'cellspacing', 'cellpadding',
148                       'bgcolor']
149
150         output = ''
151
152         for (key, val) in info.items():
153             if not key in valid_mods:
154                 continue
155             if key == 'border' and val == None:
156                 output = output + ' BORDER'
157                 continue
158             else:
159                 output = output + ' %s="%s"' % (key.upper(), val)
160
161         return output
162
163     def FormatCell(self, row, col, indent):
164         try:
165             my_info = self.cell_info[row][col]
166         except:
167             my_info = None
168
169         output = '\n' + ' '*indent + '<td'
170         if my_info:
171             output = output + self.ExtractCellInfo(my_info)
172         item = self.cells[row][col]
173         item_format = HTMLFormatObject(item, indent+4)
174         output = '%s>%s</td>' % (output, item_format)
175         return output
176
177     def FormatRow(self, row, indent):
178         try:
179             my_info = self.row_info[row]
180         except:
181             my_info = None
182
183         output = '\n' + ' '*indent + '<tr'
184         if my_info:
185             output = output + self.ExtractRowInfo(my_info)
186         output = output + '>'
187
188         for i in range(len(self.cells[row])):
189             output = output + self.FormatCell(row, i, indent + 2)
190
191         output = output + '\n' + ' '*indent + '</tr>'
192
193         return output
194
195     def Format(self, indent=0):
196         output = '\n' + ' '*indent + '<table'
197         output = output + self.ExtractTableInfo(self.opts)
198         output = output + '>'
199
200         for i in range(len(self.cells)):
201             output = output + self.FormatRow(i, indent + 2)
202
203         output = output + '\n' + ' '*indent + '</table>\n'
204
205         return output
206
207
208 class Link:
209     def __init__(self, href, text, target=None):
210         self.href = href
211         self.text = text
212         self.target = target
213
214     def Format(self, indent=0):
215         texpr = ""
216         if self.target != None:
217             texpr = ' target="%s"' % self.target
218         return '<a href="%s"%s>%s</a>' % (HTMLFormatObject(self.href, indent),
219                                           texpr,
220                                           HTMLFormatObject(self.text, indent))
221
222 class FontSize:
223     """FontSize is being deprecated - use FontAttr(..., size="...") instead."""
224     def __init__(self, size, *items):
225         self.items = list(items)
226         self.size = size
227
228     def Format(self, indent=0):
229         output = '<font size="%s">' % self.size
230         for item in self.items:
231             output = output + HTMLFormatObject(item, indent)
232         output = output + '</font>'
233         return output
234
235 class FontAttr:
236     """Present arbitrary font attributes."""
237     def __init__(self, *items, **kw):
238         self.items = list(items)
239         self.attrs = kw
240
241     def Format(self, indent=0):
242         seq = []
243         for k, v in self.attrs.items():
244             seq.append('%s="%s"' % (k, v))
245         output = '<font %s>' % SPACE.join(seq)
246         for item in self.items:
247             output = output + HTMLFormatObject(item, indent)
248         output = output + '</font>'
249         return output
250
251
252 class Container:
253     def __init__(self, *items):
254         if not items:
255             self.items = []
256         else:
257             self.items = items
258
259     def AddItem(self, obj):
260         self.items.append(obj)
261
262     def Format(self, indent=0):
263         output = []
264         for item in self.items:
265             output.append(HTMLFormatObject(item, indent))
266         return EMPTYSTRING.join(output)
267
268
269 class Label(Container):
270     align = 'right'
271
272     def __init__(self, *items):
273         Container.__init__(self, *items)
274
275     def Format(self, indent=0):
276         return ('<div align="%s">' % self.align) + \
277                Container.Format(self, indent) + \
278                '</div>'
279
280
281 # My own standard document template.  YMMV.
282 # something more abstract would be more work to use...
283
284 class Document(Container):
285     title = None
286     language = None
287     bgcolor = mm_cfg.WEB_BG_COLOR
288     suppress_head = 0
289
290     def set_language(self, lang=None):
291         self.language = lang
292
293     def set_bgcolor(self, color):
294         self.bgcolor = color
295
296     def SetTitle(self, title):
297         self.title = title
298
299     def Format(self, indent=0, **kws):
300         charset = 'us-ascii'
301         if self.language and Utils.IsLanguage(self.language):
302             charset = Utils.GetCharSet(self.language)
303         output = ['Content-Type: text/html; charset=%s' % charset]
304         output.append('Cache-control: no-cache\n')
305         if not self.suppress_head:
306             kws.setdefault('bgcolor', self.bgcolor)
307             tab = ' ' * indent
308             output.extend([tab,
309                            '<HTML>',
310                            '<HEAD>'
311                            ])
312             if mm_cfg.IMAGE_LOGOS:
313                 output.append('<LINK REL="SHORTCUT ICON" HREF="%s">' %
314                               (mm_cfg.IMAGE_LOGOS + mm_cfg.SHORTCUT_ICON))
315             # Hit all the bases
316             output.append('<META http-equiv="Content-Type" '
317                           'content="text/html; charset=%s">' % charset)
318             if self.title:
319                 output.append('%s<TITLE>%s</TITLE>' % (tab, self.title))
320             output.append('%s</HEAD>' % tab)
321             quals = []
322             # Default link colors
323             if mm_cfg.WEB_VLINK_COLOR:
324                 kws.setdefault('vlink', mm_cfg.WEB_VLINK_COLOR)
325             if mm_cfg.WEB_ALINK_COLOR:
326                 kws.setdefault('alink', mm_cfg.WEB_ALINK_COLOR)
327             if mm_cfg.WEB_LINK_COLOR:
328                 kws.setdefault('link', mm_cfg.WEB_LINK_COLOR)
329             for k, v in kws.items():
330                 quals.append('%s="%s"' % (k, v))
331             output.append('%s<BODY %s>' % (tab, SPACE.join(quals)))
332         # Always do this...
333         output.append(Container.Format(self, indent))
334         if not self.suppress_head:
335             output.append('%s</BODY>' % tab)
336             output.append('%s</HTML>' % tab)
337         return NL.join(output)
338
339     def addError(self, errmsg, tag=None):
340         if tag is None:
341             tag = _('Error: ')
342         self.AddItem(Header(3, Bold(FontAttr(
343             _(tag), color=mm_cfg.WEB_ERROR_COLOR, size='+2')).Format() +
344                             Italic(errmsg).Format()))
345
346
347 class HeadlessDocument(Document):
348     """Document without head section, for templates that provide their own."""
349     suppress_head = 1
350
351
352 class StdContainer(Container):
353     def Format(self, indent=0):
354         # If I don't start a new I ignore indent
355         output = '<%s>' % self.tag
356         output = output + Container.Format(self, indent)
357         output = '%s</%s>' % (output, self.tag)
358         return output
359
360
361 class QuotedContainer(Container):
362     def Format(self, indent=0):
363         # If I don't start a new I ignore indent
364         output = '<%s>%s</%s>' % (
365             self.tag,
366             Utils.websafe(Container.Format(self, indent)),
367             self.tag)
368         return output
369
370 class Header(StdContainer):
371     def __init__(self, num, *items):
372         self.items = items
373         self.tag = 'h%d' % num
374
375 class Address(StdContainer):
376     tag = 'address'
377
378 class Underline(StdContainer):
379     tag = 'u'
380
381 class Bold(StdContainer):
382     tag = 'strong'
383
384 class Italic(StdContainer):
385     tag = 'em'
386
387 class Preformatted(QuotedContainer):
388     tag = 'pre'
389
390 class Subscript(StdContainer):
391     tag = 'sub'
392
393 class Superscript(StdContainer):
394     tag = 'sup'
395
396 class Strikeout(StdContainer):
397     tag = 'strike'
398
399 class Center(StdContainer):
400     tag = 'center'
401
402 class Form(Container):
403     def __init__(self, action='', method='POST', encoding=None, *items):
404         apply(Container.__init__, (self,) +  items)
405         self.action = action
406         self.method = method
407         self.encoding = encoding
408
409     def set_action(self, action):
410         self.action = action
411
412     def Format(self, indent=0):
413         spaces = ' ' * indent
414         encoding = ''
415         if self.encoding:
416             encoding = 'enctype="%s"' % self.encoding
417         output = '\n%s<FORM action="%s" method="%s" %s>\n' % (
418             spaces, self.action, self.method, encoding)
419         output = output + Container.Format(self, indent+2)
420         output = '%s\n%s</FORM>\n' % (output, spaces)
421         return output
422
423
424 class InputObj:
425     def __init__(self, name, ty, value, checked, **kws):
426         self.name = name
427         self.type = ty
428         self.value = value
429         self.checked = checked
430         self.kws = kws
431
432     def Format(self, indent=0):
433         output = ['<INPUT name="%s" type="%s" value="%s"' %
434                   (self.name, self.type, self.value)]
435         for item in self.kws.items():
436             output.append('%s="%s"' % item)
437         if self.checked:
438             output.append('CHECKED')
439         output.append('>')
440         return SPACE.join(output)
441
442
443 class SubmitButton(InputObj):
444     def __init__(self, name, button_text):
445         InputObj.__init__(self, name, "SUBMIT", button_text, checked=0)
446
447 class PasswordBox(InputObj):
448     def __init__(self, name, value='', size=mm_cfg.TEXTFIELDWIDTH):
449         InputObj.__init__(self, name, "PASSWORD", value, checked=0, size=size)
450
451 class TextBox(InputObj):
452     def __init__(self, name, value='', size=mm_cfg.TEXTFIELDWIDTH):
453         if isinstance(value, str):
454             safevalue = Utils.websafe(value)
455         else:
456             safevalue = value
457         InputObj.__init__(self, name, "TEXT", safevalue, checked=0, size=size)
458
459 class Hidden(InputObj):
460     def __init__(self, name, value=''):
461         InputObj.__init__(self, name, 'HIDDEN', value, checked=0)
462
463 class TextArea:
464     def __init__(self, name, text='', rows=None, cols=None, wrap='soft',
465                  readonly=0):
466         if isinstance(text, str):
467             safetext = Utils.websafe(text)
468         else:
469             safetext = text
470         self.name = name
471         self.text = safetext
472         self.rows = rows
473         self.cols = cols
474         self.wrap = wrap
475         self.readonly = readonly
476
477     def Format(self, indent=0):
478         output = '<TEXTAREA NAME=%s' % self.name
479         if self.rows:
480             output += ' ROWS=%s' % self.rows
481         if self.cols:
482             output += ' COLS=%s' % self.cols
483         if self.wrap:
484             output += ' WRAP=%s' % self.wrap
485         if self.readonly:
486             output += ' READONLY'
487         output += '>%s</TEXTAREA>' % self.text
488         return output
489
490 class FileUpload(InputObj):
491     def __init__(self, name, rows=None, cols=None, **kws):
492         apply(InputObj.__init__, (self, name, 'FILE', '', 0), kws)
493
494 class RadioButton(InputObj):
495     def __init__(self, name, value, checked=0, **kws):
496         apply(InputObj.__init__, (self, name, 'RADIO', value, checked), kws)
497
498 class CheckBox(InputObj):
499     def __init__(self, name, value, checked=0, **kws):
500         apply(InputObj.__init__, (self, name, "CHECKBOX", value, checked), kws)
501
502 class VerticalSpacer:
503     def __init__(self, size=10):
504         self.size = size
505     def Format(self, indent=0):
506         output = '<spacer type="vertical" height="%d">' % self.size
507         return output
508
509 class WidgetArray:
510     Widget = None
511
512     def __init__(self, name, button_names, checked, horizontal, values):
513         self.name = name
514         self.button_names = button_names
515         self.checked = checked
516         self.horizontal = horizontal
517         self.values = values
518         assert len(values) == len(button_names)
519         # Don't assert `checked' because for RadioButtons it is a scalar while
520         # for CheckedBoxes it is a vector.  Subclasses will assert length.
521
522     def ischecked(self, i):
523         raise NotImplemented
524
525     def Format(self, indent=0):
526         t = Table(cellspacing=5)
527         items = []
528         for i, name, value in zip(range(len(self.button_names)),
529                                   self.button_names,
530                                   self.values):
531             ischecked = (self.ischecked(i))
532             item = self.Widget(self.name, value, ischecked).Format() + name
533             items.append(item)
534             if not self.horizontal:
535                 t.AddRow(items)
536                 items = []
537         if self.horizontal:
538             t.AddRow(items)
539         return t.Format(indent)
540
541 class RadioButtonArray(WidgetArray):
542     Widget = RadioButton
543
544     def __init__(self, name, button_names, checked=None, horizontal=1,
545                  values=None):
546         if values is None:
547             values = range(len(button_names))
548         # BAW: assert checked is a scalar...
549         WidgetArray.__init__(self, name, button_names, checked, horizontal,
550                              values)
551
552     def ischecked(self, i):
553         return self.checked == i
554
555 class CheckBoxArray(WidgetArray):
556     Widget = CheckBox
557
558     def __init__(self, name, button_names, checked=None, horizontal=0,
559                  values=None):
560         if checked is None:
561             checked = [0] * len(button_names)
562         else:
563             assert len(checked) == len(button_names)
564         if values is None:
565             values = range(len(button_names))
566         WidgetArray.__init__(self, name, button_names, checked, horizontal,
567                              values)
568
569     def ischecked(self, i):
570         return self.checked[i]
571
572 class UnorderedList(Container):
573     def Format(self, indent=0):
574         spaces = ' ' * indent
575         output = '\n%s<ul>\n' % spaces
576         for item in self.items:
577             output = output + '%s<li>%s\n' % \
578                      (spaces, HTMLFormatObject(item, indent + 2))
579         output = output + '%s</ul>\n' % spaces
580         return output
581
582 class OrderedList(Container):
583     def Format(self, indent=0):
584         spaces = ' ' * indent
585         output = '\n%s<ol>\n' % spaces
586         for item in self.items:
587             output = output + '%s<li>%s\n' % \
588                      (spaces, HTMLFormatObject(item, indent + 2))
589         output = output + '%s</ol>\n' % spaces
590         return output
591
592 class DefinitionList(Container):
593     def Format(self, indent=0):
594         spaces = ' ' * indent
595         output = '\n%s<dl>\n' % spaces
596         for dt, dd in self.items:
597             output = output + '%s<dt>%s\n<dd>%s\n' % \
598                      (spaces, HTMLFormatObject(dt, indent+2),
599                       HTMLFormatObject(dd, indent+2))
600         output = output + '%s</dl>\n' % spaces
601         return output
602
603
604 \f
605 # Logo constants
606 #
607 # These are the URLs which the image logos link to.  The Mailman home page now
608 # points at the gnu.org site instead of the www.list.org mirror.
609 #
610 from mm_cfg import MAILMAN_URL
611 PYTHON_URL  = 'http://www.python.org/'
612 GNU_URL     = 'http://www.gnu.org/'
613
614 # The names of the image logo files.  These are concatentated onto
615 # mm_cfg.IMAGE_LOGOS (not urljoined).
616 DELIVERED_BY = 'mailman.jpg'
617 PYTHON_POWERED = 'PythonPowered.png'
618 GNU_HEAD = 'gnu-head-tiny.jpg'
619
620
621 def MailmanLogo():
622     t = Table(border=0, width='100%')
623     if mm_cfg.IMAGE_LOGOS:
624         def logo(file):
625             return mm_cfg.IMAGE_LOGOS + file
626         mmlink = '<img src="%s" alt="Delivered by Mailman" border=0>' \
627                  '<br>version %s' % (logo(DELIVERED_BY), mm_cfg.VERSION)
628         pylink = '<img src="%s" alt="Python Powered" border=0>' % \
629                  logo(PYTHON_POWERED)
630         gnulink = '<img src="%s" alt="GNU\'s Not Unix" border=0>' % \
631                   logo(GNU_HEAD)
632         t.AddRow([mmlink, pylink, gnulink])
633     else:
634         # use only textual links
635         version = mm_cfg.VERSION
636         mmlink = Link(MAILMAN_URL,
637                       _('Delivered by Mailman<br>version %(version)s'))
638         pylink = Link(PYTHON_URL, _('Python Powered'))
639         gnulink = Link(GNU_URL, _("Gnu's Not Unix"))
640         t.AddRow([mmlink, pylink, gnulink])
641     return t
642
643
644 class SelectOptions:
645    def __init__(self, varname, values, legend,
646                 selected=0, size=1, multiple=None):
647       self.varname  = varname
648       self.values   = values
649       self.legend   = legend
650       self.size     = size
651       self.multiple = multiple
652       # we convert any type to tuple, commas are needed
653       if not multiple:
654          if type(selected) == types.IntType:
655              self.selected = (selected,)
656          elif type(selected) == types.TupleType:
657              self.selected = (selected[0],)
658          elif type(selected) == types.ListType:
659              self.selected = (selected[0],)
660          else:
661              self.selected = (0,)
662
663    def Format(self, indent=0):
664       spaces = " " * indent
665       items  = min( len(self.values), len(self.legend) )
666
667       # jcrey: If there is no argument, we return nothing to avoid errors
668       if items == 0:
669           return ""
670
671       text = "\n" + spaces + "<Select name=\"%s\"" % self.varname
672       if self.size > 1:
673           text = text + " size=%d" % self.size
674       if self.multiple:
675           text = text + " multiple"
676       text = text + ">\n"
677
678       for i in range(items):
679           if i in self.selected:
680               checked = " Selected"
681           else:
682               checked = ""
683
684           opt = " <option value=\"%s\"%s> %s </option>" % (
685               self.values[i], checked, self.legend[i])
686           text = text + spaces + opt + "\n"
687
688       return text + spaces + '</Select>'