Merge lp:~flo-fuchs/mailman.client/settings into lp:mailman.client

Proposed by Florian Fuchs
Status: Merged
Approved by: Barry Warsaw
Approved revision: 14
Merged at revision: 14
Proposed branch: lp:~flo-fuchs/mailman.client/settings
Merge into: lp:mailman.client
Diff against target: 248 lines (+170/-11)
2 files modified
mailman/client/_client.py (+98/-11)
mailman/client/docs/using.txt (+72/-0)
To merge this branch: bzr merge lp:~flo-fuchs/mailman.client/settings
Reviewer Review Type Date Requested Status
Florian Fuchs Needs Information
Barry Warsaw Approve
Review via email: mp+56088@code.launchpad.net

Description of the change

Hi,

I have added methods to delete mailing lists as well as a _Settings class to read and write list settings. Please feel free to make any changes you want (...and, of course, tell me if you feel this should be done completely differently... ;-)

Cheers
Florian

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

This looks good, thanks! I have just two minor suggestions.

* Where you're returning a unicode string object in the doctests, it would be better to use 'print' so you don't get the ugly u'' prefixes. E.g., instead of:

>>> settings_new['description']
u'A very meaningful description.'

use:

>>> print settings_new['description']
A very meaningful description.

* You should not need to use the u'' strings in your doctests. Mailman does a 'from __future__ import unicode_literals' in all its .py files to force literals to be unicodes (in the absence of explicit b'' prefixes). mailman.client should do the same. To make this work in doctests, you need to set the testobj's globs in the doctest's setUp(). See src/mailman/tests/test_documentation.py down about line 183 for an example.

Other than that, it looks great, and please do merge it after consideration of the above. Thanks!

review: Approve
15. By Florian Fuchs

* added email address lookup to get_member and unsubscribe methods to reflect changes in mailman 3a7
* added missing print statements in doctest

Revision history for this message
Florian Fuchs (flo-fuchs) wrote :

Hi,

I've added a few changes to make the client compatible with the current Mailman3 alpha7.

Member resources now use IDs instead of email adresses (on the API side). The client doesn't know this ID when provided with an email adress (for example to get membership information or to unsubscribe an email address from a list). In order to find the right ID, the client needs to iterate over the existing member roster. This probably isn't ideal, especially when lists have a large number of members. But it's probably not an issue that the client lib can solve differently...

Thanks!
Florian

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

This looks great; a couple of comments:

* See above for recommendations against u'' strings and for using print instead of returning strings directly in doctests.

* Could you please open a bug (tagged with 'mailman3') on the need to expose an API call to look up a member-id given an email address? I'll make sure to expose that in a8.

With the above consideration, I'll approve this change. Thanks! And feel free to land it.

review: Approve
16. By Florian Fuchs

use print instead of returning strings directly in doctests

Revision history for this message
Florian Fuchs (flo-fuchs) wrote :

Hi,

after merging the changes locally LP doesn't let me push to lp:mailman.client ("readonly transport"). Maybe that's because the approved revision is 14 while the branch is currently at rev. 16?

Thanks!
Florian

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

I think this is Launchpad's way of saying you didn't have push permissions. Have you ever been able to push to this branch? Anyway, I added ~flo-fuchs to ~mailman-coders, which owns the trunk, so please try again. (P.S. I first accidentally added your doppleganger :)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'mailman/client/_client.py'
2--- mailman/client/_client.py 2010-12-26 21:11:54 +0000
3+++ mailman/client/_client.py 2011-06-20 06:58:35 +0000
4@@ -189,6 +189,10 @@
5 'lists/{0}'.format(fqdn_listname))
6 return _List(self._connection, content['self_link'])
7
8+ def delete_list(self, fqdn_listname):
9+ response, content = self._connection.call(
10+ 'lists/{0}'.format(fqdn_listname), None, 'DELETE')
11+
12
13
14
15 class _Domain:
16@@ -282,15 +286,26 @@
17 for entry in sorted(content['entries'],
18 key=itemgetter('address'))]
19
20+ @property
21+ def settings(self):
22+ return _Settings(self._connection,
23+ 'lists/{0}/config'.format(self.fqdn_listname))
24+
25 def get_member(self, address):
26 """Get a membership.
27
28 :param address: The email address of the member for this list.
29 :return: A member proxy object.
30 """
31- response, content = self._connection.call(
32- 'lists/{0}/member/{1}'.format(self.fqdn_listname, address))
33- return _Member(self._connection, content['self_link'])
34+ # In order to get the member object we need to
35+ # iterate over the existing member list
36+ for member in self.members:
37+ if member.address == address:
38+ return member
39+ break
40+ else:
41+ raise ValueError('%s is not a member address of %s' %
42+ (address, self.fqdn_listname))
43
44 def subscribe(self, address, real_name=None):
45 """Subscribe an email address to a mailing list.
46@@ -303,7 +318,7 @@
47 """
48 data = dict(
49 fqdn_listname=self.fqdn_listname,
50- address=address,
51+ subscriber=address,
52 real_name=real_name,
53 )
54 response, content = self._connection.call('members', data)
55@@ -314,9 +329,21 @@
56
57 :param address: The address to unsubscribe.
58 """
59- self._connection.call(
60- 'lists/{0}/member/{1}'.format(self.fqdn_listname, address),
61- method='DELETE')
62+ # In order to get the member object we need to
63+ # iterate over the existing member list
64+
65+ for member in self.members:
66+ if member.address == address:
67+ self._connection.call(member.self_link, method='DELETE')
68+ break
69+ else:
70+ raise ValueError('%s is not a member address of %s' %
71+ (address, self.fqdn_listname))
72+
73+
74+ def delete(self):
75+ response, content = self._connection.call(
76+ 'lists/{0}'.format(self.fqdn_listname), None, 'DELETE')
77
78
79
80
81@@ -345,11 +372,71 @@
82 self._get_info()
83 return self._info['address']
84
85+ @property
86+ def self_link(self):
87+ self._get_info()
88+ return self._info['self_link']
89+
90+ @property
91+ def role(self):
92+ self._get_info()
93+ return self._info['role']
94+
95+ @property
96+ def user(self):
97+ self._get_info()
98+ return self._info['user']
99+
100 def unsubscribe(self):
101 """Unsubscribe the member from a mailing list.
102
103- :param address: The address to unsubscribe.
104+ :param self_link: The REST resource to delete
105 """
106- self._connection.call(
107- 'lists/{0}/member/{1}'.format(self.fqdn_listname, self.address),
108- method='DELETE')
109+ self._connection.call(self.self_link, method='DELETE')
110+
111+
112+READ_ONLY_ATTRS = ('bounces_address', 'created_at', 'digest_last_sent_at',
113+ 'fqdn_listname', 'http_etag', 'host_name', 'join_address',
114+ 'last_post_at', 'leave_address', 'list_id', 'list_name',
115+ 'next_digest_number', 'no_reply_address', 'owner_address',
116+ 'post_id', 'posting_address', 'request_address', 'scheme',
117+ 'volume', 'web_host',)
118+
119+
120+class _Settings():
121+ def __init__(self, connection, url):
122+ self._connection = connection
123+ self._url = url
124+ self._info = None
125+ self._get_info()
126+
127+ def __repr__(self):
128+ return repr(self._info)
129+
130+ def _get_info(self):
131+ if self._info is None:
132+ response, content = self._connection.call(self._url)
133+ self._info = content
134+
135+ def __iter__(self):
136+ for key in self._info.keys():
137+ yield key
138+
139+ def __getitem__(self, key):
140+ return self._info[key]
141+
142+ def __setitem__(self, key, value):
143+ self._info[key] = value
144+
145+ def __len__(self):
146+ return len(self._info)
147+
148+ def save(self):
149+ data = {}
150+ for attribute, value in self._info.items():
151+ if attribute not in READ_ONLY_ATTRS:
152+ data[attribute] = value
153+ response, content = self._connection.call(self._url, data, 'PATCH')
154+
155+
156+
157
158=== modified file 'mailman/client/docs/using.txt'
159--- mailman/client/docs/using.txt 2010-12-26 21:11:54 +0000
160+++ mailman/client/docs/using.txt 2011-06-20 06:58:35 +0000
161@@ -119,6 +119,20 @@
162 <List "test-three@example.net">
163 <List "test-two@example.com">
164
165+You can use a list instance to delete the list.
166+
167+ >>> test_three = client.get_list('test-three@example.net')
168+ >>> test_three.delete()
169+
170+You can also delete a list using the client instance's delete_list method.
171+
172+ >>> client.delete_list('test-three@example.com')
173+
174+ >>> for mlist in client.lists:
175+ ... print mlist
176+ <List "test-one@example.com">
177+ <List "test-two@example.com">
178+
179
180 Membership
181 ==========
182@@ -170,6 +184,19 @@
183 >>> cris = test_two.get_member('cris@example.com')
184 >>> cris
185 <Member "cris@example.com" on "test-two@example.com">
186+ >>> print cris.role
187+ member
188+ >>> print cris.self_link
189+ http://localhost:8001/3.0/members/4
190+ >>> print cris.user
191+ http://localhost:8001/3.0/users/3
192+
193+If you use an address which is not a member of test_two `ValueError` is raised:
194+
195+ >>> test_two.unsubscribe('nomember@example.com')
196+ Traceback (most recent call last):
197+ ...
198+ ValueError: nomember@example.com is not a member address of test-two@example.com
199
200 After a while, Anna decides to unsubscribe from the Test One mailing list,
201 though she keeps her Test Two membership active.
202@@ -188,3 +215,48 @@
203 ... print member
204 <Member "bill@example.com" on "test-one@example.com">
205 <Member "anna@example.com" on "test-two@example.com">
206+
207+If you try to unsubscribe an address which is not a member address `ValueError` is raised:
208+
209+ >>> test_one.unsubscribe('nomember@example.com')
210+ Traceback (most recent call last):
211+ ...
212+ ValueError: nomember@example.com is not a member address of test-one@example.com
213+
214+
215+List Settings
216+=============
217+
218+We can get all list settings via a lists settings attribute. A proxy object for the settings is returned which behaves much like a dictionary.
219+
220+ >>> settings = test_one.settings
221+ >>> len(settings)
222+ 48
223+
224+ >>> for attr in sorted(settings):
225+ ... print attr + ': ' + str(settings[attr])
226+ acceptable_aliases: []
227+ ...
228+ welcome_msg:
229+
230+ >>> print settings['real_name']
231+ Test-one
232+
233+We can access all valid list settings as attributes.
234+
235+ >>> print settings['fqdn_listname']
236+ test-one@example.com
237+ >>> print settings['description']
238+
239+ >>> settings['description'] = 'A very meaningful description.'
240+ >>> settings['real_name'] = 'Test Numero Uno'
241+
242+ >>> settings.save()
243+
244+ >>> settings_new = test_one.settings
245+ >>> print settings_new['description']
246+ A very meaningful description.
247+ >>> print settings_new['real_name']
248+ Test Numero Uno
249+
250+

Subscribers

People subscribed via source and target branches