diff options
| -rw-r--r-- | src/mailman/docs/NEWS.rst | 2 | ||||
| -rw-r--r-- | src/mailman/rest/docs/configuration.rst | 63 | ||||
| -rw-r--r-- | src/mailman/rest/docs/lists.rst | 139 | ||||
| -rw-r--r-- | src/mailman/rest/lists.py | 6 | ||||
| -rw-r--r-- | src/mailman/rest/root.py | 7 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_lists.py | 47 | ||||
| -rw-r--r-- | src/mailman/tests/test_documentation.py | 10 |
7 files changed, 167 insertions, 107 deletions
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst index 92186172f..8ddfd0e9f 100644 --- a/src/mailman/docs/NEWS.rst +++ b/src/mailman/docs/NEWS.rst @@ -35,7 +35,7 @@ REST * List styles are supported through the REST API. Get the list of available styles (by name) via `.../lists/styles`. Create a list in a specific style - by using POST data `style_name=<style>`. (LP: #675692) + by using POST data `style_name=<style>`. (LP: #975692) * Allow the getting/setting of IMailingList.subject_prefix via the REST API (given by Terri Oda). (LP: #1062893) * Expose a REST API for membership change (subscriptions and unsubscriptions) diff --git a/src/mailman/rest/docs/configuration.rst b/src/mailman/rest/docs/configuration.rst index 01b6e0700..27560a026 100644 --- a/src/mailman/rest/docs/configuration.rst +++ b/src/mailman/rest/docs/configuration.rst @@ -4,7 +4,7 @@ Mailing list configuration Mailing lists can be configured via the REST API. - >>> mlist = create_list('test-one@example.com') + >>> mlist = create_list('ant@example.com') >>> transaction.commit() @@ -13,8 +13,7 @@ Reading a configuration All readable attributes for a list are available on a sub-resource. - >>> dump_json('http://localhost:9001/3.0/lists/' - ... 'test-one@example.com/config') + >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/config') acceptable_aliases: [] admin_immed_notify: True admin_notify_mchanges: False @@ -30,7 +29,7 @@ All readable attributes for a list are available on a sub-resource. autoresponse_owner_text: autoresponse_postings_text: autoresponse_request_text: - bounces_address: test-one-bounces@example.com + bounces_address: ant-bounces@example.com collapse_alternatives: True convert_html_to_plaintext: False created_at: 20...T... @@ -39,27 +38,27 @@ All readable attributes for a list are available on a sub-resource. description: digest_last_sent_at: None digest_size_threshold: 30.0 - display_name: Test-one + display_name: Ant filter_content: False - fqdn_listname: test-one@example.com + fqdn_listname: ant@example.com http_etag: "..." include_rfc2369_headers: True - join_address: test-one-join@example.com + join_address: ant-join@example.com last_post_at: None - leave_address: test-one-leave@example.com - list_name: test-one + leave_address: ant-leave@example.com + list_name: ant mail_host: example.com next_digest_number: 1 no_reply_address: noreply@example.com - owner_address: test-one-owner@example.com + owner_address: ant-owner@example.com post_id: 1 - posting_address: test-one@example.com + posting_address: ant@example.com posting_pipeline: default-posting-pipeline reply_goes_to_list: no_munging - request_address: test-one-request@example.com + request_address: ant-request@example.com scheme: http send_welcome_message: True - subject_prefix: [Test-one] + subject_prefix: [Ant] volume: 1 web_host: lists.example.com welcome_message_uri: mailman:///welcome.txt @@ -74,7 +73,7 @@ all the writable attributes in one request. >>> from mailman.interfaces.action import Action >>> dump_json('http://localhost:9001/3.0/lists/' - ... 'test-one@example.com/config', + ... 'ant@example.com/config', ... dict( ... acceptable_aliases=['one@example.com', 'two@example.com'], ... admin_immed_notify=False, @@ -101,7 +100,7 @@ all the writable attributes in one request. ... collapse_alternatives=False, ... reply_goes_to_list='point_to_list', ... send_welcome_message=False, - ... subject_prefix='[test-one]', + ... subject_prefix='[ant]', ... welcome_message_uri='mailman:///welcome.txt', ... default_member_action='hold', ... default_nonmember_action='discard', @@ -115,8 +114,8 @@ all the writable attributes in one request. These values are changed permanently. >>> dump_json('http://localhost:9001/3.0/lists/' - ... 'test-one@example.com/config') - acceptable_aliases: [u'one@example.com', u'two@example.com'] + ... 'ant@example.com/config') + acceptable_aliases: ['one@example.com', 'two@example.com'] admin_immed_notify: False admin_notify_mchanges: True administrivia: False @@ -149,7 +148,7 @@ These values are changed permanently. reply_goes_to_list: point_to_list ... send_welcome_message: False - subject_prefix: [test-one] + subject_prefix: [ant] ... welcome_message_uri: mailman:///welcome.txt @@ -157,7 +156,7 @@ If you use ``PUT`` to change a list's configuration, all writable attributes must be included. It is an error to leave one or more out... >>> dump_json('http://localhost:9001/3.0/lists/' - ... 'test-one@example.com/config', + ... 'ant@example.com/config', ... dict( ... #acceptable_aliases=['one', 'two'], ... admin_immed_notify=False, @@ -184,7 +183,7 @@ must be included. It is an error to leave one or more out... ... collapse_alternatives=False, ... reply_goes_to_list='point_to_list', ... send_welcome_message=True, - ... subject_prefix='[test-one]', + ... subject_prefix='[ant]', ... welcome_message_uri='welcome message', ... default_member_action='accept', ... default_nonmember_action='accept', @@ -197,7 +196,7 @@ must be included. It is an error to leave one or more out... ...or to add an unknown one. >>> dump_json('http://localhost:9001/3.0/lists/' - ... 'test-one@example.com/config', + ... 'ant@example.com/config', ... dict( ... a_mailing_list_attribute=False, ... acceptable_aliases=['one', 'two'], @@ -220,7 +219,7 @@ must be included. It is an error to leave one or more out... ... allow_list_posts=False, ... digest_size_threshold=10.5, ... posting_pipeline='virgin', - ... subject_prefix='[test-one]', + ... subject_prefix='[ant]', ... filter_content=True, ... convert_html_to_plaintext=True, ... collapse_alternatives=False, @@ -233,7 +232,7 @@ must be included. It is an error to leave one or more out... It is also an error to spell an attribute value incorrectly... >>> dump_json('http://localhost:9001/3.0/lists/' - ... 'test-one@example.com/config', + ... 'ant@example.com/config', ... dict( ... admin_immed_notify='Nope', ... acceptable_aliases=['one', 'two'], @@ -254,7 +253,7 @@ It is also an error to spell an attribute value incorrectly... ... allow_list_posts=False, ... digest_size_threshold=10.5, ... posting_pipeline='virgin', - ... subject_prefix='[test-one]', + ... subject_prefix='[ant]', ... filter_content=True, ... convert_html_to_plaintext=True, ... collapse_alternatives=False, @@ -267,7 +266,7 @@ It is also an error to spell an attribute value incorrectly... ...or to name a pipeline that doesn't exist... >>> dump_json('http://localhost:9001/3.0/lists/' - ... 'test-one@example.com/config', + ... 'ant@example.com/config', ... dict( ... acceptable_aliases=['one', 'two'], ... admin_immed_notify=False, @@ -288,7 +287,7 @@ It is also an error to spell an attribute value incorrectly... ... allow_list_posts=False, ... digest_size_threshold=10.5, ... posting_pipeline='dummy', - ... subject_prefix='[test-one]', + ... subject_prefix='[ant]', ... filter_content=True, ... convert_html_to_plaintext=True, ... collapse_alternatives=False, @@ -301,7 +300,7 @@ It is also an error to spell an attribute value incorrectly... ...or to name an invalid auto-response enumeration value. >>> dump_json('http://localhost:9001/3.0/lists/' - ... 'test-one@example.com/config', + ... 'ant@example.com/config', ... dict( ... acceptable_aliases=['one', 'two'], ... admin_immed_notify=False, @@ -321,7 +320,7 @@ It is also an error to spell an attribute value incorrectly... ... allow_list_posts=False, ... digest_size_threshold=10.5, ... posting_pipeline='virgin', - ... subject_prefix='[test-one]', + ... subject_prefix='[ant]', ... filter_content=True, ... convert_html_to_plaintext=True, ... collapse_alternatives=False, @@ -338,7 +337,7 @@ Changing a partial configuration Using ``PATCH``, you can change just one attribute. >>> dump_json('http://localhost:9001/3.0/lists/' - ... 'test-one@example.com/config', + ... 'ant@example.com/config', ... dict(display_name='My List'), ... 'PATCH') content-length: 0 @@ -372,7 +371,7 @@ emails. By default, a mailing list has no acceptable aliases. >>> IAcceptableAliasSet(mlist).clear() >>> transaction.commit() >>> dump_json('http://localhost:9001/3.0/lists/' - ... 'test-one@example.com/config/acceptable_aliases') + ... 'ant@example.com/config/acceptable_aliases') acceptable_aliases: [] http_etag: "..." @@ -380,7 +379,7 @@ We can add a few by ``PUT``-ing them on the sub-resource. The keys in the dictionary are ignored. >>> dump_json('http://localhost:9001/3.0/lists/' - ... 'test-one@example.com/config/acceptable_aliases', + ... 'ant@example.com/config/acceptable_aliases', ... dict(acceptable_aliases=['foo@example.com', ... 'bar@example.net']), ... 'PUT') @@ -393,7 +392,7 @@ Aliases are returned as a list on the ``aliases`` key. >>> response = call_http( ... 'http://localhost:9001/3.0/lists/' - ... 'test-one@example.com/config/acceptable_aliases') + ... 'ant@example.com/config/acceptable_aliases') >>> for alias in response['acceptable_aliases']: ... print alias bar@example.net diff --git a/src/mailman/rest/docs/lists.rst b/src/mailman/rest/docs/lists.rst index 0c4bbc419..5c65a7951 100644 --- a/src/mailman/rest/docs/lists.rst +++ b/src/mailman/rest/docs/lists.rst @@ -14,48 +14,41 @@ yet though. Create a mailing list in a domain and it's accessible via the API. :: - >>> create_list('test-one@example.com') - <mailing list "test-one@example.com" at ...> + >>> mlist = create_list('ant@example.com') >>> transaction.commit() >>> dump_json('http://localhost:9001/3.0/lists') entry 0: - display_name: Test-one - fqdn_listname: test-one@example.com + display_name: Ant + fqdn_listname: ant@example.com http_etag: "..." - list_id: test-one.example.com - list_name: test-one + list_id: ant.example.com + list_name: ant mail_host: example.com member_count: 0 - self_link: http://localhost:9001/3.0/lists/test-one.example.com + self_link: http://localhost:9001/3.0/lists/ant.example.com volume: 1 http_etag: "..." start: 0 total_size: 1 You can also query for lists from a particular domain. -:: >>> dump_json('http://localhost:9001/3.0/domains/example.com/lists') entry 0: - display_name: Test-one - fqdn_listname: test-one@example.com + display_name: Ant + fqdn_listname: ant@example.com http_etag: "..." - list_id: test-one.example.com - list_name: test-one + list_id: ant.example.com + list_name: ant mail_host: example.com member_count: 0 - self_link: http://localhost:9001/3.0/lists/test-one.example.com + self_link: http://localhost:9001/3.0/lists/ant.example.com volume: 1 http_etag: "..." start: 0 total_size: 1 - >>> dump_json('http://localhost:9001/3.0/domains/no.example.org/lists') - Traceback (most recent call last): - ... - HTTPError: HTTP Error 404: 404 Not Found - Creating lists via the API ========================== @@ -64,11 +57,11 @@ New mailing lists can also be created through the API, by posting to the ``lists`` URL. >>> dump_json('http://localhost:9001/3.0/lists', { - ... 'fqdn_listname': 'test-two@example.com', + ... 'fqdn_listname': 'bee@example.com', ... }) content-length: 0 date: ... - location: http://localhost:9001/3.0/lists/test-two.example.com + location: http://localhost:9001/3.0/lists/bee.example.com ... The mailing list exists in the database. @@ -78,24 +71,29 @@ The mailing list exists in the database. >>> from zope.component import getUtility >>> list_manager = getUtility(IListManager) - >>> list_manager.get('test-two@example.com') - <mailing list "test-two@example.com" at ...> + >>> bee = list_manager.get('bee@example.com') + >>> bee + <mailing list "bee@example.com" at ...> + +The mailing list was created using the default style, which allows list posts. - # The above starts a Storm transaction, which will lock the database - # unless we abort it. + >>> bee.allow_list_posts + True + +.. Abort the Storm transaction. >>> transaction.abort() -It is also available via the location given in the response. +It is also available in the REST API via the location given in the response. - >>> dump_json('http://localhost:9001/3.0/lists/test-two.example.com') - display_name: Test-two - fqdn_listname: test-two@example.com + >>> dump_json('http://localhost:9001/3.0/lists/bee.example.com') + display_name: Bee + fqdn_listname: bee@example.com http_etag: "..." - list_id: test-two.example.com - list_name: test-two + list_id: bee.example.com + list_name: bee mail_host: example.com member_count: 0 - self_link: http://localhost:9001/3.0/lists/test-two.example.com + self_link: http://localhost:9001/3.0/lists/bee.example.com volume: 1 Normally, you access the list via its RFC 2369 list-id as shown above, but for @@ -103,35 +101,52 @@ backward compatibility purposes, you can also access it via the list's posting address, if that has never been changed (since the list-id is immutable, but the posting address is not). - >>> dump_json('http://localhost:9001/3.0/lists/test-two@example.com') - display_name: Test-two - fqdn_listname: test-two@example.com + >>> dump_json('http://localhost:9001/3.0/lists/bee@example.com') + display_name: Bee + fqdn_listname: bee@example.com http_etag: "..." - list_id: test-two.example.com - list_name: test-two + list_id: bee.example.com + list_name: bee mail_host: example.com member_count: 0 - self_link: http://localhost:9001/3.0/lists/test-two.example.com + self_link: http://localhost:9001/3.0/lists/bee.example.com volume: 1 -However, you are not allowed to create a mailing list in a domain that does -not exist. - >>> dump_json('http://localhost:9001/3.0/lists', { - ... 'fqdn_listname': 'test-three@example.org', - ... }) - Traceback (most recent call last): - ... - HTTPError: HTTP Error 400: Domain does not exist example.org +Apply a style at list creation time +----------------------------------- + +:ref:`List styles <list-styles>` allow you to more easily create mailing lists +of a particular type, e.g. discussion lists. We can see which styles are +available, and which is the default style. + + >>> dump_json('http://localhost:9001/3.0/lists/styles') + default: legacy-default + http_etag: "..." + style_names: ['legacy-announce', 'legacy-default'] -Nor can you create a mailing list that already exists. +When creating a list, if we don't specify a style to apply, the default style +is used. However, we can provide a style name in the POST data to choose a +different style. >>> dump_json('http://localhost:9001/3.0/lists', { - ... 'fqdn_listname': 'test-one@example.com', + ... 'fqdn_listname': 'cat@example.com', + ... 'style_name': 'legacy-announce', ... }) - Traceback (most recent call last): + content-length: 0 + date: ... + location: http://localhost:9001/3.0/lists/cat.example.com ... - HTTPError: HTTP Error 400: Mailing list exists + +We can tell that the list was created using the `legacy-announce` style, +because announce lists don't allow posting by the general public. + + >>> cat = list_manager.get('cat@example.com') + >>> cat.allow_list_posts + False + +.. Abort the Storm transaction. + >>> transaction.abort() Deleting lists via the API @@ -141,7 +156,7 @@ Existing mailing lists can be deleted through the API, by doing an HTTP ``DELETE`` on the mailing list URL. :: - >>> dump_json('http://localhost:9001/3.0/lists/test-two.example.com', + >>> dump_json('http://localhost:9001/3.0/lists/bee.example.com', ... method='DELETE') content-length: 0 date: ... @@ -150,32 +165,16 @@ Existing mailing lists can be deleted through the API, by doing an HTTP The mailing list does not exist. - >>> print list_manager.get('test-two@example.com') + >>> print list_manager.get('bee@example.com') None - # Unlock the database. +.. Abort the Storm transaction. >>> transaction.abort() -You cannot delete a mailing list that does not exist or has already been -deleted. -:: - - >>> dump_json('http://localhost:9001/3.0/lists/test-two.example.com', - ... method='DELETE') - Traceback (most recent call last): - ... - HTTPError: HTTP Error 404: 404 Not Found - - >>> dump_json('http://localhost:9001/3.0/lists/test-ten.example.com', - ... method='DELETE') - Traceback (most recent call last): - ... - HTTPError: HTTP Error 404: 404 Not Found - For backward compatibility purposes, you can delete a list via its posting address as well. - >>> dump_json('http://localhost:9001/3.0/lists/test-one@example.com', + >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com', ... method='DELETE') content-length: 0 date: ... @@ -184,5 +183,5 @@ address as well. The mailing list does not exist. - >>> print list_manager.get('test-one@example.com') + >>> print list_manager.get('ant@example.com') None diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py index 4a4b243b3..1fc31c6f0 100644 --- a/src/mailman/rest/lists.py +++ b/src/mailman/rest/lists.py @@ -197,12 +197,14 @@ class AllLists(_ListBase): def create(self, request): """Create a new mailing list.""" try: - validator = Validator(fqdn_listname=unicode) + validator = Validator(fqdn_listname=unicode, + style_name=unicode, + _optional=('style_name',)) mlist = create_list(**validator(request)) 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( + return http.bad_request([], b'Domain does not exist: {0}'.format( error.domain)) except ValueError as error: return http.bad_request([], str(error)) diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py index ea1650e75..13ba68ea9 100644 --- a/src/mailman/rest/root.py +++ b/src/mailman/rest/root.py @@ -33,6 +33,7 @@ from mailman.config import config from mailman.core.constants import system_preferences from mailman.core.system import system from mailman.interfaces.listmanager import IListManager +from mailman.interfaces.styles import IStyleManager from mailman.rest.addresses import AllAddresses, AnAddress from mailman.rest.domains import ADomain, AllDomains from mailman.rest.helpers import etag, path_to @@ -122,6 +123,12 @@ class TopLevel(resource.Resource): """ if len(segments) == 0: return AllLists() + elif len(segments) == 1 and segments[0] == 'styles': + manager = getUtility(IStyleManager) + style_names = sorted(style.name for style in manager.styles) + resource = dict(style_names=style_names, + default=config.styles.default) + return http.ok([], etag(resource)) else: # list-id is preferred, but for backward compatibility, # fqdn_listname is also accepted. diff --git a/src/mailman/rest/tests/test_lists.py b/src/mailman/rest/tests/test_lists.py index 9686ce6a8..913b40b6f 100644 --- a/src/mailman/rest/tests/test_lists.py +++ b/src/mailman/rest/tests/test_lists.py @@ -112,3 +112,50 @@ class TestLists(unittest.TestCase): 'http://localhost:9001/3.0/lists/test@example.com') self.assertEqual(response.status, 200) self.assertEqual(resource['member_count'], 2) + + def test_query_for_lists_in_missing_domain(self): + # You cannot ask all the mailing lists in a non-existent domain. + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.0/domains/no.example.org/lists') + self.assertEqual(cm.exception.code, 404) + + def test_cannot_create_list_in_missing_domain(self): + # You cannot create a mailing list in a domain that does not exist. + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.0/lists', { + 'fqdn_listname': 'ant@no-domain.example.org', + }) + self.assertEqual(cm.exception.code, 400) + self.assertEqual(cm.exception.reason, + 'Domain does not exist: no-domain.example.org') + + def test_cannot_create_duplicate_list(self): + # You cannot create a list that already exists. + call_api('http://localhost:9001/3.0/lists', { + 'fqdn_listname': 'ant@example.com', + }) + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.0/lists', { + 'fqdn_listname': 'ant@example.com', + }) + self.assertEqual(cm.exception.code, 400) + self.assertEqual(cm.exception.reason, 'Mailing list exists') + + def test_cannot_delete_missing_list(self): + # You cannot delete a list that does not exist. + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.0/lists/bee.example.com', + method='DELETE') + self.assertEqual(cm.exception.code, 404) + + def test_cannot_delete_already_deleted_list(self): + # You cannot delete a list twice. + call_api('http://localhost:9001/3.0/lists', { + 'fqdn_listname': 'ant@example.com', + }) + call_api('http://localhost:9001/3.0/lists/ant.example.com', + method='DELETE') + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.0/lists/ant.example.com', + method='DELETE') + self.assertEqual(cm.exception.code, 404) diff --git a/src/mailman/tests/test_documentation.py b/src/mailman/tests/test_documentation.py index b769f07d6..c00c41b83 100644 --- a/src/mailman/tests/test_documentation.py +++ b/src/mailman/tests/test_documentation.py @@ -46,6 +46,7 @@ from mailman.testing.layers import SMTPLayer DOT = '.' +COMMASPACE = ', ' @@ -133,14 +134,19 @@ def dump_json(url, data=None, method=None, username=None, password=None): if results is None: return for key in sorted(results): + value = results[key] if key == 'entries': - for i, entry in enumerate(results[key]): + for i, entry in enumerate(value): # entry is a dictionary. print 'entry %d:' % i for entry_key in sorted(entry): print ' {0}: {1}'.format(entry_key, entry[entry_key]) + elif isinstance(value, list): + printable_value = COMMASPACE.join( + "'{0}'".format(s) for s in sorted(value)) + print '{0}: [{1}]'.format(key, printable_value) else: - print '{0}: {1}'.format(key, results[key]) + print '{0}: {1}'.format(key, value) |
