diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/model/address.py | 2 | ||||
| -rw-r--r-- | src/mailman/model/docs/addresses.txt | 25 | ||||
| -rw-r--r-- | src/mailman/rest/addresses.py | 113 | ||||
| -rw-r--r-- | src/mailman/rest/docs/addresses.txt | 134 | ||||
| -rw-r--r-- | src/mailman/rest/docs/users.txt | 81 | ||||
| -rw-r--r-- | src/mailman/rest/root.py | 13 | ||||
| -rw-r--r-- | src/mailman/rest/users.py | 52 |
7 files changed, 333 insertions, 87 deletions
diff --git a/src/mailman/model/address.py b/src/mailman/model/address.py index 92f7f8986..0ba0f47d5 100644 --- a/src/mailman/model/address.py +++ b/src/mailman/model/address.py @@ -31,6 +31,7 @@ from zope.interface import implements from mailman.database.model import Model from mailman.interfaces.address import IAddress +from mailman.utilities.datetime import now @@ -55,6 +56,7 @@ class Address(Model): self.email = lower_case self.real_name = real_name self._original = (None if lower_case == email else email) + self.registered_on = now() def __str__(self): addr = (self.email if self._original is None else self._original) diff --git a/src/mailman/model/docs/addresses.txt b/src/mailman/model/docs/addresses.txt index ffbb897ab..01e68c954 100644 --- a/src/mailman/model/docs/addresses.txt +++ b/src/mailman/model/docs/addresses.txt @@ -126,31 +126,28 @@ Registration and validation =========================== Addresses have two dates, the date the address was registered on and the date -the address was validated on. Neither date is set by default. +the address was validated on. The former is set when the address is created, +but the latter must be set explicitly. >>> address_4 = user_manager.create_address( ... 'dperson@example.com', 'Dan Person') >>> print address_4.registered_on - None + 2005-08-01 07:49:23 >>> print address_4.verified_on None -The registered date takes a Python datetime object. +The verification date records when the user has completed a mail-back +verification procedure. It takes a datetime object. - >>> from datetime import datetime - >>> address_4.registered_on = datetime(2007, 5, 8, 22, 54, 1) - >>> print address_4.registered_on - 2007-05-08 22:54:01 + >>> from mailman.utilities.datetime import now + >>> address_4.verified_on = now() >>> print address_4.verified_on - None + 2005-08-01 07:49:23 -And of course, you can also set the validation date. +The address shows the verified status in its repr. - >>> address_4.verified_on = datetime(2007, 5, 13, 22, 54, 1) - >>> print address_4.registered_on - 2007-05-08 22:54:01 - >>> print address_4.verified_on - 2007-05-13 22:54:01 + >>> address_4 + <Address: Dan Person <dperson@example.com> [verified] at ...> Case-preserved addresses diff --git a/src/mailman/rest/addresses.py b/src/mailman/rest/addresses.py new file mode 100644 index 000000000..d7fae3b9b --- /dev/null +++ b/src/mailman/rest/addresses.py @@ -0,0 +1,113 @@ +# Copyright (C) 2011 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman 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 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman 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 +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""REST for addresses.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'AllAddresses', + 'AnAddress', + 'UserAddresses', + ] + + +from operator import attrgetter +from restish import http, resource +from zope.component import getUtility + +from mailman.rest.helpers import CollectionMixin, etag, path_to +from mailman.interfaces.usermanager import IUserManager + + + +class _AddressBase(resource.Resource, CollectionMixin): + """Shared base class for address representations.""" + + def _resource_as_dict(self, address): + """See `CollectionMixin`.""" + # The canonical url for an address is its lower-cased version, + # although it can be looked up with either its original or lower-cased + # email address. + representation = dict( + email=address.email, + original_email=address.original_email, + registered_on=address.registered_on, + self_link=path_to('addresses/{0}'.format(address.email)), + ) + # Add optional attributes. These can be None or the empty string. + if address.real_name: + representation['real_name'] = address.real_name + if address.verified_on: + representation['verified_on'] = address.verified_on + return representation + + def _get_collection(self, request): + """See `CollectionMixin`.""" + return list(getUtility(IUserManager).addresses) + + + +class AllAddresses(_AddressBase): + """The addresses.""" + + @resource.GET() + def collection(self, request): + """/addresses""" + resource = self._make_collection(request) + return http.ok([], etag(resource)) + + + +class AnAddress(_AddressBase): + """An address.""" + + def __init__(self, email): + """Get an address by either its original or lower-cased email. + + :param email: The email address of the `IAddress`. + :type email: string + """ + self._address = getUtility(IUserManager).get_address(email) + + @resource.GET() + def address(self, request): + """Return a single address.""" + if self._address is None: + return http.not_found() + return http.ok([], self._resource_as_json(self._address)) + + + +class UserAddresses(_AddressBase): + """The addresses of a user.""" + + def __init__(self, user): + self._user = user + super(UserAddresses, self).__init__() + + def _get_collection(self, request): + """See `CollectionMixin`.""" + return sorted(self._user.addresses, + key=attrgetter('original_email')) + + @resource.GET() + def collection(self, request): + """/addresses""" + resource = self._make_collection(request) + return http.ok([], etag(resource)) diff --git a/src/mailman/rest/docs/addresses.txt b/src/mailman/rest/docs/addresses.txt new file mode 100644 index 000000000..dbd1ecf86 --- /dev/null +++ b/src/mailman/rest/docs/addresses.txt @@ -0,0 +1,134 @@ +========= +Addresses +========= + +The REST API can be used to manage addresses. + +There are no addresses yet. + + >>> dump_json('http://localhost:9001/3.0/addresses') + http_etag: "..." + start: 0 + total_size: 0 + +When an address is created via the internal API, it is available in the REST +API. +:: + + >>> from zope.component import getUtility + >>> from mailman.interfaces.usermanager import IUserManager + >>> user_manager = getUtility(IUserManager) + >>> anne = user_manager.create_address('anne@example.com') + >>> transaction.commit() + + >>> dump_json('http://localhost:9001/3.0/addresses') + entry 0: + email: anne@example.com + http_etag: "..." + original_email: anne@example.com + registered_on: 2005-08-01T07:49:23 + self_link: http://localhost:9001/3.0/addresses/anne@example.com + http_etag: "..." + start: 0 + total_size: 1 + +Anne's address can also be accessed directly. + + >>> dump_json('http://localhost:9001/3.0/addresses/anne@example.com') + email: anne@example.com + http_etag: "..." + original_email: anne@example.com + registered_on: 2005-08-01T07:49:23 + self_link: http://localhost:9001/3.0/addresses/anne@example.com + +Bart registers with a mixed-case address. The canonical URL always includes +the lower-case version. + + >>> bart = user_manager.create_address('Bart.Person@example.com') + >>> transaction.commit() + >>> dump_json( + ... 'http://localhost:9001/3.0/addresses/bart.person@example.com') + email: bart.person@example.com + http_etag: "..." + original_email: Bart.Person@example.com + registered_on: 2005-08-01T07:49:23 + self_link: http://localhost:9001/3.0/addresses/bart.person@example.com + +But his address record can be accessed with the case-preserved version too. + + >>> dump_json( + ... 'http://localhost:9001/3.0/addresses/Bart.Person@example.com') + email: bart.person@example.com + http_etag: "..." + original_email: Bart.Person@example.com + registered_on: 2005-08-01T07:49:23 + self_link: http://localhost:9001/3.0/addresses/bart.person@example.com + +A non-existent email address can't be retrieved. + + >>> dump_json('http://localhost:9001/3.0/addresses/nobody@example.com') + Traceback (most recent call last): + ... + HTTPError: HTTP Error 404: 404 Not Found + +When an address has a real name associated with it, this is also available in +the REST API. + + >>> cris = user_manager.create_address('cris@example.com', 'Cris Person') + >>> transaction.commit() + >>> dump_json('http://localhost:9001/3.0/addresses/cris@example.com') + email: cris@example.com + http_etag: "..." + original_email: cris@example.com + real_name: Cris Person + registered_on: 2005-08-01T07:49:23 + self_link: http://localhost:9001/3.0/addresses/cris@example.com + + +Verifying +========= + +When the address gets verified, this attribute is available in the REST +representation. +:: + + >>> from mailman.utilities.datetime import now + >>> anne.verified_on = now() + >>> transaction.commit() + >>> dump_json('http://localhost:9001/3.0/addresses/anne@example.com') + email: anne@example.com + http_etag: "..." + original_email: anne@example.com + registered_on: 2005-08-01T07:49:23 + self_link: http://localhost:9001/3.0/addresses/anne@example.com + verified_on: 2005-08-01T07:49:23 + + +User addresses +============== + +Users control addresses. The canonical URLs for these user-controlled +addresses live in the /addresses namespace. +:: + + >>> dave = user_manager.create_user('dave@example.com', 'Dave Person') + >>> transaction.commit() + >>> dump_json('http://localhost:9001/3.0/users/dave@example.com/addresses') + entry 0: + email: dave@example.com + http_etag: "..." + original_email: dave@example.com + real_name: Dave Person + registered_on: 2005-08-01T07:49:23 + self_link: http://localhost:9001/3.0/addresses/dave@example.com + http_etag: "..." + start: 0 + total_size: 1 + + >>> dump_json('http://localhost:9001/3.0/addresses/dave@example.com') + email: dave@example.com + http_etag: "..." + original_email: dave@example.com + real_name: Dave Person + registered_on: 2005-08-01T07:49:23 + self_link: http://localhost:9001/3.0/addresses/dave@example.com diff --git a/src/mailman/rest/docs/users.txt b/src/mailman/rest/docs/users.txt index 4a73ab8d8..114ca4e49 100644 --- a/src/mailman/rest/docs/users.txt +++ b/src/mailman/rest/docs/users.txt @@ -3,7 +3,7 @@ Users ===== The REST API can be used to add and remove users, add and remove user -addresses, and change their preferred address, passord, or name. Users are +addresses, and change their preferred address, password, or name. Users are different than members; the latter represents an email address subscribed to a specific mailing list. Users are just people that Mailman knows about. @@ -27,7 +27,6 @@ When there are users in the database, they can be retrieved as a collection. entry 0: created_on: 2005-08-01T07:49:23 http_etag: "..." - password: None real_name: Anne Person self_link: http://localhost:9001/3.0/users/1 user_id: 1 @@ -41,6 +40,27 @@ The user ids match. >>> json['entries'][0]['user_id'] == anne.user_id True +A user might not have a real name, in which case, the attribute will not be +returned in the REST API. + + >>> dave = user_manager.create_user('dave@example.com') + >>> transaction.commit() + >>> dump_json('http://localhost:9001/3.0/users') + entry 0: + created_on: 2005-08-01T07:49:23 + http_etag: "..." + real_name: Anne Person + self_link: http://localhost:9001/3.0/users/1 + user_id: 1 + entry 1: + created_on: 2005-08-01T07:49:23 + http_etag: "..." + self_link: http://localhost:9001/3.0/users/2 + user_id: 2 + http_etag: "..." + start: 0 + total_size: 2 + Creating users via the API ========================== @@ -57,7 +77,7 @@ email address for the user, and optionally the user's full name and password. ... }) content-length: 0 date: ... - location: http://localhost:9001/3.0/users/2 + location: http://localhost:9001/3.0/users/3 server: ... status: 201 @@ -66,17 +86,17 @@ The user exists in the database. >>> bart = user_manager.get_user('bart@example.com') >>> bart - <User "Bart Person" (2) at ...> + <User "Bart Person" (3) at ...> It is also available via the location given in the response. - >>> dump_json('http://localhost:9001/3.0/users/2') + >>> dump_json('http://localhost:9001/3.0/users/3') created_on: 2005-08-01T07:49:23 http_etag: "..." password: {CLEARTEXT}bbb real_name: Bart Person - self_link: http://localhost:9001/3.0/users/2 - user_id: 2 + self_link: http://localhost:9001/3.0/users/3 + user_id: 3 Because email addresses just have an ``@`` sign in then, there's no confusing them with user ids. Thus, a user can be retrieved via its email address. @@ -86,8 +106,8 @@ them with user ids. Thus, a user can be retrieved via its email address. http_etag: "..." password: {CLEARTEXT}bbb real_name: Bart Person - self_link: http://localhost:9001/3.0/users/2 - user_id: 2 + self_link: http://localhost:9001/3.0/users/3 + user_id: 3 Users can be created without a password. A *user friendly* password will be assigned to them automatically, but this password will be encrypted and @@ -101,17 +121,17 @@ therefore cannot be retrieved. It can be reset though. ... }) content-length: 0 date: ... - location: http://localhost:9001/3.0/users/3 + location: http://localhost:9001/3.0/users/4 server: ... status: 201 - >>> dump_json('http://localhost:9001/3.0/users/3') + >>> dump_json('http://localhost:9001/3.0/users/4') created_on: 2005-08-01T07:49:23 http_etag: "..." password: {CLEARTEXT}... real_name: Cris Person - self_link: http://localhost:9001/3.0/users/3 - user_id: 3 + self_link: http://localhost:9001/3.0/users/4 + user_id: 4 Missing users @@ -152,40 +172,33 @@ sorted in lexical order by original (i.e. case-preserved) email address. key: bart.q.person@example.com at ...> >>> transaction.commit() - >>> dump_json('http://localhost:9001/3.0/users/2/addresses') + >>> dump_json('http://localhost:9001/3.0/users/3/addresses') entry 0: email: bart.q.person@example.com http_etag: "..." original_email: Bart.Q.Person@example.com - real_name: - registered_on: None + registered_on: 2005-08-01T07:49:23 self_link: - http://localhost:9001/3.0/addresses/Bart.Q.Person@example.com - verified_on: None + http://localhost:9001/3.0/addresses/bart.q.person@example.com entry 1: email: bart.person@example.com http_etag: "..." original_email: bart.person@example.com - real_name: - registered_on: None + registered_on: 2005-08-01T07:49:23 self_link: http://localhost:9001/3.0/addresses/bart.person@example.com - verified_on: None entry 2: email: bart@example.com http_etag: "..." original_email: bart@example.com real_name: Bart Person - registered_on: None + registered_on: 2005-08-01T07:49:23 self_link: http://localhost:9001/3.0/addresses/bart@example.com - verified_on: None entry 3: email: bperson@example.com http_etag: "..." original_email: bperson@example.com - real_name: - registered_on: None + registered_on: 2005-08-01T07:49:23 self_link: http://localhost:9001/3.0/addresses/bperson@example.com - verified_on: None http_etag: "..." start: 0 total_size: 4 @@ -198,29 +211,29 @@ In fact, any of these addresses can be used to look up Bart's user record. http_etag: "..." password: {CLEARTEXT}bbb real_name: Bart Person - self_link: http://localhost:9001/3.0/users/2 - user_id: 2 + self_link: http://localhost:9001/3.0/users/3 + user_id: 3 >>> dump_json('http://localhost:9001/3.0/users/bart.person@example.com') created_on: 2005-08-01T07:49:23 http_etag: "..." password: {CLEARTEXT}bbb real_name: Bart Person - self_link: http://localhost:9001/3.0/users/2 - user_id: 2 + self_link: http://localhost:9001/3.0/users/3 + user_id: 3 >>> dump_json('http://localhost:9001/3.0/users/bperson@example.com') created_on: 2005-08-01T07:49:23 http_etag: "..." password: {CLEARTEXT}bbb real_name: Bart Person - self_link: http://localhost:9001/3.0/users/2 - user_id: 2 + self_link: http://localhost:9001/3.0/users/3 + user_id: 3 >>> dump_json('http://localhost:9001/3.0/users/Bart.Q.Person@example.com') created_on: 2005-08-01T07:49:23 http_etag: "..." password: {CLEARTEXT}bbb real_name: Bart Person - self_link: http://localhost:9001/3.0/users/2 - user_id: 2 + self_link: http://localhost:9001/3.0/users/3 + user_id: 3 diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py index 3287a6be2..99f28cbb2 100644 --- a/src/mailman/rest/root.py +++ b/src/mailman/rest/root.py @@ -30,6 +30,7 @@ from restish import guard, http, resource from mailman.config import config from mailman.core.system import system +from mailman.rest.addresses import AllAddresses, AnAddress from mailman.rest.domains import ADomain, AllDomains from mailman.rest.helpers import etag, path_to from mailman.rest.lists import AList, AllLists @@ -80,6 +81,18 @@ class TopLevel(resource.Resource): return http.ok([], etag(resource)) @resource.child() + def addresses(self, request, segments): + """/<api>/addresses + /<api>/addresses/<email> + """ + if len(segments) == 0: + return AllAddresses() + elif len(segments) == 1: + return AnAddress(segments[0]), [] + else: + return http.bad_request() + + @resource.child() def domains(self, request, segments): """/<api>/domains /<api>/domains/<domain> diff --git a/src/mailman/rest/users.py b/src/mailman/rest/users.py index 14ed04316..a75b448e7 100644 --- a/src/mailman/rest/users.py +++ b/src/mailman/rest/users.py @@ -26,12 +26,12 @@ __all__ = [ ] -from operator import attrgetter 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.addresses import UserAddresses from mailman.rest.helpers import CollectionMixin, etag, path_to from mailman.rest.validator import Validator from mailman.utilities.passwords import ( @@ -44,16 +44,21 @@ class _UserBase(resource.Resource, CollectionMixin): def _resource_as_dict(self, user): """See `CollectionMixin`.""" - # The canonical URL for a user is their preferred email address, - # although we can always look up a user based on any registered and - # validated email address associated with their account. - return dict( - real_name=user.real_name, - password=user.password, + # The canonical URL for a user is their unique user id, although we + # can always look up a user based on any registered and validated + # email address associated with their account. + resource = dict( user_id=user.user_id, created_on=user.created_on, self_link=path_to('users/{0}'.format(user.user_id)), ) + # Add the password attribute, only if the user has a password. Same + # with the real name. These could be None or the empty string. + if user.password: + resource['password'] = user.password + if user.real_name: + resource['real_name'] = user.real_name + return resource def _get_collection(self, request): """See `CollectionMixin`.""" @@ -128,35 +133,4 @@ class AUser(_UserBase): @resource.child() def addresses(self, request, segments): """/users/<uid>/addresses""" - return _AllUserAddresses(self._user) - - - -class _AllUserAddresses(resource.Resource, CollectionMixin): - """All addresses that a user controls.""" - - def __init__(self, user): - self._user = user - super(_AllUserAddresses, self).__init__() - - def _resource_as_dict(self, address): - """See `CollectionMixin`.""" - return dict( - email=address.email, - original_email=address.original_email, - real_name=address.real_name, - registered_on=address.registered_on, - self_link=path_to('addresses/{0}'.format(address.original_email)), - verified_on=address.verified_on, - ) - - def _get_collection(self, request): - """See `CollectionMixin`.""" - return sorted(self._user.addresses, - key=attrgetter('original_email')) - - @resource.GET() - def collection(self, request): - """/addresses""" - resource = self._make_collection(request) - return http.ok([], etag(resource)) + return UserAddresses(self._user) |
