diff options
| -rw-r--r-- | Mailman/MailList.py | 20 | ||||
| -rw-r--r-- | Mailman/Utils.py | 14 | ||||
| -rw-r--r-- | Mailman/htmlformat.py | 7 | ||||
| -rwxr-xr-x | cgi/admin | 390 | ||||
| -rwxr-xr-x | cgi/private | 22 | ||||
| -rw-r--r-- | modules/htmlformat.py | 7 | ||||
| -rw-r--r-- | modules/maillist.py | 20 | ||||
| -rw-r--r-- | modules/mm_utils.py | 14 |
8 files changed, 335 insertions, 159 deletions
diff --git a/Mailman/MailList.py b/Mailman/MailList.py index 44339b4d9..406d1e599 100644 --- a/Mailman/MailList.py +++ b/Mailman/MailList.py @@ -75,8 +75,13 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, return '%s@%s' % (self._internal_name, self.host_name) def GetScriptURL(self, script_name): - return os.path.join(self.web_page_url, '%s/%s' % - (script_name, self._internal_name)) + if self.web_page_url: + prefix = self.web_page_url + else: + prefix = mm_cfg.DEFAULT_URL + return os.path.join(prefix, '%s/%s' % (script_name, + self._internal_name)) + def GetOptionsURL(self, addr, obscured=0): options = self.GetScriptURL('options') if obscured: @@ -85,9 +90,6 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, treated = addr return os.path.join(options, treated) - def GetOptionsURL(self, addr): - return os.path.join(self.GetScriptURL('options'), addr) - def GetUserOption(self, user, option): if option == mm_cfg.Digests: return user in self.digest_members @@ -212,10 +214,10 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, " what the list is."), ('info', mm_cfg.Text, (7, 50), 0, - 'An introductory description - a few paragraphs - about the' - 'list. It will be included, as html, at the top of the' - 'listinfo page. Carriage returns will end a paragraph - see' - 'the details for more info.', + ' An introductory description - a few paragraphs - about the' + ' list. It will be included, as html, at the top of the' + ' listinfo page. Carriage returns will end a paragraph - see' + ' the details for more info.', "The text will be treated as html <em>except</em> that newlines" " newlines will be translated to <br> - so you can use" diff --git a/Mailman/Utils.py b/Mailman/Utils.py index bc6b6a8da..419ae06a0 100644 --- a/Mailman/Utils.py +++ b/Mailman/Utils.py @@ -477,3 +477,17 @@ class StampedLogger(Logger): Logger.write(self, ' ' + l) else: Logger.write(self, l) + +def chunkify(members, chunksize=mm_cfg.ADMIN_MEMBER_CHUNKSIZE): + """ + return a list of lists of members + """ + members.sort() + res = [] + while 1: + if not members: + break + chunk = members[:chunksize] + res.append(chunk) + members = members[chunksize:] + return res diff --git a/Mailman/htmlformat.py b/Mailman/htmlformat.py index 587bb1a0a..ddb6ac229 100644 --- a/Mailman/htmlformat.py +++ b/Mailman/htmlformat.py @@ -20,7 +20,7 @@ Encapsulate HTML formatting directives in classes that act as containers for python and, recursively, for nested HTML formatting objects.""" -__version__ = "$Revision: 547 $" +__version__ = "$Revision: 635 $" # Eventually could abstract down to HtmlItem, which outputs an arbitrary html # object given start / end tags, valid options, and a value. @@ -406,6 +406,11 @@ class RadioButton(InputObj): def __init__(self, name, value, checked=0, **kws): apply(InputObj.__init__, (self, name, 'RADIO', value, checked), kws) +class CheckBox(InputObj): + def __init__(self, name, value, checked=0, **kws): + apply(InputObj.__init__, (self, name, "CHECKBOX", value, checked), kws) + + class VerticalSpacer: def __init__(self, size=10): @@ -21,12 +21,12 @@ To run stand-alone for debugging, set env var PATH_INFO to name of list and, optionally, options category.""" -__version__ = "$Revision: 564 $" +__version__ = "$Revision: 635 $" import sys -import os, cgi, string, crypt, types +import os, cgi, string, crypt, types, time import paths # path hacking -import mm_utils, maillist, mm_cfg, mm_err +import mm_utils, maillist, mm_cfg, mm_err, mm_mailcmd, Cookie from htmlformat import * try: @@ -44,12 +44,63 @@ CATEGORIES = [('general', "General Options"), ('archive', "Archival Options")] +LOGIN_PAGE = """ +<html> +<head> + <title>%(listname)s Administrative Authentication</title> +</head> +<body bgcolor="#ffffff"> +<FORM METHOD=POST ACTION="%(path)s"> +%(message)s + <TABLE WIDTH="100%%" BORDER="0" CELLSPACING="4" CELLPADDING="5"> + <TR> + <TD COLSPAN="2" WIDTH="100%%" BGCOLOR="#99CCFF" ALIGN="CENTER"> + <B><FONT COLOR="#000000" SIZE="+1">%(listname)s Administrative + Authentication</FONT></B> + </TD> + </TR> + <tr> + <TD> <div ALIGN="Right"> List Administrative Password: </div> </TD> + <TD> <INPUT TYPE=password NAME=adminpw SIZE=30></TD> + </tr> + <tr> + <td colspan=2 align=middle> <INPUT TYPE=SUBMIT name="request_login"> + </td> + </tr> + </TABLE> +</FORM> +""" + +# " <- icky emacs font-lock bug workaround + +def isAuthenticated(list, password=None): + if password is not None: # explicit login + try: + list.ConfirmAdminPassword(password) + except mm_err.MMBadPasswordError: + AddErrorMessage(doc, 'Error: Incorrect admin password.') + return 0 + else: + c = Cookie.Cookie() + c[list_name] = list.password # its crypted so this should be ok + c[list_name]['expires'] = mm_cfg.ADMIN_COOKIE_LIFE + c[list_name]["path"] = "/mailman/" + list.GetScriptURL("admin") + print c # Output the cookie + + return 1 + if os.environ.has_key('HTTP_COOKIE'): + c = Cookie.Cookie( os.environ['HTTP_COOKIE'] ) + if c.has_key(list_name): + return (c[list_name].value == list.password) + return 0 + + def main(): """Process and produce list options form. CGI input indicates that we're returning from submission of some new settings, which is processed before producing the new version.""" - global list_name, list_info + global list_name, list_info, doc doc = Document() try: @@ -81,26 +132,36 @@ def main(): if category not in map(lambda x: x[0], CATEGORIES): category = 'general' - + global cgi_data cgi_data = cgi.FieldStorage() - if len(cgi_data.keys()): - if cgi_data.has_key('VARHELP'): - FormatOptionHelp(doc, cgi_data['VARHELP'].value, lst) - print doc.Format(bgcolor="#ffffff") + is_auth = 0 + if cgi_data.has_key("adminpw"): + is_auth = isAuthenticated(lst, cgi_data["adminpw"].value) + message = FontAttr("Sorry, wrong password. Try again.", + color="ff5060", size="+1").Format() + else: + is_auth = isAuthenticated(lst) + message = "" + if not is_auth: + print "Content-type: text/html\n\n" + print LOGIN_PAGE % ({"listname": list_name, + "path": os.environ.get("REQUEST_URI", "/mailman/admin"), + "message": message}) + return + + if len(cgi_data.keys()): + if cgi_data.has_key('VARHELP'): + FormatOptionHelp(doc, cgi_data['VARHELP'].value, lst) + print doc.Format(bgcolor="#ffffff") return - if not cgi_data.has_key('adminpw'): - AddErrorMessage(doc, - 'Error: You must supply the admin password to' - ' change options.') - else: + if (cgi_data.has_key('bounce_matching_headers')): try: - lst.ConfirmAdminPassword(cgi_data['adminpw'].value) - ChangeOptions(lst, category, cgi_data, doc) - # Yuck. This shouldn't need to be here. - if not lst.digestable and not lst.nondigestable: - lst.nondigestable = 1 - except mm_err.MMBadPasswordError: - AddErrorMessage(doc, 'Error: Incorrect admin password.') + pairs = lst.parse_matching_header_opt() + except mm_err.MMBadConfigError, line: + AddErrorMessage(doc, + 'Warning: bad matching-header line' + ' (does it have the colon?)<ul> %s </ul>', + line) if not lst.digestable and len(lst.digest_members): AddErrorMessage(doc, @@ -113,17 +174,8 @@ def main(): ' but non-digestified mail is turned' ' off. They will receive mail until' ' you fix this problem.') - - if len(cgi_data.keys()): - if (cgi_data.has_key('bounce_matching_headers')): - try: - pairs = lst.parse_matching_header_opt() - except mm_err.MMBadConfigError, line: - AddErrorMessage(doc, - 'Warning: bad matching-header line' - ' (does it have the colon?)<ul> %s </ul>', - line) - + if len(cgi_data.keys()): + ChangeOptions(lst, category, cgi_data, doc) FormatConfiguration(doc, lst, category, category_suffix) print doc.Format(bgcolor="#ffffff") @@ -143,27 +195,13 @@ def FormatAdminOverview(error=None): table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2, bgcolor="#99ccff") - # XXX We need a portable way to determine the host by which we are being - # visited! An absolute URL would do... - if os.environ.has_key('HTTP_HOST'): - http_host = os.environ['HTTP_HOST'] - else: - http_host = None - advertised = [] names = mm_utils.list_names() names.sort() for n in names: l = maillist.MailList(n) l.Unlock() - if l.advertised: - if (http_host - and (string.find(http_host, l.host_name) == -1 - and string.find(l.host_name, http_host) == -1)): - # List is for different identity for this host - skip it. - continue - else: - advertised.append(l) + if l.advertised: advertised.append(l) if error: greeting = FontAttr(error, color="ff5060", size="+1") @@ -247,12 +285,12 @@ def FormatConfiguration(doc, lst, category, category_suffix): other_links.AddItem(link) these_links = UnorderedList() - url = lst.GetScriptURL('admin') for k, v in CATEGORIES: if k == category: these_links.AddItem("<b> => " + v + " <= </b>") else: - these_links.AddItem(Link(os.path.join(url, k), v)) + these_links.AddItem(Link("%s/%s" % (lst.GetScriptURL('admin'),k), + v)) links_table.AddRow([these_links, other_links]) links_table.AddRowInfo(max(links_table.GetCurrentRowIndex(), 0), @@ -261,7 +299,8 @@ def FormatConfiguration(doc, lst, category, category_suffix): doc.AddItem(links_table) doc.AddItem('<hr>') if category_suffix: - form = Form(os.path.join(lst.GetScriptURL('admin'), category)) + form = Form("%s/%s" % (lst.GetScriptURL('admin'), + category_suffix)) else: form = Form(lst.GetScriptURL('admin')) doc.AddItem(form) @@ -452,53 +491,91 @@ def FormatMembershipOptions(lst): header.AddRow([Center(Header(2, "Membership Management"))]) header.AddCellInfo(max(header.GetCurrentRowIndex(), 0), 0, colspan=2, bgcolor="#99ccff") - header.AddRow([Bold("Subscribe and Unsubscribe Members")]) - header.AddCellInfo(max(header.GetCurrentRowIndex(), 0), 0, - colspan=2, bgcolor ="#FFF0D0") container.AddItem(header) - - container.AddItem('<h3>Mass Subscribe Members</h3>') - container.AddItem('<h4>Enter one email address per line</h4>') - container.AddItem(TextArea(name='subscribees', rows=20,cols=60,wrap=None)) + user_table = Table(width="90%") + user_table.AddRow([Center(Header(4, "Membership List"))]) + user_table.AddCellInfo(user_table.GetCurrentRowIndex(), + user_table.GetCurrentCellIndex(), + bgcolor="#cccccc", colspan=8) - container.AddItem('<h3>To Unsubscribe Members...</h3>') - container.AddItem(""" - To unsubscribe members you must use your admin password in place of the - user's password on the user's edit-options page. Visit their - edit-options page (via the <a href="%s">roster</a> page) and do the - unsubscribe procedure, providing the admin password instead of the - user's password. - <p>(Note that you can, alternately, set the subscriber's no-delivery - option to inhibit delivery of their messages, if you want to only - temporarily disable their delivery.)<p>""" - % lst.GetScriptURL('roster')) + members = {} + digests = {} + for member in lst.members: + members[member] = 1 + for member in lst.digest_members: + digests[member] = 1 + all = lst.members + lst.digest_members + if len(all) > mm_cfg.ADMIN_MEMBER_CHUNKSIZE: + chunks = mm_utils.chunkify(all) + if not cgi_data.has_key("chunk"): + chunk = 0 + else: + chunk = string.atoi(cgi_data["chunk"].value) + all = chunks[chunk] + footer = ("<p><em>To View other sections, " + "click on the appropriate range listed below</em>") + chunk_indices = range(len(chunks)) + chunk_indices.remove(chunk) + buttons = [] + pi = os.environ["PATH_INFO"] + for ci in chunk_indices: + start, end = chunks[ci][0], chunks[ci][-1] + buttons.append("<a href=/mailman/admin%s?chunk=%d> from %s to %s </a>" % \ + ( pi, ci, start, end)) + buttons = apply(UnorderedList, tuple(buttons)) + footer = footer + buttons.Format() + "<p>" + else: + all.sort() + footer = "<p>" + for member in all: + cells = [member + "<input type=hidden name=user value=%s>" % (member), + "subscribed " +CheckBox(member + "_subscribed", "on", 1).Format(), + ] + if members.get(member): + cells.append("digest " + CheckBox(member + "_digest", "off", 0).Format()) + else: + cells.append("digest " + CheckBox(member + "_digest", "on", 1).Format()) + for opt in ("hide", "nomail", "ack", "norcv", "plain"): + if lst.GetUserOption(member, mm_mailcmd.option_info[opt]): + value = "on" + checked = 1 + else: + value = "off" + checked = 0 + box = CheckBox("%s_%s" % (member, opt), value, checked) + cells.append("%s %s" % (opt, box.Format())) + user_table.AddRow(cells) + container.AddItem(Center(user_table)) + container.AddItem(footer) + t = Table(width="90%") + t.AddRow([Center(Header(4, "Mass Subscribe Members"))]) + t.AddCellInfo(t.GetCurrentRowIndex(), + t.GetCurrentCellIndex(), + bgcolor="#cccccc", colspan=8) + container.AddItem(Center(t)) + container.AddItem(Center(TextArea(name='subscribees', rows=10,cols=60,wrap=None))) + container.AddItem(Center("<em> Enter One address per line</em><p>")) return container def FormatPasswordStuff(): - password_submit = Table(bgcolor="#99cccc", - border=0, cellspacing=0, cellpadding=2) - password_submit.AddRow([Center(Bold('To Submit Your Changes'))]) - password_submit.AddCellInfo(password_submit.GetCurrentRowIndex(), 0, - colspan=2) - password_submit.AddRow(['<div ALIGN="right">Enter the administrator ' - 'password:</div>', - PasswordBox('adminpw')]) - password_submit.AddRow(['<div ALIGN="right">And...</div>', - Bold(SubmitButton('submit', 'Submit Changes'))]) - change_pw_table = Table(bgcolor="#cccccc", border=0, - cellspacing=0, cellpadding=2) - change_pw_table.AddRow([Bold(Center('To Change The Administrator' - ' Password'))]) - change_pw_table.AddCellInfo(change_pw_table.GetCurrentRowIndex(), 0, - colspan=2) - change_pw_table.AddRow(['<div ALIGN="right">Enter the new ' - 'password:</div>', - PasswordBox('newpw')]) - change_pw_table.AddRow(['<div ALIGN="right">And also confirm it:</div>', - PasswordBox('confirmpw')]) - - password_stuff = Table(bgcolor="#99cccc") - password_stuff.AddRow([password_submit, change_pw_table]) + submit = Table(bgcolor="#99ccff", + border=0, cellspacing=0, cellpadding=2, width="100%") + submit.AddRow([Bold(SubmitButton('submit', 'Submit Your Changes'))]) + submit.AddCellInfo(submit.GetCurrentRowIndex(), 0, align="middle") + change_pw_table = Table(bgcolor="#99cccc", border=0, + cellspacing=0, cellpadding=2, width="90%") + change_pw_table.AddRow([Bold(Center('To Change The Administrator Password')), + '<div ALIGN="right"> Enter the new password: </div>', + PasswordBox('newpw'),]) + change_pw_table.AddCellInfo(0, 0, align="middle", colspan=2) + change_pw_table.AddRow(['<div ALIGN="right"> Enter the current password </div>', + PasswordBox('adminpw'), + '<div ALIGN="right">Again to confirm it:</div>', + PasswordBox('confirmpw')]) + password_stuff = Container() + password_stuff.AddItem(change_pw_table) + password_stuff.AddItem("<p>") + password_stuff.AddItem(submit) return password_stuff # XXX klm - looks like turn_on_moderation is orphaned. @@ -582,7 +659,38 @@ def GetValidValue(lst, prop, my_type, val, dependant): def ChangeOptions(lst, category, cgi_info, document): dirty = 0 - if category != 'members': + confirmed = 0 + if cgi_info.has_key('newpw'): + if cgi_info.has_key('confirmpw'): + if cgi_info.has_key('adminpw'): + try: + lst.ConfirmAdminPassword(cgi_info['adminpw'].value) + confirmed = 1 + except mm_err.MMBadPasswordError: + m = "Error: incorrect administrator password" + document.AddItem(Header(3, Italic(FontAttr(m, color="ff5060")))) + confirmed = 0 + if confirmed: + new = cgi_info['newpw'].value + confirm = cgi_info['confirmpw'].value + if new == confirm: + lst.password = crypt.crypt(new, mm_utils.GetRandomSeed()) + dirty = 1 + else: + m = 'Error: Passwords did not match.' + document.AddItem(Header(3, Italic(FontAttr(m, color="ff5060")))) + + else: + m = 'Error: You must type in your new password twice.' + document.AddItem( + Header(3, Italic(FontAttr(m, color="ff5060")))) + # + # for some reason, the login page mangles important values for the list + # such as .real_name so we only process these changes if the category + # is not "members" and the request is not from the login page + # -scott 19980515 + # + if category != 'members' and not cgi_info.has_key("request_login"): opt_list = GetConfigOptions(lst, category) for item in opt_list: if len(item) < 5: @@ -600,37 +708,85 @@ def ChangeOptions(lst, category, cgi_info, document): val = cgi_info[property].value value = GetValidValue(lst, property, kind, val, deps) if getattr(lst, property) != value: + print "property: ", property, "value: ", value setattr(lst, property, value) dirty = 1 + # + # mass subscription processing for members category + # if cgi_info.has_key('subscribees'): name_text = cgi_info['subscribees'].value - names = string.split(name_text, '\r\n') - for new_name in names: + name_text = string.replace(name_text, '\r', '') + names = string.split(name_text, '\n') + if '' in names: + names.remove('') + subscribe_success = [] + subscribe_errors = [] + for new_name in map(string.strip,names): + digest = 0 + if not lst.digestable: + digest = 0 + if not lst.nondigestable: + digest = 1 try: -#FIXME: The admin needs to be able to specify subscribe options - lst.AddMember(new_name, (mm_utils.GetRandomSeed() + - mm_utils.GetRandomSeed())) - dirty = 1 -#FIXME: Give some sort of an indication of which names didn't work, -# and why they didn't work... - except: - pass - if cgi_info.has_key('newpw'): - if cgi_info.has_key('confirmpw'): - new = cgi_info['newpw'].value - confirm = cgi_info['confirmpw'].value - if new == confirm: - lst.password = crypt.crypt(new, mm_utils.GetRandomSeed()) + lst.ApprovedAddMember(new_name, (mm_utils.GetRandomSeed() + + mm_utils.GetRandomSeed()), digest) + subscribe_success.append(new_name) + except mm_err.MMAlreadyAMember: + subscribe_errors.append((new_name, 'Already a member')) + + except mm_err.MMBadEmailError: + subscribe_errors.append((new_name, "Bad/Invalid email address")) + except mm_err.MMHostileAddress: + subscribe_errors.append((new_name, "Hostile Address (illegal characters)")) + if subscribe_success: + document.AddItem(Header(5, "Successfully Subscribed:")) + document.AddItem(apply(UnorderedList, tuple((subscribe_success)))) + document.AddItem("<p>") + if subscribe_errors: + document.AddItem(Header(5, "Error Subscribing:")) + items = map(lambda x: "%s -- %s" % (x[0], x[1]), subscribe_errors) + document.AddItem(apply(UnorderedList, tuple((items)))) + document.AddItem("<p>") + # + # do the user options for members category + # + if cgi_info.has_key('user'): + user = cgi_info["user"] + if type(user) is type([]): + users = [] + for ui in range(len(user)): + users.append(user[ui].value) + else: + users = [user.value] + for user in users: + if not cgi_info.has_key('%s_subscribed' % (user)): + lst.DeleteMember(user) dirty = 1 - else: - m = 'Error: Passwords did not match.' - document.AddItem( - Header(3, Italic(FontAttr(m, color="ff5060")))) + continue + if not cgi_info.has_key("%s_digest" % (user)): + if user in lst.digest_members: + list.digest_members.remove(user) + dirty = 1 + if user not in lst.members: + lst.members.append(user) + dirty = 1 + else: + if user not in lst.digest_members: + lst.digest_members.append(user) + dirty = 1 + if user in lst.members: + lst.members.remove(user) + dirty = 1 + + for opt in ("hide", "nomail", "ack", "norcv", "plain"): + if cgi_info.has_key("%s_%s" % (user, opt)): + lst.SetUserOption(user, mm_mailcmd.option_info[opt], 1) + dirty = 1 + else: + lst.SetUserOption(user, mm_mailcmd.option_info[opt], 0) + dirty = 1 - else: - m = 'Error: You must type in your new password twice.' - document.AddItem( - Header(3, Italic(FontAttr(m, color="ff5060")))) if dirty: lst.Save() diff --git a/cgi/private b/cgi/private index 72d5c6031..facca81bc 100755 --- a/cgi/private +++ b/cgi/private @@ -74,7 +74,6 @@ PAGE = ''' login_attempted = 0 _list = None -# XXX: This looks broken, should investigate name_pat = re.compile(r""" (?: / (?: \d{4} q \d\. )? # Match "/", and, optionally, 1998q1." ( [^/]* ) /? # The SIG name @@ -88,20 +87,6 @@ name_pat = re.compile(r""" ) """, re.VERBOSE) -# " XXX: Emacs turd -# the following is a potentially better rewrite - -## name_pat = re.compile( -## r'(?: / (?: \d{4} q \d\. )?' # Match "/", and, optionally, 1998q1." -## r'( [^/]* ) /?' # The SIG name -## r'/[^/]*$' # The trailing 12345.html portion -## r') | (?:' -## r'/ ( [^/.]* )' # Match matrix-sig -## r'(?:\.html)?' # Optionally match .html -## r'/?' # Optionally match a trailing slash -## r'$)' # Must match to end of string -## , re.VERBOSE) - def getListName(path): match = name_pat.search(path) if match is None: return @@ -109,13 +94,6 @@ def getListName(path): if match.group(2): return match.group(2) raise ValueError, "Can't identify SIG name" -#for i in ['/matrix-sig.html', '/1998q1.c++-sig/index.html', -# '/1998q1.string-sig/foobar.html', -# '/psa-members.html']: -# print i, `getListName(i)` -#sys.exit(0) - -## sys.exit(0) def GetListobj(list_name): """Return an unlocked instance of the named maillist, if found.""" diff --git a/modules/htmlformat.py b/modules/htmlformat.py index 587bb1a0a..ddb6ac229 100644 --- a/modules/htmlformat.py +++ b/modules/htmlformat.py @@ -20,7 +20,7 @@ Encapsulate HTML formatting directives in classes that act as containers for python and, recursively, for nested HTML formatting objects.""" -__version__ = "$Revision: 547 $" +__version__ = "$Revision: 635 $" # Eventually could abstract down to HtmlItem, which outputs an arbitrary html # object given start / end tags, valid options, and a value. @@ -406,6 +406,11 @@ class RadioButton(InputObj): def __init__(self, name, value, checked=0, **kws): apply(InputObj.__init__, (self, name, 'RADIO', value, checked), kws) +class CheckBox(InputObj): + def __init__(self, name, value, checked=0, **kws): + apply(InputObj.__init__, (self, name, "CHECKBOX", value, checked), kws) + + class VerticalSpacer: def __init__(self, size=10): diff --git a/modules/maillist.py b/modules/maillist.py index 44339b4d9..406d1e599 100644 --- a/modules/maillist.py +++ b/modules/maillist.py @@ -75,8 +75,13 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, return '%s@%s' % (self._internal_name, self.host_name) def GetScriptURL(self, script_name): - return os.path.join(self.web_page_url, '%s/%s' % - (script_name, self._internal_name)) + if self.web_page_url: + prefix = self.web_page_url + else: + prefix = mm_cfg.DEFAULT_URL + return os.path.join(prefix, '%s/%s' % (script_name, + self._internal_name)) + def GetOptionsURL(self, addr, obscured=0): options = self.GetScriptURL('options') if obscured: @@ -85,9 +90,6 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, treated = addr return os.path.join(options, treated) - def GetOptionsURL(self, addr): - return os.path.join(self.GetScriptURL('options'), addr) - def GetUserOption(self, user, option): if option == mm_cfg.Digests: return user in self.digest_members @@ -212,10 +214,10 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, " what the list is."), ('info', mm_cfg.Text, (7, 50), 0, - 'An introductory description - a few paragraphs - about the' - 'list. It will be included, as html, at the top of the' - 'listinfo page. Carriage returns will end a paragraph - see' - 'the details for more info.', + ' An introductory description - a few paragraphs - about the' + ' list. It will be included, as html, at the top of the' + ' listinfo page. Carriage returns will end a paragraph - see' + ' the details for more info.', "The text will be treated as html <em>except</em> that newlines" " newlines will be translated to <br> - so you can use" diff --git a/modules/mm_utils.py b/modules/mm_utils.py index bc6b6a8da..419ae06a0 100644 --- a/modules/mm_utils.py +++ b/modules/mm_utils.py @@ -477,3 +477,17 @@ class StampedLogger(Logger): Logger.write(self, ' ' + l) else: Logger.write(self, l) + +def chunkify(members, chunksize=mm_cfg.ADMIN_MEMBER_CHUNKSIZE): + """ + return a list of lists of members + """ + members.sort() + res = [] + while 1: + if not members: + break + chunk = members[:chunksize] + res.append(chunk) + members = members[chunksize:] + return res |
