Merge lp:~raj-abhilash1/mailman/bug_1423756 into lp:mailman
- bug_1423756
- Merge into 3.0
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Barry Warsaw | Needs Fixing | ||
Review via email: mp+254479@code.launchpad.net |
Commit message
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.
Barry Warsaw (barry) wrote : | # |
Also, the confirm.txt file lives in src/mailman/
- 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
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.
Barry Warsaw (barry) wrote : | # |
A few inlined replies.
- 7315. By Abhilash Raj
-
* implement left over methods
* add and remove owners using the address
Abhilash Raj (raj-abhilash1) wrote : | # |
@Barry: I have Updated the branch as per your comments.
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.
* 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.
More to come as I continue with the merge.
Barry Warsaw (barry) wrote : | # |
Oh, I'm adding a lot more tests for corner cases and such. :)
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.
Barry Warsaw (barry) wrote : | # |
Added doctest for domain owners.
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://
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.
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
1 | === modified file 'src/mailman/app/registrar.py' | |||
2 | --- src/mailman/app/registrar.py 2015-03-28 20:00:24 +0000 | |||
3 | +++ src/mailman/app/registrar.py 2015-04-05 22:33:23 +0000 | |||
4 | @@ -161,8 +161,6 @@ | |||
5 | 161 | # For i18n interpolation. | 161 | # For i18n interpolation. |
6 | 162 | confirm_url = mlist.domain.confirm_url(event.token) | 162 | confirm_url = mlist.domain.confirm_url(event.token) |
7 | 163 | email_address = event.pendable['email'] | 163 | email_address = event.pendable['email'] |
8 | 164 | domain_name = mlist.domain.mail_host | ||
9 | 165 | contact_address = mlist.domain.contact_address | ||
10 | 166 | # Send a verification email to the address. | 164 | # Send a verification email to the address. |
11 | 167 | template = getUtility(ITemplateLoader).get( | 165 | template = getUtility(ITemplateLoader).get( |
12 | 168 | 'mailman:///{0}/{1}/confirm.txt'.format( | 166 | 'mailman:///{0}/{1}/confirm.txt'.format( |
13 | 169 | 167 | ||
14 | === modified file 'src/mailman/commands/docs/create.rst' | |||
15 | --- src/mailman/commands/docs/create.rst 2014-04-28 15:23:35 +0000 | |||
16 | +++ src/mailman/commands/docs/create.rst 2015-04-05 22:33:23 +0000 | |||
17 | @@ -44,8 +44,7 @@ | |||
18 | 44 | 44 | ||
19 | 45 | >>> from mailman.interfaces.domain import IDomainManager | 45 | >>> from mailman.interfaces.domain import IDomainManager |
20 | 46 | >>> getUtility(IDomainManager).get('example.xx') | 46 | >>> getUtility(IDomainManager).get('example.xx') |
23 | 47 | <Domain example.xx, base_url: http://example.xx, | 47 | <Domain example.xx, base_url: http://example.xx> |
22 | 48 | contact_address: postmaster@example.xx> | ||
24 | 49 | 48 | ||
25 | 50 | You can also create mailing lists in existing domains without the | 49 | You can also create mailing lists in existing domains without the |
26 | 51 | auto-creation flag. | 50 | auto-creation flag. |
27 | 52 | 51 | ||
28 | === modified file 'src/mailman/commands/tests/test_lists.py' | |||
29 | --- src/mailman/commands/tests/test_lists.py 2015-03-14 01:16:51 +0000 | |||
30 | +++ src/mailman/commands/tests/test_lists.py 2015-04-05 22:33:23 +0000 | |||
31 | @@ -48,7 +48,7 @@ | |||
32 | 48 | # LP: #1166911 - non-matching lists were returned. | 48 | # LP: #1166911 - non-matching lists were returned. |
33 | 49 | getUtility(IDomainManager).add( | 49 | getUtility(IDomainManager).add( |
34 | 50 | 'example.net', 'An example domain.', | 50 | 'example.net', 'An example domain.', |
36 | 51 | 'http://lists.example.net', 'postmaster@example.net') | 51 | 'http://lists.example.net') |
37 | 52 | create_list('test1@example.com') | 52 | create_list('test1@example.com') |
38 | 53 | create_list('test2@example.com') | 53 | create_list('test2@example.com') |
39 | 54 | # Only this one should show up. | 54 | # Only this one should show up. |
40 | 55 | 55 | ||
41 | === added file 'src/mailman/database/alembic/versions/46e92facee7_add_serverowner_domainowner.py' | |||
42 | --- src/mailman/database/alembic/versions/46e92facee7_add_serverowner_domainowner.py 1970-01-01 00:00:00 +0000 | |||
43 | +++ src/mailman/database/alembic/versions/46e92facee7_add_serverowner_domainowner.py 2015-04-05 22:33:23 +0000 | |||
44 | @@ -0,0 +1,36 @@ | |||
45 | 1 | """add_serverowner_domainowner | ||
46 | 2 | |||
47 | 3 | Revision ID: 46e92facee7 | ||
48 | 4 | Revises: 33e1f5f6fa8 | ||
49 | 5 | Create Date: 2015-03-20 16:01:25.007242 | ||
50 | 6 | |||
51 | 7 | """ | ||
52 | 8 | |||
53 | 9 | # revision identifiers, used by Alembic. | ||
54 | 10 | revision = '46e92facee7' | ||
55 | 11 | down_revision = '33e1f5f6fa8' | ||
56 | 12 | |||
57 | 13 | from alembic import op | ||
58 | 14 | import sqlalchemy as sa | ||
59 | 15 | |||
60 | 16 | |||
61 | 17 | def upgrade(): | ||
62 | 18 | op.create_table('domain_owner', | ||
63 | 19 | sa.Column('user_id', sa.Integer(), nullable=False), | ||
64 | 20 | sa.Column('domain_id', sa.Integer(), nullable=False), | ||
65 | 21 | sa.ForeignKeyConstraint(['domain_id'], ['domain.id'], ), | ||
66 | 22 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), | ||
67 | 23 | sa.PrimaryKeyConstraint('user_id', 'domain_id') | ||
68 | 24 | ) | ||
69 | 25 | op.add_column('user', sa.Column('is_server_owner', sa.Boolean(), | ||
70 | 26 | nullable=True)) | ||
71 | 27 | if op.get_bind().dialect.name != 'sqlite': | ||
72 | 28 | op.drop_column('domain', 'contact_address') | ||
73 | 29 | |||
74 | 30 | |||
75 | 31 | def downgrade(): | ||
76 | 32 | if op.get_bind().dialect.name != 'sqlite': | ||
77 | 33 | op.drop_column('user', 'is_server_owner') | ||
78 | 34 | op.add_column('domain', sa.Column('contact_address', sa.VARCHAR(), | ||
79 | 35 | nullable=True)) | ||
80 | 36 | op.drop_table('domain_owner') | ||
81 | 0 | 37 | ||
82 | === modified file 'src/mailman/interfaces/domain.py' | |||
83 | --- src/mailman/interfaces/domain.py 2015-01-05 01:22:39 +0000 | |||
84 | +++ src/mailman/interfaces/domain.py 2015-04-05 22:33:23 +0000 | |||
85 | @@ -88,9 +88,8 @@ | |||
86 | 88 | description = Attribute( | 88 | description = Attribute( |
87 | 89 | 'The human readable description of the domain name.') | 89 | 'The human readable description of the domain name.') |
88 | 90 | 90 | ||
92 | 91 | contact_address = Attribute("""\ | 91 | owners = Attribute("""\ |
93 | 92 | The contact address for the human at this domain. | 92 | The relationship with the user database representing domain owners""") |
91 | 93 | E.g. postmaster@example.com""") | ||
94 | 94 | 93 | ||
95 | 95 | mailing_lists = Attribute( | 94 | mailing_lists = Attribute( |
96 | 96 | """All mailing lists for this domain. | 95 | """All mailing lists for this domain. |
97 | @@ -112,7 +111,7 @@ | |||
98 | 112 | class IDomainManager(Interface): | 111 | class IDomainManager(Interface): |
99 | 113 | """The manager of domains.""" | 112 | """The manager of domains.""" |
100 | 114 | 113 | ||
102 | 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): |
103 | 116 | """Add a new domain. | 115 | """Add a new domain. |
104 | 117 | 116 | ||
105 | 118 | :param mail_host: The email host name for the domain. | 117 | :param mail_host: The email host name for the domain. |
106 | @@ -123,10 +122,8 @@ | |||
107 | 123 | interface of the domain. If not given, it defaults to | 122 | interface of the domain. If not given, it defaults to |
108 | 124 | http://`mail_host`/ | 123 | http://`mail_host`/ |
109 | 125 | :type base_url: string | 124 | :type base_url: string |
114 | 126 | :param contact_address: The email contact address for the human | 125 | :param owners: List of owners of the domain, defaults to None |
115 | 127 | managing the domain. If not given, defaults to | 126 | :type owners: list |
112 | 128 | postmaster@`mail_host` | ||
113 | 129 | :type contact_address: string | ||
116 | 130 | :return: The new domain object | 127 | :return: The new domain object |
117 | 131 | :rtype: `IDomain` | 128 | :rtype: `IDomain` |
118 | 132 | :raises `BadDomainSpecificationError`: when the `mail_host` is | 129 | :raises `BadDomainSpecificationError`: when the `mail_host` is |
119 | 133 | 130 | ||
120 | === modified file 'src/mailman/model/docs/domains.rst' | |||
121 | --- src/mailman/model/docs/domains.rst 2014-12-13 18:26:05 +0000 | |||
122 | +++ src/mailman/model/docs/domains.rst 2015-04-05 22:33:23 +0000 | |||
123 | @@ -9,6 +9,10 @@ | |||
124 | 9 | >>> manager = getUtility(IDomainManager) | 9 | >>> manager = getUtility(IDomainManager) |
125 | 10 | >>> manager.remove('example.com') | 10 | >>> manager.remove('example.com') |
126 | 11 | <Domain example.com...> | 11 | <Domain example.com...> |
127 | 12 | >>> from mailman.interfaces.usermanager import IUserManager | ||
128 | 13 | >>> user_manager = getUtility(IUserManager) | ||
129 | 14 | >>> user = user_manager.create_user('test@example.org') | ||
130 | 15 | >>> config.db.commit() | ||
131 | 12 | 16 | ||
132 | 13 | Domains are how Mailman interacts with email host names and web host names. | 17 | Domains are how Mailman interacts with email host names and web host names. |
133 | 14 | :: | 18 | :: |
134 | @@ -28,17 +32,14 @@ | |||
135 | 28 | is the only required piece. The other parts are inferred from that. | 32 | is the only required piece. The other parts are inferred from that. |
136 | 29 | 33 | ||
137 | 30 | >>> manager.add('example.org') | 34 | >>> manager.add('example.org') |
140 | 31 | <Domain example.org, base_url: http://example.org, | 35 | <Domain example.org, base_url: http://example.org> |
139 | 32 | contact_address: postmaster@example.org> | ||
141 | 33 | >>> show_domains() | 36 | >>> show_domains() |
144 | 34 | <Domain example.org, base_url: http://example.org, | 37 | <Domain example.org, base_url: http://example.org> |
143 | 35 | contact_address: postmaster@example.org> | ||
145 | 36 | 38 | ||
146 | 37 | We can remove domains too. | 39 | We can remove domains too. |
147 | 38 | 40 | ||
148 | 39 | >>> manager.remove('example.org') | 41 | >>> manager.remove('example.org') |
151 | 40 | <Domain example.org, base_url: http://example.org, | 42 | <Domain example.org, base_url: http://example.org> |
150 | 41 | contact_address: postmaster@example.org> | ||
152 | 42 | >>> show_domains() | 43 | >>> show_domains() |
153 | 43 | no domains | 44 | no domains |
154 | 44 | 45 | ||
155 | @@ -46,30 +47,34 @@ | |||
156 | 46 | web interface for the domain. | 47 | web interface for the domain. |
157 | 47 | 48 | ||
158 | 48 | >>> manager.add('example.com', base_url='https://mail.example.com') | 49 | >>> manager.add('example.com', base_url='https://mail.example.com') |
161 | 49 | <Domain example.com, base_url: https://mail.example.com, | 50 | <Domain example.com, base_url: https://mail.example.com> |
160 | 50 | contact_address: postmaster@example.com> | ||
162 | 51 | >>> show_domains() | 51 | >>> show_domains() |
165 | 52 | <Domain example.com, base_url: https://mail.example.com, | 52 | <Domain example.com, base_url: https://mail.example.com> |
164 | 53 | contact_address: postmaster@example.com> | ||
166 | 54 | 53 | ||
168 | 55 | Domains can have explicit descriptions and contact addresses. | 54 | Domains can have explicit descriptions. |
169 | 56 | :: | 55 | :: |
170 | 57 | 56 | ||
171 | 58 | >>> manager.add( | 57 | >>> manager.add( |
172 | 59 | ... 'example.net', | 58 | ... 'example.net', |
173 | 60 | ... base_url='http://lists.example.net', | 59 | ... base_url='http://lists.example.net', |
176 | 61 | ... contact_address='postmaster@example.com', | 60 | ... description='The example domain', |
177 | 62 | ... description='The example domain') | 61 | ... owners=['user@domain.com']) |
178 | 63 | <Domain example.net, The example domain, | 62 | <Domain example.net, The example domain, |
181 | 64 | base_url: http://lists.example.net, | 63 | base_url: http://lists.example.net> |
180 | 65 | contact_address: postmaster@example.com> | ||
182 | 66 | 64 | ||
183 | 67 | >>> show_domains() | 65 | >>> show_domains() |
186 | 68 | <Domain example.com, base_url: https://mail.example.com, | 66 | <Domain example.com, base_url: https://mail.example.com> |
185 | 69 | contact_address: postmaster@example.com> | ||
187 | 70 | <Domain example.net, The example domain, | 67 | <Domain example.net, The example domain, |
190 | 71 | base_url: http://lists.example.net, | 68 | base_url: http://lists.example.net> |
191 | 72 | contact_address: postmaster@example.com> | 69 | |
192 | 70 | Domains can have multiple owners, ideally one of the owners should have a | ||
193 | 71 | verified preferred address. However this is not checked right now and | ||
194 | 72 | contact_address from config can be used as a fallback. | ||
195 | 73 | :: | ||
196 | 74 | |||
197 | 75 | >>> net_domain = manager['example.net'] | ||
198 | 76 | >>> net_domain.add_owner('test@example.org') | ||
199 | 77 | |||
200 | 73 | 78 | ||
201 | 74 | Domains can list all associated mailing lists with the mailing_lists property. | 79 | Domains can list all associated mailing lists with the mailing_lists property. |
202 | 75 | :: | 80 | :: |
203 | @@ -105,8 +110,7 @@ | |||
204 | 105 | 110 | ||
205 | 106 | >>> print(manager['example.net']) | 111 | >>> print(manager['example.net']) |
206 | 107 | <Domain example.net, The example domain, | 112 | <Domain example.net, The example domain, |
209 | 108 | base_url: http://lists.example.net, | 113 | base_url: http://lists.example.net> |
208 | 109 | contact_address: postmaster@example.com> | ||
210 | 110 | 114 | ||
211 | 111 | As with dictionaries, you can also get the domain. If the domain does not | 115 | As with dictionaries, you can also get the domain. If the domain does not |
212 | 112 | exist, ``None`` or a default is returned. | 116 | exist, ``None`` or a default is returned. |
213 | @@ -114,8 +118,7 @@ | |||
214 | 114 | 118 | ||
215 | 115 | >>> print(manager.get('example.net')) | 119 | >>> print(manager.get('example.net')) |
216 | 116 | <Domain example.net, The example domain, | 120 | <Domain example.net, The example domain, |
219 | 117 | base_url: http://lists.example.net, | 121 | base_url: http://lists.example.net> |
218 | 118 | contact_address: postmaster@example.com> | ||
220 | 119 | 122 | ||
221 | 120 | >>> print(manager.get('doesnotexist.com')) | 123 | >>> print(manager.get('doesnotexist.com')) |
222 | 121 | None | 124 | None |
223 | 122 | 125 | ||
224 | === modified file 'src/mailman/model/domain.py' | |||
225 | --- src/mailman/model/domain.py 2015-01-05 01:40:47 +0000 | |||
226 | +++ src/mailman/model/domain.py 2015-04-05 22:33:23 +0000 | |||
227 | @@ -28,11 +28,15 @@ | |||
228 | 28 | from mailman.interfaces.domain import ( | 28 | from mailman.interfaces.domain import ( |
229 | 29 | BadDomainSpecificationError, DomainCreatedEvent, DomainCreatingEvent, | 29 | BadDomainSpecificationError, DomainCreatedEvent, DomainCreatingEvent, |
230 | 30 | DomainDeletedEvent, DomainDeletingEvent, IDomain, IDomainManager) | 30 | DomainDeletedEvent, DomainDeletingEvent, IDomain, IDomainManager) |
231 | 31 | from mailman.interfaces.usermanager import IUserManager | ||
232 | 31 | from mailman.model.mailinglist import MailingList | 32 | from mailman.model.mailinglist import MailingList |
233 | 33 | from mailman.model.user import User, DomainOwner | ||
234 | 32 | from urllib.parse import urljoin, urlparse | 34 | from urllib.parse import urljoin, urlparse |
235 | 33 | from sqlalchemy import Column, Integer, Unicode | 35 | from sqlalchemy import Column, Integer, Unicode |
236 | 36 | from sqlalchemy.orm import relationship, backref | ||
237 | 34 | from zope.event import notify | 37 | from zope.event import notify |
238 | 35 | from zope.interface import implementer | 38 | from zope.interface import implementer |
239 | 39 | from zope.component import getUtility | ||
240 | 36 | 40 | ||
241 | 37 | 41 | ||
242 | 38 | 42 | ||
243 | 39 | 43 | ||
244 | @@ -44,15 +48,17 @@ | |||
245 | 44 | 48 | ||
246 | 45 | id = Column(Integer, primary_key=True) | 49 | id = Column(Integer, primary_key=True) |
247 | 46 | 50 | ||
249 | 47 | mail_host = Column(Unicode) # TODO: add index? | 51 | mail_host = Column(Unicode) |
250 | 48 | base_url = Column(Unicode) | 52 | base_url = Column(Unicode) |
251 | 49 | description = Column(Unicode) | 53 | description = Column(Unicode) |
253 | 50 | contact_address = Column(Unicode) | 54 | owners = relationship("User", |
254 | 55 | secondary="domain_owner", | ||
255 | 56 | backref="domains") | ||
256 | 51 | 57 | ||
257 | 52 | def __init__(self, mail_host, | 58 | def __init__(self, mail_host, |
258 | 53 | description=None, | 59 | description=None, |
259 | 54 | base_url=None, | 60 | base_url=None, |
261 | 55 | contact_address=None): | 61 | owners=[]): |
262 | 56 | """Create and register a domain. | 62 | """Create and register a domain. |
263 | 57 | 63 | ||
264 | 58 | :param mail_host: The host name for the email interface. | 64 | :param mail_host: The host name for the email interface. |
265 | @@ -63,18 +69,16 @@ | |||
266 | 63 | scheme. If not given, it will be constructed from the | 69 | scheme. If not given, it will be constructed from the |
267 | 64 | `mail_host` using the http protocol. | 70 | `mail_host` using the http protocol. |
268 | 65 | :type base_url: string | 71 | :type base_url: string |
272 | 66 | :param contact_address: The email address to contact a human for this | 72 | :param owners: List of `User` who are the owners of this domain |
273 | 67 | domain. If not given, postmaster@`mail_host` will be used. | 73 | :type owners: list |
271 | 68 | :type contact_address: string | ||
274 | 69 | """ | 74 | """ |
275 | 70 | self.mail_host = mail_host | 75 | self.mail_host = mail_host |
276 | 71 | self.base_url = (base_url | 76 | self.base_url = (base_url |
277 | 72 | if base_url is not None | 77 | if base_url is not None |
278 | 73 | else 'http://' + mail_host) | 78 | else 'http://' + mail_host) |
279 | 74 | self.description = description | 79 | self.description = description |
283 | 75 | self.contact_address = (contact_address | 80 | if len(owners): |
284 | 76 | if contact_address is not None | 81 | self.add_owners(owners) |
282 | 77 | else 'postmaster@' + mail_host) | ||
285 | 78 | 82 | ||
286 | 79 | @property | 83 | @property |
287 | 80 | def url_host(self): | 84 | def url_host(self): |
288 | @@ -103,13 +107,29 @@ | |||
289 | 103 | def __repr__(self): | 107 | def __repr__(self): |
290 | 104 | """repr(a_domain)""" | 108 | """repr(a_domain)""" |
291 | 105 | if self.description is None: | 109 | if self.description is None: |
294 | 106 | return ('<Domain {0.mail_host}, base_url: {0.base_url}, ' | 110 | return ('<Domain {0.mail_host}, base_url: {0.base_url}>').format(self) |
293 | 107 | 'contact_address: {0.contact_address}>').format(self) | ||
295 | 108 | else: | 111 | else: |
296 | 109 | return ('<Domain {0.mail_host}, {0.description}, ' | 112 | return ('<Domain {0.mail_host}, {0.description}, ' |
300 | 110 | 'base_url: {0.base_url}, ' | 113 | 'base_url: {0.base_url}>').format(self) |
301 | 111 | 'contact_address: {0.contact_address}>').format(self) | 114 | |
302 | 112 | 115 | def add_owner(self, owner): | |
303 | 116 | """Add a domain owner""" | ||
304 | 117 | user_manager = getUtility(IUserManager) | ||
305 | 118 | user = user_manager.get_user(owner) | ||
306 | 119 | if user is None: | ||
307 | 120 | user = user_manager.create_user(owner) | ||
308 | 121 | self.owners.append(user) | ||
309 | 122 | |||
310 | 123 | def add_owners(self, owners): | ||
311 | 124 | """Add multiple owners""" | ||
312 | 125 | assert(isinstance(owners, list)) | ||
313 | 126 | for owner in owners: | ||
314 | 127 | self.add_owner(owner) | ||
315 | 128 | |||
316 | 129 | def remove_owner(self, owner): | ||
317 | 130 | """ Remove a domain owner""" | ||
318 | 131 | user_manager = getUtility(IUserManager) | ||
319 | 132 | self.owners.remove(user_manager.get_user(owner)) | ||
320 | 113 | 133 | ||
321 | 114 | 134 | ||
322 | 115 | 135 | ||
323 | 116 | @implementer(IDomainManager) | 136 | @implementer(IDomainManager) |
324 | @@ -121,15 +141,16 @@ | |||
325 | 121 | mail_host, | 141 | mail_host, |
326 | 122 | description=None, | 142 | description=None, |
327 | 123 | base_url=None, | 143 | base_url=None, |
329 | 124 | contact_address=None): | 144 | owners=[]): |
330 | 125 | """See `IDomainManager`.""" | 145 | """See `IDomainManager`.""" |
331 | 126 | # Be sure the mail_host is not already registered. This is probably | 146 | # Be sure the mail_host is not already registered. This is probably |
332 | 127 | # a constraint that should (also) be maintained in the database. | 147 | # a constraint that should (also) be maintained in the database. |
333 | 128 | if self.get(mail_host) is not None: | 148 | if self.get(mail_host) is not None: |
334 | 129 | raise BadDomainSpecificationError( | 149 | raise BadDomainSpecificationError( |
335 | 130 | 'Duplicate email host: %s' % mail_host) | 150 | 'Duplicate email host: %s' % mail_host) |
336 | 151 | |||
337 | 131 | notify(DomainCreatingEvent(mail_host)) | 152 | notify(DomainCreatingEvent(mail_host)) |
339 | 132 | domain = Domain(mail_host, description, base_url, contact_address) | 153 | domain = Domain(mail_host, description, base_url, owners) |
340 | 133 | store.add(domain) | 154 | store.add(domain) |
341 | 134 | notify(DomainCreatedEvent(domain)) | 155 | notify(DomainCreatedEvent(domain)) |
342 | 135 | return domain | 156 | return domain |
343 | 136 | 157 | ||
344 | === modified file 'src/mailman/model/tests/test_domain.py' | |||
345 | --- src/mailman/model/tests/test_domain.py 2015-01-05 01:22:39 +0000 | |||
346 | +++ src/mailman/model/tests/test_domain.py 2015-04-05 22:33:23 +0000 | |||
347 | @@ -26,9 +26,11 @@ | |||
348 | 26 | import unittest | 26 | import unittest |
349 | 27 | 27 | ||
350 | 28 | from mailman.app.lifecycle import create_list | 28 | from mailman.app.lifecycle import create_list |
351 | 29 | from mailman.config import config | ||
352 | 29 | from mailman.interfaces.domain import ( | 30 | from mailman.interfaces.domain import ( |
353 | 30 | DomainCreatedEvent, DomainCreatingEvent, DomainDeletedEvent, | 31 | DomainCreatedEvent, DomainCreatingEvent, DomainDeletedEvent, |
355 | 31 | DomainDeletingEvent, IDomainManager) | 32 | DomainDeletingEvent, IDomainManager, BadDomainSpecificationError) |
356 | 33 | from mailman.interfaces.usermanager import IUserManager | ||
357 | 32 | from mailman.interfaces.listmanager import IListManager | 34 | from mailman.interfaces.listmanager import IListManager |
358 | 33 | from mailman.testing.helpers import event_subscribers | 35 | from mailman.testing.helpers import event_subscribers |
359 | 34 | from mailman.testing.layers import ConfigLayer | 36 | from mailman.testing.layers import ConfigLayer |
360 | @@ -78,6 +80,26 @@ | |||
361 | 78 | # Trying to delete a missing domain gives you a KeyError. | 80 | # Trying to delete a missing domain gives you a KeyError. |
362 | 79 | self.assertRaises(KeyError, self._manager.remove, 'doesnotexist.com') | 81 | self.assertRaises(KeyError, self._manager.remove, 'doesnotexist.com') |
363 | 80 | 82 | ||
364 | 83 | def test_domain_create_with_owner(self): | ||
365 | 84 | domain = self._manager.add('example.org', | ||
366 | 85 | owners=['someuser@example.org']) | ||
367 | 86 | self.assertEqual(len(domain.owners), 1) | ||
368 | 87 | self.assertEqual(domain.owners[0].addresses[0].email, | ||
369 | 88 | 'someuser@example.org') | ||
370 | 89 | |||
371 | 90 | def test_add_domain_owner(self): | ||
372 | 91 | domain = self._manager.add('example.org') | ||
373 | 92 | domain.add_owner('someuser@example.org') | ||
374 | 93 | self.assertEqual(len(domain.owners), 1) | ||
375 | 94 | self.assertEqual(domain.owners[0].addresses[0].email, | ||
376 | 95 | 'someuser@example.org') | ||
377 | 96 | |||
378 | 97 | def test_remove_domain_owner(self): | ||
379 | 98 | domain = self._manager.add('example.org', | ||
380 | 99 | owners=['someuser@example.org']) | ||
381 | 100 | domain.remove_owner('someuser@example.org') | ||
382 | 101 | self.assertEqual(len(domain.owners), 0) | ||
383 | 102 | |||
384 | 81 | 103 | ||
385 | 82 | 104 | ||
386 | 83 | 105 | ||
387 | 84 | class TestDomainLifecycleEvents(unittest.TestCase): | 106 | class TestDomainLifecycleEvents(unittest.TestCase): |
388 | 85 | 107 | ||
389 | === modified file 'src/mailman/model/user.py' | |||
390 | --- src/mailman/model/user.py 2015-03-20 16:38:00 +0000 | |||
391 | +++ src/mailman/model/user.py 2015-04-05 22:33:23 +0000 | |||
392 | @@ -19,6 +19,7 @@ | |||
393 | 19 | 19 | ||
394 | 20 | __all__ = [ | 20 | __all__ = [ |
395 | 21 | 'User', | 21 | 'User', |
396 | 22 | 'DomainOwner' | ||
397 | 22 | ] | 23 | ] |
398 | 23 | 24 | ||
399 | 24 | 25 | ||
400 | @@ -34,7 +35,7 @@ | |||
401 | 34 | from mailman.model.roster import Memberships | 35 | from mailman.model.roster import Memberships |
402 | 35 | from mailman.utilities.datetime import factory as date_factory | 36 | from mailman.utilities.datetime import factory as date_factory |
403 | 36 | from mailman.utilities.uid import UniqueIDFactory | 37 | from mailman.utilities.uid import UniqueIDFactory |
405 | 37 | from sqlalchemy import Column, DateTime, ForeignKey, Integer, Unicode | 38 | from sqlalchemy import Column, DateTime, ForeignKey, Integer, Unicode, Boolean |
406 | 38 | from sqlalchemy.orm import relationship, backref | 39 | from sqlalchemy.orm import relationship, backref |
407 | 39 | from zope.event import notify | 40 | from zope.event import notify |
408 | 40 | from zope.interface import implementer | 41 | from zope.interface import implementer |
409 | @@ -55,6 +56,7 @@ | |||
410 | 55 | _password = Column('password', Unicode) | 56 | _password = Column('password', Unicode) |
411 | 56 | _user_id = Column(UUID, index=True) | 57 | _user_id = Column(UUID, index=True) |
412 | 57 | _created_on = Column(DateTime) | 58 | _created_on = Column(DateTime) |
413 | 59 | is_server_owner = Column(Boolean, default=False) | ||
414 | 58 | 60 | ||
415 | 59 | addresses = relationship( | 61 | addresses = relationship( |
416 | 60 | 'Address', backref='user', | 62 | 'Address', backref='user', |
417 | @@ -176,3 +178,11 @@ | |||
418 | 176 | @property | 178 | @property |
419 | 177 | def memberships(self): | 179 | def memberships(self): |
420 | 178 | return Memberships(self) | 180 | return Memberships(self) |
421 | 181 | |||
422 | 182 | |||
423 | 183 | class DomainOwner(Model): | ||
424 | 184 | """Domain to owners(user) association class""" | ||
425 | 185 | |||
426 | 186 | __tablename__ = 'domain_owner' | ||
427 | 187 | user_id = Column(Integer, ForeignKey('user.id'), primary_key=True) | ||
428 | 188 | domain_id = Column(Integer, ForeignKey('domain.id'), primary_key=True) | ||
429 | 179 | 189 | ||
430 | === modified file 'src/mailman/rest/docs/addresses.rst' | |||
431 | --- src/mailman/rest/docs/addresses.rst 2015-02-14 01:35:35 +0000 | |||
432 | +++ src/mailman/rest/docs/addresses.rst 2015-04-05 22:33:23 +0000 | |||
433 | @@ -190,6 +190,7 @@ | |||
434 | 190 | created_on: 2005-08-01T07:49:23 | 190 | created_on: 2005-08-01T07:49:23 |
435 | 191 | display_name: Cris X. Person | 191 | display_name: Cris X. Person |
436 | 192 | http_etag: "..." | 192 | http_etag: "..." |
437 | 193 | is_server_owner: False | ||
438 | 193 | password: ... | 194 | password: ... |
439 | 194 | self_link: http://localhost:9001/3.0/users/1 | 195 | self_link: http://localhost:9001/3.0/users/1 |
440 | 195 | user_id: 1 | 196 | user_id: 1 |
441 | 196 | 197 | ||
442 | === modified file 'src/mailman/rest/docs/domains.rst' | |||
443 | --- src/mailman/rest/docs/domains.rst 2014-12-16 01:01:53 +0000 | |||
444 | +++ src/mailman/rest/docs/domains.rst 2015-04-05 22:33:23 +0000 | |||
445 | @@ -29,14 +29,12 @@ | |||
446 | 29 | >>> domain_manager.add( | 29 | >>> domain_manager.add( |
447 | 30 | ... 'example.com', 'An example domain', 'http://lists.example.com') | 30 | ... 'example.com', 'An example domain', 'http://lists.example.com') |
448 | 31 | <Domain example.com, An example domain, | 31 | <Domain example.com, An example domain, |
451 | 32 | base_url: http://lists.example.com, | 32 | base_url: http://lists.example.com> |
450 | 33 | contact_address: postmaster@example.com> | ||
452 | 34 | >>> transaction.commit() | 33 | >>> transaction.commit() |
453 | 35 | 34 | ||
454 | 36 | >>> dump_json('http://localhost:9001/3.0/domains') | 35 | >>> dump_json('http://localhost:9001/3.0/domains') |
455 | 37 | entry 0: | 36 | entry 0: |
456 | 38 | base_url: http://lists.example.com | 37 | base_url: http://lists.example.com |
457 | 39 | contact_address: postmaster@example.com | ||
458 | 40 | description: An example domain | 38 | description: An example domain |
459 | 41 | http_etag: "..." | 39 | http_etag: "..." |
460 | 42 | mail_host: example.com | 40 | mail_host: example.com |
461 | @@ -51,24 +49,19 @@ | |||
462 | 51 | 49 | ||
463 | 52 | >>> domain_manager.add( | 50 | >>> domain_manager.add( |
464 | 53 | ... 'example.org', | 51 | ... 'example.org', |
469 | 54 | ... base_url='http://mail.example.org', | 52 | ... base_url='http://mail.example.org') |
470 | 55 | ... contact_address='listmaster@example.org') | 53 | <Domain example.org, base_url: http://mail.example.org> |
467 | 56 | <Domain example.org, base_url: http://mail.example.org, | ||
468 | 57 | contact_address: listmaster@example.org> | ||
471 | 58 | >>> domain_manager.add( | 54 | >>> domain_manager.add( |
472 | 59 | ... 'lists.example.net', | 55 | ... 'lists.example.net', |
473 | 60 | ... 'Porkmasters', | 56 | ... 'Porkmasters', |
476 | 61 | ... 'http://example.net', | 57 | ... 'http://example.net') |
475 | 62 | ... 'porkmaster@example.net') | ||
477 | 63 | <Domain lists.example.net, Porkmasters, | 58 | <Domain lists.example.net, Porkmasters, |
480 | 64 | base_url: http://example.net, | 59 | base_url: http://example.net> |
479 | 65 | contact_address: porkmaster@example.net> | ||
481 | 66 | >>> transaction.commit() | 60 | >>> transaction.commit() |
482 | 67 | 61 | ||
483 | 68 | >>> dump_json('http://localhost:9001/3.0/domains') | 62 | >>> dump_json('http://localhost:9001/3.0/domains') |
484 | 69 | entry 0: | 63 | entry 0: |
485 | 70 | base_url: http://lists.example.com | 64 | base_url: http://lists.example.com |
486 | 71 | contact_address: postmaster@example.com | ||
487 | 72 | description: An example domain | 65 | description: An example domain |
488 | 73 | http_etag: "..." | 66 | http_etag: "..." |
489 | 74 | mail_host: example.com | 67 | mail_host: example.com |
490 | @@ -76,7 +69,6 @@ | |||
491 | 76 | url_host: lists.example.com | 69 | url_host: lists.example.com |
492 | 77 | entry 1: | 70 | entry 1: |
493 | 78 | base_url: http://mail.example.org | 71 | base_url: http://mail.example.org |
494 | 79 | contact_address: listmaster@example.org | ||
495 | 80 | description: None | 72 | description: None |
496 | 81 | http_etag: "..." | 73 | http_etag: "..." |
497 | 82 | mail_host: example.org | 74 | mail_host: example.org |
498 | @@ -84,7 +76,6 @@ | |||
499 | 84 | url_host: mail.example.org | 76 | url_host: mail.example.org |
500 | 85 | entry 2: | 77 | entry 2: |
501 | 86 | base_url: http://example.net | 78 | base_url: http://example.net |
502 | 87 | contact_address: porkmaster@example.net | ||
503 | 88 | description: Porkmasters | 79 | description: Porkmasters |
504 | 89 | http_etag: "..." | 80 | http_etag: "..." |
505 | 90 | mail_host: lists.example.net | 81 | mail_host: lists.example.net |
506 | @@ -103,7 +94,6 @@ | |||
507 | 103 | 94 | ||
508 | 104 | >>> dump_json('http://localhost:9001/3.0/domains/lists.example.net') | 95 | >>> dump_json('http://localhost:9001/3.0/domains/lists.example.net') |
509 | 105 | base_url: http://example.net | 96 | base_url: http://example.net |
510 | 106 | contact_address: porkmaster@example.net | ||
511 | 107 | description: Porkmasters | 97 | description: Porkmasters |
512 | 108 | http_etag: "..." | 98 | http_etag: "..." |
513 | 109 | mail_host: lists.example.net | 99 | mail_host: lists.example.net |
514 | @@ -165,7 +155,6 @@ | |||
515 | 165 | 155 | ||
516 | 166 | >>> dump_json('http://localhost:9001/3.0/domains/lists.example.com') | 156 | >>> dump_json('http://localhost:9001/3.0/domains/lists.example.com') |
517 | 167 | base_url: http://lists.example.com | 157 | base_url: http://lists.example.com |
518 | 168 | contact_address: postmaster@lists.example.com | ||
519 | 169 | description: None | 158 | description: None |
520 | 170 | http_etag: "..." | 159 | http_etag: "..." |
521 | 171 | mail_host: lists.example.com | 160 | mail_host: lists.example.com |
522 | @@ -176,9 +165,7 @@ | |||
523 | 176 | :: | 165 | :: |
524 | 177 | 166 | ||
525 | 178 | >>> domain_manager['lists.example.com'] | 167 | >>> domain_manager['lists.example.com'] |
529 | 179 | <Domain lists.example.com, | 168 | <Domain lists.example.com, base_url: http://lists.example.com> |
527 | 180 | base_url: http://lists.example.com, | ||
528 | 181 | contact_address: postmaster@lists.example.com> | ||
530 | 182 | 169 | ||
531 | 183 | # Unlock the database. | 170 | # Unlock the database. |
532 | 184 | >>> transaction.abort() | 171 | >>> transaction.abort() |
533 | @@ -190,8 +177,7 @@ | |||
534 | 190 | >>> dump_json('http://localhost:9001/3.0/domains', { | 177 | >>> dump_json('http://localhost:9001/3.0/domains', { |
535 | 191 | ... 'mail_host': 'my.example.com', | 178 | ... 'mail_host': 'my.example.com', |
536 | 192 | ... 'description': 'My new domain', | 179 | ... 'description': 'My new domain', |
539 | 193 | ... 'base_url': 'http://allmy.example.com', | 180 | ... 'base_url': 'http://allmy.example.com' |
538 | 194 | ... 'contact_address': 'helpme@example.com' | ||
540 | 195 | ... }) | 181 | ... }) |
541 | 196 | content-length: 0 | 182 | content-length: 0 |
542 | 197 | date: ... | 183 | date: ... |
543 | @@ -200,7 +186,6 @@ | |||
544 | 200 | 186 | ||
545 | 201 | >>> dump_json('http://localhost:9001/3.0/domains/my.example.com') | 187 | >>> dump_json('http://localhost:9001/3.0/domains/my.example.com') |
546 | 202 | base_url: http://allmy.example.com | 188 | base_url: http://allmy.example.com |
547 | 203 | contact_address: helpme@example.com | ||
548 | 204 | description: My new domain | 189 | description: My new domain |
549 | 205 | http_etag: "..." | 190 | http_etag: "..." |
550 | 206 | mail_host: my.example.com | 191 | mail_host: my.example.com |
551 | @@ -208,9 +193,7 @@ | |||
552 | 208 | url_host: allmy.example.com | 193 | url_host: allmy.example.com |
553 | 209 | 194 | ||
554 | 210 | >>> domain_manager['my.example.com'] | 195 | >>> domain_manager['my.example.com'] |
558 | 211 | <Domain my.example.com, My new domain, | 196 | <Domain my.example.com, My new domain, base_url: http://allmy.example.com> |
556 | 212 | base_url: http://allmy.example.com, | ||
557 | 213 | contact_address: helpme@example.com> | ||
559 | 214 | 197 | ||
560 | 215 | # Unlock the database. | 198 | # Unlock the database. |
561 | 216 | >>> transaction.abort() | 199 | >>> transaction.abort() |
562 | 217 | 200 | ||
563 | === modified file 'src/mailman/rest/docs/users.rst' | |||
564 | --- src/mailman/rest/docs/users.rst 2014-12-22 18:40:30 +0000 | |||
565 | +++ src/mailman/rest/docs/users.rst 2015-04-05 22:33:23 +0000 | |||
566 | @@ -34,6 +34,7 @@ | |||
567 | 34 | created_on: 2005-08-01T07:49:23 | 34 | created_on: 2005-08-01T07:49:23 |
568 | 35 | display_name: Anne Person | 35 | display_name: Anne Person |
569 | 36 | http_etag: "..." | 36 | http_etag: "..." |
570 | 37 | is_server_owner: False | ||
571 | 37 | self_link: http://localhost:9001/3.0/users/1 | 38 | self_link: http://localhost:9001/3.0/users/1 |
572 | 38 | user_id: 1 | 39 | user_id: 1 |
573 | 39 | http_etag: "..." | 40 | http_etag: "..." |
574 | @@ -50,11 +51,13 @@ | |||
575 | 50 | created_on: 2005-08-01T07:49:23 | 51 | created_on: 2005-08-01T07:49:23 |
576 | 51 | display_name: Anne Person | 52 | display_name: Anne Person |
577 | 52 | http_etag: "..." | 53 | http_etag: "..." |
578 | 54 | is_server_owner: False | ||
579 | 53 | self_link: http://localhost:9001/3.0/users/1 | 55 | self_link: http://localhost:9001/3.0/users/1 |
580 | 54 | user_id: 1 | 56 | user_id: 1 |
581 | 55 | entry 1: | 57 | entry 1: |
582 | 56 | created_on: 2005-08-01T07:49:23 | 58 | created_on: 2005-08-01T07:49:23 |
583 | 57 | http_etag: "..." | 59 | http_etag: "..." |
584 | 60 | is_server_owner: False | ||
585 | 58 | self_link: http://localhost:9001/3.0/users/2 | 61 | self_link: http://localhost:9001/3.0/users/2 |
586 | 59 | user_id: 2 | 62 | user_id: 2 |
587 | 60 | http_etag: "..." | 63 | http_etag: "..." |
588 | @@ -76,6 +79,7 @@ | |||
589 | 76 | created_on: 2005-08-01T07:49:23 | 79 | created_on: 2005-08-01T07:49:23 |
590 | 77 | display_name: Anne Person | 80 | display_name: Anne Person |
591 | 78 | http_etag: "..." | 81 | http_etag: "..." |
592 | 82 | is_server_owner: False | ||
593 | 79 | self_link: http://localhost:9001/3.0/users/1 | 83 | self_link: http://localhost:9001/3.0/users/1 |
594 | 80 | user_id: 1 | 84 | user_id: 1 |
595 | 81 | http_etag: "..." | 85 | http_etag: "..." |
596 | @@ -86,6 +90,7 @@ | |||
597 | 86 | entry 0: | 90 | entry 0: |
598 | 87 | created_on: 2005-08-01T07:49:23 | 91 | created_on: 2005-08-01T07:49:23 |
599 | 88 | http_etag: "..." | 92 | http_etag: "..." |
600 | 93 | is_server_owner: False | ||
601 | 89 | self_link: http://localhost:9001/3.0/users/2 | 94 | self_link: http://localhost:9001/3.0/users/2 |
602 | 90 | user_id: 2 | 95 | user_id: 2 |
603 | 91 | http_etag: "..." | 96 | http_etag: "..." |
604 | @@ -120,6 +125,7 @@ | |||
605 | 120 | >>> dump_json('http://localhost:9001/3.0/users/3') | 125 | >>> dump_json('http://localhost:9001/3.0/users/3') |
606 | 121 | created_on: 2005-08-01T07:49:23 | 126 | created_on: 2005-08-01T07:49:23 |
607 | 122 | http_etag: "..." | 127 | http_etag: "..." |
608 | 128 | is_server_owner: False | ||
609 | 123 | password: {plaintext}... | 129 | password: {plaintext}... |
610 | 124 | self_link: http://localhost:9001/3.0/users/3 | 130 | self_link: http://localhost:9001/3.0/users/3 |
611 | 125 | user_id: 3 | 131 | user_id: 3 |
612 | @@ -131,6 +137,7 @@ | |||
613 | 131 | >>> dump_json('http://localhost:9001/3.0/users/cris@example.com') | 137 | >>> dump_json('http://localhost:9001/3.0/users/cris@example.com') |
614 | 132 | created_on: 2005-08-01T07:49:23 | 138 | created_on: 2005-08-01T07:49:23 |
615 | 133 | http_etag: "..." | 139 | http_etag: "..." |
616 | 140 | is_server_owner: False | ||
617 | 134 | password: {plaintext}... | 141 | password: {plaintext}... |
618 | 135 | self_link: http://localhost:9001/3.0/users/3 | 142 | self_link: http://localhost:9001/3.0/users/3 |
619 | 136 | user_id: 3 | 143 | user_id: 3 |
620 | @@ -158,6 +165,7 @@ | |||
621 | 158 | created_on: 2005-08-01T07:49:23 | 165 | created_on: 2005-08-01T07:49:23 |
622 | 159 | display_name: Dave Person | 166 | display_name: Dave Person |
623 | 160 | http_etag: "..." | 167 | http_etag: "..." |
624 | 168 | is_server_owner: False | ||
625 | 161 | password: {plaintext}... | 169 | password: {plaintext}... |
626 | 162 | self_link: http://localhost:9001/3.0/users/4 | 170 | self_link: http://localhost:9001/3.0/users/4 |
627 | 163 | user_id: 4 | 171 | user_id: 4 |
628 | @@ -190,6 +198,7 @@ | |||
629 | 190 | created_on: 2005-08-01T07:49:23 | 198 | created_on: 2005-08-01T07:49:23 |
630 | 191 | display_name: Elly Person | 199 | display_name: Elly Person |
631 | 192 | http_etag: "..." | 200 | http_etag: "..." |
632 | 201 | is_server_owner: False | ||
633 | 193 | password: {plaintext}supersekrit | 202 | password: {plaintext}supersekrit |
634 | 194 | self_link: http://localhost:9001/3.0/users/5 | 203 | self_link: http://localhost:9001/3.0/users/5 |
635 | 195 | user_id: 5 | 204 | user_id: 5 |
636 | @@ -214,6 +223,7 @@ | |||
637 | 214 | created_on: 2005-08-01T07:49:23 | 223 | created_on: 2005-08-01T07:49:23 |
638 | 215 | display_name: David Person | 224 | display_name: David Person |
639 | 216 | http_etag: "..." | 225 | http_etag: "..." |
640 | 226 | is_server_owner: False | ||
641 | 217 | password: {plaintext}... | 227 | password: {plaintext}... |
642 | 218 | self_link: http://localhost:9001/3.0/users/4 | 228 | self_link: http://localhost:9001/3.0/users/4 |
643 | 219 | user_id: 4 | 229 | user_id: 4 |
644 | @@ -238,6 +248,7 @@ | |||
645 | 238 | created_on: 2005-08-01T07:49:23 | 248 | created_on: 2005-08-01T07:49:23 |
646 | 239 | display_name: David Person | 249 | display_name: David Person |
647 | 240 | http_etag: "..." | 250 | http_etag: "..." |
648 | 251 | is_server_owner: False | ||
649 | 241 | password: {plaintext}clockwork angels | 252 | password: {plaintext}clockwork angels |
650 | 242 | self_link: http://localhost:9001/3.0/users/4 | 253 | self_link: http://localhost:9001/3.0/users/4 |
651 | 243 | user_id: 4 | 254 | user_id: 4 |
652 | @@ -260,6 +271,7 @@ | |||
653 | 260 | created_on: 2005-08-01T07:49:23 | 271 | created_on: 2005-08-01T07:49:23 |
654 | 261 | display_name: David Personhood | 272 | display_name: David Personhood |
655 | 262 | http_etag: "..." | 273 | http_etag: "..." |
656 | 274 | is_server_owner: False | ||
657 | 263 | password: {plaintext}the garden | 275 | password: {plaintext}the garden |
658 | 264 | self_link: http://localhost:9001/3.0/users/4 | 276 | self_link: http://localhost:9001/3.0/users/4 |
659 | 265 | user_id: 4 | 277 | user_id: 4 |
660 | @@ -343,6 +355,7 @@ | |||
661 | 343 | created_on: 2005-08-01T07:49:23 | 355 | created_on: 2005-08-01T07:49:23 |
662 | 344 | display_name: Fred Person | 356 | display_name: Fred Person |
663 | 345 | http_etag: "..." | 357 | http_etag: "..." |
664 | 358 | is_server_owner: False | ||
665 | 346 | self_link: http://localhost:9001/3.0/users/6 | 359 | self_link: http://localhost:9001/3.0/users/6 |
666 | 347 | user_id: 6 | 360 | user_id: 6 |
667 | 348 | 361 | ||
668 | @@ -350,6 +363,7 @@ | |||
669 | 350 | created_on: 2005-08-01T07:49:23 | 363 | created_on: 2005-08-01T07:49:23 |
670 | 351 | display_name: Fred Person | 364 | display_name: Fred Person |
671 | 352 | http_etag: "..." | 365 | http_etag: "..." |
672 | 366 | is_server_owner: False | ||
673 | 353 | self_link: http://localhost:9001/3.0/users/6 | 367 | self_link: http://localhost:9001/3.0/users/6 |
674 | 354 | user_id: 6 | 368 | user_id: 6 |
675 | 355 | 369 | ||
676 | @@ -357,6 +371,7 @@ | |||
677 | 357 | created_on: 2005-08-01T07:49:23 | 371 | created_on: 2005-08-01T07:49:23 |
678 | 358 | display_name: Fred Person | 372 | display_name: Fred Person |
679 | 359 | http_etag: "..." | 373 | http_etag: "..." |
680 | 374 | is_server_owner: False | ||
681 | 360 | self_link: http://localhost:9001/3.0/users/6 | 375 | self_link: http://localhost:9001/3.0/users/6 |
682 | 361 | user_id: 6 | 376 | user_id: 6 |
683 | 362 | 377 | ||
684 | @@ -364,6 +379,7 @@ | |||
685 | 364 | created_on: 2005-08-01T07:49:23 | 379 | created_on: 2005-08-01T07:49:23 |
686 | 365 | display_name: Fred Person | 380 | display_name: Fred Person |
687 | 366 | http_etag: "..." | 381 | http_etag: "..." |
688 | 382 | is_server_owner: False | ||
689 | 367 | self_link: http://localhost:9001/3.0/users/6 | 383 | self_link: http://localhost:9001/3.0/users/6 |
690 | 368 | user_id: 6 | 384 | user_id: 6 |
691 | 369 | 385 | ||
692 | @@ -382,6 +398,7 @@ | |||
693 | 382 | created_on: 2005-08-01T07:49:23 | 398 | created_on: 2005-08-01T07:49:23 |
694 | 383 | display_name: Elly Person | 399 | display_name: Elly Person |
695 | 384 | http_etag: "..." | 400 | http_etag: "..." |
696 | 401 | is_server_owner: False | ||
697 | 385 | password: {plaintext}supersekrit | 402 | password: {plaintext}supersekrit |
698 | 386 | self_link: http://localhost:9001/3.0/users/5 | 403 | self_link: http://localhost:9001/3.0/users/5 |
699 | 387 | user_id: 5 | 404 | user_id: 5 |
700 | 388 | 405 | ||
701 | === modified file 'src/mailman/rest/domains.py' | |||
702 | --- src/mailman/rest/domains.py 2015-01-05 01:40:47 +0000 | |||
703 | +++ src/mailman/rest/domains.py 2015-04-05 22:33:23 +0000 | |||
704 | @@ -25,10 +25,12 @@ | |||
705 | 25 | 25 | ||
706 | 26 | from mailman.interfaces.domain import ( | 26 | from mailman.interfaces.domain import ( |
707 | 27 | BadDomainSpecificationError, IDomainManager) | 27 | BadDomainSpecificationError, IDomainManager) |
708 | 28 | from mailman.interfaces.usermanager import IUserManager | ||
709 | 28 | from mailman.rest.helpers import ( | 29 | from mailman.rest.helpers import ( |
710 | 29 | BadRequest, CollectionMixin, NotFound, bad_request, child, created, etag, | 30 | BadRequest, CollectionMixin, NotFound, bad_request, child, created, etag, |
711 | 30 | no_content, not_found, okay, path_to) | 31 | no_content, not_found, okay, path_to) |
712 | 31 | from mailman.rest.lists import ListsForDomain | 32 | from mailman.rest.lists import ListsForDomain |
713 | 33 | from mailman.rest.users import OwnersForDomain | ||
714 | 32 | from mailman.rest.validator import Validator | 34 | from mailman.rest.validator import Validator |
715 | 33 | from zope.component import getUtility | 35 | from zope.component import getUtility |
716 | 34 | 36 | ||
717 | @@ -41,7 +43,6 @@ | |||
718 | 41 | """See `CollectionMixin`.""" | 43 | """See `CollectionMixin`.""" |
719 | 42 | return dict( | 44 | return dict( |
720 | 43 | base_url=domain.base_url, | 45 | base_url=domain.base_url, |
721 | 44 | contact_address=domain.contact_address, | ||
722 | 45 | description=domain.description, | 46 | description=domain.description, |
723 | 46 | mail_host=domain.mail_host, | 47 | mail_host=domain.mail_host, |
724 | 47 | self_link=path_to('domains/{0}'.format(domain.mail_host)), | 48 | self_link=path_to('domains/{0}'.format(domain.mail_host)), |
725 | @@ -88,6 +89,17 @@ | |||
726 | 88 | else: | 89 | else: |
727 | 89 | return BadRequest(), [] | 90 | return BadRequest(), [] |
728 | 90 | 91 | ||
729 | 92 | @child() | ||
730 | 93 | def owners(self, request, segments): | ||
731 | 94 | """/domains/<domain>/owners""" | ||
732 | 95 | if len(segments) == 0: | ||
733 | 96 | domain = getUtility(IDomainManager).get(self._domain) | ||
734 | 97 | if domain is None: | ||
735 | 98 | return NotFound() | ||
736 | 99 | return OwnersForDomain(domain) | ||
737 | 100 | else: | ||
738 | 101 | return BadRequest(), [] | ||
739 | 102 | |||
740 | 91 | 103 | ||
741 | 92 | class AllDomains(_DomainBase): | 104 | class AllDomains(_DomainBase): |
742 | 93 | """The domains.""" | 105 | """The domains.""" |
743 | @@ -99,12 +111,13 @@ | |||
744 | 99 | validator = Validator(mail_host=str, | 111 | validator = Validator(mail_host=str, |
745 | 100 | description=str, | 112 | description=str, |
746 | 101 | base_url=str, | 113 | base_url=str, |
748 | 102 | contact_address=str, | 114 | owners=list, |
749 | 103 | _optional=('description', 'base_url', | 115 | _optional=('description', 'base_url', |
754 | 104 | 'contact_address')) | 116 | 'owners')) |
755 | 105 | domain = domain_manager.add(**validator(request)) | 117 | values = validator(request) |
756 | 106 | except BadDomainSpecificationError: | 118 | domain = domain_manager.add(**values) |
757 | 107 | bad_request(response, b'Domain exists') | 119 | except BadDomainSpecificationError as error: |
758 | 120 | bad_request(response, str(error)) | ||
759 | 108 | except ValueError as error: | 121 | except ValueError as error: |
760 | 109 | bad_request(response, str(error)) | 122 | bad_request(response, str(error)) |
761 | 110 | else: | 123 | else: |
762 | 111 | 124 | ||
763 | === modified file 'src/mailman/rest/lists.py' | |||
764 | --- src/mailman/rest/lists.py 2015-01-05 01:40:47 +0000 | |||
765 | +++ src/mailman/rest/lists.py 2015-04-05 22:33:23 +0000 | |||
766 | @@ -1,4 +1,4 @@ | |||
768 | 1 | # Copyright (C) 2010-2015 by the Free Software Foundation, Inc. | 1 | # Copyright (C) 2010-2015 by the Free Software Foundation, Inc. |
769 | 2 | # | 2 | # |
770 | 3 | # This file is part of GNU Mailman. | 3 | # This file is part of GNU Mailman. |
771 | 4 | # | 4 | # |
772 | 5 | 5 | ||
773 | === modified file 'src/mailman/rest/tests/test_domains.py' | |||
774 | --- src/mailman/rest/tests/test_domains.py 2015-01-05 01:40:47 +0000 | |||
775 | +++ src/mailman/rest/tests/test_domains.py 2015-04-05 22:33:23 +0000 | |||
776 | @@ -27,6 +27,7 @@ | |||
777 | 27 | from mailman.app.lifecycle import create_list | 27 | from mailman.app.lifecycle import create_list |
778 | 28 | from mailman.database.transaction import transaction | 28 | from mailman.database.transaction import transaction |
779 | 29 | from mailman.interfaces.listmanager import IListManager | 29 | from mailman.interfaces.listmanager import IListManager |
780 | 30 | from mailman.interfaces.domain import IDomainManager | ||
781 | 30 | from mailman.testing.helpers import call_api | 31 | from mailman.testing.helpers import call_api |
782 | 31 | from mailman.testing.layers import RESTLayer | 32 | from mailman.testing.layers import RESTLayer |
783 | 32 | from urllib.error import HTTPError | 33 | from urllib.error import HTTPError |
784 | @@ -41,6 +42,17 @@ | |||
785 | 41 | with transaction(): | 42 | with transaction(): |
786 | 42 | self._mlist = create_list('test@example.com') | 43 | self._mlist = create_list('test@example.com') |
787 | 43 | 44 | ||
788 | 45 | def test_create_domains(self): | ||
789 | 46 | """Test Create domain via REST""" | ||
790 | 47 | data = {'mail_host': 'example.org', | ||
791 | 48 | 'description': 'Example domain', | ||
792 | 49 | 'base_url': 'http://example.org', | ||
793 | 50 | 'owners': ['someone@example.com', | ||
794 | 51 | 'secondowner@example.com',]} | ||
795 | 52 | content, response = call_api('http://localhost:9001/3.0/domains', | ||
796 | 53 | data, method="POST") | ||
797 | 54 | self.assertEqual(response.status, 201) | ||
798 | 55 | |||
799 | 44 | def test_bogus_endpoint_extension(self): | 56 | def test_bogus_endpoint_extension(self): |
800 | 45 | # /domains/<domain>/lists/<anything> is not a valid endpoint. | 57 | # /domains/<domain>/lists/<anything> is not a valid endpoint. |
801 | 46 | with self.assertRaises(HTTPError) as cm: | 58 | with self.assertRaises(HTTPError) as cm: |
802 | 47 | 59 | ||
803 | === modified file 'src/mailman/rest/users.py' | |||
804 | --- src/mailman/rest/users.py 2015-03-20 16:38:00 +0000 | |||
805 | +++ src/mailman/rest/users.py 2015-04-05 22:33:23 +0000 | |||
806 | @@ -22,6 +22,7 @@ | |||
807 | 22 | 'AddressUser', | 22 | 'AddressUser', |
808 | 23 | 'AllUsers', | 23 | 'AllUsers', |
809 | 24 | 'Login', | 24 | 'Login', |
810 | 25 | 'OwnersForDomain', | ||
811 | 25 | ] | 26 | ] |
812 | 26 | 27 | ||
813 | 27 | 28 | ||
814 | @@ -67,8 +68,9 @@ | |||
815 | 67 | email=str, | 68 | email=str, |
816 | 68 | display_name=str, | 69 | display_name=str, |
817 | 69 | password=str, | 70 | password=str, |
820 | 70 | _optional=('display_name', 'password'), | 71 | is_server_owner=bool, |
821 | 71 | ) | 72 | _optional=('display_name', 'password', 'is_server_owner'), |
822 | 73 | ) | ||
823 | 72 | 74 | ||
824 | 73 | 75 | ||
825 | 74 | 76 | ||
826 | 75 | 77 | ||
827 | @@ -108,7 +110,8 @@ | |||
828 | 108 | user_id=user_id, | 110 | user_id=user_id, |
829 | 109 | created_on=user.created_on, | 111 | created_on=user.created_on, |
830 | 110 | self_link=path_to('users/{}'.format(user_id)), | 112 | self_link=path_to('users/{}'.format(user_id)), |
832 | 111 | ) | 113 | is_server_owner=user.is_server_owner, |
833 | 114 | ) | ||
834 | 112 | # Add the password attribute, only if the user has a password. Same | 115 | # Add the password attribute, only if the user has a password. Same |
835 | 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. |
836 | 114 | if user.password: | 117 | if user.password: |
837 | @@ -293,7 +296,8 @@ | |||
838 | 293 | del fields['email'] | 296 | del fields['email'] |
839 | 294 | fields['user_id'] = int | 297 | fields['user_id'] = int |
840 | 295 | fields['auto_create'] = as_boolean | 298 | fields['auto_create'] = as_boolean |
842 | 296 | fields['_optional'] = fields['_optional'] + ('user_id', 'auto_create') | 299 | fields['_optional'] = fields['_optional'] + ('user_id', 'auto_create', |
843 | 300 | 'is_server_owner') | ||
844 | 297 | try: | 301 | try: |
845 | 298 | validator = Validator(**fields) | 302 | validator = Validator(**fields) |
846 | 299 | arguments = validator(request) | 303 | arguments = validator(request) |
847 | @@ -328,7 +332,8 @@ | |||
848 | 328 | # Process post data and check for an existing user. | 332 | # Process post data and check for an existing user. |
849 | 329 | fields = CREATION_FIELDS.copy() | 333 | fields = CREATION_FIELDS.copy() |
850 | 330 | fields['user_id'] = int | 334 | fields['user_id'] = int |
852 | 331 | fields['_optional'] = fields['_optional'] + ('user_id', 'email') | 335 | fields['_optional'] = fields['_optional'] + ('user_id', 'email', |
853 | 336 | 'is_server_owner') | ||
854 | 332 | try: | 337 | try: |
855 | 333 | validator = Validator(**fields) | 338 | validator = Validator(**fields) |
856 | 334 | arguments = validator(request) | 339 | arguments = validator(request) |
857 | @@ -377,3 +382,41 @@ | |||
858 | 377 | no_content(response) | 382 | no_content(response) |
859 | 378 | else: | 383 | else: |
860 | 379 | forbidden(response) | 384 | forbidden(response) |
861 | 385 | |||
862 | 386 | class OwnersForDomain(_UserBase): | ||
863 | 387 | """Owners for a particular domain.""" | ||
864 | 388 | |||
865 | 389 | def __init__(self, domain): | ||
866 | 390 | self._domain = domain | ||
867 | 391 | |||
868 | 392 | def on_get(self, request, response): | ||
869 | 393 | """/domains/<domain>/owners""" | ||
870 | 394 | resource = self._make_collection(request) | ||
871 | 395 | okay(response, etag(resource)) | ||
872 | 396 | |||
873 | 397 | def on_post(self, request, response): | ||
874 | 398 | """POST to /domains/<domain>/owners """ | ||
875 | 399 | validator = Validator(owner=GetterSetter(str)) | ||
876 | 400 | try: | ||
877 | 401 | values = validator(request) | ||
878 | 402 | except ValueError as error: | ||
879 | 403 | bad_request(response, str(error)) | ||
880 | 404 | return | ||
881 | 405 | self._domain.add_owner(values['owner']) | ||
882 | 406 | return no_content(response) | ||
883 | 407 | |||
884 | 408 | def on_delete(self, request, response): | ||
885 | 409 | """DELETE to /domains/<domain>/owners""" | ||
886 | 410 | validator = Validator(owner=GetterSetter(str)) | ||
887 | 411 | try: | ||
888 | 412 | values = validator(request) | ||
889 | 413 | except ValueError as error: | ||
890 | 414 | bad_request(response, str(error)) | ||
891 | 415 | return | ||
892 | 416 | self._domain.remove_owner(owner) | ||
893 | 417 | return no_content(response) | ||
894 | 418 | |||
895 | 419 | @paginate | ||
896 | 420 | def _get_collection(self, request): | ||
897 | 421 | """See `CollectionMixin`.""" | ||
898 | 422 | return list(self._domain.owners) | ||
899 | 380 | 423 | ||
900 | === modified file 'src/mailman/testing/layers.py' | |||
901 | --- src/mailman/testing/layers.py 2015-01-05 01:22:39 +0000 | |||
902 | +++ src/mailman/testing/layers.py 2015-04-05 22:33:23 +0000 | |||
903 | @@ -200,7 +200,7 @@ | |||
904 | 200 | with transaction(): | 200 | with transaction(): |
905 | 201 | getUtility(IDomainManager).add( | 201 | getUtility(IDomainManager).add( |
906 | 202 | 'example.com', 'An example domain.', | 202 | 'example.com', 'An example domain.', |
908 | 203 | 'http://lists.example.com', 'postmaster@example.com') | 203 | 'http://lists.example.com') |
909 | 204 | 204 | ||
910 | 205 | @classmethod | 205 | @classmethod |
911 | 206 | def testTearDown(cls): | 206 | def testTearDown(cls): |
Thanks Abhilash. Looks pretty good so far, but I have a number of questions and comments inlined.