Merge lp:~raj-abhilash1/mailman/bug_1423756 into lp:mailman

Proposed by Abhilash Raj
Status: Merged
Merge reported by: Barry Warsaw
Merged at revision: not available
Proposed branch: lp:~raj-abhilash1/mailman/bug_1423756
Merge into: lp:mailman
Diff against target: 907 lines (+247/-92)
17 files modified
src/mailman/app/registrar.py (+0/-2)
src/mailman/commands/docs/create.rst (+1/-2)
src/mailman/commands/tests/test_lists.py (+1/-1)
src/mailman/database/alembic/versions/46e92facee7_add_serverowner_domainowner.py (+36/-0)
src/mailman/interfaces/domain.py (+5/-8)
src/mailman/model/docs/domains.rst (+26/-23)
src/mailman/model/domain.py (+37/-16)
src/mailman/model/tests/test_domain.py (+23/-1)
src/mailman/model/user.py (+11/-1)
src/mailman/rest/docs/addresses.rst (+1/-0)
src/mailman/rest/docs/domains.rst (+8/-25)
src/mailman/rest/docs/users.rst (+17/-0)
src/mailman/rest/domains.py (+19/-6)
src/mailman/rest/lists.py (+1/-1)
src/mailman/rest/tests/test_domains.py (+12/-0)
src/mailman/rest/users.py (+48/-5)
src/mailman/testing/layers.py (+1/-1)
To merge this branch: bzr merge lp:~raj-abhilash1/mailman/bug_1423756
Reviewer Review Type Date Requested Status
Barry Warsaw Needs Fixing
Review via email: mp+254479@code.launchpad.net

Description of the change

Ability to define a server owner and domain owner.

@barry: I couldn't find where are variables filled in the confirm.txt template for verification of addresses so two tests related to the same are failing.

To post a comment you must log in.
Revision history for this message
Barry Warsaw (barry) wrote :

Thanks Abhilash. Looks pretty good so far, but I have a number of questions and comments inlined.

review: Needs Fixing
Revision history for this message
Barry Warsaw (barry) wrote :

Also, the confirm.txt file lives in src/mailman/templates/en/confirm.txt and it gets filled in inside the handle_ConfirmationNeededEvent() function in src/mailman/app/registrar.py

7314. By Abhilash Raj

* Add `drop_column` inside sqlite check, fix indentation
* Change `Owner` to `DomainOwner`
* Fix indentation errors in docs
* add multiple owners using `add_owners`
* all dummy addresses should be using example.com, example.org to avoid conflict ever
* add dummy tests

Revision history for this message
Abhilash Raj (raj-abhilash1) wrote :

I did check out that function before, saw where $confirm_url and $email_address variables are filled in from. But I couldn't find ${domain_name} and $contact_address.

Revision history for this message
Barry Warsaw (barry) wrote :

A few inlined replies.

7315. By Abhilash Raj

* implement left over methods
* add and remove owners using the address

Revision history for this message
Abhilash Raj (raj-abhilash1) wrote :

@Barry: I have Updated the branch as per your comments.

Revision history for this message
Barry Warsaw (barry) wrote :

Here's some general feedback as I work through the merge:

* Be careful about single quotes vs. double quotes. In general, I prefer single quotes except for multiline strings (including docstrings) and of course any string with an apostrophe in it.

* I fixed the interface for IDomainManager.add() - it still mentioned owners_id.

* You might want to use a Python linter to check for basic problems. E.g. I use pyflakes (hooked up to Emacs) so I noticed immediately some unused imports.

* Don't use mutables (e.g. []) in argument lists. If the mutable were to change, it would affect *every* call to the function. Use and check for `None` instead.

* You don't need to `assert isinstance(owners, list)` because we should accept any sequence. Probably not worth type checking the argument, as iteration ought to work, but of course it'll do the wrong thing if say owner is a string. That's okay, "we're all adults here" :) (Also, assert is not a function call so it doesn't need parentheses.)

* Implementations of interface methods can just have their docstring refer to the interface.

* I left a note in Domain.add_owners(). While I think the API is good, we might at some later point want to make that a bit more efficient than just calling add_owner(). Maybe.

More to come as I continue with the merge.

Revision history for this message
Barry Warsaw (barry) wrote :

Oh, I'm adding a lot more tests for corner cases and such. :)

Revision history for this message
Barry Warsaw (barry) wrote :

Added copyright template to alembic migration file, and fixed a few style nits.

Sort lines in __all__, and always use trailing commas in multline lists.

Sort from-imports.

Doctests for is_server_owner (model and REST layers).

Made is_server_owner PATCH and PUT-able for user resources in REST API.

Revision history for this message
Barry Warsaw (barry) wrote :

Added doctest for domain owners.

Revision history for this message
Barry Warsaw (barry) wrote :

I'm not so sure about the OwnersForDomain resource.

First, there should be no arguments to DELETE. You should be able to say:

dump_json('http://localhost:<email address hidden>', method='DELETE')

but you cannot currently do that.

Also, I wonder if it makes sense to allow for POSTing multiple email addresses to the <domain>/owners resource to add many owners.

Are there tests for OwnersForDomain? I'm writing some but they fail on these examples.

review: Needs Fixing
Revision history for this message
Barry Warsaw (barry) wrote :

So, actually you will be able to add more than one owner at a time, either after the fact by posting to <domain>/owners, or when the domain is created. You need to use multiple `owner` keys in the POST data.

DELETEing <domain>/owners deletes all owners.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/mailman/app/registrar.py'
--- src/mailman/app/registrar.py 2015-03-28 20:00:24 +0000
+++ src/mailman/app/registrar.py 2015-04-05 22:33:23 +0000
@@ -161,8 +161,6 @@
161 # For i18n interpolation.161 # For i18n interpolation.
162 confirm_url = mlist.domain.confirm_url(event.token)162 confirm_url = mlist.domain.confirm_url(event.token)
163 email_address = event.pendable['email']163 email_address = event.pendable['email']
164 domain_name = mlist.domain.mail_host
165 contact_address = mlist.domain.contact_address
166 # Send a verification email to the address.164 # Send a verification email to the address.
167 template = getUtility(ITemplateLoader).get(165 template = getUtility(ITemplateLoader).get(
168 'mailman:///{0}/{1}/confirm.txt'.format(166 'mailman:///{0}/{1}/confirm.txt'.format(
169167
=== modified file 'src/mailman/commands/docs/create.rst'
--- src/mailman/commands/docs/create.rst 2014-04-28 15:23:35 +0000
+++ src/mailman/commands/docs/create.rst 2015-04-05 22:33:23 +0000
@@ -44,8 +44,7 @@
4444
45 >>> from mailman.interfaces.domain import IDomainManager45 >>> from mailman.interfaces.domain import IDomainManager
46 >>> getUtility(IDomainManager).get('example.xx')46 >>> getUtility(IDomainManager).get('example.xx')
47 <Domain example.xx, base_url: http://example.xx,47 <Domain example.xx, base_url: http://example.xx>
48 contact_address: postmaster@example.xx>
4948
50You can also create mailing lists in existing domains without the49You can also create mailing lists in existing domains without the
51auto-creation flag.50auto-creation flag.
5251
=== modified file 'src/mailman/commands/tests/test_lists.py'
--- src/mailman/commands/tests/test_lists.py 2015-03-14 01:16:51 +0000
+++ src/mailman/commands/tests/test_lists.py 2015-04-05 22:33:23 +0000
@@ -48,7 +48,7 @@
48 # LP: #1166911 - non-matching lists were returned.48 # LP: #1166911 - non-matching lists were returned.
49 getUtility(IDomainManager).add(49 getUtility(IDomainManager).add(
50 'example.net', 'An example domain.',50 'example.net', 'An example domain.',
51 'http://lists.example.net', 'postmaster@example.net')51 'http://lists.example.net')
52 create_list('test1@example.com')52 create_list('test1@example.com')
53 create_list('test2@example.com')53 create_list('test2@example.com')
54 # Only this one should show up.54 # Only this one should show up.
5555
=== added file 'src/mailman/database/alembic/versions/46e92facee7_add_serverowner_domainowner.py'
--- src/mailman/database/alembic/versions/46e92facee7_add_serverowner_domainowner.py 1970-01-01 00:00:00 +0000
+++ src/mailman/database/alembic/versions/46e92facee7_add_serverowner_domainowner.py 2015-04-05 22:33:23 +0000
@@ -0,0 +1,36 @@
1"""add_serverowner_domainowner
2
3Revision ID: 46e92facee7
4Revises: 33e1f5f6fa8
5Create Date: 2015-03-20 16:01:25.007242
6
7"""
8
9# revision identifiers, used by Alembic.
10revision = '46e92facee7'
11down_revision = '33e1f5f6fa8'
12
13from alembic import op
14import sqlalchemy as sa
15
16
17def upgrade():
18 op.create_table('domain_owner',
19 sa.Column('user_id', sa.Integer(), nullable=False),
20 sa.Column('domain_id', sa.Integer(), nullable=False),
21 sa.ForeignKeyConstraint(['domain_id'], ['domain.id'], ),
22 sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
23 sa.PrimaryKeyConstraint('user_id', 'domain_id')
24 )
25 op.add_column('user', sa.Column('is_server_owner', sa.Boolean(),
26 nullable=True))
27 if op.get_bind().dialect.name != 'sqlite':
28 op.drop_column('domain', 'contact_address')
29
30
31def downgrade():
32 if op.get_bind().dialect.name != 'sqlite':
33 op.drop_column('user', 'is_server_owner')
34 op.add_column('domain', sa.Column('contact_address', sa.VARCHAR(),
35 nullable=True))
36 op.drop_table('domain_owner')
037
=== modified file 'src/mailman/interfaces/domain.py'
--- src/mailman/interfaces/domain.py 2015-01-05 01:22:39 +0000
+++ src/mailman/interfaces/domain.py 2015-04-05 22:33:23 +0000
@@ -88,9 +88,8 @@
88 description = Attribute(88 description = Attribute(
89 'The human readable description of the domain name.')89 'The human readable description of the domain name.')
9090
91 contact_address = Attribute("""\91 owners = Attribute("""\
92 The contact address for the human at this domain.92 The relationship with the user database representing domain owners""")
93 E.g. postmaster@example.com""")
9493
95 mailing_lists = Attribute(94 mailing_lists = Attribute(
96 """All mailing lists for this domain.95 """All mailing lists for this domain.
@@ -112,7 +111,7 @@
112class IDomainManager(Interface):111class IDomainManager(Interface):
113 """The manager of domains."""112 """The manager of domains."""
114113
115 def add(mail_host, description=None, base_url=None, contact_address=None):114 def add(mail_host, description=None, base_url=None, owner_id=None):
116 """Add a new domain.115 """Add a new domain.
117116
118 :param mail_host: The email host name for the domain.117 :param mail_host: The email host name for the domain.
@@ -123,10 +122,8 @@
123 interface of the domain. If not given, it defaults to122 interface of the domain. If not given, it defaults to
124 http://`mail_host`/123 http://`mail_host`/
125 :type base_url: string124 :type base_url: string
126 :param contact_address: The email contact address for the human125 :param owners: List of owners of the domain, defaults to None
127 managing the domain. If not given, defaults to126 :type owners: list
128 postmaster@`mail_host`
129 :type contact_address: string
130 :return: The new domain object127 :return: The new domain object
131 :rtype: `IDomain`128 :rtype: `IDomain`
132 :raises `BadDomainSpecificationError`: when the `mail_host` is129 :raises `BadDomainSpecificationError`: when the `mail_host` is
133130
=== modified file 'src/mailman/model/docs/domains.rst'
--- src/mailman/model/docs/domains.rst 2014-12-13 18:26:05 +0000
+++ src/mailman/model/docs/domains.rst 2015-04-05 22:33:23 +0000
@@ -9,6 +9,10 @@
9 >>> manager = getUtility(IDomainManager)9 >>> manager = getUtility(IDomainManager)
10 >>> manager.remove('example.com')10 >>> manager.remove('example.com')
11 <Domain example.com...>11 <Domain example.com...>
12 >>> from mailman.interfaces.usermanager import IUserManager
13 >>> user_manager = getUtility(IUserManager)
14 >>> user = user_manager.create_user('test@example.org')
15 >>> config.db.commit()
1216
13Domains are how Mailman interacts with email host names and web host names.17Domains are how Mailman interacts with email host names and web host names.
14::18::
@@ -28,17 +32,14 @@
28is the only required piece. The other parts are inferred from that.32is the only required piece. The other parts are inferred from that.
2933
30 >>> manager.add('example.org')34 >>> manager.add('example.org')
31 <Domain example.org, base_url: http://example.org,35 <Domain example.org, base_url: http://example.org>
32 contact_address: postmaster@example.org>
33 >>> show_domains()36 >>> show_domains()
34 <Domain example.org, base_url: http://example.org,37 <Domain example.org, base_url: http://example.org>
35 contact_address: postmaster@example.org>
3638
37We can remove domains too.39We can remove domains too.
3840
39 >>> manager.remove('example.org')41 >>> manager.remove('example.org')
40 <Domain example.org, base_url: http://example.org,42 <Domain example.org, base_url: http://example.org>
41 contact_address: postmaster@example.org>
42 >>> show_domains()43 >>> show_domains()
43 no domains44 no domains
4445
@@ -46,30 +47,34 @@
46web interface for the domain.47web interface for the domain.
4748
48 >>> manager.add('example.com', base_url='https://mail.example.com')49 >>> manager.add('example.com', base_url='https://mail.example.com')
49 <Domain example.com, base_url: https://mail.example.com,50 <Domain example.com, base_url: https://mail.example.com>
50 contact_address: postmaster@example.com>
51 >>> show_domains()51 >>> show_domains()
52 <Domain example.com, base_url: https://mail.example.com,52 <Domain example.com, base_url: https://mail.example.com>
53 contact_address: postmaster@example.com>
5453
55Domains can have explicit descriptions and contact addresses.54Domains can have explicit descriptions.
56::55::
5756
58 >>> manager.add(57 >>> manager.add(
59 ... 'example.net',58 ... 'example.net',
60 ... base_url='http://lists.example.net',59 ... base_url='http://lists.example.net',
61 ... contact_address='postmaster@example.com',60 ... description='The example domain',
62 ... description='The example domain')61 ... owners=['user@domain.com'])
63 <Domain example.net, The example domain,62 <Domain example.net, The example domain,
64 base_url: http://lists.example.net,63 base_url: http://lists.example.net>
65 contact_address: postmaster@example.com>
6664
67 >>> show_domains()65 >>> show_domains()
68 <Domain example.com, base_url: https://mail.example.com,66 <Domain example.com, base_url: https://mail.example.com>
69 contact_address: postmaster@example.com>
70 <Domain example.net, The example domain,67 <Domain example.net, The example domain,
71 base_url: http://lists.example.net,68 base_url: http://lists.example.net>
72 contact_address: postmaster@example.com>69
70Domains can have multiple owners, ideally one of the owners should have a
71verified preferred address. However this is not checked right now and
72contact_address from config can be used as a fallback.
73::
74
75 >>> net_domain = manager['example.net']
76 >>> net_domain.add_owner('test@example.org')
77
7378
74Domains can list all associated mailing lists with the mailing_lists property.79Domains can list all associated mailing lists with the mailing_lists property.
75::80::
@@ -105,8 +110,7 @@
105110
106 >>> print(manager['example.net'])111 >>> print(manager['example.net'])
107 <Domain example.net, The example domain,112 <Domain example.net, The example domain,
108 base_url: http://lists.example.net,113 base_url: http://lists.example.net>
109 contact_address: postmaster@example.com>
110114
111As with dictionaries, you can also get the domain. If the domain does not115As with dictionaries, you can also get the domain. If the domain does not
112exist, ``None`` or a default is returned.116exist, ``None`` or a default is returned.
@@ -114,8 +118,7 @@
114118
115 >>> print(manager.get('example.net'))119 >>> print(manager.get('example.net'))
116 <Domain example.net, The example domain,120 <Domain example.net, The example domain,
117 base_url: http://lists.example.net,121 base_url: http://lists.example.net>
118 contact_address: postmaster@example.com>
119122
120 >>> print(manager.get('doesnotexist.com'))123 >>> print(manager.get('doesnotexist.com'))
121 None124 None
122125
=== modified file 'src/mailman/model/domain.py'
--- src/mailman/model/domain.py 2015-01-05 01:40:47 +0000
+++ src/mailman/model/domain.py 2015-04-05 22:33:23 +0000
@@ -28,11 +28,15 @@
28from mailman.interfaces.domain import (28from mailman.interfaces.domain import (
29 BadDomainSpecificationError, DomainCreatedEvent, DomainCreatingEvent,29 BadDomainSpecificationError, DomainCreatedEvent, DomainCreatingEvent,
30 DomainDeletedEvent, DomainDeletingEvent, IDomain, IDomainManager)30 DomainDeletedEvent, DomainDeletingEvent, IDomain, IDomainManager)
31from mailman.interfaces.usermanager import IUserManager
31from mailman.model.mailinglist import MailingList32from mailman.model.mailinglist import MailingList
33from mailman.model.user import User, DomainOwner
32from urllib.parse import urljoin, urlparse34from urllib.parse import urljoin, urlparse
33from sqlalchemy import Column, Integer, Unicode35from sqlalchemy import Column, Integer, Unicode
36from sqlalchemy.orm import relationship, backref
34from zope.event import notify37from zope.event import notify
35from zope.interface import implementer38from zope.interface import implementer
39from zope.component import getUtility
3640
3741
3842
3943
@@ -44,15 +48,17 @@
4448
45 id = Column(Integer, primary_key=True)49 id = Column(Integer, primary_key=True)
4650
47 mail_host = Column(Unicode) # TODO: add index?51 mail_host = Column(Unicode)
48 base_url = Column(Unicode)52 base_url = Column(Unicode)
49 description = Column(Unicode)53 description = Column(Unicode)
50 contact_address = Column(Unicode)54 owners = relationship("User",
55 secondary="domain_owner",
56 backref="domains")
5157
52 def __init__(self, mail_host,58 def __init__(self, mail_host,
53 description=None,59 description=None,
54 base_url=None,60 base_url=None,
55 contact_address=None):61 owners=[]):
56 """Create and register a domain.62 """Create and register a domain.
5763
58 :param mail_host: The host name for the email interface.64 :param mail_host: The host name for the email interface.
@@ -63,18 +69,16 @@
63 scheme. If not given, it will be constructed from the69 scheme. If not given, it will be constructed from the
64 `mail_host` using the http protocol.70 `mail_host` using the http protocol.
65 :type base_url: string71 :type base_url: string
66 :param contact_address: The email address to contact a human for this72 :param owners: List of `User` who are the owners of this domain
67 domain. If not given, postmaster@`mail_host` will be used.73 :type owners: list
68 :type contact_address: string
69 """74 """
70 self.mail_host = mail_host75 self.mail_host = mail_host
71 self.base_url = (base_url76 self.base_url = (base_url
72 if base_url is not None77 if base_url is not None
73 else 'http://' + mail_host)78 else 'http://' + mail_host)
74 self.description = description79 self.description = description
75 self.contact_address = (contact_address80 if len(owners):
76 if contact_address is not None81 self.add_owners(owners)
77 else 'postmaster@' + mail_host)
7882
79 @property83 @property
80 def url_host(self):84 def url_host(self):
@@ -103,13 +107,29 @@
103 def __repr__(self):107 def __repr__(self):
104 """repr(a_domain)"""108 """repr(a_domain)"""
105 if self.description is None:109 if self.description is None:
106 return ('<Domain {0.mail_host}, base_url: {0.base_url}, '110 return ('<Domain {0.mail_host}, base_url: {0.base_url}>').format(self)
107 'contact_address: {0.contact_address}>').format(self)
108 else:111 else:
109 return ('<Domain {0.mail_host}, {0.description}, '112 return ('<Domain {0.mail_host}, {0.description}, '
110 'base_url: {0.base_url}, '113 'base_url: {0.base_url}>').format(self)
111 'contact_address: {0.contact_address}>').format(self)114
112115 def add_owner(self, owner):
116 """Add a domain owner"""
117 user_manager = getUtility(IUserManager)
118 user = user_manager.get_user(owner)
119 if user is None:
120 user = user_manager.create_user(owner)
121 self.owners.append(user)
122
123 def add_owners(self, owners):
124 """Add multiple owners"""
125 assert(isinstance(owners, list))
126 for owner in owners:
127 self.add_owner(owner)
128
129 def remove_owner(self, owner):
130 """ Remove a domain owner"""
131 user_manager = getUtility(IUserManager)
132 self.owners.remove(user_manager.get_user(owner))
113133
114134
115135
116@implementer(IDomainManager)136@implementer(IDomainManager)
@@ -121,15 +141,16 @@
121 mail_host,141 mail_host,
122 description=None,142 description=None,
123 base_url=None,143 base_url=None,
124 contact_address=None):144 owners=[]):
125 """See `IDomainManager`."""145 """See `IDomainManager`."""
126 # Be sure the mail_host is not already registered. This is probably146 # Be sure the mail_host is not already registered. This is probably
127 # a constraint that should (also) be maintained in the database.147 # a constraint that should (also) be maintained in the database.
128 if self.get(mail_host) is not None:148 if self.get(mail_host) is not None:
129 raise BadDomainSpecificationError(149 raise BadDomainSpecificationError(
130 'Duplicate email host: %s' % mail_host)150 'Duplicate email host: %s' % mail_host)
151
131 notify(DomainCreatingEvent(mail_host))152 notify(DomainCreatingEvent(mail_host))
132 domain = Domain(mail_host, description, base_url, contact_address)153 domain = Domain(mail_host, description, base_url, owners)
133 store.add(domain)154 store.add(domain)
134 notify(DomainCreatedEvent(domain))155 notify(DomainCreatedEvent(domain))
135 return domain156 return domain
136157
=== modified file 'src/mailman/model/tests/test_domain.py'
--- src/mailman/model/tests/test_domain.py 2015-01-05 01:22:39 +0000
+++ src/mailman/model/tests/test_domain.py 2015-04-05 22:33:23 +0000
@@ -26,9 +26,11 @@
26import unittest26import unittest
2727
28from mailman.app.lifecycle import create_list28from mailman.app.lifecycle import create_list
29from mailman.config import config
29from mailman.interfaces.domain import (30from mailman.interfaces.domain import (
30 DomainCreatedEvent, DomainCreatingEvent, DomainDeletedEvent,31 DomainCreatedEvent, DomainCreatingEvent, DomainDeletedEvent,
31 DomainDeletingEvent, IDomainManager)32 DomainDeletingEvent, IDomainManager, BadDomainSpecificationError)
33from mailman.interfaces.usermanager import IUserManager
32from mailman.interfaces.listmanager import IListManager34from mailman.interfaces.listmanager import IListManager
33from mailman.testing.helpers import event_subscribers35from mailman.testing.helpers import event_subscribers
34from mailman.testing.layers import ConfigLayer36from mailman.testing.layers import ConfigLayer
@@ -78,6 +80,26 @@
78 # Trying to delete a missing domain gives you a KeyError.80 # Trying to delete a missing domain gives you a KeyError.
79 self.assertRaises(KeyError, self._manager.remove, 'doesnotexist.com')81 self.assertRaises(KeyError, self._manager.remove, 'doesnotexist.com')
8082
83 def test_domain_create_with_owner(self):
84 domain = self._manager.add('example.org',
85 owners=['someuser@example.org'])
86 self.assertEqual(len(domain.owners), 1)
87 self.assertEqual(domain.owners[0].addresses[0].email,
88 'someuser@example.org')
89
90 def test_add_domain_owner(self):
91 domain = self._manager.add('example.org')
92 domain.add_owner('someuser@example.org')
93 self.assertEqual(len(domain.owners), 1)
94 self.assertEqual(domain.owners[0].addresses[0].email,
95 'someuser@example.org')
96
97 def test_remove_domain_owner(self):
98 domain = self._manager.add('example.org',
99 owners=['someuser@example.org'])
100 domain.remove_owner('someuser@example.org')
101 self.assertEqual(len(domain.owners), 0)
102
81103
82104
83105
84class TestDomainLifecycleEvents(unittest.TestCase):106class TestDomainLifecycleEvents(unittest.TestCase):
85107
=== modified file 'src/mailman/model/user.py'
--- src/mailman/model/user.py 2015-03-20 16:38:00 +0000
+++ src/mailman/model/user.py 2015-04-05 22:33:23 +0000
@@ -19,6 +19,7 @@
1919
20__all__ = [20__all__ = [
21 'User',21 'User',
22 'DomainOwner'
22 ]23 ]
2324
2425
@@ -34,7 +35,7 @@
34from mailman.model.roster import Memberships35from mailman.model.roster import Memberships
35from mailman.utilities.datetime import factory as date_factory36from mailman.utilities.datetime import factory as date_factory
36from mailman.utilities.uid import UniqueIDFactory37from mailman.utilities.uid import UniqueIDFactory
37from sqlalchemy import Column, DateTime, ForeignKey, Integer, Unicode38from sqlalchemy import Column, DateTime, ForeignKey, Integer, Unicode, Boolean
38from sqlalchemy.orm import relationship, backref39from sqlalchemy.orm import relationship, backref
39from zope.event import notify40from zope.event import notify
40from zope.interface import implementer41from zope.interface import implementer
@@ -55,6 +56,7 @@
55 _password = Column('password', Unicode)56 _password = Column('password', Unicode)
56 _user_id = Column(UUID, index=True)57 _user_id = Column(UUID, index=True)
57 _created_on = Column(DateTime)58 _created_on = Column(DateTime)
59 is_server_owner = Column(Boolean, default=False)
5860
59 addresses = relationship(61 addresses = relationship(
60 'Address', backref='user',62 'Address', backref='user',
@@ -176,3 +178,11 @@
176 @property178 @property
177 def memberships(self):179 def memberships(self):
178 return Memberships(self)180 return Memberships(self)
181
182
183class DomainOwner(Model):
184 """Domain to owners(user) association class"""
185
186 __tablename__ = 'domain_owner'
187 user_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
188 domain_id = Column(Integer, ForeignKey('domain.id'), primary_key=True)
179189
=== modified file 'src/mailman/rest/docs/addresses.rst'
--- src/mailman/rest/docs/addresses.rst 2015-02-14 01:35:35 +0000
+++ src/mailman/rest/docs/addresses.rst 2015-04-05 22:33:23 +0000
@@ -190,6 +190,7 @@
190 created_on: 2005-08-01T07:49:23190 created_on: 2005-08-01T07:49:23
191 display_name: Cris X. Person191 display_name: Cris X. Person
192 http_etag: "..."192 http_etag: "..."
193 is_server_owner: False
193 password: ...194 password: ...
194 self_link: http://localhost:9001/3.0/users/1195 self_link: http://localhost:9001/3.0/users/1
195 user_id: 1196 user_id: 1
196197
=== modified file 'src/mailman/rest/docs/domains.rst'
--- src/mailman/rest/docs/domains.rst 2014-12-16 01:01:53 +0000
+++ src/mailman/rest/docs/domains.rst 2015-04-05 22:33:23 +0000
@@ -29,14 +29,12 @@
29 >>> domain_manager.add(29 >>> domain_manager.add(
30 ... 'example.com', 'An example domain', 'http://lists.example.com')30 ... 'example.com', 'An example domain', 'http://lists.example.com')
31 <Domain example.com, An example domain,31 <Domain example.com, An example domain,
32 base_url: http://lists.example.com,32 base_url: http://lists.example.com>
33 contact_address: postmaster@example.com>
34 >>> transaction.commit()33 >>> transaction.commit()
3534
36 >>> dump_json('http://localhost:9001/3.0/domains')35 >>> dump_json('http://localhost:9001/3.0/domains')
37 entry 0:36 entry 0:
38 base_url: http://lists.example.com37 base_url: http://lists.example.com
39 contact_address: postmaster@example.com
40 description: An example domain38 description: An example domain
41 http_etag: "..."39 http_etag: "..."
42 mail_host: example.com40 mail_host: example.com
@@ -51,24 +49,19 @@
5149
52 >>> domain_manager.add(50 >>> domain_manager.add(
53 ... 'example.org',51 ... 'example.org',
54 ... base_url='http://mail.example.org',52 ... base_url='http://mail.example.org')
55 ... contact_address='listmaster@example.org')53 <Domain example.org, base_url: http://mail.example.org>
56 <Domain example.org, base_url: http://mail.example.org,
57 contact_address: listmaster@example.org>
58 >>> domain_manager.add(54 >>> domain_manager.add(
59 ... 'lists.example.net',55 ... 'lists.example.net',
60 ... 'Porkmasters',56 ... 'Porkmasters',
61 ... 'http://example.net',57 ... 'http://example.net')
62 ... 'porkmaster@example.net')
63 <Domain lists.example.net, Porkmasters,58 <Domain lists.example.net, Porkmasters,
64 base_url: http://example.net,59 base_url: http://example.net>
65 contact_address: porkmaster@example.net>
66 >>> transaction.commit()60 >>> transaction.commit()
6761
68 >>> dump_json('http://localhost:9001/3.0/domains')62 >>> dump_json('http://localhost:9001/3.0/domains')
69 entry 0:63 entry 0:
70 base_url: http://lists.example.com64 base_url: http://lists.example.com
71 contact_address: postmaster@example.com
72 description: An example domain65 description: An example domain
73 http_etag: "..."66 http_etag: "..."
74 mail_host: example.com67 mail_host: example.com
@@ -76,7 +69,6 @@
76 url_host: lists.example.com69 url_host: lists.example.com
77 entry 1:70 entry 1:
78 base_url: http://mail.example.org71 base_url: http://mail.example.org
79 contact_address: listmaster@example.org
80 description: None72 description: None
81 http_etag: "..."73 http_etag: "..."
82 mail_host: example.org74 mail_host: example.org
@@ -84,7 +76,6 @@
84 url_host: mail.example.org76 url_host: mail.example.org
85 entry 2:77 entry 2:
86 base_url: http://example.net78 base_url: http://example.net
87 contact_address: porkmaster@example.net
88 description: Porkmasters79 description: Porkmasters
89 http_etag: "..."80 http_etag: "..."
90 mail_host: lists.example.net81 mail_host: lists.example.net
@@ -103,7 +94,6 @@
10394
104 >>> dump_json('http://localhost:9001/3.0/domains/lists.example.net')95 >>> dump_json('http://localhost:9001/3.0/domains/lists.example.net')
105 base_url: http://example.net96 base_url: http://example.net
106 contact_address: porkmaster@example.net
107 description: Porkmasters97 description: Porkmasters
108 http_etag: "..."98 http_etag: "..."
109 mail_host: lists.example.net99 mail_host: lists.example.net
@@ -165,7 +155,6 @@
165155
166 >>> dump_json('http://localhost:9001/3.0/domains/lists.example.com')156 >>> dump_json('http://localhost:9001/3.0/domains/lists.example.com')
167 base_url: http://lists.example.com157 base_url: http://lists.example.com
168 contact_address: postmaster@lists.example.com
169 description: None158 description: None
170 http_etag: "..."159 http_etag: "..."
171 mail_host: lists.example.com160 mail_host: lists.example.com
@@ -176,9 +165,7 @@
176::165::
177166
178 >>> domain_manager['lists.example.com']167 >>> domain_manager['lists.example.com']
179 <Domain lists.example.com,168 <Domain lists.example.com, base_url: http://lists.example.com>
180 base_url: http://lists.example.com,
181 contact_address: postmaster@lists.example.com>
182169
183 # Unlock the database.170 # Unlock the database.
184 >>> transaction.abort()171 >>> transaction.abort()
@@ -190,8 +177,7 @@
190 >>> dump_json('http://localhost:9001/3.0/domains', {177 >>> dump_json('http://localhost:9001/3.0/domains', {
191 ... 'mail_host': 'my.example.com',178 ... 'mail_host': 'my.example.com',
192 ... 'description': 'My new domain',179 ... 'description': 'My new domain',
193 ... 'base_url': 'http://allmy.example.com',180 ... 'base_url': 'http://allmy.example.com'
194 ... 'contact_address': 'helpme@example.com'
195 ... })181 ... })
196 content-length: 0182 content-length: 0
197 date: ...183 date: ...
@@ -200,7 +186,6 @@
200186
201 >>> dump_json('http://localhost:9001/3.0/domains/my.example.com')187 >>> dump_json('http://localhost:9001/3.0/domains/my.example.com')
202 base_url: http://allmy.example.com188 base_url: http://allmy.example.com
203 contact_address: helpme@example.com
204 description: My new domain189 description: My new domain
205 http_etag: "..."190 http_etag: "..."
206 mail_host: my.example.com191 mail_host: my.example.com
@@ -208,9 +193,7 @@
208 url_host: allmy.example.com193 url_host: allmy.example.com
209194
210 >>> domain_manager['my.example.com']195 >>> domain_manager['my.example.com']
211 <Domain my.example.com, My new domain,196 <Domain my.example.com, My new domain, base_url: http://allmy.example.com>
212 base_url: http://allmy.example.com,
213 contact_address: helpme@example.com>
214197
215 # Unlock the database.198 # Unlock the database.
216 >>> transaction.abort()199 >>> transaction.abort()
217200
=== modified file 'src/mailman/rest/docs/users.rst'
--- src/mailman/rest/docs/users.rst 2014-12-22 18:40:30 +0000
+++ src/mailman/rest/docs/users.rst 2015-04-05 22:33:23 +0000
@@ -34,6 +34,7 @@
34 created_on: 2005-08-01T07:49:2334 created_on: 2005-08-01T07:49:23
35 display_name: Anne Person35 display_name: Anne Person
36 http_etag: "..."36 http_etag: "..."
37 is_server_owner: False
37 self_link: http://localhost:9001/3.0/users/138 self_link: http://localhost:9001/3.0/users/1
38 user_id: 139 user_id: 1
39 http_etag: "..."40 http_etag: "..."
@@ -50,11 +51,13 @@
50 created_on: 2005-08-01T07:49:2351 created_on: 2005-08-01T07:49:23
51 display_name: Anne Person52 display_name: Anne Person
52 http_etag: "..."53 http_etag: "..."
54 is_server_owner: False
53 self_link: http://localhost:9001/3.0/users/155 self_link: http://localhost:9001/3.0/users/1
54 user_id: 156 user_id: 1
55 entry 1:57 entry 1:
56 created_on: 2005-08-01T07:49:2358 created_on: 2005-08-01T07:49:23
57 http_etag: "..."59 http_etag: "..."
60 is_server_owner: False
58 self_link: http://localhost:9001/3.0/users/261 self_link: http://localhost:9001/3.0/users/2
59 user_id: 262 user_id: 2
60 http_etag: "..."63 http_etag: "..."
@@ -76,6 +79,7 @@
76 created_on: 2005-08-01T07:49:2379 created_on: 2005-08-01T07:49:23
77 display_name: Anne Person80 display_name: Anne Person
78 http_etag: "..."81 http_etag: "..."
82 is_server_owner: False
79 self_link: http://localhost:9001/3.0/users/183 self_link: http://localhost:9001/3.0/users/1
80 user_id: 184 user_id: 1
81 http_etag: "..."85 http_etag: "..."
@@ -86,6 +90,7 @@
86 entry 0:90 entry 0:
87 created_on: 2005-08-01T07:49:2391 created_on: 2005-08-01T07:49:23
88 http_etag: "..."92 http_etag: "..."
93 is_server_owner: False
89 self_link: http://localhost:9001/3.0/users/294 self_link: http://localhost:9001/3.0/users/2
90 user_id: 295 user_id: 2
91 http_etag: "..."96 http_etag: "..."
@@ -120,6 +125,7 @@
120 >>> dump_json('http://localhost:9001/3.0/users/3')125 >>> dump_json('http://localhost:9001/3.0/users/3')
121 created_on: 2005-08-01T07:49:23126 created_on: 2005-08-01T07:49:23
122 http_etag: "..."127 http_etag: "..."
128 is_server_owner: False
123 password: {plaintext}...129 password: {plaintext}...
124 self_link: http://localhost:9001/3.0/users/3130 self_link: http://localhost:9001/3.0/users/3
125 user_id: 3131 user_id: 3
@@ -131,6 +137,7 @@
131 >>> dump_json('http://localhost:9001/3.0/users/cris@example.com')137 >>> dump_json('http://localhost:9001/3.0/users/cris@example.com')
132 created_on: 2005-08-01T07:49:23138 created_on: 2005-08-01T07:49:23
133 http_etag: "..."139 http_etag: "..."
140 is_server_owner: False
134 password: {plaintext}...141 password: {plaintext}...
135 self_link: http://localhost:9001/3.0/users/3142 self_link: http://localhost:9001/3.0/users/3
136 user_id: 3143 user_id: 3
@@ -158,6 +165,7 @@
158 created_on: 2005-08-01T07:49:23165 created_on: 2005-08-01T07:49:23
159 display_name: Dave Person166 display_name: Dave Person
160 http_etag: "..."167 http_etag: "..."
168 is_server_owner: False
161 password: {plaintext}...169 password: {plaintext}...
162 self_link: http://localhost:9001/3.0/users/4170 self_link: http://localhost:9001/3.0/users/4
163 user_id: 4171 user_id: 4
@@ -190,6 +198,7 @@
190 created_on: 2005-08-01T07:49:23198 created_on: 2005-08-01T07:49:23
191 display_name: Elly Person199 display_name: Elly Person
192 http_etag: "..."200 http_etag: "..."
201 is_server_owner: False
193 password: {plaintext}supersekrit202 password: {plaintext}supersekrit
194 self_link: http://localhost:9001/3.0/users/5203 self_link: http://localhost:9001/3.0/users/5
195 user_id: 5204 user_id: 5
@@ -214,6 +223,7 @@
214 created_on: 2005-08-01T07:49:23223 created_on: 2005-08-01T07:49:23
215 display_name: David Person224 display_name: David Person
216 http_etag: "..."225 http_etag: "..."
226 is_server_owner: False
217 password: {plaintext}...227 password: {plaintext}...
218 self_link: http://localhost:9001/3.0/users/4228 self_link: http://localhost:9001/3.0/users/4
219 user_id: 4229 user_id: 4
@@ -238,6 +248,7 @@
238 created_on: 2005-08-01T07:49:23248 created_on: 2005-08-01T07:49:23
239 display_name: David Person249 display_name: David Person
240 http_etag: "..."250 http_etag: "..."
251 is_server_owner: False
241 password: {plaintext}clockwork angels252 password: {plaintext}clockwork angels
242 self_link: http://localhost:9001/3.0/users/4253 self_link: http://localhost:9001/3.0/users/4
243 user_id: 4254 user_id: 4
@@ -260,6 +271,7 @@
260 created_on: 2005-08-01T07:49:23271 created_on: 2005-08-01T07:49:23
261 display_name: David Personhood272 display_name: David Personhood
262 http_etag: "..."273 http_etag: "..."
274 is_server_owner: False
263 password: {plaintext}the garden275 password: {plaintext}the garden
264 self_link: http://localhost:9001/3.0/users/4276 self_link: http://localhost:9001/3.0/users/4
265 user_id: 4277 user_id: 4
@@ -343,6 +355,7 @@
343 created_on: 2005-08-01T07:49:23355 created_on: 2005-08-01T07:49:23
344 display_name: Fred Person356 display_name: Fred Person
345 http_etag: "..."357 http_etag: "..."
358 is_server_owner: False
346 self_link: http://localhost:9001/3.0/users/6359 self_link: http://localhost:9001/3.0/users/6
347 user_id: 6360 user_id: 6
348361
@@ -350,6 +363,7 @@
350 created_on: 2005-08-01T07:49:23363 created_on: 2005-08-01T07:49:23
351 display_name: Fred Person364 display_name: Fred Person
352 http_etag: "..."365 http_etag: "..."
366 is_server_owner: False
353 self_link: http://localhost:9001/3.0/users/6367 self_link: http://localhost:9001/3.0/users/6
354 user_id: 6368 user_id: 6
355369
@@ -357,6 +371,7 @@
357 created_on: 2005-08-01T07:49:23371 created_on: 2005-08-01T07:49:23
358 display_name: Fred Person372 display_name: Fred Person
359 http_etag: "..."373 http_etag: "..."
374 is_server_owner: False
360 self_link: http://localhost:9001/3.0/users/6375 self_link: http://localhost:9001/3.0/users/6
361 user_id: 6376 user_id: 6
362377
@@ -364,6 +379,7 @@
364 created_on: 2005-08-01T07:49:23379 created_on: 2005-08-01T07:49:23
365 display_name: Fred Person380 display_name: Fred Person
366 http_etag: "..."381 http_etag: "..."
382 is_server_owner: False
367 self_link: http://localhost:9001/3.0/users/6383 self_link: http://localhost:9001/3.0/users/6
368 user_id: 6384 user_id: 6
369385
@@ -382,6 +398,7 @@
382 created_on: 2005-08-01T07:49:23398 created_on: 2005-08-01T07:49:23
383 display_name: Elly Person399 display_name: Elly Person
384 http_etag: "..."400 http_etag: "..."
401 is_server_owner: False
385 password: {plaintext}supersekrit402 password: {plaintext}supersekrit
386 self_link: http://localhost:9001/3.0/users/5403 self_link: http://localhost:9001/3.0/users/5
387 user_id: 5404 user_id: 5
388405
=== modified file 'src/mailman/rest/domains.py'
--- src/mailman/rest/domains.py 2015-01-05 01:40:47 +0000
+++ src/mailman/rest/domains.py 2015-04-05 22:33:23 +0000
@@ -25,10 +25,12 @@
2525
26from mailman.interfaces.domain import (26from mailman.interfaces.domain import (
27 BadDomainSpecificationError, IDomainManager)27 BadDomainSpecificationError, IDomainManager)
28from mailman.interfaces.usermanager import IUserManager
28from mailman.rest.helpers import (29from mailman.rest.helpers import (
29 BadRequest, CollectionMixin, NotFound, bad_request, child, created, etag,30 BadRequest, CollectionMixin, NotFound, bad_request, child, created, etag,
30 no_content, not_found, okay, path_to)31 no_content, not_found, okay, path_to)
31from mailman.rest.lists import ListsForDomain32from mailman.rest.lists import ListsForDomain
33from mailman.rest.users import OwnersForDomain
32from mailman.rest.validator import Validator34from mailman.rest.validator import Validator
33from zope.component import getUtility35from zope.component import getUtility
3436
@@ -41,7 +43,6 @@
41 """See `CollectionMixin`."""43 """See `CollectionMixin`."""
42 return dict(44 return dict(
43 base_url=domain.base_url,45 base_url=domain.base_url,
44 contact_address=domain.contact_address,
45 description=domain.description,46 description=domain.description,
46 mail_host=domain.mail_host,47 mail_host=domain.mail_host,
47 self_link=path_to('domains/{0}'.format(domain.mail_host)),48 self_link=path_to('domains/{0}'.format(domain.mail_host)),
@@ -88,6 +89,17 @@
88 else:89 else:
89 return BadRequest(), []90 return BadRequest(), []
9091
92 @child()
93 def owners(self, request, segments):
94 """/domains/<domain>/owners"""
95 if len(segments) == 0:
96 domain = getUtility(IDomainManager).get(self._domain)
97 if domain is None:
98 return NotFound()
99 return OwnersForDomain(domain)
100 else:
101 return BadRequest(), []
102
91103
92class AllDomains(_DomainBase):104class AllDomains(_DomainBase):
93 """The domains."""105 """The domains."""
@@ -99,12 +111,13 @@
99 validator = Validator(mail_host=str,111 validator = Validator(mail_host=str,
100 description=str,112 description=str,
101 base_url=str,113 base_url=str,
102 contact_address=str,114 owners=list,
103 _optional=('description', 'base_url',115 _optional=('description', 'base_url',
104 'contact_address'))116 'owners'))
105 domain = domain_manager.add(**validator(request))117 values = validator(request)
106 except BadDomainSpecificationError:118 domain = domain_manager.add(**values)
107 bad_request(response, b'Domain exists')119 except BadDomainSpecificationError as error:
120 bad_request(response, str(error))
108 except ValueError as error:121 except ValueError as error:
109 bad_request(response, str(error))122 bad_request(response, str(error))
110 else:123 else:
111124
=== modified file 'src/mailman/rest/lists.py'
--- src/mailman/rest/lists.py 2015-01-05 01:40:47 +0000
+++ src/mailman/rest/lists.py 2015-04-05 22:33:23 +0000
@@ -1,4 +1,4 @@
1# Copyright (C) 2010-2015 by the Free Software Foundation, Inc.1 # Copyright (C) 2010-2015 by the Free Software Foundation, Inc.
2#2#
3# This file is part of GNU Mailman.3# This file is part of GNU Mailman.
4#4#
55
=== modified file 'src/mailman/rest/tests/test_domains.py'
--- src/mailman/rest/tests/test_domains.py 2015-01-05 01:40:47 +0000
+++ src/mailman/rest/tests/test_domains.py 2015-04-05 22:33:23 +0000
@@ -27,6 +27,7 @@
27from mailman.app.lifecycle import create_list27from mailman.app.lifecycle import create_list
28from mailman.database.transaction import transaction28from mailman.database.transaction import transaction
29from mailman.interfaces.listmanager import IListManager29from mailman.interfaces.listmanager import IListManager
30from mailman.interfaces.domain import IDomainManager
30from mailman.testing.helpers import call_api31from mailman.testing.helpers import call_api
31from mailman.testing.layers import RESTLayer32from mailman.testing.layers import RESTLayer
32from urllib.error import HTTPError33from urllib.error import HTTPError
@@ -41,6 +42,17 @@
41 with transaction():42 with transaction():
42 self._mlist = create_list('test@example.com')43 self._mlist = create_list('test@example.com')
4344
45 def test_create_domains(self):
46 """Test Create domain via REST"""
47 data = {'mail_host': 'example.org',
48 'description': 'Example domain',
49 'base_url': 'http://example.org',
50 'owners': ['someone@example.com',
51 'secondowner@example.com',]}
52 content, response = call_api('http://localhost:9001/3.0/domains',
53 data, method="POST")
54 self.assertEqual(response.status, 201)
55
44 def test_bogus_endpoint_extension(self):56 def test_bogus_endpoint_extension(self):
45 # /domains/<domain>/lists/<anything> is not a valid endpoint.57 # /domains/<domain>/lists/<anything> is not a valid endpoint.
46 with self.assertRaises(HTTPError) as cm:58 with self.assertRaises(HTTPError) as cm:
4759
=== modified file 'src/mailman/rest/users.py'
--- src/mailman/rest/users.py 2015-03-20 16:38:00 +0000
+++ src/mailman/rest/users.py 2015-04-05 22:33:23 +0000
@@ -22,6 +22,7 @@
22 'AddressUser',22 'AddressUser',
23 'AllUsers',23 'AllUsers',
24 'Login',24 'Login',
25 'OwnersForDomain',
25 ]26 ]
2627
2728
@@ -67,8 +68,9 @@
67 email=str,68 email=str,
68 display_name=str,69 display_name=str,
69 password=str,70 password=str,
70 _optional=('display_name', 'password'),71 is_server_owner=bool,
71 )72 _optional=('display_name', 'password', 'is_server_owner'),
73)
7274
7375
7476
7577
@@ -108,7 +110,8 @@
108 user_id=user_id,110 user_id=user_id,
109 created_on=user.created_on,111 created_on=user.created_on,
110 self_link=path_to('users/{}'.format(user_id)),112 self_link=path_to('users/{}'.format(user_id)),
111 )113 is_server_owner=user.is_server_owner,
114 )
112 # Add the password attribute, only if the user has a password. Same115 # Add the password attribute, only if the user has a password. Same
113 # with the real name. These could be None or the empty string.116 # with the real name. These could be None or the empty string.
114 if user.password:117 if user.password:
@@ -293,7 +296,8 @@
293 del fields['email']296 del fields['email']
294 fields['user_id'] = int297 fields['user_id'] = int
295 fields['auto_create'] = as_boolean298 fields['auto_create'] = as_boolean
296 fields['_optional'] = fields['_optional'] + ('user_id', 'auto_create')299 fields['_optional'] = fields['_optional'] + ('user_id', 'auto_create',
300 'is_server_owner')
297 try:301 try:
298 validator = Validator(**fields)302 validator = Validator(**fields)
299 arguments = validator(request)303 arguments = validator(request)
@@ -328,7 +332,8 @@
328 # Process post data and check for an existing user.332 # Process post data and check for an existing user.
329 fields = CREATION_FIELDS.copy()333 fields = CREATION_FIELDS.copy()
330 fields['user_id'] = int334 fields['user_id'] = int
331 fields['_optional'] = fields['_optional'] + ('user_id', 'email')335 fields['_optional'] = fields['_optional'] + ('user_id', 'email',
336 'is_server_owner')
332 try:337 try:
333 validator = Validator(**fields)338 validator = Validator(**fields)
334 arguments = validator(request)339 arguments = validator(request)
@@ -377,3 +382,41 @@
377 no_content(response)382 no_content(response)
378 else:383 else:
379 forbidden(response)384 forbidden(response)
385
386class OwnersForDomain(_UserBase):
387 """Owners for a particular domain."""
388
389 def __init__(self, domain):
390 self._domain = domain
391
392 def on_get(self, request, response):
393 """/domains/<domain>/owners"""
394 resource = self._make_collection(request)
395 okay(response, etag(resource))
396
397 def on_post(self, request, response):
398 """POST to /domains/<domain>/owners """
399 validator = Validator(owner=GetterSetter(str))
400 try:
401 values = validator(request)
402 except ValueError as error:
403 bad_request(response, str(error))
404 return
405 self._domain.add_owner(values['owner'])
406 return no_content(response)
407
408 def on_delete(self, request, response):
409 """DELETE to /domains/<domain>/owners"""
410 validator = Validator(owner=GetterSetter(str))
411 try:
412 values = validator(request)
413 except ValueError as error:
414 bad_request(response, str(error))
415 return
416 self._domain.remove_owner(owner)
417 return no_content(response)
418
419 @paginate
420 def _get_collection(self, request):
421 """See `CollectionMixin`."""
422 return list(self._domain.owners)
380423
=== modified file 'src/mailman/testing/layers.py'
--- src/mailman/testing/layers.py 2015-01-05 01:22:39 +0000
+++ src/mailman/testing/layers.py 2015-04-05 22:33:23 +0000
@@ -200,7 +200,7 @@
200 with transaction():200 with transaction():
201 getUtility(IDomainManager).add(201 getUtility(IDomainManager).add(
202 'example.com', 'An example domain.',202 'example.com', 'An example domain.',
203 'http://lists.example.com', 'postmaster@example.com')203 'http://lists.example.com')
204204
205 @classmethod205 @classmethod
206 def testTearDown(cls):206 def testTearDown(cls):