diff options
Diffstat (limited to 'Mailman/Cgi/admin.py')
-rw-r--r-- | Mailman/Cgi/admin.py | 1407 |
1 files changed, 1407 insertions, 0 deletions
diff --git a/Mailman/Cgi/admin.py b/Mailman/Cgi/admin.py new file mode 100644 index 00000000..49c6efbf --- /dev/null +++ b/Mailman/Cgi/admin.py @@ -0,0 +1,1407 @@ +# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Process and produce the list-administration options forms. + +""" + +# For Python 2.1.x compatibility +from __future__ import nested_scopes + +import sys +import os +import re +import cgi +import sha +import urllib +import signal +from types import * +from string import lowercase, digits + +from email.Utils import unquote, parseaddr, formataddr + +from Mailman import mm_cfg +from Mailman import Utils +from Mailman import MailList +from Mailman import Errors +from Mailman import MemberAdaptor +from Mailman import i18n +from Mailman.UserDesc import UserDesc +from Mailman.htmlformat import * +from Mailman.Cgi import Auth +from Mailman.Logging.Syslog import syslog + +# Set up i18n +_ = i18n._ +i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE) + +NL = '\n' +OPTCOLUMNS = 11 + + + +def main(): + # Try to find out which list is being administered + parts = Utils.GetPathPieces() + if not parts: + # None, so just do the admin overview and be done with it + admin_overview() + return + # Get the list object + listname = parts[0].lower() + try: + mlist = MailList.MailList(listname, lock=0) + except Errors.MMListError, e: + # Avoid cross-site scripting attacks + safelistname = Utils.websafe(listname) + admin_overview(_('No such list <em>%(safelistname)s</em>')) + syslog('error', 'admin.py access for non-existent list: %s', + listname) + return + # Now that we know what list has been requested, all subsequent admin + # pages are shown in that list's preferred language. + i18n.set_language(mlist.preferred_language) + # If the user is not authenticated, we're done. + cgidata = cgi.FieldStorage(keep_blank_values=1) + + if not mlist.WebAuthenticate((mm_cfg.AuthListAdmin, + mm_cfg.AuthSiteAdmin), + cgidata.getvalue('adminpw', '')): + if cgidata.has_key('adminpw'): + # This is a re-authorization attempt + msg = Bold(FontSize('+1', _('Authorization failed.'))).Format() + else: + msg = '' + Auth.loginpage(mlist, 'admin', msg=msg) + return + + # Which subcategory was requested? Default is `general' + if len(parts) == 1: + category = 'general' + subcat = None + elif len(parts) == 2: + category = parts[1] + subcat = None + else: + category = parts[1] + subcat = parts[2] + + # Is this a log-out request? + if category == 'logout': + print mlist.ZapCookie(mm_cfg.AuthListAdmin) + Auth.loginpage(mlist, 'admin', frontpage=1) + return + + # Sanity check + if category not in mlist.GetConfigCategories().keys(): + category = 'general' + + # Is the request for variable details? + varhelp = None + qsenviron = os.environ.get('QUERY_STRING') + parsedqs = None + if qsenviron: + parsedqs = cgi.parse_qs(qsenviron) + if cgidata.has_key('VARHELP'): + varhelp = cgidata.getvalue('VARHELP') + elif parsedqs: + # POST methods, even if their actions have a query string, don't get + # put into FieldStorage's keys :-( + qs = parsedqs.get('VARHELP') + if qs and isinstance(qs, ListType): + varhelp = qs[0] + if varhelp: + option_help(mlist, varhelp) + return + + # The html page document + doc = Document() + doc.set_language(mlist.preferred_language) + + # From this point on, the MailList object must be locked. However, we + # must release the lock no matter how we exit. try/finally isn't enough, + # because of this scenario: user hits the admin page which may take a long + # time to render; user gets bored and hits the browser's STOP button; + # browser shuts down socket; server tries to write to broken socket and + # gets a SIGPIPE. Under Apache 1.3/mod_cgi, Apache catches this SIGPIPE + # (I presume it is buffering output from the cgi script), then turns + # around and SIGTERMs the cgi process. Apache waits three seconds and + # then SIGKILLs the cgi process. We /must/ catch the SIGTERM and do the + # most reasonable thing we can in as short a time period as possible. If + # we get the SIGKILL we're screwed (because it's uncatchable and we'll + # have no opportunity to clean up after ourselves). + # + # This signal handler catches the SIGTERM, unlocks the list, and then + # exits the process. The effect of this is that the changes made to the + # MailList object will be aborted, which seems like the only sensible + # semantics. + # + # BAW: This may not be portable to other web servers or cgi execution + # models. + def sigterm_handler(signum, frame, mlist=mlist): + # Make sure the list gets unlocked... + mlist.Unlock() + # ...and ensure we exit, otherwise race conditions could cause us to + # enter MailList.Save() while we're in the unlocked state, and that + # could be bad! + sys.exit(0) + + mlist.Lock() + try: + # Install the emergency shutdown signal handler + signal.signal(signal.SIGTERM, sigterm_handler) + + if cgidata.keys(): + # There are options to change + change_options(mlist, category, subcat, cgidata, doc) + # Let the list sanity check the changed values + mlist.CheckValues() + # Additional sanity checks + if not mlist.digestable and not mlist.nondigestable: + doc.addError( + _('''You have turned off delivery of both digest and + non-digest messages. This is an incompatible state of + affairs. You must turn on either digest delivery or + non-digest delivery or your mailing list will basically be + unusable.'''), tag=_('Warning: ')) + + if not mlist.digestable and mlist.getDigestMemberKeys(): + doc.addError( + _('''You have digest members, but digests are turned + off. Those people will not receive mail.'''), + tag=_('Warning: ')) + if not mlist.nondigestable and mlist.getRegularMemberKeys(): + doc.addError( + _('''You have regular list members but non-digestified mail is + turned off. They will receive mail until you fix this + problem.'''), tag=_('Warning: ')) + # Glom up the results page and print it out + show_results(mlist, doc, category, subcat, cgidata) + print doc.Format() + mlist.Save() + finally: + # Now be sure to unlock the list. It's okay if we get a signal here + # because essentially, the signal handler will do the same thing. And + # unlocking is unconditional, so it's not an error if we unlock while + # we're already unlocked. + mlist.Unlock() + + + +def admin_overview(msg=''): + # Show the administrative overview page, with the list of all the lists on + # this host. msg is an optional error message to display at the top of + # the page. + # + # This page should be displayed in the server's default language, which + # should have already been set. + hostname = Utils.get_domain() + legend = _('%(hostname)s mailing lists - Admin Links') + # The html `document' + doc = Document() + doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE) + doc.SetTitle(legend) + # The table that will hold everything + table = Table(border=0, width="100%") + table.AddRow([Center(Header(2, legend))]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2, + bgcolor=mm_cfg.WEB_HEADER_COLOR) + # Skip any mailing list that isn't advertised. + advertised = [] + listnames = Utils.list_names() + listnames.sort() + + for name in listnames: + mlist = MailList.MailList(name, lock=0) + if mlist.advertised: + if mm_cfg.VIRTUAL_HOST_OVERVIEW and \ + mlist.web_page_url.find(hostname) == -1: + # List is for different identity of this host - skip it. + continue + else: + advertised.append(mlist) + + # Greeting depends on whether there was an error or not + if msg: + greeting = FontAttr(msg, color="ff5060", size="+1") + else: + greeting = _("Welcome!") + + welcome = [] + mailmanlink = Link(mm_cfg.MAILMAN_URL, _('Mailman')).Format() + if not advertised: + welcome.extend([ + greeting, + _('''<p>There currently are no publicly-advertised %(mailmanlink)s + mailing lists on %(hostname)s.'''), + ]) + else: + welcome.extend([ + greeting, + _('''<p>Below is the collection of publicly-advertised + %(mailmanlink)s mailing lists on %(hostname)s. Click on a list + name to visit the configuration pages for that list.'''), + ]) + + creatorurl = Utils.ScriptURL('create') + mailman_owner = Utils.get_site_email() + extra = msg and _('right ') or '' + welcome.extend([ + _('''To visit the administrators configuration page for an + unadvertised list, open a URL similar to this one, but with a '/' and + the %(extra)slist name appended. If you have the proper authority, + you can also <a href="%(creatorurl)s">create a new mailing list</a>. + + <p>General list information can be found at '''), + Link(Utils.ScriptURL('listinfo'), + _('the mailing list overview page')), + '.', + _('<p>(Send questions and comments to '), + Link('mailto:%s' % mailman_owner, mailman_owner), + '.)<p>', + ]) + + table.AddRow([Container(*welcome)]) + table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2) + + if advertised: + table.AddRow([' ', ' ']) + table.AddRow([Bold(FontAttr(_('List'), size='+2')), + Bold(FontAttr(_('Description'), size='+2')) + ]) + highlight = 1 + for mlist in advertised: + table.AddRow( + [Link(mlist.GetScriptURL('admin'), Bold(mlist.real_name)), + mlist.description or Italic(_('[no description available]'))]) + if highlight and mm_cfg.WEB_HIGHLIGHT_COLOR: + table.AddRowInfo(table.GetCurrentRowIndex(), + bgcolor=mm_cfg.WEB_HIGHLIGHT_COLOR) + highlight = not highlight + + doc.AddItem(table) + doc.AddItem('<hr>') + doc.AddItem(MailmanLogo()) + print doc.Format() + + + +def option_help(mlist, varhelp): + # The html page document + doc = Document() + doc.set_language(mlist.preferred_language) + # Find out which category and variable help is being requested for. + item = None + reflist = varhelp.split('/') + if len(reflist) >= 2: + category = subcat = None + if len(reflist) == 2: + category, varname = reflist + elif len(reflist) == 3: + category, subcat, varname = reflist + options = mlist.GetConfigInfo(category, subcat) + for i in options: + if i and i[0] == varname: + item = i + break + # Print an error message if we couldn't find a valid one + if not item: + bad = _('No valid variable name found.') + doc.addError(bad) + doc.AddItem(mlist.GetMailmanFooter()) + print doc.Format() + return + # Get the details about the variable + varname, kind, params, dependancies, description, elaboration = \ + get_item_characteristics(item) + # Set up the document + realname = mlist.real_name + legend = _("""%(realname)s Mailing list Configuration Help + <br><em>%(varname)s</em> Option""") + + header = Table(width='100%') + header.AddRow([Center(Header(3, legend))]) + header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2, + bgcolor=mm_cfg.WEB_HEADER_COLOR) + doc.SetTitle(_("Mailman %(varname)s List Option Help")) + doc.AddItem(header) + doc.AddItem("<b>%s</b> (%s): %s<p>" % (varname, category, description)) + if elaboration: + doc.AddItem("%s<p>" % elaboration) + + if subcat: + url = '%s/%s/%s' % (mlist.GetScriptURL('admin'), category, subcat) + else: + url = '%s/%s' % (mlist.GetScriptURL('admin'), category) + form = Form(url) + valtab = Table(cellspacing=3, cellpadding=4, width='100%') + add_options_table_item(mlist, category, subcat, valtab, item, detailsp=0) + form.AddItem(valtab) + form.AddItem('<p>') + form.AddItem(Center(submit_button())) + doc.AddItem(Center(form)) + + doc.AddItem(_("""<em><strong>Warning:</strong> changing this option here + could cause other screens to be out-of-sync. Be sure to reload any other + pages that are displaying this option for this mailing list. You can also + """)) + + adminurl = mlist.GetScriptURL('admin') + if subcat: + url = '%s/%s/%s' % (adminurl, category, subcat) + else: + url = '%s/%s' % (adminurl, category) + categoryname = mlist.GetConfigCategories()[category][0] + doc.AddItem(Link(url, _('return to the %(categoryname)s options page.'))) + doc.AddItem('</em>') + doc.AddItem(mlist.GetMailmanFooter()) + print doc.Format() + + + +def show_results(mlist, doc, category, subcat, cgidata): + # Produce the results page + adminurl = mlist.GetScriptURL('admin') + categories = mlist.GetConfigCategories() + label = _(categories[category][0]) + + # Set up the document's headers + realname = mlist.real_name + doc.SetTitle(_('%(realname)s Administration (%(label)s)')) + doc.AddItem(Center(Header(2, _( + '%(realname)s mailing list administration<br>%(label)s Section')))) + doc.AddItem('<hr>') + # Now we need to craft the form that will be submitted, which will contain + # all the variable settings, etc. This is a bit of a kludge because we + # know that the autoreply and members categories supports file uploads. + encoding = None + if category in ('autoreply', 'members'): + encoding = 'multipart/form-data' + if subcat: + form = Form('%s/%s/%s' % (adminurl, category, subcat), + encoding=encoding) + else: + form = Form('%s/%s' % (adminurl, category), encoding=encoding) + # This holds the two columns of links + linktable = Table(valign='top', width='100%') + linktable.AddRow([Center(Bold(_("Configuration Categories"))), + Center(Bold(_("Other Administrative Activities")))]) + # The `other links' are stuff in the right column. + otherlinks = UnorderedList() + otherlinks.AddItem(Link(mlist.GetScriptURL('admindb'), + _('Tend to pending moderator requests'))) + otherlinks.AddItem(Link(mlist.GetScriptURL('listinfo'), + _('Go to the general list information page'))) + otherlinks.AddItem(Link(mlist.GetScriptURL('edithtml'), + _('Edit the public HTML pages'))) + otherlinks.AddItem(Link(mlist.GetBaseArchiveURL(), + _('Go to list archives')).Format() + + '<br> <br>') + # We do not allow through-the-web deletion of the site list! + if mm_cfg.OWNERS_CAN_DELETE_THEIR_OWN_LISTS and \ + mlist.internal_name() <> mm_cfg.MAILMAN_SITE_LIST: + otherlinks.AddItem(Link(mlist.GetScriptURL('rmlist'), + _('Delete this mailing list')).Format() + + _(' (requires confirmation)<br> <br>')) + otherlinks.AddItem(Link('%s/logout' % adminurl, + # BAW: What I really want is a blank line, but + # adding an won't do it because of the + # bullet added to the list item. + '<FONT SIZE="+2"><b>%s</b></FONT>' % + _('Logout'))) + # These are links to other categories and live in the left column + categorylinks_1 = categorylinks = UnorderedList() + categorylinks_2 = '' + categorykeys = categories.keys() + half = len(categorykeys) / 2 + counter = 0 + subcat = None + for k in categorykeys: + label = _(categories[k][0]) + url = '%s/%s' % (adminurl, k) + if k == category: + # Handle subcategories + subcats = mlist.GetConfigSubCategories(k) + if subcats: + subcat = Utils.GetPathPieces()[-1] + for k, v in subcats: + if k == subcat: + break + else: + # The first subcategory in the list is the default + subcat = subcats[0][0] + subcat_items = [] + for sub, text in subcats: + if sub == subcat: + text = Bold('[%s]' % text).Format() + subcat_items.append(Link(url + '/' + sub, text)) + categorylinks.AddItem( + Bold(label).Format() + + UnorderedList(*subcat_items).Format()) + else: + categorylinks.AddItem(Link(url, Bold('[%s]' % label))) + else: + categorylinks.AddItem(Link(url, label)) + counter += 1 + if counter >= half: + categorylinks_2 = categorylinks = UnorderedList() + counter = -len(categorykeys) + # Make the emergency stop switch a rude solo light + etable = Table() + # Add all the links to the links table... + etable.AddRow([categorylinks_1, categorylinks_2]) + etable.AddRowInfo(etable.GetCurrentRowIndex(), valign='top') + if mlist.emergency: + label = _('Emergency moderation of all list traffic is enabled') + etable.AddRow([Center( + Link('?VARHELP=general/emergency', Bold(label)))]) + color = mm_cfg.WEB_ERROR_COLOR + etable.AddCellInfo(etable.GetCurrentRowIndex(), 0, + colspan=2, bgcolor=color) + linktable.AddRow([etable, otherlinks]) + # ...and add the links table to the document. + form.AddItem(linktable) + form.AddItem('<hr>') + form.AddItem( + _('''Make your changes in the following section, then submit them + using the <em>Submit Your Changes</em> button below.''') + + '<p>') + + # The members and passwords categories are special in that they aren't + # defined in terms of gui elements. Create those pages here. + if category == 'members': + # Figure out which subcategory we should display + subcat = Utils.GetPathPieces()[-1] + if subcat not in ('list', 'add', 'remove'): + subcat = 'list' + # Add member category specific tables + form.AddItem(membership_options(mlist, subcat, cgidata, doc, form)) + form.AddItem(Center(submit_button('setmemberopts_btn'))) + # In "list" subcategory, we can also search for members + if subcat == 'list': + form.AddItem('<hr>\n') + table = Table(width='100%') + table.AddRow([Center(Header(2, _('Additional Member Tasks')))]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2, + bgcolor=mm_cfg.WEB_HEADER_COLOR) + # Add a blank separator row + table.AddRow([' ', ' ']) + # Add a section to set the moderation bit for all members + table.AddRow([_("""<li>Set everyone's moderation bit, including + those members not currently visible""")]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2) + table.AddRow([RadioButtonArray('allmodbit_val', + (_('Off'), _('On')), + mlist.default_member_moderation), + SubmitButton('allmodbit_btn', _('Set'))]) + form.AddItem(table) + elif category == 'passwords': + form.AddItem(Center(password_inputs(mlist))) + form.AddItem(Center(submit_button())) + else: + form.AddItem(show_variables(mlist, category, subcat, cgidata, doc)) + form.AddItem(Center(submit_button())) + # And add the form + doc.AddItem(form) + doc.AddItem(mlist.GetMailmanFooter()) + + + +def show_variables(mlist, category, subcat, cgidata, doc): + options = mlist.GetConfigInfo(category, subcat) + + # The table containing the results + table = Table(cellspacing=3, cellpadding=4, width='100%') + + # Get and portray the text label for the category. + categories = mlist.GetConfigCategories() + label = _(categories[category][0]) + + table.AddRow([Center(Header(2, label))]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2, + bgcolor=mm_cfg.WEB_HEADER_COLOR) + + # The very first item in the config info will be treated as a general + # description if it is a string + description = options[0] + if isinstance(description, StringType): + table.AddRow([description]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2) + options = options[1:] + + if not options: + return table + + # Add the global column headers + table.AddRow([Center(Bold(_('Description'))), + Center(Bold(_('Value')))]) + table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, + width='15%') + table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 1, + width='85%') + + for item in options: + if type(item) == StringType: + # The very first banner option (string in an options list) is + # treated as a general description, while any others are + # treated as section headers - centered and italicized... + table.AddRow([Center(Italic(item))]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2) + else: + add_options_table_item(mlist, category, subcat, table, item) + table.AddRow(['<br>']) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2) + return table + + + +def add_options_table_item(mlist, category, subcat, table, item, detailsp=1): + # Add a row to an options table with the item description and value. + varname, kind, params, extra, descr, elaboration = \ + get_item_characteristics(item) + if elaboration is None: + elaboration = descr + descr = get_item_gui_description(mlist, category, subcat, + varname, descr, elaboration, detailsp) + val = get_item_gui_value(mlist, category, kind, varname, params, extra) + table.AddRow([descr, val]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, + bgcolor=mm_cfg.WEB_ADMINITEM_COLOR) + table.AddCellInfo(table.GetCurrentRowIndex(), 1, + bgcolor=mm_cfg.WEB_ADMINITEM_COLOR) + + + +def get_item_characteristics(record): + # Break out the components of an item description from its description + # record: + # + # 0 -- option-var name + # 1 -- type + # 2 -- entry size + # 3 -- ?dependancies? + # 4 -- Brief description + # 5 -- Optional description elaboration + if len(record) == 5: + elaboration = None + varname, kind, params, dependancies, descr = record + elif len(record) == 6: + varname, kind, params, dependancies, descr, elaboration = record + else: + raise ValueError, _('Badly formed options entry:\n %(record)s') + return varname, kind, params, dependancies, descr, elaboration + + + +def get_item_gui_value(mlist, category, kind, varname, params, extra): + """Return a representation of an item's settings.""" + # Give the category a chance to return the value for the variable + value = None + label, gui = mlist.GetConfigCategories()[category] + if hasattr(gui, 'getValue'): + value = gui.getValue(mlist, kind, varname, params) + # Filter out None, and volatile attributes + if value is None and not varname.startswith('_'): + value = getattr(mlist, varname) + # Now create the widget for this value + if kind == mm_cfg.Radio or kind == mm_cfg.Toggle: + # If we are returning the option for subscribe policy and this site + # doesn't allow open subscribes, then we have to alter the value of + # mlist.subscribe_policy as passed to RadioButtonArray in order to + # compensate for the fact that there is one fewer option. + # Correspondingly, we alter the value back in the change options + # function -scott + # + # TBD: this is an ugly ugly hack. + if varname.startswith('_'): + checked = 0 + else: + checked = value + if varname == 'subscribe_policy' and not mm_cfg.ALLOW_OPEN_SUBSCRIBE: + checked = checked - 1 + # For Radio buttons, we're going to interpret the extra stuff as a + # horizontal/vertical flag. For backwards compatibility, the value 0 + # means horizontal, so we use "not extra" to get the parity right. + return RadioButtonArray(varname, params, checked, not extra) + elif (kind == mm_cfg.String or kind == mm_cfg.Email or + kind == mm_cfg.Host or kind == mm_cfg.Number): + return TextBox(varname, value, params) + elif kind == mm_cfg.Text: + if params: + r, c = params + else: + r, c = None, None + return TextArea(varname, value or '', r, c) + elif kind in (mm_cfg.EmailList, mm_cfg.EmailListEx): + if params: + r, c = params + else: + r, c = None, None + res = NL.join(value) + return TextArea(varname, res, r, c, wrap='off') + elif kind == mm_cfg.FileUpload: + # like a text area, but also with uploading + if params: + r, c = params + else: + r, c = None, None + container = Container() + container.AddItem(_('<em>Enter the text below, or...</em><br>')) + container.AddItem(TextArea(varname, value or '', r, c)) + container.AddItem(_('<br><em>...specify a file to upload</em><br>')) + container.AddItem(FileUpload(varname+'_upload', r, c)) + return container + elif kind == mm_cfg.Select: + if params: + values, legend, selected = params + else: + values = mlist.GetAvailableLanguages() + legend = map(_, map(Utils.GetLanguageDescr, values)) + selected = values.index(mlist.preferred_language) + return SelectOptions(varname, values, legend, selected) + elif kind == mm_cfg.Topics: + # A complex and specialized widget type that allows for setting of a + # topic name, a mark button, a regexp text box, an "add after mark", + # and a delete button. Yeesh! params are ignored. + table = Table(border=0) + # This adds the html for the entry widget + def makebox(i, name, pattern, desc, empty=0, table=table): + deltag = 'topic_delete_%02d' % i + boxtag = 'topic_box_%02d' % i + reboxtag = 'topic_rebox_%02d' % i + desctag = 'topic_desc_%02d' % i + wheretag = 'topic_where_%02d' % i + addtag = 'topic_add_%02d' % i + newtag = 'topic_new_%02d' % i + if empty: + table.AddRow([Center(Bold(_('Topic %(i)d'))), + Hidden(newtag)]) + else: + table.AddRow([Center(Bold(_('Topic %(i)d'))), + SubmitButton(deltag, _('Delete'))]) + table.AddRow([Label(_('Topic name:')), + TextBox(boxtag, value=name, size=30)]) + table.AddRow([Label(_('Regexp:')), + TextArea(reboxtag, text=pattern, + rows=4, cols=30, wrap='off')]) + table.AddRow([Label(_('Description:')), + TextArea(desctag, text=desc, + rows=4, cols=30, wrap='soft')]) + if not empty: + table.AddRow([SubmitButton(addtag, _('Add new item...')), + SelectOptions(wheretag, ('before', 'after'), + (_('...before this one.'), + _('...after this one.')), + selected=1), + ]) + table.AddRow(['<hr>']) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2) + # Now for each element in the existing data, create a widget + i = 1 + data = getattr(mlist, varname) + for name, pattern, desc, empty in data: + makebox(i, name, pattern, desc, empty) + i += 1 + # Add one more non-deleteable widget as the first blank entry, but + # only if there are no real entries. + if i == 1: + makebox(i, '', '', '', empty=1) + return table + elif kind == mm_cfg.Checkbox: + return CheckBoxArray(varname, *params) + else: + assert 0, 'Bad gui widget type: %s' % kind + + + +def get_item_gui_description(mlist, category, subcat, + varname, descr, elaboration, detailsp): + # Return the item's description, with link to details. + # + # Details are not included if this is a VARHELP page, because that /is/ + # the details page! + if detailsp: + if subcat: + varhelp = '/?VARHELP=%s/%s/%s' % (category, subcat, varname) + else: + varhelp = '/?VARHELP=%s/%s' % (category, varname) + if descr == elaboration: + linktext = _('<br>(Edit <b>%(varname)s</b>)') + else: + linktext = _('<br>(Details for <b>%(varname)s</b>)') + link = Link(mlist.GetScriptURL('admin') + varhelp, + linktext).Format() + text = Label('%s %s' % (descr, link)).Format() + else: + text = Label(descr).Format() + if varname[0] == '_': + text += Label(_('''<br><em><strong>Note:</strong> + setting this value performs an immediate action but does not modify + permanent state.</em>''')).Format() + return text + + + +def membership_options(mlist, subcat, cgidata, doc, form): + # Show the main stuff + adminurl = mlist.GetScriptURL('admin', absolute=1) + container = Container() + header = Table(width="100%") + # If we're in the list subcategory, show the membership list + if subcat == 'add': + header.AddRow([Center(Header(2, _('Mass Subscriptions')))]) + header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2, + bgcolor=mm_cfg.WEB_HEADER_COLOR) + container.AddItem(header) + mass_subscribe(mlist, container) + return container + if subcat == 'remove': + header.AddRow([Center(Header(2, _('Mass Removals')))]) + header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2, + bgcolor=mm_cfg.WEB_HEADER_COLOR) + container.AddItem(header) + mass_remove(mlist, container) + return container + # Otherwise... + header.AddRow([Center(Header(2, _('Membership List')))]) + header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2, + bgcolor=mm_cfg.WEB_HEADER_COLOR) + container.AddItem(header) + # Add a "search for member" button + table = Table(width='100%') + link = Link('http://www.python.org/doc/current/lib/re-syntax.html', + _('(help)')).Format() + table.AddRow([Label(_('Find member %(link)s:')), + TextBox('findmember', + value=cgidata.getvalue('findmember', '')), + SubmitButton('findmember_btn', _('Search...'))]) + container.AddItem(table) + container.AddItem('<hr><p>') + usertable = Table(width="90%", border='2') + # If there are more members than allowed by chunksize, then we split the + # membership up alphabetically. Otherwise just display them all. + chunksz = mlist.admin_member_chunksize + all = mlist.getMembers() + all.sort(lambda x, y: cmp(x.lower(), y.lower())) + # See if the query has a regular expression + regexp = cgidata.getvalue('findmember', '').strip() + if regexp: + try: + cre = re.compile(regexp, re.IGNORECASE) + except re.error: + doc.addError(_('Bad regular expression: ') + regexp) + else: + # BAW: There's got to be a more efficient way of doing this! + names = [mlist.getMemberName(s) or '' for s in all] + all = [a for n, a in zip(names, all) + if cre.search(n) or cre.search(a)] + chunkindex = None + bucket = None + actionurl = None + if len(all) < chunksz: + members = all + else: + # Split them up alphabetically, and then split the alphabetical + # listing by chunks + buckets = {} + for addr in all: + members = buckets.setdefault(addr[0].lower(), []) + members.append(addr) + # Now figure out which bucket we want + bucket = None + qs = {} + # POST methods, even if their actions have a query string, don't get + # put into FieldStorage's keys :-( + qsenviron = os.environ.get('QUERY_STRING') + if qsenviron: + qs = cgi.parse_qs(qsenviron) + bucket = qs.get('letter', 'a')[0].lower() + if bucket not in digits + lowercase: + bucket = None + if not bucket or not buckets.has_key(bucket): + keys = buckets.keys() + keys.sort() + bucket = keys[0] + members = buckets[bucket] + action = adminurl + '/members?letter=%s' % bucket + if len(members) <= chunksz: + form.set_action(action) + else: + i, r = divmod(len(members), chunksz) + numchunks = i + (not not r * 1) + # Now chunk them up + chunkindex = 0 + if qs.has_key('chunk'): + try: + chunkindex = int(qs['chunk'][0]) + except ValueError: + chunkindex = 0 + if chunkindex < 0 or chunkindex > numchunks: + chunkindex = 0 + members = members[chunkindex*chunksz:(chunkindex+1)*chunksz] + # And set the action URL + form.set_action(action + '&chunk=%s' % chunkindex) + # So now members holds all the addresses we're going to display + allcnt = len(all) + if bucket: + membercnt = len(members) + usertable.AddRow([Center(Italic(_( + '%(allcnt)s members total, %(membercnt)s shown')))]) + else: + usertable.AddRow([Center(Italic(_('%(allcnt)s members total')))]) + usertable.AddCellInfo(usertable.GetCurrentRowIndex(), + usertable.GetCurrentCellIndex(), + colspan=OPTCOLUMNS, + bgcolor=mm_cfg.WEB_ADMINITEM_COLOR) + # Add the alphabetical links + if bucket: + cells = [] + for letter in digits + lowercase: + if not buckets.get(letter): + continue + url = adminurl + '/members?letter=%s' % letter + if letter == bucket: + show = Bold('[%s]' % letter.upper()).Format() + else: + show = letter.upper() + cells.append(Link(url, show).Format()) + joiner = ' '*2 + '\n' + usertable.AddRow([Center(joiner.join(cells))]) + usertable.AddCellInfo(usertable.GetCurrentRowIndex(), + usertable.GetCurrentCellIndex(), + colspan=OPTCOLUMNS, + bgcolor=mm_cfg.WEB_ADMINITEM_COLOR) + usertable.AddRow([Center(h) for h in (_('unsub'), + _('member address<br>member name'), + _('mod'), _('hide'), + _('nomail<br>[reason]'), + _('ack'), _('not metoo'), + _('nodupes'), + _('digest'), _('plain'), + _('language'))]) + rowindex = usertable.GetCurrentRowIndex() + for i in range(OPTCOLUMNS): + usertable.AddCellInfo(rowindex, i, bgcolor=mm_cfg.WEB_ADMINITEM_COLOR) + # Find the longest name in the list + longest = 0 + if members: + names = filter(None, [mlist.getMemberName(s) for s in members]) + # Make the name field at least as long as the longest email address + longest = max([len(s) for s in names + members]) + # Abbreviations for delivery status details + ds_abbrevs = {MemberAdaptor.UNKNOWN : _('?'), + MemberAdaptor.BYUSER : _('U'), + MemberAdaptor.BYADMIN : _('A'), + MemberAdaptor.BYBOUNCE: _('B'), + } + # Now populate the rows + for addr in members: + link = Link(mlist.GetOptionsURL(addr, obscure=1), + mlist.getMemberCPAddress(addr)) + fullname = Utils.uncanonstr(mlist.getMemberName(addr), + mlist.preferred_language) + name = TextBox(addr + '_realname', fullname, size=longest).Format() + cells = [Center(CheckBox(addr + '_unsub', 'off', 0).Format()), + link.Format() + '<br>' + + name + + Hidden('user', urllib.quote(addr)).Format(), + ] + # Do the `mod' option + if mlist.getMemberOption(addr, mm_cfg.Moderate): + value = 'on' + checked = 1 + else: + value = 'off' + checked = 0 + box = CheckBox('%s_mod' % addr, value, checked) + cells.append(Center(box).Format()) + for opt in ('hide', 'nomail', 'ack', 'notmetoo', 'nodupes'): + extra = '' + if opt == 'nomail': + status = mlist.getDeliveryStatus(addr) + if status == MemberAdaptor.ENABLED: + value = 'off' + checked = 0 + else: + value = 'on' + checked = 1 + extra = '[%s]' % ds_abbrevs[status] + elif mlist.getMemberOption(addr, mm_cfg.OPTINFO[opt]): + value = 'on' + checked = 1 + else: + value = 'off' + checked = 0 + box = CheckBox('%s_%s' % (addr, opt), value, checked) + cells.append(Center(box.Format() + extra)) + # This code is less efficient than the original which did a has_key on + # the underlying dictionary attribute. This version is slower and + # less memory efficient. It points to a new MemberAdaptor interface + # method. + if addr in mlist.getRegularMemberKeys(): + cells.append(Center(CheckBox(addr + '_digest', 'off', 0).Format())) + else: + cells.append(Center(CheckBox(addr + '_digest', 'on', 1).Format())) + if mlist.getMemberOption(addr, mm_cfg.OPTINFO['plain']): + value = 'on' + checked = 1 + else: + value = 'off' + checked = 0 + cells.append(Center(CheckBox('%s_plain' % addr, value, checked))) + # User's preferred language + langpref = mlist.getMemberLanguage(addr) + langs = mlist.GetAvailableLanguages() + langdescs = [_(Utils.GetLanguageDescr(lang)) for lang in langs] + try: + selected = langs.index(langpref) + except ValueError: + selected = 0 + cells.append(Center(SelectOptions(addr + '_language', langs, + langdescs, selected)).Format()) + usertable.AddRow(cells) + # Add the usertable and a legend + legend = UnorderedList() + legend.AddItem( + _('<b>unsub</b> -- Click on this to unsubscribe the member.')) + legend.AddItem( + _("""<b>mod</b> -- The user's personal moderation flag. If this is + set, postings from them will be moderated, otherwise they will be + approved.""")) + legend.AddItem( + _("""<b>hide</b> -- Is the member's address concealed on + the list of subscribers?""")) + legend.AddItem(_( + """<b>nomail</b> -- Is delivery to the member disabled? If so, an + abbreviation will be given describing the reason for the disabled + delivery: + <ul><li><b>U</b> -- Delivery was disabled by the user via their + personal options page. + <li><b>A</b> -- Delivery was disabled by the list + administrators. + <li><b>B</b> -- Delivery was disabled by the system due to + excessive bouncing from the member's address. + <li><b>?</b> -- The reason for disabled delivery isn't known. + This is the case for all memberships which were disabled + in older versions of Mailman. + </ul>""")) + legend.AddItem( + _('''<b>ack</b> -- Does the member get acknowledgements of their + posts?''')) + legend.AddItem( + _('''<b>not metoo</b> -- Does the member want to avoid copies of their + own postings?''')) + legend.AddItem( + _('''<b>nodupes</b> -- Does the member want to avoid duplicates of the + same message?''')) + legend.AddItem( + _('''<b>digest</b> -- Does the member get messages in digests? + (otherwise, individual messages)''')) + legend.AddItem( + _('''<b>plain</b> -- If getting digests, does the member get plain + text digests? (otherwise, MIME)''')) + legend.AddItem(_("<b>language</b> -- Language preferred by the user")) + addlegend = '' + parsedqs = 0 + qsenviron = os.environ.get('QUERY_STRING') + if qsenviron: + qs = cgi.parse_qs(qsenviron).get('legend') + if qs and isinstance(qs, ListType): + qs = qs[0] + if qs == 'yes': + addlegend = 'legend=yes&' + if addlegend: + container.AddItem(legend.Format() + '<p>') + container.AddItem( + Link(adminurl + '/members/list', + _('Click here to hide the legend for this table.'))) + else: + container.AddItem( + Link(adminurl + '/members/list?legend=yes', + _('Click here to include the legend for this table.'))) + container.AddItem(Center(usertable)) + + # There may be additional chunks + if chunkindex is not None: + buttons = [] + url = adminurl + '/members?%sletter=%s&' % (addlegend, bucket) + footer = _('''<p><em>To view more members, click on the appropriate + range listed below:</em>''') + chunkmembers = buckets[bucket] + last = len(chunkmembers) + for i in range(numchunks): + if i == chunkindex: + continue + start = chunkmembers[i*chunksz] + end = chunkmembers[min((i+1)*chunksz, last)-1] + link = Link(url + 'chunk=%d' % i, _('from %(start)s to %(end)s')) + buttons.append(link) + buttons = UnorderedList(*buttons) + container.AddItem(footer + buttons.Format() + '<p>') + return container + + + +def mass_subscribe(mlist, container): + # MASS SUBSCRIBE + GREY = mm_cfg.WEB_ADMINITEM_COLOR + table = Table(width='90%') + table.AddRow([ + Label(_('Subscribe these users now or invite them?')), + RadioButtonArray('subscribe_or_invite', + (_('Subscribe'), _('Invite')), + 0, values=(0, 1)) + ]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY) + table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY) + table.AddRow([ + Label(_('Send welcome messages to new subscribees?')), + RadioButtonArray('send_welcome_msg_to_this_batch', + (_('No'), _('Yes')), + mlist.send_welcome_msg, + values=(0, 1)) + ]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY) + table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY) + table.AddRow([ + Label(_('Send notifications of new subscriptions to the list owner?')), + RadioButtonArray('send_notifications_to_list_owner', + (_('No'), _('Yes')), + mlist.admin_notify_mchanges, + values=(0,1)) + ]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY) + table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY) + table.AddRow([Italic(_('Enter one address per line below...'))]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2) + table.AddRow([Center(TextArea(name='subscribees', + rows=10, cols='70%', wrap=None))]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2) + table.AddRow([Italic(Label(_('...or specify a file to upload:'))), + FileUpload('subscribees_upload', cols='50')]) + container.AddItem(Center(table)) + # Invitation text + table.AddRow([' ', ' ']) + table.AddRow([Italic(_("""Below, enter additional text to be added to the + top of your invitation or the subscription notification. Include at least + one blank line at the end..."""))]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2) + table.AddRow([Center(TextArea(name='invitation', + rows=10, cols='70%', wrap=None))]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2) + + + +def mass_remove(mlist, container): + # MASS UNSUBSCRIBE + GREY = mm_cfg.WEB_ADMINITEM_COLOR + table = Table(width='90%') + table.AddRow([ + Label(_('Send unsubscription acknowledgement to the user?')), + RadioButtonArray('send_unsub_ack_to_this_batch', + (_('No'), _('Yes')), + 0, values=(0, 1)) + ]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY) + table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY) + table.AddRow([ + Label(_('Send notifications to the list owner?')), + RadioButtonArray('send_unsub_notifications_to_list_owner', + (_('No'), _('Yes')), + mlist.admin_notify_mchanges, + values=(0, 1)) + ]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY) + table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY) + table.AddRow([Italic(_('Enter one address per line below...'))]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2) + table.AddRow([Center(TextArea(name='unsubscribees', + rows=10, cols='70%', wrap=None))]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2) + table.AddRow([Italic(Label(_('...or specify a file to upload:'))), + FileUpload('unsubscribees_upload', cols='50')]) + container.AddItem(Center(table)) + + + +def password_inputs(mlist): + adminurl = mlist.GetScriptURL('admin', absolute=1) + table = Table(cellspacing=3, cellpadding=4) + table.AddRow([Center(Header(2, _('Change list ownership passwords')))]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2, + bgcolor=mm_cfg.WEB_HEADER_COLOR) + table.AddRow([_("""\ +The <em>list administrators</em> are the people who have ultimate control over +all parameters of this mailing list. They are able to change any list +configuration variable available through these administration web pages. + +<p>The <em>list moderators</em> have more limited permissions; they are not +able to change any list configuration variable, but they are allowed to tend +to pending administration requests, including approving or rejecting held +subscription requests, and disposing of held postings. Of course, the +<em>list administrators</em> can also tend to pending requests. + +<p>In order to split the list ownership duties into administrators and +moderators, you must set a separate moderator password in the fields below, +and also provide the email addresses of the list moderators in the +<a href="%(adminurl)s/general">general options section</a>.""")]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2) + # Set up the admin password table on the left + atable = Table(border=0, cellspacing=3, cellpadding=4, + bgcolor=mm_cfg.WEB_ADMINPW_COLOR) + atable.AddRow([Label(_('Enter new administrator password:')), + PasswordBox('newpw', size=20)]) + atable.AddRow([Label(_('Confirm administrator password:')), + PasswordBox('confirmpw', size=20)]) + # Set up the moderator password table on the right + mtable = Table(border=0, cellspacing=3, cellpadding=4, + bgcolor=mm_cfg.WEB_ADMINPW_COLOR) + mtable.AddRow([Label(_('Enter new moderator password:')), + PasswordBox('newmodpw', size=20)]) + mtable.AddRow([Label(_('Confirm moderator password:')), + PasswordBox('confirmmodpw', size=20)]) + # Add these tables to the overall password table + table.AddRow([atable, mtable]) + return table + + + +def submit_button(name='submit'): + table = Table(border=0, cellspacing=0, cellpadding=2) + table.AddRow([Bold(SubmitButton(name, _('Submit Your Changes')))]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, align='middle') + return table + + + +def change_options(mlist, category, subcat, cgidata, doc): + def safeint(formvar, defaultval=None): + try: + return int(cgidata.getvalue(formvar)) + except (ValueError, TypeError): + return defaultval + confirmed = 0 + # Handle changes to the list moderator password. Do this before checking + # the new admin password, since the latter will force a reauthentication. + new = cgidata.getvalue('newmodpw', '').strip() + confirm = cgidata.getvalue('confirmmodpw', '').strip() + if new or confirm: + if new == confirm: + mlist.mod_password = sha.new(new).hexdigest() + # No re-authentication necessary because the moderator's + # password doesn't get you into these pages. + else: + doc.addError(_('Moderator passwords did not match')) + # Handle changes to the list administrator password + new = cgidata.getvalue('newpw', '').strip() + confirm = cgidata.getvalue('confirmpw', '').strip() + if new or confirm: + if new == confirm: + mlist.password = sha.new(new).hexdigest() + # Set new cookie + print mlist.MakeCookie(mm_cfg.AuthListAdmin) + else: + doc.addError(_('Administrator passwords did not match')) + # Give the individual gui item a chance to process the form data + categories = mlist.GetConfigCategories() + label, gui = categories[category] + # BAW: We handle the membership page special... for now. + if category <> 'members': + gui.handleForm(mlist, category, subcat, cgidata, doc) + # mass subscription, removal processing for members category + subscribers = '' + subscribers += cgidata.getvalue('subscribees', '') + subscribers += cgidata.getvalue('subscribees_upload', '') + if subscribers: + entries = filter(None, [n.strip() for n in subscribers.splitlines()]) + send_welcome_msg = safeint('send_welcome_msg_to_this_batch', + mlist.send_welcome_msg) + send_admin_notif = safeint('send_notifications_to_list_owner', + mlist.admin_notify_mchanges) + # Default is to subscribe + subscribe_or_invite = safeint('subscribe_or_invite', 0) + invitation = cgidata.getvalue('invitation', '') + digest = 0 + if not mlist.digestable: + digest = 0 + if not mlist.nondigestable: + digest = 1 + subscribe_errors = [] + subscribe_success = [] + # Now cruise through all the subscribees and do the deed. BAW: we + # should limit the number of "Successfully subscribed" status messages + # we display. Try uploading a file with 10k names -- it takes a while + # to render the status page. + for entry in entries: + fullname, address = parseaddr(entry) + # Canonicalize the full name + fullname = Utils.canonstr(fullname, mlist.preferred_language) + userdesc = UserDesc(address, fullname, + Utils.MakeRandomPassword(), + digest, mlist.preferred_language) + try: + if subscribe_or_invite: + if mlist.isMember(address): + raise Errors.MMAlreadyAMember + else: + mlist.InviteNewMember(userdesc, invitation) + else: + mlist.ApprovedAddMember(userdesc, send_welcome_msg, + send_admin_notif, invitation) + except Errors.MMAlreadyAMember: + subscribe_errors.append((entry, _('Already a member'))) + except Errors.MMBadEmailError: + if userdesc.address == '': + subscribe_errors.append((_('<blank line>'), + _('Bad/Invalid email address'))) + else: + subscribe_errors.append((entry, + _('Bad/Invalid email address'))) + except Errors.MMHostileAddress: + subscribe_errors.append( + (entry, _('Hostile address (illegal characters)'))) + else: + member = Utils.uncanonstr(formataddr((fullname, address))) + subscribe_success.append(Utils.websafe(member)) + if subscribe_success: + if subscribe_or_invite: + doc.AddItem(Header(5, _('Successfully invited:'))) + else: + doc.AddItem(Header(5, _('Successfully subscribed:'))) + doc.AddItem(UnorderedList(*subscribe_success)) + doc.AddItem('<p>') + if subscribe_errors: + if subscribe_or_invite: + doc.AddItem(Header(5, _('Error inviting:'))) + else: + doc.AddItem(Header(5, _('Error subscribing:'))) + items = ['%s -- %s' % (x0, x1) for x0, x1 in subscribe_errors] + doc.AddItem(UnorderedList(*items)) + doc.AddItem('<p>') + # Unsubscriptions + removals = '' + if cgidata.has_key('unsubscribees'): + removals += cgidata['unsubscribees'].value + if cgidata.has_key('unsubscribees_upload') and \ + cgidata['unsubscribees_upload'].value: + removals += cgidata['unsubscribees_upload'].value + if removals: + names = filter(None, [n.strip() for n in removals.splitlines()]) + send_unsub_notifications = int( + cgidata['send_unsub_notifications_to_list_owner'].value) + userack = int( + cgidata['send_unsub_ack_to_this_batch'].value) + unsubscribe_errors = [] + unsubscribe_success = [] + for addr in names: + try: + mlist.ApprovedDeleteMember( + addr, whence='admin mass unsub', + admin_notif=send_unsub_notifications, + userack=userack) + unsubscribe_success.append(addr) + except Errors.NotAMemberError: + unsubscribe_errors.append(addr) + if unsubscribe_success: + doc.AddItem(Header(5, _('Successfully Unsubscribed:'))) + doc.AddItem(UnorderedList(*unsubscribe_success)) + doc.AddItem('<p>') + if unsubscribe_errors: + doc.AddItem(Header(3, Bold(FontAttr( + _('Cannot unsubscribe non-members:'), + color='#ff0000', size='+2')).Format())) + doc.AddItem(UnorderedList(*unsubscribe_errors)) + doc.AddItem('<p>') + # See if this was a moderation bit operation + if cgidata.has_key('allmodbit_btn'): + val = cgidata.getvalue('allmodbit_val') + try: + val = int(val) + except VallueError: + val = None + if val not in (0, 1): + doc.addError(_('Bad moderation flag value')) + else: + for member in mlist.getMembers(): + mlist.setMemberOption(member, mm_cfg.Moderate, val) + # do the user options for members category + if cgidata.has_key('setmemberopts_btn') and cgidata.has_key('user'): + user = cgidata['user'] + if type(user) is ListType: + users = [] + for ui in range(len(user)): + users.append(urllib.unquote(user[ui].value)) + else: + users = [urllib.unquote(user.value)] + errors = [] + removes = [] + for user in users: + if cgidata.has_key('%s_unsub' % user): + try: + mlist.ApprovedDeleteMember(user) + removes.append(user) + except Errors.NotAMemberError: + errors.append((user, _('Not subscribed'))) + continue + if not mlist.isMember(user): + doc.addError(_('Ignoring changes to deleted member: %(user)s'), + tag=_('Warning: ')) + continue + value = cgidata.has_key('%s_digest' % user) + try: + mlist.setMemberOption(user, mm_cfg.Digests, value) + except (Errors.AlreadyReceivingDigests, + Errors.AlreadyReceivingRegularDeliveries, + Errors.CantDigestError, + Errors.MustDigestError): + # BAW: Hmm... + pass + + newname = cgidata.getvalue(user+'_realname', '') + newname = Utils.canonstr(newname, mlist.preferred_language) + mlist.setMemberName(user, newname) + + newlang = cgidata.getvalue(user+'_language') + oldlang = mlist.getMemberLanguage(user) + if newlang and newlang <> oldlang: + mlist.setMemberLanguage(user, newlang) + + moderate = not not cgidata.getvalue(user+'_mod') + mlist.setMemberOption(user, mm_cfg.Moderate, moderate) + + # Set the `nomail' flag, but only if the user isn't already + # disabled (otherwise we might change BYUSER into BYADMIN). + if cgidata.has_key('%s_nomail' % user): + if mlist.getDeliveryStatus(user) == MemberAdaptor.ENABLED: + mlist.setDeliveryStatus(user, MemberAdaptor.BYADMIN) + else: + mlist.setDeliveryStatus(user, MemberAdaptor.ENABLED) + for opt in ('hide', 'ack', 'notmetoo', 'nodupes', 'plain'): + opt_code = mm_cfg.OPTINFO[opt] + if cgidata.has_key('%s_%s' % (user, opt)): + mlist.setMemberOption(user, opt_code, 1) + else: + mlist.setMemberOption(user, opt_code, 0) + # Give some feedback on who's been removed + if removes: + doc.AddItem(Header(5, _('Successfully Removed:'))) + doc.AddItem(UnorderedList(*removes)) + doc.AddItem('<p>') + if errors: + doc.AddItem(Header(5, _("Error Unsubscribing:"))) + items = ['%s -- %s' % (x[0], x[1]) for x in errors] + doc.AddItem(apply(UnorderedList, tuple((items)))) + doc.AddItem("<p>") |