summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBarry Warsaw2012-12-30 14:34:25 -0500
committerBarry Warsaw2012-12-30 14:34:25 -0500
commit5ec8a131c602f9b00d6b25d914ffc923cd1aa964 (patch)
tree2d95a4745cf549ad4430fff6d21590156e4608ec
parent9b95b54fb0913786a2d61049670cabcea6f78cce (diff)
downloadmailman-5ec8a131c602f9b00d6b25d914ffc923cd1aa964.tar.gz
mailman-5ec8a131c602f9b00d6b25d914ffc923cd1aa964.tar.zst
mailman-5ec8a131c602f9b00d6b25d914ffc923cd1aa964.zip
-rw-r--r--src/mailman/docs/NEWS.rst2
-rw-r--r--src/mailman/rest/docs/configuration.rst63
-rw-r--r--src/mailman/rest/docs/lists.rst139
-rw-r--r--src/mailman/rest/lists.py6
-rw-r--r--src/mailman/rest/root.py7
-rw-r--r--src/mailman/rest/tests/test_lists.py47
-rw-r--r--src/mailman/tests/test_documentation.py10
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)