diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/rest/docs/lists.txt | 6 | ||||
| -rw-r--r-- | src/mailman/rest/docs/membership.txt | 33 | ||||
| -rw-r--r-- | src/mailman/rest/domains.py | 8 | ||||
| -rw-r--r-- | src/mailman/rest/lists.py | 140 | ||||
| -rw-r--r-- | src/mailman/rest/members.py | 138 | ||||
| -rw-r--r-- | src/mailman/rest/root.py | 4 | ||||
| -rw-r--r-- | src/mailman/rest/webservice.py | 210 |
7 files changed, 293 insertions, 246 deletions
diff --git a/src/mailman/rest/docs/lists.txt b/src/mailman/rest/docs/lists.txt index ede0706fd..d04fed0b3 100644 --- a/src/mailman/rest/docs/lists.txt +++ b/src/mailman/rest/docs/lists.txt @@ -7,7 +7,7 @@ top level collection that can return all the mailing lists. There aren't any yet though. >>> dump_json('http://localhost:8001/3.0/lists') - resource_type_link: http://localhost:8001/3.0/#lists + http_etag: "..." start: None total_size: 0 @@ -24,9 +24,8 @@ Create a mailing list in a domain and it's accessible via the API. http_etag: "..." list_name: test-one real_name: Test-one - resource_type_link: http://localhost:8001/3.0/#list self_link: http://localhost:8001/3.0/lists/test-one@example.com - resource_type_link: http://localhost:8001/3.0/#lists + http_etag: "..." start: 0 total_size: 1 @@ -64,7 +63,6 @@ It is also available via the location given in the response. http_etag: "..." list_name: test-two real_name: Test-two - resource_type_link: http://localhost:8001/3.0/#list self_link: http://localhost:8001/3.0/lists/test-two@example.com However, you are not allowed to create a mailing list in a domain that does diff --git a/src/mailman/rest/docs/membership.txt b/src/mailman/rest/docs/membership.txt index 776713f5f..e86f598f1 100644 --- a/src/mailman/rest/docs/membership.txt +++ b/src/mailman/rest/docs/membership.txt @@ -9,7 +9,7 @@ returns all the members of all known mailing lists. There are no mailing lists and no members yet. >>> dump_json('http://localhost:8001/3.0/members') - resource_type_link: http://localhost:8001/3.0/#members + http_etag: "..." start: None total_size: 0 @@ -19,7 +19,7 @@ We create a mailing list, which starts out with no members. >>> transaction.commit() >>> dump_json('http://localhost:8001/3.0/members') - resource_type_link: http://localhost:8001/3.0/#members + http_etag: "..." start: None total_size: 0 @@ -49,9 +49,8 @@ the REST interface. >>> dump_json('http://localhost:8001/3.0/members') entry 0: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/bperson@example.com - resource_type_link: http://localhost:8001/3.0/#members + http_etag: "..." start: 0 total_size: 1 @@ -62,13 +61,11 @@ the REST interface. >>> dump_json('http://localhost:8001/3.0/members') entry 0: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/bperson@example.com entry 1: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/cperson@example.com - resource_type_link: http://localhost:8001/3.0/#members + http_etag: "..." start: 0 total_size: 2 @@ -80,17 +77,14 @@ subscribes, she is returned first. >>> dump_json('http://localhost:8001/3.0/members') entry 0: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/aperson@example.com entry 1: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/bperson@example.com entry 2: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/cperson@example.com - resource_type_link: http://localhost:8001/3.0/#members + http_etag: "..." start: 0 total_size: 3 @@ -104,25 +98,20 @@ address. Anna and Cris subscribe to this new mailing list. >>> dump_json('http://localhost:8001/3.0/members') entry 0: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/alpha@example.com/member/aperson@example.com entry 1: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/alpha@example.com/member/cperson@example.com entry 2: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/aperson@example.com entry 3: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/bperson@example.com entry 4: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/cperson@example.com - resource_type_link: http://localhost:8001/3.0/#members + http_etag: "..." start: 0 total_size: 5 @@ -140,33 +129,26 @@ test-one mailing list. >>> dump_json('http://localhost:8001/3.0/members') entry 0: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/alpha@example.com/moderator/dperson@example.com entry 1: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/alpha@example.com/member/aperson@example.com entry 2: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/alpha@example.com/member/cperson@example.com entry 3: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/test-one@example.com/owner/cperson@example.com entry 4: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/aperson@example.com entry 5: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/bperson@example.com entry 6: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/test-one@example.com/member/cperson@example.com - resource_type_link: http://localhost:8001/3.0/#members + http_etag: "..." start: 0 total_size: 7 @@ -206,7 +188,6 @@ Elly is now a member of the mailing list. ... entry 3: http_etag: ... - resource_type_link: http://localhost:8001/3.0/#member self_link: http://localhost:8001/3.0/lists/alpha@example.com/member/eperson@example.com ... diff --git a/src/mailman/rest/domains.py b/src/mailman/rest/domains.py index ed63834b9..32f010534 100644 --- a/src/mailman/rest/domains.py +++ b/src/mailman/rest/domains.py @@ -96,12 +96,12 @@ class AllDomains(_DomainBase): if len(domains) == 0: resource = dict(start=None, total_size=0) return http.ok([], etag(resource)) - domain_data = [self._domain_data(domain) for domain in domains] - # Tag this domain data, but ignore the results. - [etag(data) for data in domain_data] + entries = [self._domain_data(domain) for domain in domains] + # Tag the domain entries, but use the dictionaries. + [etag(data) for data in entries] resource = dict( start=0, total_size=len(domains), - entries=domain_data, + entries=entries, ) return http.ok([], etag(resource)) diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py new file mode 100644 index 000000000..682353373 --- /dev/null +++ b/src/mailman/rest/lists.py @@ -0,0 +1,140 @@ +# Copyright (C) 2010 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 mailing lists.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'AList', + 'AllLists', + ] + + +from restish import http, resource +from zope.component import getUtility + +from mailman.interfaces.domain import BadDomainSpecificationError +from mailman.interfaces.listmanager import ( + IListManager, ListAlreadyExistsError) +from mailman.interfaces.member import MemberRole +from mailman.rest.helpers import etag, path_to +from mailman.rest.members import AMember + + + +def member_matcher(request, segments): + """A matcher of member URLs inside mailing lists. + + e.g. /member/aperson@example.org + """ + if len(segments) != 2: + return None + try: + role = MemberRole[segments[0]] + except ValueError: + # Not a valid role. + return None + # No more segments. + # XXX 2010-02-25 barry Matchers are undocumented in restish; they return a + # 3-tuple of (match_args, match_kws, segments). + return (), dict(role=role, address=segments[1]), () + +# XXX 2010-02-24 barry Seems like contrary to the documentation, matchers +# cannot be plain function, because matchers must have a .score attribute. +# OTOH, I think they support regexps, so that might be a better way to go. +member_matcher.score = () + + + +class _ListBase(resource.Resource): + """Shared base class for mailing list representations.""" + + def _list_data(self, mlist): + """Return the list data for a single mailing list.""" + return dict( + fqdn_listname=mlist.fqdn_listname, + host_name=mlist.host_name, + list_name=mlist.list_name, + real_name=mlist.real_name, + self_link=path_to('lists/{0}'.format(mlist.fqdn_listname)), + ) + + def _format_list(self, mlist): + """Format the mailing list for a single mailing list.""" + return etag(self._list_data(mlist)) + + +class AList(_ListBase): + """A mailing list.""" + + def __init__(self, list_name): + self._mlist = getUtility(IListManager).get(list_name) + + @resource.GET() + def mailing_list(self, request): + """Return a single mailing list end-point.""" + if self._mlist is None: + return http.not_found() + return http.ok([], self._format_list(self._mlist)) + + @resource.child(member_matcher) + def member(self, request, segments, role, address): + return AMember(self._mlist, role, address) + + +class AllLists(_ListBase): + """The mailing lists.""" + + @resource.POST() + def create(self, request): + """Create a new mailing list.""" + # XXX 2010-02-23 barry Sanity check the POST arguments by + # introspection of the target method, or via descriptors. + list_manager = getUtility(IListManager) + try: + # webob gives this to us as a string, but we need unicodes. + kws = dict((key, unicode(value)) + for key, value in request.POST.items()) + mlist = list_manager.new(**kws) + except ListAlreadyExistsError: + return http.bad_request([], b'Mailing list exists') + except BadDomainSpecificationError as error: + return http.bad_request([], b'Domain does not exist {0}'.format( + error.domain)) + # wsgiref wants headers to be bytes, not unicodes. + location = path_to('lists/{0}'.format(mlist.fqdn_listname)) + # Include no extra headers or body. + return http.created(location, [], None) + + @resource.GET() + def container(self, request): + """Return the /lists end-point.""" + mlists = list(getUtility(IListManager)) + if len(mlists) == 0: + resource = dict(start=None, total_size=0) + return http.ok([], etag(resource)) + entries = [self._list_data(mlist) for mlist in mlists] + # Tag the list entries, but use the dictionaries. + [etag(data) for data in entries] + resource = dict( + start=0, + total_size=len(mlists), + entries=entries, + ) + return http.ok([], etag(resource)) diff --git a/src/mailman/rest/members.py b/src/mailman/rest/members.py new file mode 100644 index 000000000..b18097935 --- /dev/null +++ b/src/mailman/rest/members.py @@ -0,0 +1,138 @@ +# Copyright (C) 2010 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 members.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'AMember', + 'AllMembers', + ] + + +from restish import http, resource +from zope.component import getUtility + +from mailman.app.membership import delete_member +from mailman.interfaces.address import InvalidEmailAddressError +from mailman.interfaces.listmanager import NoSuchListError +from mailman.interfaces.member import AlreadySubscribedError, MemberRole +from mailman.interfaces.membership import ISubscriptionService +from mailman.rest.helpers import etag, path_to + + + +class _MemberBase(resource.Resource): + """Shared base class for member representations.""" + + def _member_data(self, member): + """Return the member data for a single member.""" + enum, dot, role = str(member.role).partition('.') + return dict( + self_link=path_to('lists/{0}/{1}/{2}'.format( + member.mailing_list, role, member.address.address)), + ) + + def _format_member(self, member): + """Format the data for a single member.""" + return etag(self._member_data(member)) + + +class AMember(_MemberBase): + """A member.""" + + def __init__(self, mailing_list, role, address): + self._mlist = mailing_list + self._role = role + self._address = address + # XXX 2010-02-24 barry There should be a more direct way to get a + # member out of a mailing list. + if self._role is MemberRole.member: + roster = self._mlist.members + elif self._role is MemberRole.owner: + roster = self._mlist.owners + elif self._role is MemberRole.moderator: + roster = self._mlist.moderators + else: + raise AssertionError( + 'Undefined MemberRole: {0}'.format(self._role)) + self._member = roster.get_member(self._address) + + @resource.GET() + def member(self, request): + """Return a single member end-point.""" + return http.ok([], self._format_member(self._member)) + + @resource.DELETE() + def delete(self, request): + """Delete the member (i.e. unsubscribe).""" + # Leaving a list is a bit different than deleting a moderator or + # owner. Handle the former case first. For now too, we will not send + # an admin or user notification. + if self._role is MemberRole.member: + delete_member(self._mlist, self._address, False, False) + else: + self._member.unsubscribe() + return http.ok([], '') + + +class AllMembers(_MemberBase): + """The members.""" + + @resource.POST() + def create(self, request): + """Create a new member.""" + # XXX 2010-02-23 barry Sanity check the POST arguments by + # introspection of the target method, or via descriptors. + service = getUtility(ISubscriptionService) + try: + # webob gives this to us as a string, but we need unicodes. + kws = dict((key, unicode(value)) + for key, value in request.POST.items()) + member = service.join(**kws) + except AlreadySubscribedError: + return http.bad_request([], b'Member already subscribed') + except NoSuchListError: + return http.bad_request([], b'No such list') + except InvalidEmailAddressError: + return http.bad_request([], b'Invalid email address') + except ValueError as error: + return http.bad_request([], str(error)) + # wsgiref wants headers to be bytes, not unicodes. + location = path_to('lists/{0}/member/{1}'.format( + member.mailing_list, member.address.address)) + # Include no extra headers or body. + return http.created(location, [], None) + + @resource.GET() + def container(self, request): + """Return the /members end-point.""" + members = list(getUtility(ISubscriptionService)) + if len(members) == 0: + resource = dict(start=None, total_size=0) + return http.ok([], etag(resource)) + entries = [self._member_data(member) for member in members] + # Tag the domain entries, but use the dictionaries. + [etag(data) for data in entries] + resource = dict( + start=0, + total_size=len(members), + entries=entries, + ) + return http.ok([], etag(resource)) diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py index e524f3632..6835586b8 100644 --- a/src/mailman/rest/root.py +++ b/src/mailman/rest/root.py @@ -31,8 +31,8 @@ from mailman.config import config from mailman.core.system import system from mailman.rest.domains import ADomain, AllDomains from mailman.rest.helpers import etag, path_to -from mailman.rest.webservice import ( - AList, AllLists, AllMembers) +from mailman.rest.lists import AList, AllLists +from mailman.rest.members import AllMembers diff --git a/src/mailman/rest/webservice.py b/src/mailman/rest/webservice.py index 403948de8..c8aa470b9 100644 --- a/src/mailman/rest/webservice.py +++ b/src/mailman/rest/webservice.py @@ -52,213 +52,3 @@ log = logging.getLogger('mailman.http') -class _ListBase(resource.Resource): - """Shared base class for mailing list representations.""" - - def _format_list(self, mlist): - """Format the mailing list for a single domain.""" - list_data = dict( - fqdn_listname=mlist.fqdn_listname, - host_name=mlist.host_name, - list_name=mlist.list_name, - real_name=mlist.real_name, - resource_type_link='http://localhost:8001/3.0/#list', - self_link='http://localhost:8001/3.0/lists/{0}'.format( - mlist.fqdn_listname), - ) - etag = hashlib.sha1(repr(list_data)).hexdigest() - list_data['http_etag'] = '"{0}"'.format(etag) - return list_data - - -def member_matcher(request, segments): - """A matcher of member URLs inside mailing lists. - - e.g. /member/aperson@example.org - """ - if len(segments) != 2: - return None - try: - role = MemberRole[segments[0]] - except ValueError: - # Not a valid role. - return None - # No more segments. - return (), dict(role=role, address=segments[1]), () - -# XXX 2010-02-24 barry Seems like contrary to the documentation, matchers -# cannot be plain function, because matchers must have a .score attribute. -# OTOH, I think they support regexps, so that might be a better way to go. -member_matcher.score = () - - -class AList(_ListBase): - """A mailing list.""" - - def __init__(self, list_name): - self._mlist = getUtility(IListManager).get(list_name) - - @resource.GET() - def mailing_list(self, request): - """Return a single mailing list end-point.""" - if self._mlist is None: - return http.not_found() - return http.ok([], json.dumps(self._format_list(self._mlist))) - - @resource.child(member_matcher) - def member(self, request, segments, role, address): - return AMember(self._mlist, role, address) - - -class AllLists(_ListBase): - """The mailing lists.""" - - @resource.POST() - def create(self, request): - """Create a new mailing list.""" - # XXX 2010-02-23 barry Sanity check the POST arguments by - # introspection of the target method, or via descriptors. - list_manager = getUtility(IListManager) - try: - # webob gives this to us as a string, but we need unicodes. - kws = dict((key, unicode(value)) - for key, value in request.POST.items()) - mlist = list_manager.new(**kws) - except ListAlreadyExistsError: - return http.bad_request([], b'Mailing list exists') - except BadDomainSpecificationError as error: - return http.bad_request([], b'Domain does not exist {0}'.format( - error.domain)) - # wsgiref wants headers to be bytes, not unicodes. - location = b'http://localhost:8001/3.0/lists/{0}'.format( - mlist.fqdn_listname) - # Include no extra headers or body. - return http.created(location, [], None) - - @resource.GET() - def container(self, request): - """Return the /lists end-point.""" - mlists = list(getUtility(IListManager)) - if len(mlists) == 0: - return http.ok( - [], json.dumps(dict(resource_type_link= - 'http://localhost:8001/3.0/#lists', - start=None, - total_size=0))) - entries = [] - response = dict( - resource_type_link='http://localhost:8001/3.0/#lists', - start=0, - total_size=len(mlists), - entries=entries, - ) - for mlist in mlists: - list_data = self._format_list(mlist) - entries.append(list_data) - return http.ok([], json.dumps(response)) - - - -class _MemberBase(resource.Resource): - """Shared base class for member representations.""" - - def _format_member(self, member): - """Format the data for a single member.""" - enum, dot, role = str(member.role).partition('.') - member_data = dict( - resource_type_link='http://localhost:8001/3.0/#member', - self_link='http://localhost:8001/3.0/lists/{0}/{1}/{2}'.format( - member.mailing_list, role, member.address.address), - ) - etag = hashlib.sha1(repr(member_data)).hexdigest() - member_data['http_etag'] = '"{0}"'.format(etag) - return member_data - - -class AMember(_MemberBase): - """A member.""" - - def __init__(self, mailing_list, role, address): - self._mlist = mailing_list - self._role = role - self._address = address - # XXX 2010-02-24 barry There should be a more direct way to get a - # member out of a mailing list. - if self._role is MemberRole.member: - roster = self._mlist.members - elif self._role is MemberRole.owner: - roster = self._mlist.owners - elif self._role is MemberRole.moderator: - roster = self._mlist.moderators - else: - raise AssertionError( - 'Undefined MemberRole: {0}'.format(self._role)) - self._member = roster.get_member(self._address) - - @resource.GET() - def member(self, request): - """Return a single member end-point.""" - return http.ok([], json.dumps(self._format_member(self._member))) - - @resource.DELETE() - def delete(self, request): - """Delete the member (i.e. unsubscribe).""" - # Leaving a list is a bit different than deleting a moderator or - # owner. Handle the former case first. For now too, we will not send - # an admin or user notification. - if self._role is MemberRole.member: - delete_member(self._mlist, self._address, False, False) - else: - self._member.unsubscribe() - return http.ok([], '') - - -class AllMembers(_MemberBase): - """The members.""" - - @resource.POST() - def create(self, request): - """Create a new member.""" - # XXX 2010-02-23 barry Sanity check the POST arguments by - # introspection of the target method, or via descriptors. - service = getUtility(ISubscriptionService) - try: - # webob gives this to us as a string, but we need unicodes. - kws = dict((key, unicode(value)) - for key, value in request.POST.items()) - member = service.join(**kws) - except AlreadySubscribedError: - return http.bad_request([], b'Member already subscribed') - except NoSuchListError: - return http.bad_request([], b'No such list') - except InvalidEmailAddressError: - return http.bad_request([], b'Invalid email address') - except ValueError as error: - return http.bad_request([], str(error)) - # wsgiref wants headers to be bytes, not unicodes. - location = b'http://localhost:8001/3.0/lists/{0}/member/{1}'.format( - member.mailing_list, member.address.address) - # Include no extra headers or body. - return http.created(location, [], None) - - @resource.GET() - def container(self, request): - """Return the /members end-point.""" - members = list(getUtility(ISubscriptionService)) - if len(members) == 0: - return http.ok( - [], json.dumps(dict(resource_type_link= - 'http://localhost:8001/3.0/#members', - start=None, - total_size=0))) - entries = [] - response = dict( - resource_type_link='http://localhost:8001/3.0/#members', - start=0, - total_size=len(members), - entries=entries, - ) - for member in members: - member_data = self._format_member(member) - entries.append(member_data) - return http.ok([], json.dumps(response)) |
