diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/config/schema.cfg | 4 | ||||
| -rw-r--r-- | src/mailman/interfaces/address.py | 6 | ||||
| -rw-r--r-- | src/mailman/model/user.py | 5 | ||||
| -rw-r--r-- | src/mailman/rest/docs/users.txt | 68 | ||||
| -rw-r--r-- | src/mailman/rest/root.py | 6 | ||||
| -rw-r--r-- | src/mailman/rest/users.py | 29 | ||||
| -rw-r--r-- | src/mailman/testing/layers.py | 14 | ||||
| -rw-r--r-- | src/mailman/utilities/datetime.py | 8 | ||||
| -rw-r--r-- | src/mailman/utilities/uid.py | 58 |
9 files changed, 180 insertions, 18 deletions
diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg index 09f575459..188f1771c 100644 --- a/src/mailman/config/schema.cfg +++ b/src/mailman/config/schema.cfg @@ -134,6 +134,10 @@ enabled: no # enabled. This way messages can't be accidentally sent to real addresses. recipient: +# This gets set by the testing layers so that the qrunner subprocesses produce +# predictable dates and times. +testing: no + [passwords] # When Mailman generates them, this is the default length of member passwords. diff --git a/src/mailman/interfaces/address.py b/src/mailman/interfaces/address.py index 391eae849..c051c9b0c 100644 --- a/src/mailman/interfaces/address.py +++ b/src/mailman/interfaces/address.py @@ -40,6 +40,12 @@ from mailman.interfaces.errors import MailmanError class AddressError(MailmanError): """A general address-related error occurred.""" + def __init__(self, address): + self.address = address + + def __str__(self): + return self.address + class ExistingAddressError(AddressError): """The given email address already exists.""" diff --git a/src/mailman/model/user.py b/src/mailman/model/user.py index 05ce356ca..39f4fa240 100644 --- a/src/mailman/model/user.py +++ b/src/mailman/model/user.py @@ -58,7 +58,10 @@ class User(Model): def __init__(self, real_name=None, preferences=None): super(User, self).__init__() self._created_on = date_factory.now() - self._user_id = uid_factory.new_uid() + user_id = uid_factory.new_uid() + assert config.db.store.find(User, _user_id=user_id).count() == 0, ( + 'Duplicate user id {0}'.format(user_id)) + self._user_id = user_id self.real_name = ('' if real_name is None else real_name) self.preferences = preferences config.db.store.add(self) diff --git a/src/mailman/rest/docs/users.txt b/src/mailman/rest/docs/users.txt index 418b07f4c..fcdbdd952 100644 --- a/src/mailman/rest/docs/users.txt +++ b/src/mailman/rest/docs/users.txt @@ -14,3 +14,71 @@ There are no users yet. start: 0 total_size: 0 +When there are users in the database, they can be retrieved as a collection. +:: + + >>> from zope.component import getUtility + >>> from mailman.interfaces.usermanager import IUserManager + >>> user_manager = getUtility(IUserManager) + + >>> anne = user_manager.create_user('anne@example.com', 'Anne Person') + >>> transaction.commit() + >>> dump_json('http://localhost:9001/3.0/users') + entry 0: + created_on: 2005-08-01T07:49:23 + http_etag: "..." + password: None + real_name: Anne Person + user_id: 1 + http_etag: "..." + start: 0 + total_size: 1 + +The user ids match. + + >>> json = call_http('http://localhost:9001/3.0/users') + >>> json['entries'][0]['user_id'] == anne.user_id + True + + +Creating users via the API +========================== + +New users can be created through the REST API. To do so requires the initial +email address for the user, and optionally the user's full name and password. +:: + + >>> transaction.abort() + >>> dump_json('http://localhost:9001/3.0/users', { + ... 'email': 'bart@example.com', + ... 'real_name': 'Bart Person', + ... 'password': 'bbb', + ... }) + content-length: 0 + date: ... + location: http://localhost:9001/3.0/users/2 + server: ... + status: 201 + +The user exists in the database. +:: + + >>> user_manager.get_user('bart@example.com') + <User "Bart Person" (2) at ...> + +It is also available via the location given in the response. + + >>> dump_json('http://localhost:9001/3.0/users/2') + created_on: 2005-08-01T07:49:23 + http_etag: "..." + password: None + real_name: Bart Person + user_id: 2 + +It is of course an error to access a non-existent user id. + + >>> dump_json('http://localhost:9001/3.0/users/99') + Traceback (most recent call last): + ... + HTTPError: HTTP Error 404: 404 Not Found + diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py index 35bd8e12d..3287a6be2 100644 --- a/src/mailman/rest/root.py +++ b/src/mailman/rest/root.py @@ -34,7 +34,7 @@ from mailman.rest.domains import ADomain, AllDomains from mailman.rest.helpers import etag, path_to from mailman.rest.lists import AList, AllLists from mailman.rest.members import AllMembers -from mailman.rest.users import AllUsers +from mailman.rest.users import AUser, AllUsers @@ -115,4 +115,6 @@ class TopLevel(resource.Resource): """/<api>/users""" if len(segments) == 0: return AllUsers() - return http.bad_request() + else: + user_id = segments.pop(0) + return AUser(user_id), segments diff --git a/src/mailman/rest/users.py b/src/mailman/rest/users.py index cf66a6d4e..b8972f3f5 100644 --- a/src/mailman/rest/users.py +++ b/src/mailman/rest/users.py @@ -29,8 +29,10 @@ __all__ = [ from restish import http, resource from zope.component import getUtility +from mailman.interfaces.address import ExistingAddressError from mailman.interfaces.usermanager import IUserManager -from mailman.rest.helpers import CollectionMixin, etag +from mailman.rest.helpers import CollectionMixin, etag, path_to +from mailman.rest.validator import Validator @@ -46,6 +48,7 @@ class _UserBase(resource.Resource, CollectionMixin): real_name=user.real_name, password=user.password, user_id=user.user_id, + created_on=user.created_on, ) def _get_collection(self, request): @@ -63,6 +66,30 @@ class AllUsers(_UserBase): resource = self._make_collection(request) return http.ok([], etag(resource)) + @resource.POST() + def create(self, request): + """Create a new user.""" + try: + validator = Validator(email=unicode, + real_name=unicode, + password=unicode, + _optional=('real_name', 'password')) + arguments = validator(request) + except ValueError as error: + return http.bad_request([], str(error)) + # We can't pass the 'password' argument to the user creation method, + # so strip that out (if it exists), then create the user, adding the + # password after the fact if successful. + password = arguments.pop('password', None) + try: + user = getUtility(IUserManager).create_user(**arguments) + except ExistingAddressError as error: + return http.bad_request([], b'Address already exists {0}'.format( + error.email)) + # XXX ignore password for now. + location = path_to('users/{0}'.format(user.user_id)) + return http.created(location, [], None) + class AUser(_UserBase): diff --git a/src/mailman/testing/layers.py b/src/mailman/testing/layers.py index 2e765ea3e..45311ac91 100644 --- a/src/mailman/testing/layers.py +++ b/src/mailman/testing/layers.py @@ -25,6 +25,7 @@ __all__ = [ 'MockAndMonkeyLayer', 'RESTLayer', 'SMTPLayer', + 'is_testing', ] @@ -67,6 +68,7 @@ class MockAndMonkeyLayer: @classmethod def testTearDown(cls): + print >> sys.stderr, 'testTearDown' for reset in cls._resets: reset() @@ -106,6 +108,8 @@ class ConfigLayer(MockAndMonkeyLayer): layout: testing [paths.testing] var_dir: %s + [devmode] + testing: yes """ % cls.var_dir) # Read the testing config and push it. test_config += resource_string('mailman.testing', 'testing.cfg') @@ -288,3 +292,13 @@ class RESTLayer(SMTPLayer): assert cls.server is not None, 'Layer not set up' cls.server.stop() cls.server = None + + + +def is_testing(): + """Return a 'testing' flag for use with the predictable factories. + + :return: True when in testing mode. + :rtype: bool + """ + return MockAndMonkeyLayer.testing_mode or config.devmode.testing diff --git a/src/mailman/utilities/datetime.py b/src/mailman/utilities/datetime.py index 9dcd21f1e..1ee727da4 100644 --- a/src/mailman/utilities/datetime.py +++ b/src/mailman/utilities/datetime.py @@ -36,7 +36,7 @@ __all__ = [ import datetime -from mailman.testing.layers import MockAndMonkeyLayer +from mailman.testing import layers @@ -51,12 +51,12 @@ class DateFactory: # We can't automatically fast-forward because some tests require us to # stay on the same day for a while, e.g. autorespond.txt. return (self.predictable_now - if MockAndMonkeyLayer.testing_mode + if layers.is_testing() else datetime.datetime.now(tz)) def today(self): return (self.predictable_today - if MockAndMonkeyLayer.testing_mode + if layers.is_testing() else datetime.date.today()) @classmethod @@ -74,4 +74,4 @@ factory = DateFactory() factory.reset() today = factory.today now = factory.now -MockAndMonkeyLayer.register_reset(factory.reset) +layers.MockAndMonkeyLayer.register_reset(factory.reset) diff --git a/src/mailman/utilities/uid.py b/src/mailman/utilities/uid.py index cc2cf3b12..6fceeb606 100644 --- a/src/mailman/utilities/uid.py +++ b/src/mailman/utilities/uid.py @@ -32,9 +32,13 @@ __all__ = [ import os import time +import errno import hashlib -from mailman.testing.layers import MockAndMonkeyLayer +from flufl.lock import Lock + +from mailman.config import config +from mailman.testing import layers from mailman.utilities.passwords import SALT_LENGTH @@ -42,14 +46,31 @@ from mailman.utilities.passwords import SALT_LENGTH class UniqueIDFactory: """A factory for unique ids.""" - # The predictable id. - predictable_id = None + def __init__(self): + # We can't call reset() when the factory is created below, because + # config.VAR_DIR will not be set at that time. So initialize it at + # the first use. + self._uid_file = None + self._lock_file = None + self._lock = None def new_uid(self, bytes=None): - if MockAndMonkeyLayer.testing_mode: - uid = self.predictable_id - self.predictable_id += 1 - return unicode(uid) + if layers.is_testing(): + if self._lock is None: + # These will get automatically cleaned up by the test + # infrastructure. + self._uid_file = os.path.join(config.VAR_DIR, '.uid') + self._lock_file = self._uid_file + '.lock' + self._lock = Lock(self._lock_file) + # When in testing mode we want to produce predictable id, but we + # need to coordinate this among separate processes. We could use + # the database, but I don't want to add schema just to handle this + # case, and besides transactions could get aborted, causing some + # ids to be recycled. So we'll use a data file with a lock. This + # may still not be ideal due to race conditions, but I think the + # tests will be serialized enough (and the ids reset between + # tests) that it will not be a problem. Maybe. + return self._next_uid() salt = os.urandom(SALT_LENGTH) h = hashlib.sha1(repr(time.time())) h.update(salt) @@ -57,11 +78,28 @@ class UniqueIDFactory: h.update(bytes) return unicode(h.hexdigest(), 'us-ascii') + def _next_uid(self): + with self._lock: + try: + with open(self._uid_file) as fp: + uid = fp.read().strip() + next_uid = int(uid) + 1 + with open(self._uid_file, 'w') as fp: + fp.write(str(next_uid)) + except IOError as error: + if error.errno != errno.ENOENT: + raise + with open(self._uid_file, 'w') as fp: + fp.write('2') + return '1' + return unicode(uid, 'us-ascii') + def reset(self): - self.predictable_id = 1 + with self._lock: + with open(self._uid_file, 'w') as fp: + fp.write('1') factory = UniqueIDFactory() -factory.reset() -MockAndMonkeyLayer.register_reset(factory.reset) +layers.MockAndMonkeyLayer.register_reset(factory.reset) |
