1
# Copyright (C) 1998-2008 by the Free Software Foundation, Inc.
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.
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.
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,
18
"""Process and produce the list-administration options forms."""
28
from email.Utils import unquote, parseaddr, formataddr
29
from string import lowercase, digits
31
from Mailman import Errors
32
from Mailman import MailList
33
from Mailman import MemberAdaptor
34
from Mailman import Utils
35
from Mailman import i18n
36
from Mailman import passwords
37
from Mailman.Cgi import Auth
38
from Mailman.UserDesc import UserDesc
39
from Mailman.configuration import config
40
from Mailman.htmlformat import *
44
i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
49
log = logging.getLogger('mailman.error')
54
# Try to find out which list is being administered
55
parts = Utils.GetPathPieces()
57
# None, so just do the admin overview and be done with it
61
listname = parts[0].lower()
63
mlist = MailList.MailList(listname, lock=False)
64
except Errors.MMListError, e:
65
# Avoid cross-site scripting attacks
66
safelistname = Utils.websafe(listname)
67
admin_overview(_('No such list <em>%(safelistname)s</em>'))
68
log.error('admin.py access for non-existent list: %s', listname)
70
# Now that we know what list has been requested, all subsequent admin
71
# pages are shown in that list's preferred language.
72
i18n.set_language(mlist.preferred_language)
73
# If the user is not authenticated, we're done.
74
cgidata = cgi.FieldStorage(keep_blank_values=1)
76
if not mlist.WebAuthenticate((config.AuthListAdmin,
77
config.AuthSiteAdmin),
78
cgidata.getvalue('adminpw', '')):
79
if cgidata.has_key('adminpw'):
80
# This is a re-authorization attempt
81
msg = Bold(FontSize('+1', _('Authorization failed.'))).Format()
84
Auth.loginpage(mlist, 'admin', msg=msg)
87
# Which subcategory was requested? Default is `general'
98
# Is this a log-out request?
99
if category == 'logout':
100
print mlist.ZapCookie(config.AuthListAdmin)
101
Auth.loginpage(mlist, 'admin', frontpage=True)
105
if category not in mlist.GetConfigCategories().keys():
108
# Is the request for variable details?
110
qsenviron = os.environ.get('QUERY_STRING')
113
parsedqs = cgi.parse_qs(qsenviron)
114
if cgidata.has_key('VARHELP'):
115
varhelp = cgidata.getvalue('VARHELP')
117
# POST methods, even if their actions have a query string, don't get
118
# put into FieldStorage's keys :-(
119
qs = parsedqs.get('VARHELP')
120
if qs and isinstance(qs, list):
123
option_help(mlist, varhelp)
126
# The html page document
128
doc.set_language(mlist.preferred_language)
132
# There are options to change
133
change_options(mlist, category, subcat, cgidata, doc)
134
# Let the list sanity check the changed values
136
# Additional sanity checks
137
if not mlist.digestable and not mlist.nondigestable:
139
_('''You have turned off delivery of both digest and
140
non-digest messages. This is an incompatible state of
141
affairs. You must turn on either digest delivery or
142
non-digest delivery or your mailing list will basically be
143
unusable.'''), tag=_('Warning: '))
145
if not mlist.digestable and mlist.getDigestMemberKeys():
147
_('''You have digest members, but digests are turned
148
off. Those people will not receive mail.'''),
150
if not mlist.nondigestable and mlist.getRegularMemberKeys():
152
_('''You have regular list members but non-digestified mail is
153
turned off. They will receive mail until you fix this
154
problem.'''), tag=_('Warning: '))
155
# Glom up the results page and print it out
156
show_results(mlist, doc, category, subcat, cgidata)
164
def admin_overview(msg=''):
165
# Show the administrative overview page, with the list of all the lists on
166
# this host. msg is an optional error message to display at the top of
169
# This page should be displayed in the server's default language, which
170
# should have already been set.
171
hostname = Utils.get_request_domain()
172
legend = _('%(hostname)s mailing lists - Admin Links')
173
# The html `document'
175
doc.set_language(config.DEFAULT_SERVER_LANGUAGE)
177
# The table that will hold everything
178
table = Table(border=0, width="100%")
179
table.AddRow([Center(Header(2, legend))])
180
table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
181
bgcolor=config.WEB_HEADER_COLOR)
182
# Skip any mailing list that isn't advertised.
184
for name in sorted(config.list_manager.names):
185
mlist = MailList.MailList(name, lock=False)
187
if hostname not in mlist.web_page_url:
188
# This list is situated in a different virtual domain
191
advertised.append((mlist.GetScriptURL('admin'),
194
# Greeting depends on whether there was an error or not
196
greeting = FontAttr(msg, color="ff5060", size="+1")
198
greeting = _("Welcome!")
201
mailmanlink = Link(config.MAILMAN_URL, _('Mailman')).Format()
205
_('''<p>There currently are no publicly-advertised %(mailmanlink)s
206
mailing lists on %(hostname)s.'''),
211
_('''<p>Below is the collection of publicly-advertised
212
%(mailmanlink)s mailing lists on %(hostname)s. Click on a list
213
name to visit the configuration pages for that list.'''),
216
creatorurl = Utils.ScriptURL('create')
217
mailman_owner = Utils.get_site_noreply()
218
extra = msg and _('right ') or ''
220
_('''To visit the administrators configuration page for an
221
unadvertised list, open a URL similar to this one, but with a '/' and
222
the %(extra)slist name appended. If you have the proper authority,
223
you can also <a href="%(creatorurl)s">create a new mailing list</a>.
225
<p>General list information can be found at '''),
226
Link(Utils.ScriptURL('listinfo'),
227
_('the mailing list overview page')),
229
_('<p>(Send questions and comments to '),
230
Link('mailto:%s' % mailman_owner, mailman_owner),
234
table.AddRow([Container(*welcome)])
235
table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2)
238
table.AddRow([' ', ' '])
239
table.AddRow([Bold(FontAttr(_('List'), size='+2')),
240
Bold(FontAttr(_('Description'), size='+2'))
243
for url, real_name, description in advertised:
245
[Link(url, Bold(real_name)),
246
description or Italic(_('[no description available]'))])
247
if highlight and config.WEB_HIGHLIGHT_COLOR:
248
table.AddRowInfo(table.GetCurrentRowIndex(),
249
bgcolor=config.WEB_HIGHLIGHT_COLOR)
250
highlight = not highlight
254
doc.AddItem(MailmanLogo())
259
def option_help(mlist, varhelp):
260
# The html page document
262
doc.set_language(mlist.preferred_language)
263
# Find out which category and variable help is being requested for.
265
reflist = varhelp.split('/')
266
if len(reflist) >= 2:
267
category = subcat = None
268
if len(reflist) == 2:
269
category, varname = reflist
270
elif len(reflist) == 3:
271
category, subcat, varname = reflist
272
options = mlist.GetConfigInfo(category, subcat)
275
if i and i[0] == varname:
278
# Print an error message if we couldn't find a valid one
280
bad = _('No valid variable name found.')
282
doc.AddItem(mlist.GetMailmanFooter())
285
# Get the details about the variable
286
varname, kind, params, dependancies, description, elaboration = \
287
get_item_characteristics(item)
288
# Set up the document
289
realname = mlist.real_name
290
legend = _("""%(realname)s Mailing list Configuration Help
291
<br><em>%(varname)s</em> Option""")
293
header = Table(width='100%')
294
header.AddRow([Center(Header(3, legend))])
295
header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
296
bgcolor=config.WEB_HEADER_COLOR)
297
doc.SetTitle(_("Mailman %(varname)s List Option Help"))
299
doc.AddItem("<b>%s</b> (%s): %s<p>" % (varname, category, description))
301
doc.AddItem("%s<p>" % elaboration)
304
url = '%s/%s/%s' % (mlist.GetScriptURL('admin'), category, subcat)
306
url = '%s/%s' % (mlist.GetScriptURL('admin'), category)
308
valtab = Table(cellspacing=3, cellpadding=4, width='100%')
309
add_options_table_item(mlist, category, subcat, valtab, item, detailsp=0)
312
form.AddItem(Center(submit_button()))
313
doc.AddItem(Center(form))
315
doc.AddItem(_("""<em><strong>Warning:</strong> changing this option here
316
could cause other screens to be out-of-sync. Be sure to reload any other
317
pages that are displaying this option for this mailing list. You can also
320
adminurl = mlist.GetScriptURL('admin')
322
url = '%s/%s/%s' % (adminurl, category, subcat)
324
url = '%s/%s' % (adminurl, category)
325
categoryname = mlist.GetConfigCategories()[category][0]
326
doc.AddItem(Link(url, _('return to the %(categoryname)s options page.')))
328
doc.AddItem(mlist.GetMailmanFooter())
333
def show_results(mlist, doc, category, subcat, cgidata):
334
# Produce the results page
335
adminurl = mlist.GetScriptURL('admin')
336
categories = mlist.GetConfigCategories()
337
label = _(categories[category][0])
339
# Set up the document's headers
340
realname = mlist.real_name
341
doc.SetTitle(_('%(realname)s Administration (%(label)s)'))
342
doc.AddItem(Center(Header(2, _(
343
'%(realname)s mailing list administration<br>%(label)s Section'))))
345
# Now we need to craft the form that will be submitted, which will contain
346
# all the variable settings, etc. This is a bit of a kludge because we
347
# know that the autoreply and members categories supports file uploads.
349
if category in ('autoreply', 'members'):
350
encoding = 'multipart/form-data'
352
form = Form('%s/%s/%s' % (adminurl, category, subcat),
355
form = Form('%s/%s' % (adminurl, category), encoding=encoding)
356
# This holds the two columns of links
357
linktable = Table(valign='top', width='100%')
358
linktable.AddRow([Center(Bold(_("Configuration Categories"))),
359
Center(Bold(_("Other Administrative Activities")))])
360
# The `other links' are stuff in the right column.
361
otherlinks = UnorderedList()
362
otherlinks.AddItem(Link(mlist.GetScriptURL('admindb'),
363
_('Tend to pending moderator requests')))
364
otherlinks.AddItem(Link(mlist.GetScriptURL('listinfo'),
365
_('Go to the general list information page')))
366
otherlinks.AddItem(Link(mlist.GetScriptURL('edithtml'),
367
_('Edit the public HTML pages and text files')))
368
otherlinks.AddItem(Link(mlist.GetBaseArchiveURL(),
369
_('Go to list archives')).Format() +
371
if config.OWNERS_CAN_DELETE_THEIR_OWN_LISTS:
372
otherlinks.AddItem(Link(mlist.GetScriptURL('rmlist'),
373
_('Delete this mailing list')).Format() +
374
_(' (requires confirmation)<br> <br>'))
375
otherlinks.AddItem(Link('%s/logout' % adminurl,
376
# BAW: What I really want is a blank line, but
377
# adding an won't do it because of the
378
# bullet added to the list item.
379
'<FONT SIZE="+2"><b>%s</b></FONT>' %
381
# These are links to other categories and live in the left column
382
categorylinks_1 = categorylinks = UnorderedList()
384
categorykeys = categories.keys()
385
half = len(categorykeys) / 2
388
for k in categorykeys:
389
label = _(categories[k][0])
390
url = '%s/%s' % (adminurl, k)
392
# Handle subcategories
393
subcats = mlist.GetConfigSubCategories(k)
395
subcat = Utils.GetPathPieces()[-1]
400
# The first subcategory in the list is the default
401
subcat = subcats[0][0]
403
for sub, text in subcats:
405
text = Bold('[%s]' % text).Format()
406
subcat_items.append(Link(url + '/' + sub, text))
407
categorylinks.AddItem(
408
Bold(label).Format() +
409
UnorderedList(*subcat_items).Format())
411
categorylinks.AddItem(Link(url, Bold('[%s]' % label)))
413
categorylinks.AddItem(Link(url, label))
416
categorylinks_2 = categorylinks = UnorderedList()
417
counter = -len(categorykeys)
418
# Make the emergency stop switch a rude solo light
420
# Add all the links to the links table...
421
etable.AddRow([categorylinks_1, categorylinks_2])
422
etable.AddRowInfo(etable.GetCurrentRowIndex(), valign='top')
424
label = _('Emergency moderation of all list traffic is enabled')
425
etable.AddRow([Center(
426
Link('?VARHELP=general/emergency', Bold(label)))])
427
color = config.WEB_ERROR_COLOR
428
etable.AddCellInfo(etable.GetCurrentRowIndex(), 0,
429
colspan=2, bgcolor=color)
430
linktable.AddRow([etable, otherlinks])
431
# ...and add the links table to the document.
432
form.AddItem(linktable)
435
_('''Make your changes in the following section, then submit them
436
using the <em>Submit Your Changes</em> button below.''')
439
# The members and passwords categories are special in that they aren't
440
# defined in terms of gui elements. Create those pages here.
441
if category == 'members':
442
# Figure out which subcategory we should display
443
subcat = Utils.GetPathPieces()[-1]
444
if subcat not in ('list', 'add', 'remove'):
446
# Add member category specific tables
447
form.AddItem(membership_options(mlist, subcat, cgidata, doc, form))
448
form.AddItem(Center(submit_button('setmemberopts_btn')))
449
# In "list" subcategory, we can also search for members
451
form.AddItem('<hr>\n')
452
table = Table(width='100%')
453
table.AddRow([Center(Header(2, _('Additional Member Tasks')))])
454
table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
455
bgcolor=config.WEB_HEADER_COLOR)
456
# Add a blank separator row
457
table.AddRow([' ', ' '])
458
# Add a section to set the moderation bit for all members
459
table.AddRow([_("""<li>Set everyone's moderation bit, including
460
those members not currently visible""")])
461
table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
462
table.AddRow([RadioButtonArray('allmodbit_val',
464
mlist.default_member_moderation),
465
SubmitButton('allmodbit_btn', _('Set'))])
467
elif category == 'passwords':
468
form.AddItem(Center(password_inputs(mlist)))
469
form.AddItem(Center(submit_button()))
471
form.AddItem(show_variables(mlist, category, subcat, cgidata, doc))
472
form.AddItem(Center(submit_button()))
475
doc.AddItem(mlist.GetMailmanFooter())
479
def show_variables(mlist, category, subcat, cgidata, doc):
480
options = mlist.GetConfigInfo(category, subcat)
482
# The table containing the results
483
table = Table(cellspacing=3, cellpadding=4, width='100%')
485
# Get and portray the text label for the category.
486
categories = mlist.GetConfigCategories()
487
label = _(categories[category][0])
489
table.AddRow([Center(Header(2, label))])
490
table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
491
bgcolor=config.WEB_HEADER_COLOR)
493
# The very first item in the config info will be treated as a general
494
# description if it is a string
495
description = options[0]
496
if isinstance(description, basestring):
497
table.AddRow([description])
498
table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
499
options = options[1:]
504
# Add the global column headers
505
table.AddRow([Center(Bold(_('Description'))),
506
Center(Bold(_('Value')))])
507
table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0,
509
table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 1,
513
if isinstance(item, basestring):
514
# The very first banner option (string in an options list) is
515
# treated as a general description, while any others are
516
# treated as section headers - centered and italicized...
517
table.AddRow([Center(Italic(item))])
518
table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
520
add_options_table_item(mlist, category, subcat, table, item)
521
table.AddRow(['<br>'])
522
table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
527
def add_options_table_item(mlist, category, subcat, table, item, detailsp=1):
528
# Add a row to an options table with the item description and value.
529
varname, kind, params, extra, descr, elaboration = \
530
get_item_characteristics(item)
531
if elaboration is None:
533
descr = get_item_gui_description(mlist, category, subcat,
534
varname, descr, elaboration, detailsp)
535
val = get_item_gui_value(mlist, category, kind, varname, params, extra)
536
table.AddRow([descr, val])
537
table.AddCellInfo(table.GetCurrentRowIndex(), 0,
538
bgcolor=config.WEB_ADMINITEM_COLOR)
539
table.AddCellInfo(table.GetCurrentRowIndex(), 1,
540
bgcolor=config.WEB_ADMINITEM_COLOR)
544
def get_item_characteristics(record):
545
# Break out the components of an item description from its description
548
# 0 -- option-var name
551
# 3 -- ?dependancies?
552
# 4 -- Brief description
553
# 5 -- Optional description elaboration
556
varname, kind, params, dependancies, descr = record
557
elif len(record) == 6:
558
varname, kind, params, dependancies, descr, elaboration = record
560
raise ValueError, _('Badly formed options entry:\n %(record)s')
561
return varname, kind, params, dependancies, descr, elaboration
565
def get_item_gui_value(mlist, category, kind, varname, params, extra):
566
"""Return a representation of an item's settings."""
567
# Give the category a chance to return the value for the variable
569
label, gui = mlist.GetConfigCategories()[category]
570
if hasattr(gui, 'getValue'):
571
value = gui.getValue(mlist, kind, varname, params)
572
# Filter out None, and volatile attributes
573
if value is None and not varname.startswith('_'):
574
value = getattr(mlist, varname)
575
# Now create the widget for this value
576
if kind == config.Radio or kind == config.Toggle:
577
# If we are returning the option for subscribe policy and this site
578
# doesn't allow open subscribes, then we have to alter the value of
579
# mlist.subscribe_policy as passed to RadioButtonArray in order to
580
# compensate for the fact that there is one fewer option.
581
# Correspondingly, we alter the value back in the change options
584
# TBD: this is an ugly ugly hack.
585
if varname.startswith('_'):
589
if varname == 'subscribe_policy' and not config.ALLOW_OPEN_SUBSCRIBE:
590
checked = checked - 1
591
# For Radio buttons, we're going to interpret the extra stuff as a
592
# horizontal/vertical flag. For backwards compatibility, the value 0
593
# means horizontal, so we use "not extra" to get the parity right.
594
return RadioButtonArray(varname, params, checked, not extra)
595
elif (kind == config.String or kind == config.Email or
596
kind == config.Host or kind == config.Number):
597
return TextBox(varname, value, params)
598
elif kind == config.Text:
603
return TextArea(varname, value or '', r, c)
604
elif kind in (config.EmailList, config.EmailListEx):
610
return TextArea(varname, res, r, c, wrap='off')
611
elif kind == config.FileUpload:
612
# like a text area, but also with uploading
617
container = Container()
618
container.AddItem(_('<em>Enter the text below, or...</em><br>'))
619
container.AddItem(TextArea(varname, value or '', r, c))
620
container.AddItem(_('<br><em>...specify a file to upload</em><br>'))
621
container.AddItem(FileUpload(varname+'_upload', r, c))
623
elif kind == config.Select:
625
values, legend, selected = params
627
codes = mlist.language_codes
628
legend = [config.languages.get_description(code) for code in codes]
629
selected = codes.index(mlist.preferred_language)
630
return SelectOptions(varname, values, legend, selected)
631
elif kind == config.Topics:
632
# A complex and specialized widget type that allows for setting of a
633
# topic name, a mark button, a regexp text box, an "add after mark",
634
# and a delete button. Yeesh! params are ignored.
635
table = Table(border=0)
636
# This adds the html for the entry widget
637
def makebox(i, name, pattern, desc, empty=False, table=table):
638
deltag = 'topic_delete_%02d' % i
639
boxtag = 'topic_box_%02d' % i
640
reboxtag = 'topic_rebox_%02d' % i
641
desctag = 'topic_desc_%02d' % i
642
wheretag = 'topic_where_%02d' % i
643
addtag = 'topic_add_%02d' % i
644
newtag = 'topic_new_%02d' % i
646
table.AddRow([Center(Bold(_('Topic %(i)d'))),
649
table.AddRow([Center(Bold(_('Topic %(i)d'))),
650
SubmitButton(deltag, _('Delete'))])
651
table.AddRow([Label(_('Topic name:')),
652
TextBox(boxtag, value=name, size=30)])
653
table.AddRow([Label(_('Regexp:')),
654
TextArea(reboxtag, text=pattern,
655
rows=4, cols=30, wrap='off')])
656
table.AddRow([Label(_('Description:')),
657
TextArea(desctag, text=desc,
658
rows=4, cols=30, wrap='soft')])
660
table.AddRow([SubmitButton(addtag, _('Add new item...')),
661
SelectOptions(wheretag, ('before', 'after'),
662
(_('...before this one.'),
663
_('...after this one.')),
666
table.AddRow(['<hr>'])
667
table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
668
# Now for each element in the existing data, create a widget
670
data = getattr(mlist, varname)
671
for name, pattern, desc, empty in data:
672
makebox(i, name, pattern, desc, empty)
674
# Add one more non-deleteable widget as the first blank entry, but
675
# only if there are no real entries.
677
makebox(i, '', '', '', empty=True)
679
elif kind == config.HeaderFilter:
680
# A complex and specialized widget type that allows for setting of a
681
# spam filter rule including, a mark button, a regexp text box, an
682
# "add after mark", up and down buttons, and a delete button. Yeesh!
683
# params are ignored.
684
table = Table(border=0)
685
# This adds the html for the entry widget
686
def makebox(i, pattern, action, empty=False, table=table):
687
deltag = 'hdrfilter_delete_%02d' % i
688
reboxtag = 'hdrfilter_rebox_%02d' % i
689
actiontag = 'hdrfilter_action_%02d' % i
690
wheretag = 'hdrfilter_where_%02d' % i
691
addtag = 'hdrfilter_add_%02d' % i
692
newtag = 'hdrfilter_new_%02d' % i
693
uptag = 'hdrfilter_up_%02d' % i
694
downtag = 'hdrfilter_down_%02d' % i
696
table.AddRow([Center(Bold(_('Spam Filter Rule %(i)d'))),
699
table.AddRow([Center(Bold(_('Spam Filter Rule %(i)d'))),
700
SubmitButton(deltag, _('Delete'))])
701
table.AddRow([Label(_('Spam Filter Regexp:')),
702
TextArea(reboxtag, text=pattern,
703
rows=4, cols=30, wrap='off')])
704
values = [config.DEFER, config.HOLD, config.REJECT,
705
config.DISCARD, config.ACCEPT]
707
checked = values.index(action)
710
radio = RadioButtonArray(
712
(_('Defer'), _('Hold'), _('Reject'),
713
_('Discard'), _('Accept')),
715
checked=checked).Format()
716
table.AddRow([Label(_('Action:')), radio])
718
table.AddRow([SubmitButton(addtag, _('Add new item...')),
719
SelectOptions(wheretag, ('before', 'after'),
720
(_('...before this one.'),
721
_('...after this one.')),
724
# BAW: IWBNI we could disable the up and down buttons for the
725
# first and last item respectively, but it's not easy to know
726
# which is the last item, so let's not worry about that for
728
table.AddRow([SubmitButton(uptag, _('Move rule up')),
729
SubmitButton(downtag, _('Move rule down'))])
730
table.AddRow(['<hr>'])
731
table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
732
# Now for each element in the existing data, create a widget
734
data = getattr(mlist, varname)
735
for pattern, action, empty in data:
736
makebox(i, pattern, action, empty)
738
# Add one more non-deleteable widget as the first blank entry, but
739
# only if there are no real entries.
741
makebox(i, '', config.DEFER, empty=True)
743
elif kind == config.Checkbox:
744
return CheckBoxArray(varname, *params)
746
assert 0, 'Bad gui widget type: %s' % kind
750
def get_item_gui_description(mlist, category, subcat,
751
varname, descr, elaboration, detailsp):
752
# Return the item's description, with link to details.
754
# Details are not included if this is a VARHELP page, because that /is/
758
varhelp = '?VARHELP=%s/%s/%s' % (category, subcat, varname)
760
varhelp = '?VARHELP=%s/%s' % (category, varname)
761
if descr == elaboration:
762
linktext = _('<br>(Edit <b>%(varname)s</b>)')
764
linktext = _('<br>(Details for <b>%(varname)s</b>)')
765
link = Link(mlist.GetScriptURL('admin') + varhelp,
767
text = Label('%s %s' % (descr, link)).Format()
769
text = Label(descr).Format()
770
if varname[0] == '_':
771
text += Label(_('''<br><em><strong>Note:</strong>
772
setting this value performs an immediate action but does not modify
773
permanent state.</em>''')).Format()
778
def membership_options(mlist, subcat, cgidata, doc, form):
779
# Show the main stuff
780
adminurl = mlist.GetScriptURL('admin')
781
container = Container()
782
header = Table(width="100%")
783
# If we're in the list subcategory, show the membership list
785
header.AddRow([Center(Header(2, _('Mass Subscriptions')))])
786
header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
787
bgcolor=config.WEB_HEADER_COLOR)
788
container.AddItem(header)
789
mass_subscribe(mlist, container)
791
if subcat == 'remove':
792
header.AddRow([Center(Header(2, _('Mass Removals')))])
793
header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
794
bgcolor=config.WEB_HEADER_COLOR)
795
container.AddItem(header)
796
mass_remove(mlist, container)
799
header.AddRow([Center(Header(2, _('Membership List')))])
800
header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
801
bgcolor=config.WEB_HEADER_COLOR)
802
container.AddItem(header)
803
# Add a "search for member" button
804
table = Table(width='100%')
805
link = Link('http://www.python.org/doc/current/lib/re-syntax.html',
806
_('(help)')).Format()
807
table.AddRow([Label(_('Find member %(link)s:')),
808
TextBox('findmember',
809
value=cgidata.getvalue('findmember', '')),
810
SubmitButton('findmember_btn', _('Search...'))])
811
container.AddItem(table)
812
container.AddItem('<hr><p>')
813
usertable = Table(width="90%", border='2')
814
# If there are more members than allowed by chunksize, then we split the
815
# membership up alphabetically. Otherwise just display them all.
816
chunksz = mlist.admin_member_chunksize
817
# The email addresses had /better/ be ASCII, but might be encoded in the
818
# database as Unicodes.
819
all = [_m.encode() for _m in mlist.getMembers()]
820
all.sort(lambda x, y: cmp(x.lower(), y.lower()))
821
# See if the query has a regular expression
822
regexp = cgidata.getvalue('findmember', '').strip()
825
cre = re.compile(regexp, re.IGNORECASE)
827
doc.addError(_('Bad regular expression: ') + regexp)
829
# BAW: There's got to be a more efficient way of doing this!
830
names = [mlist.getMemberName(s) or '' for s in all]
831
all = [a for n, a in zip(names, all)
832
if cre.search(n) or cre.search(a)]
836
if len(all) < chunksz:
839
# Split them up alphabetically, and then split the alphabetical
843
members = buckets.setdefault(addr[0].lower(), [])
845
# Now figure out which bucket we want
848
# POST methods, even if their actions have a query string, don't get
849
# put into FieldStorage's keys :-(
850
qsenviron = os.environ.get('QUERY_STRING')
852
qs = cgi.parse_qs(qsenviron)
853
bucket = qs.get('letter', 'a')[0].lower()
854
if bucket not in digits + lowercase:
856
if not bucket or not buckets.has_key(bucket):
857
keys = buckets.keys()
860
members = buckets[bucket]
861
action = adminurl + '/members?letter=%s' % bucket
862
if len(members) <= chunksz:
863
form.set_action(action)
865
i, r = divmod(len(members), chunksz)
866
numchunks = i + (not not r * 1)
869
if qs.has_key('chunk'):
871
chunkindex = int(qs['chunk'][0])
874
if chunkindex < 0 or chunkindex > numchunks:
876
members = members[chunkindex*chunksz:(chunkindex+1)*chunksz]
877
# And set the action URL
878
form.set_action(action + '&chunk=%s' % chunkindex)
879
# So now members holds all the addresses we're going to display
882
membercnt = len(members)
883
usertable.AddRow([Center(Italic(_(
884
'%(allcnt)s members total, %(membercnt)s shown')))])
886
usertable.AddRow([Center(Italic(_('%(allcnt)s members total')))])
887
usertable.AddCellInfo(usertable.GetCurrentRowIndex(),
888
usertable.GetCurrentCellIndex(),
890
bgcolor=config.WEB_ADMINITEM_COLOR)
891
# Add the alphabetical links
894
for letter in digits + lowercase:
895
if not buckets.get(letter):
897
url = adminurl + '/members?letter=%s' % letter
899
show = Bold('[%s]' % letter.upper()).Format()
901
show = letter.upper()
902
cells.append(Link(url, show).Format())
903
joiner = ' '*2 + '\n'
904
usertable.AddRow([Center(joiner.join(cells))])
905
usertable.AddCellInfo(usertable.GetCurrentRowIndex(),
906
usertable.GetCurrentCellIndex(),
908
bgcolor=config.WEB_ADMINITEM_COLOR)
909
usertable.AddRow([Center(h) for h in (_('unsub'),
910
_('member address<br>member name'),
912
_('nomail<br>[reason]'),
913
_('ack'), _('not metoo'),
915
_('digest'), _('plain'),
917
rowindex = usertable.GetCurrentRowIndex()
918
for i in range(OPTCOLUMNS):
919
usertable.AddCellInfo(rowindex, i, bgcolor=config.WEB_ADMINITEM_COLOR)
920
# Find the longest name in the list
923
names = filter(None, [mlist.getMemberName(s) for s in members])
924
# Make the name field at least as long as the longest email address
925
longest = max([len(s) for s in names + members])
926
# Abbreviations for delivery status details
927
ds_abbrevs = {MemberAdaptor.UNKNOWN : _('?'),
928
MemberAdaptor.BYUSER : _('U'),
929
MemberAdaptor.BYADMIN : _('A'),
930
MemberAdaptor.BYBOUNCE: _('B'),
932
# Now populate the rows
934
link = Link(mlist.GetOptionsURL(addr, obscure=1),
935
mlist.getMemberCPAddress(addr))
936
fullname = mlist.getMemberName(addr)
937
name = TextBox(addr + '_realname', fullname, size=longest).Format()
938
cells = [Center(CheckBox(addr + '_unsub', 'off', 0).Format()),
939
link.Format() + '<br>' +
941
Hidden('user', urllib.quote(addr)).Format(),
943
# Do the `mod' option
944
if mlist.getMemberOption(addr, config.Moderate):
950
box = CheckBox('%s_mod' % addr, value, checked)
951
cells.append(Center(box).Format())
952
for opt in ('hide', 'nomail', 'ack', 'notmetoo', 'nodupes'):
955
status = mlist.getDeliveryStatus(addr)
956
if status == MemberAdaptor.ENABLED:
962
extra = '[%s]' % ds_abbrevs[status]
963
elif mlist.getMemberOption(addr, config.OPTINFO[opt]):
969
box = CheckBox('%s_%s' % (addr, opt), value, checked)
970
cells.append(Center(box.Format() + extra))
971
# This code is less efficient than the original which did a has_key on
972
# the underlying dictionary attribute. This version is slower and
973
# less memory efficient. It points to a new MemberAdaptor interface
975
if addr in mlist.getRegularMemberKeys():
976
cells.append(Center(CheckBox(addr + '_digest', 'off', 0).Format()))
978
cells.append(Center(CheckBox(addr + '_digest', 'on', 1).Format()))
979
if mlist.getMemberOption(addr, config.OPTINFO['plain']):
985
cells.append(Center(CheckBox('%s_plain' % addr, value, checked)))
986
# User's preferred language
987
langpref = mlist.getMemberLanguage(addr)
988
langs = mlist.language_codes
989
langdescs = [_(config.languges.get_description(code))
992
selected = langs.index(langpref)
995
cells.append(Center(SelectOptions(addr + '_language', langs,
996
langdescs, selected)).Format())
997
usertable.AddRow(cells)
998
# Add the usertable and a legend
999
legend = UnorderedList()
1001
_('<b>unsub</b> -- Click on this to unsubscribe the member.'))
1003
_("""<b>mod</b> -- The user's personal moderation flag. If this is
1004
set, postings from them will be moderated, otherwise they will be
1007
_("""<b>hide</b> -- Is the member's address concealed on
1008
the list of subscribers?"""))
1010
"""<b>nomail</b> -- Is delivery to the member disabled? If so, an
1011
abbreviation will be given describing the reason for the disabled
1013
<ul><li><b>U</b> -- Delivery was disabled by the user via their
1014
personal options page.
1015
<li><b>A</b> -- Delivery was disabled by the list
1017
<li><b>B</b> -- Delivery was disabled by the system due to
1018
excessive bouncing from the member's address.
1019
<li><b>?</b> -- The reason for disabled delivery isn't known.
1020
This is the case for all memberships which were disabled
1021
in older versions of Mailman.
1024
_('''<b>ack</b> -- Does the member get acknowledgements of their
1027
_('''<b>not metoo</b> -- Does the member want to avoid copies of their
1030
_('''<b>nodupes</b> -- Does the member want to avoid duplicates of the
1033
_('''<b>digest</b> -- Does the member get messages in digests?
1034
(otherwise, individual messages)'''))
1036
_('''<b>plain</b> -- If getting digests, does the member get plain
1037
text digests? (otherwise, MIME)'''))
1038
legend.AddItem(_("<b>language</b> -- Language preferred by the user"))
1041
qsenviron = os.environ.get('QUERY_STRING')
1043
qs = cgi.parse_qs(qsenviron).get('legend')
1044
if qs and isinstance(qs, list):
1047
addlegend = 'legend=yes&'
1049
container.AddItem(legend.Format() + '<p>')
1051
Link(adminurl + '/members/list',
1052
_('Click here to hide the legend for this table.')))
1055
Link(adminurl + '/members/list?legend=yes',
1056
_('Click here to include the legend for this table.')))
1057
container.AddItem(Center(usertable))
1059
# There may be additional chunks
1060
if chunkindex is not None:
1062
url = adminurl + '/members?%sletter=%s&' % (addlegend, bucket)
1063
footer = _('''<p><em>To view more members, click on the appropriate
1064
range listed below:</em>''')
1065
chunkmembers = buckets[bucket]
1066
last = len(chunkmembers)
1067
for i in range(numchunks):
1070
start = chunkmembers[i*chunksz]
1071
end = chunkmembers[min((i+1)*chunksz, last)-1]
1072
link = Link(url + 'chunk=%d' % i, _('from %(start)s to %(end)s'))
1073
buttons.append(link)
1074
buttons = UnorderedList(*buttons)
1075
container.AddItem(footer + buttons.Format() + '<p>')
1080
def mass_subscribe(mlist, container):
1082
GREY = config.WEB_ADMINITEM_COLOR
1083
table = Table(width='90%')
1085
Label(_('Subscribe these users now or invite them?')),
1086
RadioButtonArray('subscribe_or_invite',
1087
(_('Subscribe'), _('Invite')),
1090
table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
1091
table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
1093
Label(_('Send welcome messages to new subscribees?')),
1094
RadioButtonArray('send_welcome_msg_to_this_batch',
1095
(_('No'), _('Yes')),
1096
mlist.send_welcome_msg,
1099
table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
1100
table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
1102
Label(_('Send notifications of new subscriptions to the list owner?')),
1103
RadioButtonArray('send_notifications_to_list_owner',
1104
(_('No'), _('Yes')),
1105
mlist.admin_notify_mchanges,
1108
table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
1109
table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
1110
table.AddRow([Italic(_('Enter one address per line below...'))])
1111
table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1112
table.AddRow([Center(TextArea(name='subscribees',
1113
rows=10, cols='70%', wrap=None))])
1114
table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1115
table.AddRow([Italic(Label(_('...or specify a file to upload:'))),
1116
FileUpload('subscribees_upload', cols='50')])
1117
container.AddItem(Center(table))
1119
table.AddRow([' ', ' '])
1120
table.AddRow([Italic(_("""Below, enter additional text to be added to the
1121
top of your invitation or the subscription notification. Include at least
1122
one blank line at the end..."""))])
1123
table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1124
table.AddRow([Center(TextArea(name='invitation',
1125
rows=10, cols='70%', wrap=None))])
1126
table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1130
def mass_remove(mlist, container):
1132
GREY = config.WEB_ADMINITEM_COLOR
1133
table = Table(width='90%')
1135
Label(_('Send unsubscription acknowledgement to the user?')),
1136
RadioButtonArray('send_unsub_ack_to_this_batch',
1137
(_('No'), _('Yes')),
1140
table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
1141
table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
1143
Label(_('Send notifications to the list owner?')),
1144
RadioButtonArray('send_unsub_notifications_to_list_owner',
1145
(_('No'), _('Yes')),
1146
mlist.admin_notify_mchanges,
1149
table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
1150
table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
1151
table.AddRow([Italic(_('Enter one address per line below...'))])
1152
table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1153
table.AddRow([Center(TextArea(name='unsubscribees',
1154
rows=10, cols='70%', wrap=None))])
1155
table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1156
table.AddRow([Italic(Label(_('...or specify a file to upload:'))),
1157
FileUpload('unsubscribees_upload', cols='50')])
1158
container.AddItem(Center(table))
1162
def password_inputs(mlist):
1163
adminurl = mlist.GetScriptURL('admin')
1164
table = Table(cellspacing=3, cellpadding=4)
1165
table.AddRow([Center(Header(2, _('Change list ownership passwords')))])
1166
table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
1167
bgcolor=config.WEB_HEADER_COLOR)
1168
table.AddRow([_("""\
1169
The <em>list administrators</em> are the people who have ultimate control over
1170
all parameters of this mailing list. They are able to change any list
1171
configuration variable available through these administration web pages.
1173
<p>The <em>list moderators</em> have more limited permissions; they are not
1174
able to change any list configuration variable, but they are allowed to tend
1175
to pending administration requests, including approving or rejecting held
1176
subscription requests, and disposing of held postings. Of course, the
1177
<em>list administrators</em> can also tend to pending requests.
1179
<p>In order to split the list ownership duties into administrators and
1180
moderators, you must set a separate moderator password in the fields below,
1181
and also provide the email addresses of the list moderators in the
1182
<a href="%(adminurl)s/general">general options section</a>.""")])
1183
table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1184
# Set up the admin password table on the left
1185
atable = Table(border=0, cellspacing=3, cellpadding=4,
1186
bgcolor=config.WEB_ADMINPW_COLOR)
1187
atable.AddRow([Label(_('Enter new administrator password:')),
1188
PasswordBox('newpw', size=20)])
1189
atable.AddRow([Label(_('Confirm administrator password:')),
1190
PasswordBox('confirmpw', size=20)])
1191
# Set up the moderator password table on the right
1192
mtable = Table(border=0, cellspacing=3, cellpadding=4,
1193
bgcolor=config.WEB_ADMINPW_COLOR)
1194
mtable.AddRow([Label(_('Enter new moderator password:')),
1195
PasswordBox('newmodpw', size=20)])
1196
mtable.AddRow([Label(_('Confirm moderator password:')),
1197
PasswordBox('confirmmodpw', size=20)])
1198
# Add these tables to the overall password table
1199
table.AddRow([atable, mtable])
1204
def submit_button(name='submit'):
1205
table = Table(border=0, cellspacing=0, cellpadding=2)
1206
table.AddRow([Bold(SubmitButton(name, _('Submit Your Changes')))])
1207
table.AddCellInfo(table.GetCurrentRowIndex(), 0, align='middle')
1212
def change_options(mlist, category, subcat, cgidata, doc):
1213
def safeint(formvar, defaultval=None):
1215
return int(cgidata.getvalue(formvar))
1216
except (ValueError, TypeError):
1219
# Handle changes to the list moderator password. Do this before checking
1220
# the new admin password, since the latter will force a reauthentication.
1221
new = cgidata.getvalue('newmodpw', '').strip()
1222
confirm = cgidata.getvalue('confirmmodpw', '').strip()
1225
mlist.mod_password = passwords.make_secret(
1226
new, config.PASSWORD_SCHEME)
1227
# No re-authentication necessary because the moderator's
1228
# password doesn't get you into these pages.
1230
doc.addError(_('Moderator passwords did not match'))
1231
# Handle changes to the list administrator password
1232
new = cgidata.getvalue('newpw', '').strip()
1233
confirm = cgidata.getvalue('confirmpw', '').strip()
1236
mlist.password = passwords.make_secret(new, config.PASSWORD_SCHEME)
1238
print mlist.MakeCookie(config.AuthListAdmin)
1240
doc.addError(_('Administrator passwords did not match'))
1241
# Give the individual gui item a chance to process the form data
1242
categories = mlist.GetConfigCategories()
1243
label, gui = categories[category]
1244
# BAW: We handle the membership page special... for now.
1245
if category <> 'members':
1246
gui.handleForm(mlist, category, subcat, cgidata, doc)
1247
# mass subscription, removal processing for members category
1249
subscribers += cgidata.getvalue('subscribees', '')
1250
subscribers += cgidata.getvalue('subscribees_upload', '')
1252
entries = filter(None, [n.strip() for n in subscribers.splitlines()])
1253
send_welcome_msg = safeint('send_welcome_msg_to_this_batch',
1254
mlist.send_welcome_msg)
1255
send_admin_notif = safeint('send_notifications_to_list_owner',
1256
mlist.admin_notify_mchanges)
1257
# Default is to subscribe
1258
subscribe_or_invite = safeint('subscribe_or_invite', 0)
1259
invitation = cgidata.getvalue('invitation', '')
1260
digest = mlist.digest_is_default
1261
if not mlist.digestable:
1263
if not mlist.nondigestable:
1265
subscribe_errors = []
1266
subscribe_success = []
1267
# Now cruise through all the subscribees and do the deed. BAW: we
1268
# should limit the number of "Successfully subscribed" status messages
1269
# we display. Try uploading a file with 10k names -- it takes a while
1270
# to render the status page.
1271
for entry in entries:
1272
fullname, address = parseaddr(entry)
1273
# Canonicalize the full name
1274
fullname = Utils.canonstr(fullname, mlist.preferred_language)
1275
userdesc = UserDesc(address, fullname,
1276
Utils.MakeRandomPassword(),
1277
digest, mlist.preferred_language)
1279
if subscribe_or_invite:
1280
if mlist.isMember(address):
1281
raise Errors.MMAlreadyAMember
1283
mlist.InviteNewMember(userdesc, invitation)
1285
mlist.ApprovedAddMember(userdesc, send_welcome_msg,
1286
send_admin_notif, invitation,
1287
whence='admin mass sub')
1288
except Errors.MMAlreadyAMember:
1289
subscribe_errors.append((entry, _('Already a member')))
1290
except Errors.InvalidEmailAddress:
1291
if userdesc.address == '':
1292
subscribe_errors.append((_('<blank line>'),
1293
_('Bad/Invalid email address')))
1295
subscribe_errors.append((entry,
1296
_('Bad/Invalid email address')))
1297
except Errors.MembershipIsBanned, pattern:
1298
subscribe_errors.append(
1299
(entry, _('Banned address (matched %(pattern)s)')))
1301
member = Utils.uncanonstr(formataddr((fullname, address)))
1302
subscribe_success.append(Utils.websafe(member))
1303
if subscribe_success:
1304
if subscribe_or_invite:
1305
doc.AddItem(Header(5, _('Successfully invited:')))
1307
doc.AddItem(Header(5, _('Successfully subscribed:')))
1308
doc.AddItem(UnorderedList(*subscribe_success))
1310
if subscribe_errors:
1311
if subscribe_or_invite:
1312
doc.AddItem(Header(5, _('Error inviting:')))
1314
doc.AddItem(Header(5, _('Error subscribing:')))
1315
items = ['%s -- %s' % (x0, x1) for x0, x1 in subscribe_errors]
1316
doc.AddItem(UnorderedList(*items))
1320
if cgidata.has_key('unsubscribees'):
1321
removals += cgidata['unsubscribees'].value
1322
if cgidata.has_key('unsubscribees_upload') and \
1323
cgidata['unsubscribees_upload'].value:
1324
removals += cgidata['unsubscribees_upload'].value
1326
names = filter(None, [n.strip() for n in removals.splitlines()])
1327
send_unsub_notifications = int(
1328
cgidata['send_unsub_notifications_to_list_owner'].value)
1330
cgidata['send_unsub_ack_to_this_batch'].value)
1331
unsubscribe_errors = []
1332
unsubscribe_success = []
1335
mlist.ApprovedDeleteMember(
1336
addr, whence='admin mass unsub',
1337
admin_notif=send_unsub_notifications,
1339
unsubscribe_success.append(addr)
1340
except Errors.NotAMemberError:
1341
unsubscribe_errors.append(addr)
1342
if unsubscribe_success:
1343
doc.AddItem(Header(5, _('Successfully Unsubscribed:')))
1344
doc.AddItem(UnorderedList(*unsubscribe_success))
1346
if unsubscribe_errors:
1347
doc.AddItem(Header(3, Bold(FontAttr(
1348
_('Cannot unsubscribe non-members:'),
1349
color='#ff0000', size='+2')).Format()))
1350
doc.AddItem(UnorderedList(*unsubscribe_errors))
1352
# See if this was a moderation bit operation
1353
if cgidata.has_key('allmodbit_btn'):
1354
val = cgidata.getvalue('allmodbit_val')
1359
if val not in (0, 1):
1360
doc.addError(_('Bad moderation flag value'))
1362
for member in mlist.getMembers():
1363
mlist.setMemberOption(member, config.Moderate, val)
1364
# do the user options for members category
1365
if cgidata.has_key('setmemberopts_btn') and cgidata.has_key('user'):
1366
user = cgidata['user']
1367
if isinstance(user, list):
1369
for ui in range(len(user)):
1370
users.append(urllib.unquote(user[ui].value))
1372
users = [urllib.unquote(user.value)]
1376
if cgidata.has_key('%s_unsub' % user):
1378
mlist.ApprovedDeleteMember(user, whence='member mgt page')
1379
removes.append(user)
1380
except Errors.NotAMemberError:
1381
errors.append((user, _('Not subscribed')))
1383
if not mlist.isMember(user):
1384
doc.addError(_('Ignoring changes to deleted member: %(user)s'),
1387
value = cgidata.has_key('%s_digest' % user)
1389
mlist.setMemberOption(user, config.Digests, value)
1390
except (Errors.AlreadyReceivingDigests,
1391
Errors.AlreadyReceivingRegularDeliveries,
1392
Errors.CantDigestError,
1393
Errors.MustDigestError):
1397
newname = cgidata.getvalue(user+'_realname', '')
1398
newname = Utils.canonstr(newname, mlist.preferred_language)
1399
mlist.setMemberName(user, newname)
1401
newlang = cgidata.getvalue(user+'_language')
1402
oldlang = mlist.getMemberLanguage(user)
1403
if (newlang not in config.languages.enabled_codes
1404
and newlang <> oldlang):
1406
mlist.setMemberLanguage(user, newlang)
1408
moderate = not not cgidata.getvalue(user+'_mod')
1409
mlist.setMemberOption(user, config.Moderate, moderate)
1411
# Set the `nomail' flag, but only if the user isn't already
1412
# disabled (otherwise we might change BYUSER into BYADMIN).
1413
if cgidata.has_key('%s_nomail' % user):
1414
if mlist.getDeliveryStatus(user) == MemberAdaptor.ENABLED:
1415
mlist.setDeliveryStatus(user, MemberAdaptor.BYADMIN)
1417
mlist.setDeliveryStatus(user, MemberAdaptor.ENABLED)
1418
for opt in ('hide', 'ack', 'notmetoo', 'nodupes', 'plain'):
1419
opt_code = config.OPTINFO[opt]
1420
if cgidata.has_key('%s_%s' % (user, opt)):
1421
mlist.setMemberOption(user, opt_code, 1)
1423
mlist.setMemberOption(user, opt_code, 0)
1424
# Give some feedback on who's been removed
1426
doc.AddItem(Header(5, _('Successfully Removed:')))
1427
doc.AddItem(UnorderedList(*removes))
1430
doc.AddItem(Header(5, _("Error Unsubscribing:")))
1431
items = ['%s -- %s' % (x[0], x[1]) for x in errors]
1432
doc.AddItem(apply(UnorderedList, tuple((items))))