summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Mailman/database/__init__.py6
-rw-r--r--Mailman/database/model/__init__.py1
-rw-r--r--Mailman/database/model/pending.py2
-rw-r--r--Mailman/database/model/requests.py121
-rw-r--r--Mailman/docs/requests.txt149
-rw-r--r--Mailman/interfaces/__init__.py7
-rw-r--r--Mailman/interfaces/pending.py8
-rw-r--r--Mailman/interfaces/requests.py87
8 files changed, 377 insertions, 4 deletions
diff --git a/Mailman/database/__init__.py b/Mailman/database/__init__.py
index db105e21d..9c3cf39ea 100644
--- a/Mailman/database/__init__.py
+++ b/Mailman/database/__init__.py
@@ -33,8 +33,10 @@ from Mailman.database.listmanager import ListManager
from Mailman.database.usermanager import UserManager
from Mailman.database.messagestore import MessageStore
from Mailman.database.model import Pendings
+from Mailman.database.model import Requests
-# Test suite convenience.
+# Test suite convenience. Application code should use config.db.flush()
+# instead.
flush = None
@@ -51,6 +53,7 @@ class StockDatabase:
self.user_manager = None
self.message_store = None
self.pendings = None
+ self.requests = None
def initialize(self):
from Mailman.LockFile import LockFile
@@ -65,6 +68,7 @@ class StockDatabase:
self.user_manager = UserManager()
self.message_store = MessageStore()
self.pendings = Pendings()
+ self.requests = Requests()
self.flush()
def flush(self):
diff --git a/Mailman/database/model/__init__.py b/Mailman/database/model/__init__.py
index 2da97f63d..ed91fe018 100644
--- a/Mailman/database/model/__init__.py
+++ b/Mailman/database/model/__init__.py
@@ -48,6 +48,7 @@ from Mailman.database.model.member import Member
from Mailman.database.model.message import Message
from Mailman.database.model.pending import Pendings
from Mailman.database.model.preferences import Preferences
+from Mailman.database.model.requests import Requests
from Mailman.database.model.user import User
from Mailman.database.model.version import Version
diff --git a/Mailman/database/model/pending.py b/Mailman/database/model/pending.py
index 0ac438535..430f31937 100644
--- a/Mailman/database/model/pending.py
+++ b/Mailman/database/model/pending.py
@@ -15,6 +15,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
+"""Implementations of the IPendable and IPending interfaces."""
+
import time
import random
import hashlib
diff --git a/Mailman/database/model/requests.py b/Mailman/database/model/requests.py
new file mode 100644
index 000000000..59013452b
--- /dev/null
+++ b/Mailman/database/model/requests.py
@@ -0,0 +1,121 @@
+# Copyright (C) 2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""Implementations of the IRequests and IListRequests interfaces."""
+
+from datetime import timedelta
+from elixir import *
+from zope.interface import implements
+
+from Mailman.configuration import config
+from Mailman.database.types import EnumType
+from Mailman.interfaces import IListRequests, IPendable, IRequests, RequestType
+
+
+MAILINGLIST_KIND = 'Mailman.database.model.mailinglist.MailingList'
+
+
+__metaclass__ = type
+__all__ = [
+ 'Requests',
+ ]
+
+
+
+class DataPendable(dict):
+ implements(IPendable)
+
+
+
+class ListRequests:
+ implements(IListRequests)
+
+ def __init__(self, mailing_list):
+ self.mailing_list = mailing_list
+
+ @property
+ def count(self):
+ results = _Request.select_by(mailing_list=self.mailing_list._data)
+ return len(results)
+
+ @property
+ def held_requests(self):
+ results = _Request.select_by(mailing_list=self.mailing_list._data)
+ for request in results:
+ yield request.id, request.type
+
+ def hold_request(self, request_type, key, data=None):
+ if request_type not in RequestType:
+ raise TypeError(request_type)
+ if data is None:
+ data_hash = None
+ else:
+ # We're abusing the pending database as a way of storing arbitrary
+ # key/value pairs, where both are strings. This isn't ideal but
+ # it lets us get auxiliary data almost for free. We may need to
+ # lock this down more later.
+ pendable = DataPendable()
+ pendable.update(data)
+ token = config.db.pendings.add(pendable, timedelta(days=5000))
+ data_hash = token
+ result = _Request(key=key, type=request_type,
+ mailing_list=self.mailing_list._data,
+ data_hash=data_hash)
+ # XXX We need a handle on last_inserted_ids() instead of requiring a
+ # flush of the database to get a valid id.
+ config.db.flush()
+ return result.id
+
+ def get_request(self, request_id):
+ result = _Request.get(request_id)
+ if result is None:
+ return None
+ if result.data_hash is None:
+ return result.key, result.data_hash
+ pendable = config.db.pendings.confirm(result.data_hash, expunge=False)
+ data = dict()
+ data.update(pendable)
+ return result.key, data
+
+ def delete_request(self, request_id):
+ result = _Request.get(request_id)
+ if result is None:
+ raise KeyError(request_id)
+ # Throw away the pended data.
+ config.db.pendings.confirm(result.data_hash)
+ result.delete()
+
+
+
+class Requests:
+ implements(IRequests)
+
+ def get_list_requests(self, mailing_list):
+ return ListRequests(mailing_list)
+
+
+
+class _Request(Entity):
+ """Table for mailing list hold requests."""
+
+ has_field('key', Unicode)
+ has_field('type', EnumType)
+ has_field('data_hash', Unicode)
+ # Relationships
+ belongs_to('mailing_list', of_kind=MAILINGLIST_KIND)
+ # Options
+ using_options(shortnames=True)
diff --git a/Mailman/docs/requests.txt b/Mailman/docs/requests.txt
new file mode 100644
index 000000000..09bce6e3c
--- /dev/null
+++ b/Mailman/docs/requests.txt
@@ -0,0 +1,149 @@
+Held requests
+=============
+
+Various actions will be held for moderator approval, such as subscriptions to
+closed lists, or postings by non-members. The requests database is the low
+level interface to these actions requiring approval.
+
+ >>> from Mailman.configuration import config
+ >>> from Mailman.database import flush
+
+Here is a helper function for printing out held requests.
+
+ >>> def show_holds(requests):
+ ... for request in requests.held_requests:
+ ... print request[0], str(request[1])
+
+
+Mailing list centric
+--------------------
+
+A set of requests are always centric to a particular mailing list, so given a
+mailing list you need to get its requests object.
+
+ >>> from Mailman.interfaces import IListRequests, IRequests
+ >>> from zope.interface.verify import verifyObject
+ >>> verifyObject(IRequests, config.db.requests)
+ True
+ >>> mlist = config.db.list_manager.create('test@example.com')
+ >>> flush()
+ >>> requests = config.db.requests.get_list_requests(mlist)
+ >>> verifyObject(IListRequests, requests)
+ True
+ >>> requests.mailing_list
+ <mailing list "test@example.com" (unlocked) at ...>
+
+
+Holding requests
+----------------
+
+The list's requests database starts out empty.
+
+ >>> requests.count
+ 0
+ >>> list(requests.held_requests)
+ []
+
+At the lowest level, the requests database is very simple. Holding a request
+requires a request type (as an enum value), a key, and an optional dictionary
+of associated data. The request database assigns no semantics to the held
+data, except for the request type. Here we hold some simple bits of data.
+
+ >>> from Mailman.interfaces import RequestType
+ >>> id_1 = requests.hold_request(RequestType.held_message, 'hold_1')
+ >>> id_2 = requests.hold_request(RequestType.subscription, 'hold_2')
+ >>> id_3 = requests.hold_request(RequestType.unsubscription, 'hold_3')
+ >>> id_4 = requests.hold_request(RequestType.held_message, 'hold_4')
+ >>> id_1, id_2, id_3, id_4
+ (1, 2, 3, 4)
+ >>> flush()
+
+And of course, now we can see that there are four requests being held.
+
+ >>> requests.count
+ 4
+ >>> show_holds(requests)
+ 1 RequestType.held_message
+ 2 RequestType.subscription
+ 3 RequestType.unsubscription
+ 4 RequestType.held_message
+
+If we try to hold a request with a bogus type, we get an exception.
+
+ >>> requests.hold_request(5, 'foo')
+ Traceback (most recent call last):
+ ...
+ TypeError: 5
+
+We can hold requests with additional data.
+
+ >>> data = dict(foo='yes', bar='no')
+ >>> id_5 = requests.hold_request(RequestType.held_message, 'hold_5', data)
+ >>> flush()
+ >>> id_5
+ 5
+ >>> requests.count
+ 5
+ >>> show_holds(requests)
+ 1 RequestType.held_message
+ 2 RequestType.subscription
+ 3 RequestType.unsubscription
+ 4 RequestType.held_message
+ 5 RequestType.held_message
+
+
+Getting requests
+----------------
+
+We can ask the requests database for a specific request, by providing the id
+of the request data we want. This returns a 2-tuple of the key and data we
+originally held.
+
+ >>> key, data = requests.get_request(2)
+ >>> key
+ 'hold_2'
+
+Because we did not store additional data with request 2, it comes back as None
+now.
+
+ >>> print data
+ None
+
+However, if we ask for a request that had data, we'd get it back now.
+
+ >>> key, data = requests.get_request(5)
+ >>> key
+ 'hold_5'
+ >>> sorted(data.items())
+ [('bar', 'no'), ('foo', 'yes')]
+
+If we ask for a request that is not in the database, we get None back.
+
+ >>> print requests.get_request(801)
+ None
+
+
+Deleting requests
+-----------------
+
+Once a specific request has been handled, it will be deleted from the requests
+database.
+
+ >>> requests.delete_request(2)
+ >>> flush()
+ >>> requests.count
+ 4
+ >>> show_holds(requests)
+ 1 RequestType.held_message
+ 3 RequestType.unsubscription
+ 4 RequestType.held_message
+ 5 RequestType.held_message
+ >>> print requests.get_request(2)
+ None
+
+We get an exception if we ask to delete a request that isn't in the database.
+
+ >>> requests.delete_request(801)
+ Traceback (most recent call last):
+ ...
+ KeyError: 801
diff --git a/Mailman/interfaces/__init__.py b/Mailman/interfaces/__init__.py
index e04ad1e78..7e8a3e123 100644
--- a/Mailman/interfaces/__init__.py
+++ b/Mailman/interfaces/__init__.py
@@ -18,6 +18,7 @@
import os
import sys
+from munepy import Enum
from zope.interface import implementedBy
from zope.interface.interfaces import IInterface
@@ -38,7 +39,11 @@ def _populate():
module = sys.modules[modname]
for name in dir(module):
obj = getattr(module, name)
- if IInterface.providedBy(obj):
+ try:
+ is_enum = issubclass(obj, Enum)
+ except TypeError:
+ is_enum = False
+ if IInterface.providedBy(obj) or is_enum:
setattr(iface_mod, name, obj)
__all__.append(name)
diff --git a/Mailman/interfaces/pending.py b/Mailman/interfaces/pending.py
index 5e26fbf6e..68a4c41de 100644
--- a/Mailman/interfaces/pending.py
+++ b/Mailman/interfaces/pending.py
@@ -15,9 +15,13 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-"""Interfaces for the pending database."""
+"""Interfaces for the pending database.
+
+The pending database contains events that must be confirmed by the user. It
+maps these events to a unique hash that can be used as a token for end user
+confirmation.
+"""
-from munepy import Enum
from zope.interface import Interface, Attribute
diff --git a/Mailman/interfaces/requests.py b/Mailman/interfaces/requests.py
new file mode 100644
index 000000000..0a817d43c
--- /dev/null
+++ b/Mailman/interfaces/requests.py
@@ -0,0 +1,87 @@
+# Copyright (C) 2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""Interfaces for the request database.
+
+The request database handles events that must be approved by the list
+moderators, such as subscription requests and held messages.
+"""
+
+from munepy import Enum
+from zope.interface import Interface, Attribute
+
+
+
+class RequestType(Enum):
+ held_message = 1
+ subscription = 2
+ unsubscription = 3
+
+
+
+class IListRequests(Interface):
+ """Held requests for a specific mailing list."""
+
+ mailing_list = Attribute(
+ """The IMailingList for these requests.""")
+
+ count = Attribute(
+ """The total number of requests held for the mailing list.""")
+
+ def hold_request(request_type, key, data=None):
+ """Hold some data for moderator approval.
+
+ :param request_type: A `Request` enum value.
+ :param key: The key piece of request data being held.
+ :param data: Additional optional data in the form of a dictionary that
+ is associated with the held request.
+ :return: A unique id for this held request.
+ """
+
+ held_requests = Attribute(
+ """An iterator over the held requests, yielding a 2-tuple.
+
+ The tuple has the form: (id, type) where `id` is the held request's
+ unique id and the `type` is a `Request` enum value.
+ """)
+
+ def get_request(request_id):
+ """Get the data associated with the request id, or None.
+
+ :param request_id: The unique id for the request.
+ :return: A 2-tuple of the key and data originally held, or None if the
+ `request_id` is not in the database.
+ """
+
+ def delete_request(request_id):
+ """Delete the request associated with the id.
+
+ :param request_id: The unique id for the request.
+ :raises KeyError: If `request_id` is not in the database.
+ """
+
+
+
+class IRequests(Interface):
+ """The requests database."""
+
+ def get_list_requests(mailing_list):
+ """Return the `IListRequests` object for the given mailing list.
+
+ :param mailing_list: An `IMailingList`.
+ :return: An `IListRequests` object for the mailing list.
+ """