[Mailman-developers] membership management developments: chunkifying

Scott scott@chronis.icgroup.com
Tue, 21 Apr 1998 11:59:27 -0400


i have somehow managed to get the membership management section
chunkified, with proper authentication.

there is a new variable in mm_defaults called DEFAULT_CHUNK_SIZE,
which as commented sets the size of the chunk of members presented at
a time to admins editing their options/ unsubscribing them.

this section of membership management uses cookie authentication.  As
Authentication is getting particulary messy in the program cgi/admin,
i wouldn't suggest (m)any futher develops that involve special
authentication without rewriting the script from the bottom up.

anyway, here is a patch, as applied against the distribution version
that i posted about the other day that does an improved job of
membership management stuff. 

scott


Index: cgi/admin
===================================================================
RCS file: /usr/local/cvsroot/mailman/cgi/admin,v
retrieving revision 1.1.1.1
diff -c -r1.1.1.1 admin
*** admin	1998/04/21 03:27:57	1.1.1.1
--- admin	1998/04/21 15:45:15
***************
*** 9,17 ****
--- 9,20 ----
  
  import sys
  sys.path.append('/home/mailman/mailman/modules')
+ # XXX need to fix this, this directory should be configurable.
+ sys.path.append('/home/mailman/cgi-bin')
  import os, cgi, string, crypt, types
  import mm_utils, maillist, mm_cfg, mm_err
  from htmlformat import *
+ import Cookie
  
  try:
      sys.stderr = mm_utils.StampedLogger("error", label = 'admin',
***************
*** 27,32 ****
--- 30,38 ----
                ('bounce', "Bounce Options"),
                ('archive', "Archival Options")]
  
+ #
+ # XXX this this data is also in mm_mailcmd (i think), it should become shared
+ #
  option_info = { 'digest' : 0,
                  'nomail' : mm_cfg.DisableDelivery,
                  'norcv'  : mm_cfg.DontReceiveOwnPosts,
***************
*** 36,42 ****
                  }
  
  
- 
  def main():
      """Process and produce list options form.
  
--- 42,47 ----
***************
*** 80,86 ****
  		FormatOptionHelp(doc, cgi_data['VARHELP'].value, list)
  		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.')
--- 85,125 ----
  		FormatOptionHelp(doc, cgi_data['VARHELP'].value, list)
  		print doc.Format(bgcolor="#ffffff")
                  return
!             if cgi_data.has_key('adminpw') and cgi_data.has_key('edit_members'):
!                 adminpw = cgi_data["adminpw"]
!                 if type(adminpw) is type([]):
!                     adminpw = adminpw[0].value # use the first one, i guess -scott cotton
!                 else:
!                     adminpw = adminpw.value
!                 try:
! 		    list.ConfirmAdminPassword(adminpw)
!                     FormatConfiguration(doc, list, category, category_suffix, cgi_data, 0)
!                     c = Cookie.Cookie()
!                     c[list.real_name] = hash(list.real_name)
!                     print c
!                 except mm_err.MMBadPasswordError:
!  		    AddErrorMessage(doc, 'Error: Incorrect admin password.')
!                     FormatConfiguration(doc, list, category, category_suffix, cgi_data, 1)                    
!                 print doc.Format(bgcolor="#ffffff")
!                 return
!             elif cgi_data.has_key('chunk'):
!                 if os.environ.has_key("HTTP_COOKIE"):
!                     cookie = Cookie.Cookie(os.environ["HTTP_COOKIE"])
!                     if cookie.has_key(list.real_name):
!                         if cookie[list.real_name].value == hash(list.real_name):
!                             FormatConfiguration(doc, list, category, category_suffix, cgi_data, 0)
!                             print doc.Format(bgcolor="#ffffff")
!                             return
!                         else:
!                             AddErrorMessage(doc, "Error:  Invalid Cookie value")
!                     else:
!                         AddErrorMessage(doc, "Error, no cookie for list!")
!                 else:
!                     AddErrorMessage(doc, "no cookie!")
!                 FormatConfiguration(doc, list, category, category_suffix, cgi_data)
!                 print doc.Format(bgcolor="#ffffff")
!                 return
! 	    elif not cgi_data.has_key('adminpw') :
                  AddErrorMessage(doc,
                                  'Error: You must supply the admin password to'
                                  ' change options.')
***************
*** 90,103 ****
                      adminpw = adminpw[0].value # use the first one, i guess -scott cotton
                  else:
                      adminpw = adminpw.value
! 		try:
  		    list.ConfirmAdminPassword(adminpw)
! 		    ChangeOptions(list, category, cgi_data, doc)
  		    # Yuck.  This shouldn't need to be here. WTF?!?
! 		    if not list.digestable and not list.nondigestable:
  			list.nondigestable = 1
! 		except mm_err.MMBadPasswordError:
! 		    AddErrorMessage(doc, 'Error: Incorrect admin password.')
  
  	if not list.digestable and len(list.digest_members):
  	    AddErrorMessage(doc,
--- 129,142 ----
                      adminpw = adminpw[0].value # use the first one, i guess -scott cotton
                  else:
                      adminpw = adminpw.value
!                 try:
  		    list.ConfirmAdminPassword(adminpw)
!                     ChangeOptions(list, category, cgi_data, doc)
  		    # Yuck.  This shouldn't need to be here. WTF?!?
!                     if not list.digestable and not list.nondigestable:
  			list.nondigestable = 1
!                 except mm_err.MMBadPasswordError:
!  		    AddErrorMessage(doc, 'Error: Incorrect admin password.')
  
  	if not list.digestable and len(list.digest_members):
  	    AddErrorMessage(doc,
***************
*** 121,127 ****
                                      ' (does it have the colon?)<ul> %s </ul>',
                                      line)
  
!         FormatConfiguration(doc, list, category, category_suffix, cgi_data)
  	print doc.Format(bgcolor="#ffffff")
  
      finally:
--- 160,169 ----
                                      ' (does it have the colon?)<ul> %s </ul>',
                                      line)
  
!         if cgi_data.has_key('edit_member_options'): # passwd already checked
!             FormatConfiguration(doc, list, category, category_suffix, cgi_data, 0)
!         else:
!             FormatConfiguration(doc, list, category, category_suffix, cgi_data)            
  	print doc.Format(bgcolor="#ffffff")
  
      finally:
***************
*** 204,210 ****
  
      print doc.Format(bgcolor="#ffffff")
  
! def FormatConfiguration(doc, list, category, category_suffix, cgi_data):
      """Produce the overall doc, *except* any processing error messages."""
      for k, v in CATEGORIES:
          if k == category: label = v
--- 246,252 ----
  
      print doc.Format(bgcolor="#ffffff")
  
! def FormatConfiguration(doc, list, category, category_suffix, cgi_data, front_page=1):
      """Produce the overall doc, *except* any processing error messages."""
      for k, v in CATEGORIES:
          if k == category: label = v
***************
*** 253,269 ****
                   " bottom.  (You can also change your password there,"
                   " as well.)<p>")
  
!     form.AddItem(FormatOptionsSection(category, list, cgi_data))
  
      form.AddItem(Center(FormatPasswordStuff()))
  
      form.AddItem(list.GetMailmanFooter())
  
! def FormatOptionsSection(category, list, cgi_data):
      """Produce the category-specific options table."""
      if category == 'members':
          # Special case for members section.
!         return FormatMembershipOptions(list, cgi_data)
      options = GetConfigOptions(list, category)
  
      big_table = Table(cellspacing=3, cellpadding=4)
--- 295,311 ----
                   " bottom.  (You can also change your password there,"
                   " as well.)<p>")
  
!     form.AddItem(FormatOptionsSection(category, list, cgi_data, front_page))
  
      form.AddItem(Center(FormatPasswordStuff()))
  
      form.AddItem(list.GetMailmanFooter())
  
! def FormatOptionsSection(category, list, cgi_data, front_page):
      """Produce the category-specific options table."""
      if category == 'members':
          # Special case for members section.
!         return FormatMembershipOptions(list, cgi_data, front_page)
      options = GetConfigOptions(list, category)
  
      big_table = Table(cellspacing=3, cellpadding=4)
***************
*** 398,404 ****
          descr = descr + "</div>"
      return [descr, gui_part]
  
! def FormatMembershipOptions(list, cgi_data, option_info=option_info):
      container = Container()
      header = Table(width="100%")
      header.AddRow([Center(Header(2, "Membership Management"))])
--- 440,446 ----
          descr = descr + "</div>"
      return [descr, gui_part]
  
! def FormatMembershipOptions(list, cgi_data, front_page=1, option_info=option_info):
      container = Container()
      header = Table(width="100%")
      header.AddRow([Center(Header(2, "Membership Management"))])
***************
*** 408,428 ****
      header.AddCellInfo(max(header.GetCurrentRowIndex(), 0), 0,
                         colspan=2, bgcolor ="#FFF0D0")
      container.AddItem(header)
!     if cgi_data.has_key("adminpw"):
!         adminpw = cgi_data["adminpw"]
!         if type(adminpw) is type([]): # admin password specified more than once
!             adminpw = adminpw[0].value # use the first one, i guess
!         else:
!             adminpw = adminpw.value
!         try:
!             list.ConfirmAdminPassword(adminpw)
!             confirmed = 1
!         except (mm_err.MMBadPasswordError):
!             confirmed = 0
!     else:
!         confirmed = 0
!     if not cgi_data.has_key('edit_members') or (cgi_data.has_key('edit_members') and
!                                                 not confirmed):
          container.AddItem(Center('\n<h3>To Unsubscribe Members or Edit their Options...</h3>'))
          container.AddItem(Center("""
          Enter the adminitrative password: <input type=password name=adminpw size=10>
--- 450,456 ----
      header.AddCellInfo(max(header.GetCurrentRowIndex(), 0), 0,
                         colspan=2, bgcolor ="#FFF0D0")
      container.AddItem(header)
!     if front_page:
          container.AddItem(Center('\n<h3>To Unsubscribe Members or Edit their Options...</h3>'))
          container.AddItem(Center("""
          Enter the adminitrative password: <input type=password name=adminpw size=10>
***************
*** 435,465 ****
      else:
          user_table = Table(width="90%")
          digest_user_table = Table(width="90%")
!         user_table.AddRow([Center(Header(4, "Members"))])
          user_table.AddCellInfo(user_table.GetCurrentRowIndex(),
                                 user_table.GetCurrentCellIndex(),
                                 bgcolor="#cccccc", colspan=8)
  
          for member in list.members:
!             cells = [member,
!                      "subscribed " +CheckBox(member + "_subscribed", "on", 1).Format(),
!                      "digest " + CheckBox(member+"_digest", "off").Format(),
!                      ]
!             for opt in ("hide", "nomail", "ack", "norcv", "plain"):
!                 if list.GetUserOption(member, 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)
          for member in list.digest_members:
!             cells = [member,
                       "subscribed " +CheckBox(member + "_subscribed", "on", 1).Format(),
-                      "digest " + CheckBox(member+"_digest", "on", 1).Format(),
                       ]
              for opt in ("hide", "nomail", "ack", "norcv", "plain"):
                  if list.GetUserOption(member, option_info[opt]):
                      value = "on"
--- 463,508 ----
      else:
          user_table = Table(width="90%")
          digest_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)
  
+         members = {}
+         digests = {}
          for member in list.members:
!             members[member] = 1
          for member in list.digest_members:
!             digests[member] = 1
!         all = list.members + list.digest_members
!         if len(all) > mm_cfg.DEFAULT_CHUNK_SIZE:
!             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 list.GetUserOption(member, option_info[opt]):
                      value = "on"
***************
*** 471,483 ****
                  cells.append("%s %s" % (opt, box.Format()))
              user_table.AddRow(cells)
          container.AddItem(Center(user_table))
          # trigger member list viewing again and change_options with hidden tags...
          # password authentication is still done, so this should be safe
!         container.AddItem("<input type=hidden name=edit_member_options value=on>"
!                           "<input type=hidden name=edit_members value=on><p>")
!         container.AddItem("Back to <a href=/mailman/admin/%s/members>Mass Subscribing</a><p>" % \
!                           (list.real_name))
!                           
      return container
  
  def FormatPasswordStuff():
--- 514,529 ----
                  cells.append("%s %s" % (opt, box.Format()))
              user_table.AddRow(cells)
          container.AddItem(Center(user_table))
+         t = Table(width="90%")
+         t.AddRow([Center(Header(4, "<a href=/mailman/admin/%s/members>Back to Mass Subscriptions</a>" % list.real_name))])
+         t.AddCellInfo(t.GetCurrentRowIndex(),
+                       t.GetCurrentCellIndex(),
+                       bgcolor="#cccccc", colspan=8)
+         container.AddItem(Center(t))
+         container.AddItem(footer)
          # trigger member list viewing again and change_options with hidden tags...
          # password authentication is still done, so this should be safe
!         container.AddItem("<input type=hidden name=edit_member_options value=on>")
      return container
  
  def FormatPasswordStuff():
***************
*** 590,620 ****
              if getattr(list, property) != value:
                  setattr(list, property, value)
                  dirty = 1
-     elif cgi_info.has_key('edit_member_options'):
-         members = []
-         digest_members = []
-         org_user_opts = list.user_options.copy()
-         for member in (list.members + list.digest_members)[:]:
-             if not cgi_info.has_key(member + "_subscribed"):
-                 list.DeleteMember(member, "cgi: admin/members")
-                 continue
-             if not cgi_info.has_key(member + "_digest"): # no digest
-                 members.append(member)
-             else:
-                 digest_members.append(member)
-             for opt in ("hide", "nomail", "ack", "norcv", "plain"):
-                 if cgi_info.has_key("%s_%s" % (member, opt)):
-                     list.SetUserOption(member, option_info[opt], 1)
-                 else:
-                     list.SetUserOption(member, option_info[opt], 0)
-             if (list.user_options != org_user_opts):
-                 dirty = 1
-             if list.members != members:
-                 list.members = members
-                 dirty = 1
-             if  list.digest_members != digest_members:
-                 list.digest_members = digest_members
-                 dirty = 1
      elif cgi_info.has_key('subscribees'):
  	name_text = cgi_info['subscribees'].value
          name_text = string.replace(name_text, '\r', '')
--- 636,641 ----
***************
*** 644,649 ****
--- 665,705 ----
              items = map(lambda x: "%s -- %s" % (x[0], x[1]), subscribe_errors)
              document.AddItem(apply(UnorderedList, tuple((items))))
              document.AddItem("<p>")
+     elif 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)):
+                 list.DeleteMember(user)
+                 dirty = 1
+                 continue
+             if not cgi_info.has_key("%s_digest" % (user)):
+                 if user in list.digest_members:
+                     list.digest_members.remove(user)
+                     dirty = 1
+                 if user not in list.members:
+                     list.members.append(user)
+                     dirty = 1
+             else:
+                 if user not in list.digest_members:
+                     list.digest_members.append(user)
+                     dirty = 1
+                 if user in list.members:
+                     list.members.remove(user)
+                     dirty = 1
+                 
+             for opt in ("hide", "nomail", "ack", "norcv", "plain"):
+                 if cgi_info.has_key("%s_%s" % (user, opt)):
+                     list.SetUserOption(user, option_info[opt], 1)
+                     dirty = 1
+                 else:
+                     list.SetUserOption(user, option_info[opt], 0)
+                     dirty = 1
      if cgi_info.has_key('newpw'):
  	if cgi_info.has_key('confirmpw'):
  	    new = cgi_info['newpw'].value
***************
*** 661,667 ****
  	    m = 'Error: You must type in your new password twice.'
  	    document.AddItem(
                  Header(3, Italic(FontAttr(m, color="ff5060"))))
! 
      if dirty:
          list.Save()
  
--- 717,723 ----
  	    m = 'Error: You must type in your new password twice.'
  	    document.AddItem(
                  Header(3, Italic(FontAttr(m, color="ff5060"))))
!             
      if dirty:
          list.Save()
  
Index: modules/mm_defaults.py
===================================================================
RCS file: /usr/local/cvsroot/mailman/modules/mm_defaults.py,v
retrieving revision 1.1.1.1
diff -c -r1.1.1.1 mm_defaults.py
*** mm_defaults.py	1998/04/21 03:27:58	1.1.1.1
--- mm_defaults.py	1998/04/21 15:52:05
***************
*** 37,42 ****
--- 37,48 ----
  HOME_PAGE         = 'index.html'
  MAILMAN_OWNER     = 'mailman-owner@%s' % DEFAULT_HOST_NAME
  
+ #
+ # break up presentation of editing member options under admin/listname/members
+ # into chunks of this size.
+ #
+ DEFAULT_CHUNK_SIZE=20
+ 
  # System ceiling on number of batches into which deliveries are divided:
  MAX_SPAWNS        = 40
  
Index: modules/mm_utils.py
===================================================================
RCS file: /usr/local/cvsroot/mailman/modules/mm_utils.py,v
retrieving revision 1.1.1.1
diff -c -r1.1.1.1 mm_utils.py
*** mm_utils.py	1998/04/21 03:27:58	1.1.1.1
--- mm_utils.py	1998/04/21 08:27:54
***************
*** 394,400 ****
--- 394,418 ----
  		    Logger.write(self, l)
  
  
+ def chunkify(members, chunksize=mm_cfg.DEFAULT_CHUNK_SIZE):
+     """
+     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
  
+         
+         
+         
+         
+