Merge lp:~abompard/mailman/import21 into lp:mailman
- import21
- Merge into 3.0
Status: | Merged |
---|---|
Merge reported by: | Barry Warsaw |
Merged at revision: | not available |
Proposed branch: | lp:~abompard/mailman/import21 |
Merge into: | lp:mailman |
Diff against target: |
1337 lines (+1121/-19) 8 files modified
src/mailman/app/templates.py (+1/-1) src/mailman/app/tests/test_templates.py (+11/-0) src/mailman/commands/cli_import.py (+6/-2) src/mailman/handlers/decorate.py (+7/-2) src/mailman/model/listmanager.py (+2/-0) src/mailman/model/tests/test_listmanager.py (+15/-0) src/mailman/utilities/importer.py (+331/-7) src/mailman/utilities/tests/test_import.py (+748/-7) |
To merge this branch: | bzr merge lp:~abompard/mailman/import21 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Barry Warsaw | Abstain | ||
Review via email: mp+192146@code.launchpad.net |
Commit message
Description of the change
This branch contains my work on the import21 command.
Most list properties are imported, as well as their rosters.
There are a few exceptions that I don't know how to handle, such as umbrella_list & umbrella_
There are currently 67 unit tests (which are great for conversions), and it's been tested on about 300 mailing-lists (the Fedora project's lists).
Aurélien Bompard (abompard) wrote : | # |
Aurélien Bompard (abompard) wrote : | # |
Another thing, it contains a fix on mailman.
Aurélien Bompard (abompard) wrote : | # |
Hey Barry! If there's anything I can do help you merging this branch, please feel free to ask.
- 7235. By Aurélien Bompard
-
Merge from the main branch
Kẏra (thekyriarchy) wrote : | # |
Barry, is there a reason this hasn't been reviewed and approved/rejected yet?
Barry Warsaw (barry) wrote : | # |
Just to let you know, I am working my way through a review/merge of this branch.
Preview Diff
1 | === modified file 'src/mailman/app/templates.py' | |||
2 | --- src/mailman/app/templates.py 2014-01-01 14:59:42 +0000 | |||
3 | +++ src/mailman/app/templates.py 2014-01-27 11:01:58 +0000 | |||
4 | @@ -103,4 +103,4 @@ | |||
5 | 103 | def get(self, uri): | 103 | def get(self, uri): |
6 | 104 | """See `ITemplateLoader`.""" | 104 | """See `ITemplateLoader`.""" |
7 | 105 | with closing(urllib2.urlopen(uri)) as fp: | 105 | with closing(urllib2.urlopen(uri)) as fp: |
9 | 106 | return fp.read() | 106 | return unicode(fp.read(), "utf-8") |
10 | 107 | 107 | ||
11 | === modified file 'src/mailman/app/tests/test_templates.py' | |||
12 | --- src/mailman/app/tests/test_templates.py 2014-01-01 14:59:42 +0000 | |||
13 | +++ src/mailman/app/tests/test_templates.py 2014-01-27 11:01:58 +0000 | |||
14 | @@ -126,3 +126,14 @@ | |||
15 | 126 | with self.assertRaises(urllib2.URLError) as cm: | 126 | with self.assertRaises(urllib2.URLError) as cm: |
16 | 127 | self._loader.get('mailman:///missing@example.com/en/foo/demo.txt') | 127 | self._loader.get('mailman:///missing@example.com/en/foo/demo.txt') |
17 | 128 | self.assertEqual(cm.exception.reason, 'No such file') | 128 | self.assertEqual(cm.exception.reason, 'No such file') |
18 | 129 | |||
19 | 130 | def test_non_ascii(self): | ||
20 | 131 | # mailman://demo.txt with non-ascii content | ||
21 | 132 | test_text = b'\xe4\xb8\xad' | ||
22 | 133 | path = os.path.join(self.var_dir, 'templates', 'site', 'it') | ||
23 | 134 | os.makedirs(path) | ||
24 | 135 | with open(os.path.join(path, 'demo.txt'), 'w') as fp: | ||
25 | 136 | print(test_text, end='', file=fp) | ||
26 | 137 | content = self._loader.get('mailman:///it/demo.txt') | ||
27 | 138 | self.assertTrue(isinstance(content, unicode)) | ||
28 | 139 | self.assertEqual(content, test_text.decode("utf-8")) | ||
29 | 129 | 140 | ||
30 | === modified file 'src/mailman/commands/cli_import.py' | |||
31 | --- src/mailman/commands/cli_import.py 2014-01-01 14:59:42 +0000 | |||
32 | +++ src/mailman/commands/cli_import.py 2014-01-27 11:01:58 +0000 | |||
33 | @@ -35,7 +35,7 @@ | |||
34 | 35 | from mailman.database.transaction import transactional | 35 | from mailman.database.transaction import transactional |
35 | 36 | from mailman.interfaces.command import ICLISubCommand | 36 | from mailman.interfaces.command import ICLISubCommand |
36 | 37 | from mailman.interfaces.listmanager import IListManager | 37 | from mailman.interfaces.listmanager import IListManager |
38 | 38 | from mailman.utilities.importer import import_config_pck | 38 | from mailman.utilities.importer import import_config_pck, Import21Error |
39 | 39 | 39 | ||
40 | 40 | 40 | ||
41 | 41 | 41 | ||
42 | 42 | 42 | ||
43 | @@ -93,4 +93,8 @@ | |||
44 | 93 | print(_('Ignoring non-dictionary: {0!r}').format( | 93 | print(_('Ignoring non-dictionary: {0!r}').format( |
45 | 94 | config_dict), file=sys.stderr) | 94 | config_dict), file=sys.stderr) |
46 | 95 | continue | 95 | continue |
48 | 96 | import_config_pck(mlist, config_dict) | 96 | try: |
49 | 97 | import_config_pck(mlist, config_dict) | ||
50 | 98 | except Import21Error, e: | ||
51 | 99 | print(e, file=sys.stderr) | ||
52 | 100 | sys.exit(1) | ||
53 | 97 | 101 | ||
54 | === modified file 'src/mailman/handlers/decorate.py' | |||
55 | --- src/mailman/handlers/decorate.py 2014-01-01 14:59:42 +0000 | |||
56 | +++ src/mailman/handlers/decorate.py 2014-01-27 11:01:58 +0000 | |||
57 | @@ -201,8 +201,8 @@ | |||
58 | 201 | 201 | ||
59 | 202 | 202 | ||
60 | 203 | 203 | ||
61 | 204 | def decorate(mlist, uri, extradict=None): | 204 | def decorate(mlist, uri, extradict=None): |
64 | 205 | """Expand the decoration template.""" | 205 | """Expand the decoration template from its URI.""" |
65 | 206 | if uri is None: | 206 | if uri is None or uri == '': |
66 | 207 | return '' | 207 | return '' |
67 | 208 | # Get the decorator template. | 208 | # Get the decorator template. |
68 | 209 | loader = getUtility(ITemplateLoader) | 209 | loader = getUtility(ITemplateLoader) |
69 | @@ -211,6 +211,11 @@ | |||
70 | 211 | language=mlist.preferred_language.code, | 211 | language=mlist.preferred_language.code, |
71 | 212 | )) | 212 | )) |
72 | 213 | template = loader.get(template_uri) | 213 | template = loader.get(template_uri) |
73 | 214 | return decorate_template(mlist, template, extradict) | ||
74 | 215 | |||
75 | 216 | |||
76 | 214 | 217 | ||
77 | 218 | def decorate_template(mlist, template, extradict=None): | ||
78 | 219 | """Expand the decoration template.""" | ||
79 | 215 | # Create a dictionary which includes the default set of interpolation | 220 | # Create a dictionary which includes the default set of interpolation |
80 | 216 | # variables allowed in headers and footers. These will be augmented by | 221 | # variables allowed in headers and footers. These will be augmented by |
81 | 217 | # any key/value pairs in the extradict. | 222 | # any key/value pairs in the extradict. |
82 | 218 | 223 | ||
83 | === modified file 'src/mailman/model/listmanager.py' | |||
84 | --- src/mailman/model/listmanager.py 2014-01-01 14:59:42 +0000 | |||
85 | +++ src/mailman/model/listmanager.py 2014-01-27 11:01:58 +0000 | |||
86 | @@ -34,6 +34,7 @@ | |||
87 | 34 | IListManager, ListAlreadyExistsError, ListCreatedEvent, ListCreatingEvent, | 34 | IListManager, ListAlreadyExistsError, ListCreatedEvent, ListCreatingEvent, |
88 | 35 | ListDeletedEvent, ListDeletingEvent) | 35 | ListDeletedEvent, ListDeletingEvent) |
89 | 36 | from mailman.model.mailinglist import MailingList | 36 | from mailman.model.mailinglist import MailingList |
90 | 37 | from mailman.model.mime import ContentFilter | ||
91 | 37 | from mailman.utilities.datetime import now | 38 | from mailman.utilities.datetime import now |
92 | 38 | 39 | ||
93 | 39 | 40 | ||
94 | @@ -79,6 +80,7 @@ | |||
95 | 79 | """See `IListManager`.""" | 80 | """See `IListManager`.""" |
96 | 80 | fqdn_listname = mlist.fqdn_listname | 81 | fqdn_listname = mlist.fqdn_listname |
97 | 81 | notify(ListDeletingEvent(mlist)) | 82 | notify(ListDeletingEvent(mlist)) |
98 | 83 | store.find(ContentFilter, ContentFilter.mailing_list == mlist).remove() | ||
99 | 82 | store.remove(mlist) | 84 | store.remove(mlist) |
100 | 83 | notify(ListDeletedEvent(fqdn_listname)) | 85 | notify(ListDeletedEvent(fqdn_listname)) |
101 | 84 | 86 | ||
102 | 85 | 87 | ||
103 | === modified file 'src/mailman/model/tests/test_listmanager.py' | |||
104 | --- src/mailman/model/tests/test_listmanager.py 2014-01-01 14:59:42 +0000 | |||
105 | +++ src/mailman/model/tests/test_listmanager.py 2014-01-27 11:01:58 +0000 | |||
106 | @@ -30,6 +30,7 @@ | |||
107 | 30 | import unittest | 30 | import unittest |
108 | 31 | 31 | ||
109 | 32 | from zope.component import getUtility | 32 | from zope.component import getUtility |
110 | 33 | from storm.locals import Store | ||
111 | 33 | 34 | ||
112 | 34 | from mailman.app.lifecycle import create_list | 35 | from mailman.app.lifecycle import create_list |
113 | 35 | from mailman.app.moderator import hold_message | 36 | from mailman.app.moderator import hold_message |
114 | @@ -40,6 +41,7 @@ | |||
115 | 40 | from mailman.interfaces.requests import IListRequests | 41 | from mailman.interfaces.requests import IListRequests |
116 | 41 | from mailman.interfaces.subscriptions import ISubscriptionService | 42 | from mailman.interfaces.subscriptions import ISubscriptionService |
117 | 42 | from mailman.interfaces.usermanager import IUserManager | 43 | from mailman.interfaces.usermanager import IUserManager |
118 | 44 | from mailman.model.mime import ContentFilter | ||
119 | 43 | from mailman.testing.helpers import ( | 45 | from mailman.testing.helpers import ( |
120 | 44 | event_subscribers, specialized_message_from_string) | 46 | event_subscribers, specialized_message_from_string) |
121 | 45 | from mailman.testing.layers import ConfigLayer | 47 | from mailman.testing.layers import ConfigLayer |
122 | @@ -129,6 +131,19 @@ | |||
123 | 129 | saved_message = getUtility(IMessageStore).get_message_by_id('<argon>') | 131 | saved_message = getUtility(IMessageStore).get_message_by_id('<argon>') |
124 | 130 | self.assertEqual(saved_message.as_string(), msg.as_string()) | 132 | self.assertEqual(saved_message.as_string(), msg.as_string()) |
125 | 131 | 133 | ||
126 | 134 | def test_content_filters_are_deleted_when_mailing_list_is_deleted(self): | ||
127 | 135 | # When a mailing list with content filters is deleted, the filters must | ||
128 | 136 | # be deleted fist or an IntegrityError will be raised | ||
129 | 137 | filter_names = ("filter_types", "pass_types", | ||
130 | 138 | "filter_extensions", "pass_extensions") | ||
131 | 139 | for fname in filter_names: | ||
132 | 140 | setattr(self._ant, fname, ["test-filter-1", "test-filter-2"]) | ||
133 | 141 | getUtility(IListManager).delete(self._ant) | ||
134 | 142 | store = Store.of(self._ant) | ||
135 | 143 | filters = store.find(ContentFilter, | ||
136 | 144 | ContentFilter.mailing_list == self._ant) | ||
137 | 145 | self.assertEqual(filters.count(), 0) | ||
138 | 146 | |||
139 | 132 | 147 | ||
140 | 133 | 148 | ||
141 | 134 | 149 | ||
142 | 135 | class TestListCreation(unittest.TestCase): | 150 | class TestListCreation(unittest.TestCase): |
143 | 136 | 151 | ||
144 | === modified file 'src/mailman/utilities/importer.py' | |||
145 | --- src/mailman/utilities/importer.py 2014-01-01 14:59:42 +0000 | |||
146 | +++ src/mailman/utilities/importer.py 2014-01-27 11:01:58 +0000 | |||
147 | @@ -22,45 +22,183 @@ | |||
148 | 22 | __metaclass__ = type | 22 | __metaclass__ = type |
149 | 23 | __all__ = [ | 23 | __all__ = [ |
150 | 24 | 'import_config_pck', | 24 | 'import_config_pck', |
151 | 25 | 'Import21Error', | ||
152 | 25 | ] | 26 | ] |
153 | 26 | 27 | ||
154 | 27 | 28 | ||
155 | 28 | import sys | 29 | import sys |
156 | 29 | import datetime | 30 | import datetime |
157 | 31 | import os | ||
158 | 32 | from urllib2 import URLError | ||
159 | 30 | 33 | ||
162 | 31 | from mailman.interfaces.action import FilterAction | 34 | from mailman.config import config |
163 | 32 | from mailman.interfaces.archiver import ArchivePolicy | 35 | from mailman.core.errors import MailmanError |
164 | 36 | from mailman.interfaces.action import FilterAction, Action | ||
165 | 33 | from mailman.interfaces.autorespond import ResponseAction | 37 | from mailman.interfaces.autorespond import ResponseAction |
166 | 34 | from mailman.interfaces.digests import DigestFrequency | 38 | from mailman.interfaces.digests import DigestFrequency |
167 | 35 | from mailman.interfaces.mailinglist import Personalization, ReplyToMunging | 39 | from mailman.interfaces.mailinglist import Personalization, ReplyToMunging |
168 | 36 | from mailman.interfaces.nntp import NewsgroupModeration | 40 | from mailman.interfaces.nntp import NewsgroupModeration |
170 | 37 | 41 | from mailman.interfaces.archiver import ArchivePolicy | |
171 | 42 | from mailman.interfaces.bans import IBanManager | ||
172 | 43 | from mailman.interfaces.mailinglist import IAcceptableAliasSet | ||
173 | 44 | from mailman.interfaces.bounce import UnrecognizedBounceDisposition | ||
174 | 45 | from mailman.interfaces.usermanager import IUserManager | ||
175 | 46 | from mailman.interfaces.member import DeliveryMode, DeliveryStatus, MemberRole | ||
176 | 47 | from mailman.interfaces.languages import ILanguageManager | ||
177 | 48 | from mailman.handlers.decorate import decorate, decorate_template | ||
178 | 49 | from mailman.utilities.i18n import search | ||
179 | 50 | from zope.component import getUtility | ||
180 | 51 | |||
181 | 52 | |||
182 | 53 | |||
183 | 38 | 54 | ||
184 | 55 | class Import21Error(MailmanError): | ||
185 | 56 | pass | ||
186 | 57 | |||
187 | 58 | |||
188 | 39 | 59 | ||
189 | 60 | def str_to_unicode(value): | ||
190 | 61 | # Convert a string to unicode when the encoding is not declared | ||
191 | 62 | if isinstance(value, unicode): | ||
192 | 63 | return value | ||
193 | 64 | for encoding in ("ascii", "utf-8"): | ||
194 | 65 | try: | ||
195 | 66 | return unicode(value, encoding) | ||
196 | 67 | except UnicodeDecodeError: | ||
197 | 68 | continue | ||
198 | 69 | # we did our best, use replace | ||
199 | 70 | return unicode(value, 'ascii', 'replace') | ||
200 | 40 | 71 | ||
201 | 41 | 72 | ||
202 | 42 | 73 | ||
203 | 43 | def seconds_to_delta(value): | 74 | def seconds_to_delta(value): |
204 | 44 | return datetime.timedelta(seconds=value) | 75 | return datetime.timedelta(seconds=value) |
205 | 45 | 76 | ||
206 | 77 | |||
207 | 46 | 78 | ||
208 | 79 | def days_to_delta(value): | ||
209 | 80 | return datetime.timedelta(days=value) | ||
210 | 81 | |||
211 | 82 | |||
212 | 47 | 83 | ||
213 | 84 | def list_members_to_unicode(value): | ||
214 | 85 | return [ unicode(item) for item in value ] | ||
215 | 86 | |||
216 | 87 | |||
217 | 48 | 88 | ||
218 | 89 | def filter_action_mapping(value): | ||
219 | 90 | # The filter_action enum values have changed. In Mailman 2.1 the order was | ||
220 | 91 | # 'Discard', 'Reject', 'Forward to List Owner', 'Preserve'. | ||
221 | 92 | # In 3.0 it's 'hold', 'reject', 'discard', 'accept', 'defer', 'forward', | ||
222 | 93 | # 'preserve' | ||
223 | 94 | if value == 0: | ||
224 | 95 | return FilterAction.discard | ||
225 | 96 | elif value == 1: | ||
226 | 97 | return FilterAction.reject | ||
227 | 98 | elif value == 2: | ||
228 | 99 | return FilterAction.forward | ||
229 | 100 | elif value == 3: | ||
230 | 101 | return FilterAction.preserve | ||
231 | 102 | else: | ||
232 | 103 | raise ValueError("Unknown filter_action value: %s" % value) | ||
233 | 104 | |||
234 | 105 | |||
235 | 49 | 106 | ||
236 | 107 | def member_action_mapping(value): | ||
237 | 108 | # The mlist.default_member_action and mlist.default_nonmember_action enum | ||
238 | 109 | # values are different in Mailman 2.1, because they have been merged into a | ||
239 | 110 | # single enum in Mailman 3 | ||
240 | 111 | # For default_member_action, which used to be called | ||
241 | 112 | # member_moderation_action, the values were: | ||
242 | 113 | # 0==Hold, 1=Reject, 2==Discard | ||
243 | 114 | if value == 0: | ||
244 | 115 | return Action.hold | ||
245 | 116 | elif value == 1: | ||
246 | 117 | return Action.reject | ||
247 | 118 | elif value == 2: | ||
248 | 119 | return Action.discard | ||
249 | 120 | def nonmember_action_mapping(value): | ||
250 | 121 | # For default_nonmember_action, which used to be called | ||
251 | 122 | # generic_nonmember_action, the values were: | ||
252 | 123 | # 0==Accept, 1==Hold, 2==Reject, 3==Discard | ||
253 | 124 | if value == 0: | ||
254 | 125 | return Action.accept | ||
255 | 126 | elif value == 1: | ||
256 | 127 | return Action.hold | ||
257 | 128 | elif value == 2: | ||
258 | 129 | return Action.reject | ||
259 | 130 | elif value == 3: | ||
260 | 131 | return Action.discard | ||
261 | 132 | |||
262 | 133 | |||
263 | 50 | 134 | ||
264 | 135 | def unicode_to_string(value): | ||
265 | 136 | return str(value) if value is not None else None | ||
266 | 137 | |||
267 | 138 | |||
268 | 51 | 139 | ||
269 | 140 | def check_language_code(code): | ||
270 | 141 | if code is None: | ||
271 | 142 | return None | ||
272 | 143 | code = unicode(code) | ||
273 | 144 | if code not in getUtility(ILanguageManager): | ||
274 | 145 | msg = """Missing language: {0} | ||
275 | 146 | You must add a section describing this language in your mailman.cfg file. | ||
276 | 147 | This section should look like this: | ||
277 | 148 | [language.{0}] | ||
278 | 149 | # The English name for the language. | ||
279 | 150 | description: CHANGE ME | ||
280 | 151 | # And the default character set for the language. | ||
281 | 152 | charset: utf-8 | ||
282 | 153 | # Whether the language is enabled or not. | ||
283 | 154 | enabled: yes | ||
284 | 155 | """.format(code) | ||
285 | 156 | raise Import21Error(msg) | ||
286 | 157 | return code | ||
287 | 158 | |||
288 | 52 | 159 | ||
289 | 53 | # Attributes in Mailman 2 which have a different type in Mailman 3. | 160 | # Attributes in Mailman 2 which have a different type in Mailman 3. |
290 | 54 | TYPES = dict( | 161 | TYPES = dict( |
291 | 55 | autorespond_owner=ResponseAction, | 162 | autorespond_owner=ResponseAction, |
292 | 56 | autorespond_postings=ResponseAction, | 163 | autorespond_postings=ResponseAction, |
293 | 57 | autorespond_requests=ResponseAction, | 164 | autorespond_requests=ResponseAction, |
294 | 165 | autoresponse_grace_period=days_to_delta, | ||
295 | 58 | bounce_info_stale_after=seconds_to_delta, | 166 | bounce_info_stale_after=seconds_to_delta, |
296 | 59 | bounce_you_are_disabled_warnings_interval=seconds_to_delta, | 167 | bounce_you_are_disabled_warnings_interval=seconds_to_delta, |
297 | 60 | digest_volume_frequency=DigestFrequency, | 168 | digest_volume_frequency=DigestFrequency, |
299 | 61 | filter_action=FilterAction, | 169 | filter_action=filter_action_mapping, |
300 | 62 | newsgroup_moderation=NewsgroupModeration, | 170 | newsgroup_moderation=NewsgroupModeration, |
301 | 63 | personalize=Personalization, | 171 | personalize=Personalization, |
302 | 64 | reply_goes_to_list=ReplyToMunging, | 172 | reply_goes_to_list=ReplyToMunging, |
303 | 173 | filter_types=list_members_to_unicode, | ||
304 | 174 | pass_types=list_members_to_unicode, | ||
305 | 175 | filter_extensions=list_members_to_unicode, | ||
306 | 176 | pass_extensions=list_members_to_unicode, | ||
307 | 177 | forward_unrecognized_bounces_to=UnrecognizedBounceDisposition, | ||
308 | 178 | default_member_action=member_action_mapping, | ||
309 | 179 | default_nonmember_action=nonmember_action_mapping, | ||
310 | 180 | moderator_password=unicode_to_string, | ||
311 | 181 | preferred_language=check_language_code, | ||
312 | 65 | ) | 182 | ) |
313 | 66 | 183 | ||
314 | 67 | 184 | ||
315 | 68 | # Attribute names in Mailman 2 which are renamed in Mailman 3. | 185 | # Attribute names in Mailman 2 which are renamed in Mailman 3. |
316 | 69 | NAME_MAPPINGS = dict( | 186 | NAME_MAPPINGS = dict( |
317 | 70 | host_name='mail_host', | ||
318 | 71 | include_list_post_header='allow_list_posts', | 187 | include_list_post_header='allow_list_posts', |
319 | 72 | real_name='display_name', | 188 | real_name='display_name', |
320 | 189 | last_post_time='last_post_at', | ||
321 | 190 | autoresponse_graceperiod='autoresponse_grace_period', | ||
322 | 191 | autorespond_admin='autorespond_owner', | ||
323 | 192 | autoresponse_admin_text='autoresponse_owner_text', | ||
324 | 193 | filter_mime_types='filter_types', | ||
325 | 194 | pass_mime_types='pass_types', | ||
326 | 195 | filter_filename_extensions='filter_extensions', | ||
327 | 196 | pass_filename_extensions='pass_extensions', | ||
328 | 197 | bounce_processing='process_bounces', | ||
329 | 198 | bounce_unrecognized_goes_to_list_owner='forward_unrecognized_bounces_to', | ||
330 | 199 | mod_password='moderator_password', | ||
331 | 200 | news_moderation='newsgroup_moderation', | ||
332 | 201 | news_prefix_subject_too='nntp_prefix_subject_too', | ||
333 | 202 | send_welcome_msg='send_welcome_message', | ||
334 | 203 | send_goodbye_msg='send_goodbye_message', | ||
335 | 204 | member_moderation_action='default_member_action', | ||
336 | 205 | generic_nonmember_action='default_nonmember_action', | ||
337 | 206 | ) | ||
338 | 207 | |||
339 | 208 | EXCLUDES = ( | ||
340 | 209 | "members", | ||
341 | 210 | "digest_members", | ||
342 | 73 | ) | 211 | ) |
343 | 74 | 212 | ||
344 | 75 | 213 | ||
345 | @@ -74,14 +212,19 @@ | |||
346 | 74 | :type config_dict: dict | 212 | :type config_dict: dict |
347 | 75 | """ | 213 | """ |
348 | 76 | for key, value in config_dict.items(): | 214 | for key, value in config_dict.items(): |
349 | 215 | # Some attributes must not be directly imported | ||
350 | 216 | if key in EXCLUDES: | ||
351 | 217 | continue | ||
352 | 77 | # Some attributes from Mailman 2 were renamed in Mailman 3. | 218 | # Some attributes from Mailman 2 were renamed in Mailman 3. |
353 | 78 | key = NAME_MAPPINGS.get(key, key) | 219 | key = NAME_MAPPINGS.get(key, key) |
354 | 79 | # Handle the simple case where the key is an attribute of the | 220 | # Handle the simple case where the key is an attribute of the |
355 | 80 | # IMailingList and the types are the same (modulo 8-bit/unicode | 221 | # IMailingList and the types are the same (modulo 8-bit/unicode |
356 | 81 | # strings). | 222 | # strings). |
358 | 82 | if hasattr(mlist, key): | 223 | # When attributes raise an exception, hasattr may think they don't |
359 | 224 | # exist (see python issue 9666). Add them here. | ||
360 | 225 | if hasattr(mlist, key) or key in ("preferred_language", ): | ||
361 | 83 | if isinstance(value, str): | 226 | if isinstance(value, str): |
363 | 84 | value = unicode(value, 'ascii') | 227 | value = str_to_unicode(value) |
364 | 85 | # Some types require conversion. | 228 | # Some types require conversion. |
365 | 86 | converter = TYPES.get(key) | 229 | converter = TYPES.get(key) |
366 | 87 | if converter is not None: | 230 | if converter is not None: |
367 | @@ -103,3 +246,184 @@ | |||
368 | 103 | mlist.archive_policy = ArchivePolicy.public | 246 | mlist.archive_policy = ArchivePolicy.public |
369 | 104 | else: | 247 | else: |
370 | 105 | mlist.archive_policy = ArchivePolicy.never | 248 | mlist.archive_policy = ArchivePolicy.never |
371 | 249 | # Handle ban list | ||
372 | 250 | for addr in config_dict.get('ban_list', []): | ||
373 | 251 | IBanManager(mlist).ban(str_to_unicode(addr)) | ||
374 | 252 | # Handle acceptable aliases | ||
375 | 253 | acceptable_aliases = config_dict.get('acceptable_aliases', '') | ||
376 | 254 | if isinstance(acceptable_aliases, basestring): | ||
377 | 255 | acceptable_aliases = acceptable_aliases.splitlines() | ||
378 | 256 | for addr in acceptable_aliases: | ||
379 | 257 | addr = addr.strip() | ||
380 | 258 | if not addr: | ||
381 | 259 | continue | ||
382 | 260 | addr = str_to_unicode(addr) | ||
383 | 261 | try: | ||
384 | 262 | IAcceptableAliasSet(mlist).add(addr) | ||
385 | 263 | except ValueError: | ||
386 | 264 | IAcceptableAliasSet(mlist).add("^" + addr) | ||
387 | 265 | # Handle conversion to URIs | ||
388 | 266 | convert_to_uri = { | ||
389 | 267 | "welcome_msg": "welcome_message_uri", | ||
390 | 268 | "goodbye_msg": "goodbye_message_uri", | ||
391 | 269 | "msg_header": "header_uri", | ||
392 | 270 | "msg_footer": "footer_uri", | ||
393 | 271 | "digest_header": "digest_header_uri", | ||
394 | 272 | "digest_footer": "digest_footer_uri", | ||
395 | 273 | } | ||
396 | 274 | convert_placeholders = { # only the most common ones | ||
397 | 275 | "%(real_name)s": "$display_name", | ||
398 | 276 | "%(real_name)s@%(host_name)s": "$fqdn_listname", | ||
399 | 277 | "%(web_page_url)slistinfo%(cgiext)s/%(_internal_name)s": "$listinfo_uri", | ||
400 | 278 | } | ||
401 | 279 | # Collect defaults | ||
402 | 280 | defaults = {} | ||
403 | 281 | for oldvar, newvar in convert_to_uri.iteritems(): | ||
404 | 282 | default_value = getattr(mlist, newvar) | ||
405 | 283 | if not default_value: | ||
406 | 284 | continue | ||
407 | 285 | # Check if the value changed from the default | ||
408 | 286 | try: | ||
409 | 287 | default_text = decorate(mlist, default_value) | ||
410 | 288 | except (URLError, KeyError): | ||
411 | 289 | # Use case: importing the old a@ex.com into b@ex.com | ||
412 | 290 | # We can't check if it changed from the default | ||
413 | 291 | # -> don't import, we may do more harm than good and it's easy to | ||
414 | 292 | # change if needed | ||
415 | 293 | continue | ||
416 | 294 | defaults[newvar] = (default_value, default_text) | ||
417 | 295 | for oldvar, newvar in convert_to_uri.iteritems(): | ||
418 | 296 | if oldvar not in config_dict: | ||
419 | 297 | continue | ||
420 | 298 | text = config_dict[oldvar] | ||
421 | 299 | text = unicode(text, "utf-8", "replace") | ||
422 | 300 | for oldph, newph in convert_placeholders.iteritems(): | ||
423 | 301 | text = text.replace(oldph, newph) | ||
424 | 302 | default_value, default_text = defaults.get(newvar, (None, None)) | ||
425 | 303 | if not text and not (default_value or default_text): | ||
426 | 304 | continue # both are empty, leave it | ||
427 | 305 | # Check if the value changed from the default | ||
428 | 306 | try: | ||
429 | 307 | expanded_text = decorate_template(mlist, text) | ||
430 | 308 | except KeyError: | ||
431 | 309 | # Use case: importing the old a@ex.com into b@ex.com | ||
432 | 310 | # We can't check if it changed from the default | ||
433 | 311 | # -> don't import, we may do more harm than good and it's easy to | ||
434 | 312 | # change if needed | ||
435 | 313 | continue | ||
436 | 314 | if expanded_text and default_text \ | ||
437 | 315 | and expanded_text.strip() == default_text.strip(): | ||
438 | 316 | continue # keep the default | ||
439 | 317 | # Write the custom value to the right file | ||
440 | 318 | base_uri = "mailman:///$listname/$language/" | ||
441 | 319 | if default_value: | ||
442 | 320 | filename = default_value.rpartition("/")[2] | ||
443 | 321 | else: | ||
444 | 322 | filename = "%s.txt" % newvar[:-4] | ||
445 | 323 | if not default_value or not default_value.startswith(base_uri): | ||
446 | 324 | setattr(mlist, newvar, base_uri + filename) | ||
447 | 325 | filepath = list(search(filename, mlist))[0] | ||
448 | 326 | try: | ||
449 | 327 | os.makedirs(os.path.dirname(filepath)) | ||
450 | 328 | except OSError, e: | ||
451 | 329 | if e.errno != 17: # Already exists | ||
452 | 330 | raise | ||
453 | 331 | with open(filepath, "w") as template: | ||
454 | 332 | template.write(text.encode('utf-8')) | ||
455 | 333 | # Import rosters | ||
456 | 334 | members = set(config_dict.get("members", {}).keys() | ||
457 | 335 | + config_dict.get("digest_members", {}).keys()) | ||
458 | 336 | import_roster(mlist, config_dict, members, MemberRole.member) | ||
459 | 337 | import_roster(mlist, config_dict, config_dict.get("owner", []), | ||
460 | 338 | MemberRole.owner) | ||
461 | 339 | import_roster(mlist, config_dict, config_dict.get("moderator", []), | ||
462 | 340 | MemberRole.moderator) | ||
463 | 341 | |||
464 | 342 | |||
465 | 343 | |||
466 | 106 | 344 | ||
467 | 345 | def import_roster(mlist, config_dict, members, role): | ||
468 | 346 | """ | ||
469 | 347 | Import members lists from a config.pck configuration dictionary to a | ||
470 | 348 | mailing list. | ||
471 | 349 | |||
472 | 350 | :param mlist: The mailing list. | ||
473 | 351 | :type mlist: IMailingList | ||
474 | 352 | :param config_dict: The Mailman 2.1 configuration dictionary. | ||
475 | 353 | :type config_dict: dict | ||
476 | 354 | :param members: The members list to import | ||
477 | 355 | :type members: list | ||
478 | 356 | :param role: The MemberRole to import them as | ||
479 | 357 | :type role: MemberRole enum | ||
480 | 358 | """ | ||
481 | 359 | usermanager = getUtility(IUserManager) | ||
482 | 360 | for email in members: | ||
483 | 361 | # for owners and members, the emails can have a mixed case, so | ||
484 | 362 | # lowercase them all | ||
485 | 363 | email = str_to_unicode(email).lower() | ||
486 | 364 | roster = mlist.get_roster(role) | ||
487 | 365 | if roster.get_member(email) is not None: | ||
488 | 366 | print("%s is already imported with role %s" % (email, role), | ||
489 | 367 | file=sys.stderr) | ||
490 | 368 | continue | ||
491 | 369 | address = usermanager.get_address(email) | ||
492 | 370 | user = usermanager.get_user(email) | ||
493 | 371 | if user is None: | ||
494 | 372 | user = usermanager.create_user() | ||
495 | 373 | if address is None: | ||
496 | 374 | merged_members = {} | ||
497 | 375 | merged_members.update(config_dict.get("members", {})) | ||
498 | 376 | merged_members.update(config_dict.get("digest_members", {})) | ||
499 | 377 | if merged_members.get(email, 0) != 0: | ||
500 | 378 | original_email = str_to_unicode(merged_members[email]) | ||
501 | 379 | else: | ||
502 | 380 | original_email = email | ||
503 | 381 | address = usermanager.create_address(original_email) | ||
504 | 382 | address.verified_on = datetime.datetime.now() | ||
505 | 383 | user.link(address) | ||
506 | 384 | mlist.subscribe(address, role) | ||
507 | 385 | member = roster.get_member(email) | ||
508 | 386 | assert member is not None | ||
509 | 387 | prefs = config_dict.get("user_options", {}).get(email, 0) | ||
510 | 388 | if email in config_dict.get("members", {}): | ||
511 | 389 | member.preferences.delivery_mode = DeliveryMode.regular | ||
512 | 390 | elif email in config_dict.get("digest_members", {}): | ||
513 | 391 | if prefs & 8: # DisableMime | ||
514 | 392 | member.preferences.delivery_mode = DeliveryMode.plaintext_digests | ||
515 | 393 | else: | ||
516 | 394 | member.preferences.delivery_mode = DeliveryMode.mime_digests | ||
517 | 395 | else: | ||
518 | 396 | # probably not adding a member role here | ||
519 | 397 | pass | ||
520 | 398 | if email in config_dict.get("language", {}): | ||
521 | 399 | member.preferences.preferred_language = \ | ||
522 | 400 | check_language_code(config_dict["language"][email]) | ||
523 | 401 | # if the user already exists, display_name and password will be | ||
524 | 402 | # overwritten | ||
525 | 403 | if email in config_dict.get("usernames", {}): | ||
526 | 404 | address.display_name = \ | ||
527 | 405 | str_to_unicode(config_dict["usernames"][email]) | ||
528 | 406 | user.display_name = \ | ||
529 | 407 | str_to_unicode(config_dict["usernames"][email]) | ||
530 | 408 | if email in config_dict.get("passwords", {}): | ||
531 | 409 | user.password = config.password_context.encrypt( | ||
532 | 410 | config_dict["passwords"][email]) | ||
533 | 411 | # delivery_status | ||
534 | 412 | oldds = config_dict.get("delivery_status", {}).get(email, (0, 0))[0] | ||
535 | 413 | if oldds == 0: | ||
536 | 414 | member.preferences.delivery_status = DeliveryStatus.enabled | ||
537 | 415 | elif oldds == 1: | ||
538 | 416 | member.preferences.delivery_status = DeliveryStatus.unknown | ||
539 | 417 | elif oldds == 2: | ||
540 | 418 | member.preferences.delivery_status = DeliveryStatus.by_user | ||
541 | 419 | elif oldds == 3: | ||
542 | 420 | member.preferences.delivery_status = DeliveryStatus.by_moderator | ||
543 | 421 | elif oldds == 4: | ||
544 | 422 | member.preferences.delivery_status = DeliveryStatus.by_bounces | ||
545 | 423 | # moderation | ||
546 | 424 | if prefs & 128: | ||
547 | 425 | member.moderation_action = Action.hold | ||
548 | 426 | # other preferences | ||
549 | 427 | member.preferences.acknowledge_posts = bool(prefs & 4) # AcknowledgePosts | ||
550 | 428 | member.preferences.hide_address = bool(prefs & 16) # ConcealSubscription | ||
551 | 429 | member.preferences.receive_own_postings = not bool(prefs & 2) # DontReceiveOwnPosts | ||
552 | 430 | member.preferences.receive_list_copy = not bool(prefs & 256) # DontReceiveDuplicates | ||
553 | 107 | 431 | ||
554 | === modified file 'src/mailman/utilities/tests/test_import.py' | |||
555 | --- src/mailman/utilities/tests/test_import.py 2014-01-01 14:59:42 +0000 | |||
556 | +++ src/mailman/utilities/tests/test_import.py 2014-01-27 11:01:58 +0000 | |||
557 | @@ -26,15 +26,41 @@ | |||
558 | 26 | ] | 26 | ] |
559 | 27 | 27 | ||
560 | 28 | 28 | ||
561 | 29 | import os | ||
562 | 29 | import cPickle | 30 | import cPickle |
563 | 30 | import unittest | 31 | import unittest |
564 | 32 | from datetime import timedelta, datetime | ||
565 | 33 | from traceback import format_exc | ||
566 | 31 | 34 | ||
567 | 35 | from mailman.config import config | ||
568 | 32 | from mailman.app.lifecycle import create_list, remove_list | 36 | from mailman.app.lifecycle import create_list, remove_list |
569 | 37 | from mailman.testing.layers import ConfigLayer | ||
570 | 38 | from mailman.utilities.importer import import_config_pck, Import21Error | ||
571 | 33 | from mailman.interfaces.archiver import ArchivePolicy | 39 | from mailman.interfaces.archiver import ArchivePolicy |
574 | 34 | from mailman.testing.layers import ConfigLayer | 40 | from mailman.interfaces.action import Action, FilterAction |
575 | 35 | from mailman.utilities.importer import import_config_pck | 41 | from mailman.interfaces.address import ExistingAddressError |
576 | 42 | from mailman.interfaces.bounce import UnrecognizedBounceDisposition | ||
577 | 43 | from mailman.interfaces.bans import IBanManager | ||
578 | 44 | from mailman.interfaces.mailinglist import IAcceptableAliasSet | ||
579 | 45 | from mailman.interfaces.nntp import NewsgroupModeration | ||
580 | 46 | from mailman.interfaces.autorespond import ResponseAction | ||
581 | 47 | from mailman.interfaces.templates import ITemplateLoader | ||
582 | 48 | from mailman.interfaces.usermanager import IUserManager | ||
583 | 49 | from mailman.interfaces.member import DeliveryMode, DeliveryStatus | ||
584 | 50 | from mailman.interfaces.languages import ILanguageManager | ||
585 | 51 | from mailman.model.address import Address | ||
586 | 52 | from mailman.handlers.decorate import decorate | ||
587 | 53 | from mailman.utilities.string import expand | ||
588 | 36 | from pkg_resources import resource_filename | 54 | from pkg_resources import resource_filename |
590 | 37 | 55 | from enum import Enum | |
591 | 56 | from zope.component import getUtility | ||
592 | 57 | from storm.locals import Store | ||
593 | 58 | |||
594 | 59 | |||
595 | 60 | |||
596 | 38 | 61 | ||
597 | 62 | class DummyEnum(Enum): | ||
598 | 63 | # For testing purposes | ||
599 | 64 | val = 42 | ||
600 | 39 | 65 | ||
601 | 40 | 66 | ||
602 | 41 | 67 | ||
603 | 42 | class TestBasicImport(unittest.TestCase): | 68 | class TestBasicImport(unittest.TestCase): |
604 | @@ -58,11 +84,12 @@ | |||
605 | 58 | self._import() | 84 | self._import() |
606 | 59 | self.assertEqual(self._mlist.display_name, 'Test') | 85 | self.assertEqual(self._mlist.display_name, 'Test') |
607 | 60 | 86 | ||
610 | 61 | def test_mail_host(self): | 87 | def test_mail_host_invariant(self): |
611 | 62 | # The mlist.mail_host gets set. | 88 | # The mlist.mail_host must not be updated when importing (it will |
612 | 89 | # change the list_id property, which is supposed to be read-only) | ||
613 | 63 | self.assertEqual(self._mlist.mail_host, 'example.com') | 90 | self.assertEqual(self._mlist.mail_host, 'example.com') |
614 | 64 | self._import() | 91 | self._import() |
616 | 65 | self.assertEqual(self._mlist.mail_host, 'heresy.example.org') | 92 | self.assertEqual(self._mlist.mail_host, 'example.com') |
617 | 66 | 93 | ||
618 | 67 | def test_rfc2369_headers(self): | 94 | def test_rfc2369_headers(self): |
619 | 68 | self._mlist.allow_list_posts = False | 95 | self._mlist.allow_list_posts = False |
620 | @@ -71,6 +98,204 @@ | |||
621 | 71 | self.assertTrue(self._mlist.allow_list_posts) | 98 | self.assertTrue(self._mlist.allow_list_posts) |
622 | 72 | self.assertTrue(self._mlist.include_rfc2369_headers) | 99 | self.assertTrue(self._mlist.include_rfc2369_headers) |
623 | 73 | 100 | ||
624 | 101 | def test_no_overwrite_rosters(self): | ||
625 | 102 | # The mlist.members and mlist.digest_members rosters must not be | ||
626 | 103 | # overwritten. | ||
627 | 104 | for rname in ("members", "digest_members"): | ||
628 | 105 | roster = getattr(self._mlist, rname) | ||
629 | 106 | self.assertFalse(isinstance(roster, dict)) | ||
630 | 107 | self._import() | ||
631 | 108 | self.assertFalse(isinstance(roster, dict), | ||
632 | 109 | "The %s roster has been overwritten by the import" % rname) | ||
633 | 110 | |||
634 | 111 | def test_last_post_time(self): | ||
635 | 112 | # last_post_time -> last_post_at | ||
636 | 113 | self._pckdict["last_post_time"] = 1270420800.274485 | ||
637 | 114 | self.assertEqual(self._mlist.last_post_at, None) | ||
638 | 115 | self._import() | ||
639 | 116 | # convert 1270420800.2744851 to datetime | ||
640 | 117 | expected = datetime(2010, 4, 4, 22, 40, 0, 274485) | ||
641 | 118 | self.assertEqual(self._mlist.last_post_at, expected) | ||
642 | 119 | |||
643 | 120 | def test_autoresponse_grace_period(self): | ||
644 | 121 | # autoresponse_graceperiod -> autoresponse_grace_period | ||
645 | 122 | # must be a timedelta, not an int | ||
646 | 123 | self._mlist.autoresponse_grace_period = timedelta(days=42) | ||
647 | 124 | self._import() | ||
648 | 125 | self.assertTrue(isinstance( | ||
649 | 126 | self._mlist.autoresponse_grace_period, timedelta)) | ||
650 | 127 | self.assertEqual(self._mlist.autoresponse_grace_period, | ||
651 | 128 | timedelta(days=90)) | ||
652 | 129 | |||
653 | 130 | def test_autoresponse_admin_to_owner(self): | ||
654 | 131 | # admin -> owner | ||
655 | 132 | self._mlist.autorespond_owner = DummyEnum.val | ||
656 | 133 | self._mlist.autoresponse_owner_text = 'DUMMY' | ||
657 | 134 | self._import() | ||
658 | 135 | self.assertEqual(self._mlist.autorespond_owner, ResponseAction.none) | ||
659 | 136 | self.assertEqual(self._mlist.autoresponse_owner_text, '') | ||
660 | 137 | |||
661 | 138 | #def test_administrative(self): | ||
662 | 139 | # # administrivia -> administrative | ||
663 | 140 | # self._mlist.administrative = None | ||
664 | 141 | # self._import() | ||
665 | 142 | # self.assertTrue(self._mlist.administrative) | ||
666 | 143 | |||
667 | 144 | def test_filter_pass_renames(self): | ||
668 | 145 | # mime_types -> types | ||
669 | 146 | # filename_extensions -> extensions | ||
670 | 147 | self._mlist.filter_types = ["dummy"] | ||
671 | 148 | self._mlist.pass_types = ["dummy"] | ||
672 | 149 | self._mlist.filter_extensions = ["dummy"] | ||
673 | 150 | self._mlist.pass_extensions = ["dummy"] | ||
674 | 151 | self._import() | ||
675 | 152 | self.assertEqual(list(self._mlist.filter_types), []) | ||
676 | 153 | self.assertEqual(list(self._mlist.filter_extensions), | ||
677 | 154 | ['exe', 'bat', 'cmd', 'com', 'pif', | ||
678 | 155 | 'scr', 'vbs', 'cpl']) | ||
679 | 156 | self.assertEqual(list(self._mlist.pass_types), | ||
680 | 157 | ['multipart/mixed', 'multipart/alternative', 'text/plain']) | ||
681 | 158 | self.assertEqual(list(self._mlist.pass_extensions), []) | ||
682 | 159 | |||
683 | 160 | def test_process_bounces(self): | ||
684 | 161 | # bounce_processing -> process_bounces | ||
685 | 162 | self._mlist.process_bounces = None | ||
686 | 163 | self._import() | ||
687 | 164 | self.assertTrue(self._mlist.process_bounces) | ||
688 | 165 | |||
689 | 166 | def test_forward_unrecognized_bounces_to(self): | ||
690 | 167 | # bounce_unrecognized_goes_to_list_owner -> forward_unrecognized_bounces_to | ||
691 | 168 | self._mlist.forward_unrecognized_bounces_to = DummyEnum.val | ||
692 | 169 | self._import() | ||
693 | 170 | self.assertEqual(self._mlist.forward_unrecognized_bounces_to, | ||
694 | 171 | UnrecognizedBounceDisposition.administrators) | ||
695 | 172 | |||
696 | 173 | def test_moderator_password(self): | ||
697 | 174 | # mod_password -> moderator_password | ||
698 | 175 | self._mlist.moderator_password = str("TESTDATA") | ||
699 | 176 | self._import() | ||
700 | 177 | self.assertEqual(self._mlist.moderator_password, None) | ||
701 | 178 | |||
702 | 179 | def test_moderator_password_str(self): | ||
703 | 180 | # moderator_password must not be unicode | ||
704 | 181 | self._pckdict[b"mod_password"] = b'TESTVALUE' | ||
705 | 182 | self._import() | ||
706 | 183 | self.assertFalse(isinstance(self._mlist.moderator_password, unicode)) | ||
707 | 184 | self.assertEqual(self._mlist.moderator_password, b'TESTVALUE') | ||
708 | 185 | |||
709 | 186 | def test_newsgroup_moderation(self): | ||
710 | 187 | # news_moderation -> newsgroup_moderation | ||
711 | 188 | # news_prefix_subject_too -> nntp_prefix_subject_too | ||
712 | 189 | self._mlist.newsgroup_moderation = DummyEnum.val | ||
713 | 190 | self._mlist.nntp_prefix_subject_too = None | ||
714 | 191 | self._import() | ||
715 | 192 | self.assertEqual(self._mlist.newsgroup_moderation, | ||
716 | 193 | NewsgroupModeration.none) | ||
717 | 194 | self.assertTrue(self._mlist.nntp_prefix_subject_too) | ||
718 | 195 | |||
719 | 196 | def test_msg_to_message(self): | ||
720 | 197 | # send_welcome_msg -> send_welcome_message | ||
721 | 198 | # send_goodbye_msg -> send_goodbye_message | ||
722 | 199 | self._mlist.send_welcome_message = None | ||
723 | 200 | self._mlist.send_goodbye_message = None | ||
724 | 201 | self._import() | ||
725 | 202 | self.assertTrue(self._mlist.send_welcome_message) | ||
726 | 203 | self.assertTrue(self._mlist.send_goodbye_message) | ||
727 | 204 | |||
728 | 205 | def test_ban_list(self): | ||
729 | 206 | banned = [ | ||
730 | 207 | ("anne@example.com", "anne@example.com"), | ||
731 | 208 | ("^.*@example.com", "bob@example.com"), | ||
732 | 209 | ("non-ascii-\xe8@example.com", "non-ascii-\ufffd@example.com"), | ||
733 | 210 | ] | ||
734 | 211 | self._pckdict["ban_list"] = [ b[0].encode("iso-8859-1") for b in banned ] | ||
735 | 212 | try: | ||
736 | 213 | self._import() | ||
737 | 214 | except UnicodeDecodeError, e: | ||
738 | 215 | print(format_exc()) | ||
739 | 216 | self.fail(e) | ||
740 | 217 | for _pattern, addr in banned: | ||
741 | 218 | self.assertTrue(IBanManager(self._mlist).is_banned(addr)) | ||
742 | 219 | |||
743 | 220 | def test_acceptable_aliases(self): | ||
744 | 221 | # it used to be a plain-text field (values are newline-separated) | ||
745 | 222 | aliases = ["alias1@example.com", | ||
746 | 223 | "alias2@exemple.com", | ||
747 | 224 | "non-ascii-\xe8@example.com", | ||
748 | 225 | ] | ||
749 | 226 | self._pckdict[b"acceptable_aliases"] = \ | ||
750 | 227 | ("\n".join(aliases)).encode("utf-8") | ||
751 | 228 | self._import() | ||
752 | 229 | alias_set = IAcceptableAliasSet(self._mlist) | ||
753 | 230 | self.assertEqual(sorted(alias_set.aliases), aliases) | ||
754 | 231 | |||
755 | 232 | def test_acceptable_aliases_invalid(self): | ||
756 | 233 | # values without an '@' sign used to be matched against the local part, | ||
757 | 234 | # now we need to add the '^' sign | ||
758 | 235 | aliases = ["invalid-value", ] | ||
759 | 236 | self._pckdict[b"acceptable_aliases"] = \ | ||
760 | 237 | ("\n".join(aliases)).encode("utf-8") | ||
761 | 238 | try: | ||
762 | 239 | self._import() | ||
763 | 240 | except ValueError, e: | ||
764 | 241 | print(format_exc()) | ||
765 | 242 | self.fail("Invalid value '%s' caused a crash" % e) | ||
766 | 243 | alias_set = IAcceptableAliasSet(self._mlist) | ||
767 | 244 | self.assertEqual(sorted(alias_set.aliases), | ||
768 | 245 | [ ("^" + a) for a in aliases ]) | ||
769 | 246 | |||
770 | 247 | def test_acceptable_aliases_as_list(self): | ||
771 | 248 | # in some versions of the pickle, it can be a list, not a string | ||
772 | 249 | # (seen in the wild) | ||
773 | 250 | aliases = [b"alias1@example.com", b"alias2@exemple.com" ] | ||
774 | 251 | self._pckdict[b"acceptable_aliases"] = aliases | ||
775 | 252 | try: | ||
776 | 253 | self._import() | ||
777 | 254 | except AttributeError: | ||
778 | 255 | print(format_exc()) | ||
779 | 256 | self.fail("Import does not handle acceptable_aliases as list") | ||
780 | 257 | alias_set = IAcceptableAliasSet(self._mlist) | ||
781 | 258 | self.assertEqual(sorted(alias_set.aliases), aliases) | ||
782 | 259 | |||
783 | 260 | def test_info_non_ascii(self): | ||
784 | 261 | # info can contain non-ascii chars | ||
785 | 262 | info = 'O idioma aceito \xe9 somente Portugu\xeas do Brasil' | ||
786 | 263 | self._pckdict[b"info"] = info.encode("utf-8") | ||
787 | 264 | self._import() | ||
788 | 265 | self.assertEqual(self._mlist.info, info, | ||
789 | 266 | "Encoding to UTF-8 is not handled") | ||
790 | 267 | # test fallback to ascii with replace | ||
791 | 268 | self._pckdict[b"info"] = info.encode("iso-8859-1") | ||
792 | 269 | self._import() | ||
793 | 270 | self.assertEqual(self._mlist.info, | ||
794 | 271 | unicode(self._pckdict[b"info"], "ascii", "replace"), | ||
795 | 272 | "We don't fall back to replacing non-ascii chars") | ||
796 | 273 | |||
797 | 274 | def test_preferred_language(self): | ||
798 | 275 | self._pckdict[b"preferred_language"] = b'ja' | ||
799 | 276 | english = getUtility(ILanguageManager).get('en') | ||
800 | 277 | japanese = getUtility(ILanguageManager).get('ja') | ||
801 | 278 | self.assertEqual(self._mlist.preferred_language, english) | ||
802 | 279 | self._import() | ||
803 | 280 | self.assertEqual(self._mlist.preferred_language, japanese) | ||
804 | 281 | |||
805 | 282 | def test_preferred_language_unknown_previous(self): | ||
806 | 283 | # when the previous language is unknown, it should not fail | ||
807 | 284 | self._mlist._preferred_language = 'xx' # non-existant | ||
808 | 285 | self._import() | ||
809 | 286 | english = getUtility(ILanguageManager).get('en') | ||
810 | 287 | self.assertEqual(self._mlist.preferred_language, english) | ||
811 | 288 | |||
812 | 289 | def test_new_language(self): | ||
813 | 290 | self._pckdict[b"preferred_language"] = b'xx_XX' | ||
814 | 291 | try: | ||
815 | 292 | self._import() | ||
816 | 293 | except Import21Error, e: | ||
817 | 294 | # check the message | ||
818 | 295 | self.assertTrue("[language.xx_XX]" in str(e)) | ||
819 | 296 | else: | ||
820 | 297 | self.fail("Import21Error was not raised") | ||
821 | 298 | |||
822 | 74 | 299 | ||
823 | 75 | 300 | ||
824 | 76 | 301 | ||
825 | 77 | class TestArchiveImport(unittest.TestCase): | 302 | class TestArchiveImport(unittest.TestCase): |
826 | @@ -83,7 +308,10 @@ | |||
827 | 83 | 308 | ||
828 | 84 | def setUp(self): | 309 | def setUp(self): |
829 | 85 | self._mlist = create_list('blank@example.com') | 310 | self._mlist = create_list('blank@example.com') |
831 | 86 | self._mlist.archive_policy = 'INITIAL-TEST-VALUE' | 311 | self._mlist.archive_policy = DummyEnum.val |
832 | 312 | |||
833 | 313 | def tearDown(self): | ||
834 | 314 | remove_list(self._mlist) | ||
835 | 87 | 315 | ||
836 | 88 | def _do_test(self, pckdict, expected): | 316 | def _do_test(self, pckdict, expected): |
837 | 89 | import_config_pck(self._mlist, pckdict) | 317 | import_config_pck(self._mlist, pckdict) |
838 | @@ -123,3 +351,516 @@ | |||
839 | 123 | # For some reason, the old list was missing an `archive_private` key. | 351 | # For some reason, the old list was missing an `archive_private` key. |
840 | 124 | # For maximum safety, we treat this as private archiving. | 352 | # For maximum safety, we treat this as private archiving. |
841 | 125 | self._do_test(dict(archive=True), ArchivePolicy.private) | 353 | self._do_test(dict(archive=True), ArchivePolicy.private) |
842 | 354 | |||
843 | 355 | |||
844 | 356 | |||
845 | 126 | 357 | ||
846 | 358 | class TestFilterActionImport(unittest.TestCase): | ||
847 | 359 | # The mlist.filter_action enum values have changed. In Mailman 2.1 the | ||
848 | 360 | # order was 'Discard', 'Reject', 'Forward to List Owner', 'Preserve'. | ||
849 | 361 | |||
850 | 362 | layer = ConfigLayer | ||
851 | 363 | |||
852 | 364 | def setUp(self): | ||
853 | 365 | self._mlist = create_list('blank@example.com') | ||
854 | 366 | self._mlist.filter_action = DummyEnum.val | ||
855 | 367 | |||
856 | 368 | def tearDown(self): | ||
857 | 369 | remove_list(self._mlist) | ||
858 | 370 | |||
859 | 371 | def _do_test(self, original, expected): | ||
860 | 372 | import_config_pck(self._mlist, dict(filter_action=original)) | ||
861 | 373 | self.assertEqual(self._mlist.filter_action, expected) | ||
862 | 374 | |||
863 | 375 | def test_discard(self): | ||
864 | 376 | self._do_test(0, FilterAction.discard) | ||
865 | 377 | |||
866 | 378 | def test_reject(self): | ||
867 | 379 | self._do_test(1, FilterAction.reject) | ||
868 | 380 | |||
869 | 381 | def test_forward(self): | ||
870 | 382 | self._do_test(2, FilterAction.forward) | ||
871 | 383 | |||
872 | 384 | def test_preserve(self): | ||
873 | 385 | self._do_test(3, FilterAction.preserve) | ||
874 | 386 | |||
875 | 387 | |||
876 | 388 | |||
877 | 127 | 389 | ||
878 | 390 | class TestMemberActionImport(unittest.TestCase): | ||
879 | 391 | # The mlist.default_member_action and mlist.default_nonmember_action enum | ||
880 | 392 | # values are different in Mailman 2.1, they have been merged into a | ||
881 | 393 | # single enum in Mailman 3 | ||
882 | 394 | # For default_member_action, which used to be called | ||
883 | 395 | # member_moderation_action, the values were: | ||
884 | 396 | # 0==Hold, 1=Reject, 2==Discard | ||
885 | 397 | # For default_nonmember_action, which used to be called | ||
886 | 398 | # generic_nonmember_action, the values were: | ||
887 | 399 | # 0==Accept, 1==Hold, 2==Reject, 3==Discard | ||
888 | 400 | |||
889 | 401 | layer = ConfigLayer | ||
890 | 402 | |||
891 | 403 | def setUp(self): | ||
892 | 404 | self._mlist = create_list('blank@example.com') | ||
893 | 405 | self._mlist.default_member_action = DummyEnum.val | ||
894 | 406 | self._mlist.default_nonmember_action = DummyEnum.val | ||
895 | 407 | self._pckdict = dict( | ||
896 | 408 | member_moderation_action=DummyEnum.val, | ||
897 | 409 | generic_nonmember_action=DummyEnum.val, | ||
898 | 410 | ) | ||
899 | 411 | |||
900 | 412 | def tearDown(self): | ||
901 | 413 | remove_list(self._mlist) | ||
902 | 414 | |||
903 | 415 | def _do_test(self, expected): | ||
904 | 416 | import_config_pck(self._mlist, self._pckdict) | ||
905 | 417 | for key, value in expected.iteritems(): | ||
906 | 418 | self.assertEqual(getattr(self._mlist, key), value) | ||
907 | 419 | |||
908 | 420 | def test_member_hold(self): | ||
909 | 421 | self._pckdict[b"member_moderation_action"] = 0 | ||
910 | 422 | self._do_test(dict(default_member_action=Action.hold)) | ||
911 | 423 | |||
912 | 424 | def test_member_reject(self): | ||
913 | 425 | self._pckdict[b"member_moderation_action"] = 1 | ||
914 | 426 | self._do_test(dict(default_member_action=Action.reject)) | ||
915 | 427 | |||
916 | 428 | def test_member_discard(self): | ||
917 | 429 | self._pckdict[b"member_moderation_action"] = 2 | ||
918 | 430 | self._do_test(dict(default_member_action=Action.discard)) | ||
919 | 431 | |||
920 | 432 | def test_nonmember_accept(self): | ||
921 | 433 | self._pckdict[b"generic_nonmember_action"] = 0 | ||
922 | 434 | self._do_test(dict(default_nonmember_action=Action.accept)) | ||
923 | 435 | |||
924 | 436 | def test_nonmember_hold(self): | ||
925 | 437 | self._pckdict[b"generic_nonmember_action"] = 1 | ||
926 | 438 | self._do_test(dict(default_nonmember_action=Action.hold)) | ||
927 | 439 | |||
928 | 440 | def test_nonmember_reject(self): | ||
929 | 441 | self._pckdict[b"generic_nonmember_action"] = 2 | ||
930 | 442 | self._do_test(dict(default_nonmember_action=Action.reject)) | ||
931 | 443 | |||
932 | 444 | def test_nonmember_discard(self): | ||
933 | 445 | self._pckdict[b"generic_nonmember_action"] = 3 | ||
934 | 446 | self._do_test(dict(default_nonmember_action=Action.discard)) | ||
935 | 447 | |||
936 | 448 | |||
937 | 449 | |||
938 | 128 | 450 | ||
939 | 451 | class TestConvertToURI(unittest.TestCase): | ||
940 | 452 | # The following values were plain text, and are now URIs in Mailman 3: | ||
941 | 453 | # - welcome_message_uri | ||
942 | 454 | # - goodbye_message_uri | ||
943 | 455 | # - header_uri | ||
944 | 456 | # - footer_uri | ||
945 | 457 | # - digest_header_uri | ||
946 | 458 | # - digest_footer_uri | ||
947 | 459 | # | ||
948 | 460 | # The templates contain variables that must be replaced: | ||
949 | 461 | # - %(real_name)s -> %(display_name)s | ||
950 | 462 | # - %(real_name)s@%(host_name)s -> %(fqdn_listname)s | ||
951 | 463 | # - %(web_page_url)slistinfo%(cgiext)s/%(_internal_name)s -> %(listinfo_uri)s | ||
952 | 464 | |||
953 | 465 | layer = ConfigLayer | ||
954 | 466 | |||
955 | 467 | def setUp(self): | ||
956 | 468 | self._mlist = create_list('blank@example.com') | ||
957 | 469 | self._conf_mapping = dict( | ||
958 | 470 | welcome_msg="welcome_message_uri", | ||
959 | 471 | goodbye_msg="goodbye_message_uri", | ||
960 | 472 | msg_header="header_uri", | ||
961 | 473 | msg_footer="footer_uri", | ||
962 | 474 | digest_header="digest_header_uri", | ||
963 | 475 | digest_footer="digest_footer_uri", | ||
964 | 476 | ) | ||
965 | 477 | self._pckdict = dict() | ||
966 | 478 | #self._pckdict = { | ||
967 | 479 | # "preferred_language": "XX", # templates are lang-specific | ||
968 | 480 | #} | ||
969 | 481 | |||
970 | 482 | def tearDown(self): | ||
971 | 483 | remove_list(self._mlist) | ||
972 | 484 | |||
973 | 485 | def test_text_to_uri(self): | ||
974 | 486 | for oldvar, newvar in self._conf_mapping.iteritems(): | ||
975 | 487 | self._pckdict[str(oldvar)] = b"TEST VALUE" | ||
976 | 488 | import_config_pck(self._mlist, self._pckdict) | ||
977 | 489 | newattr = getattr(self._mlist, newvar) | ||
978 | 490 | text = decorate(self._mlist, newattr) | ||
979 | 491 | self.assertEqual(text, "TEST VALUE", | ||
980 | 492 | "Old variable %s was not properly imported to %s" | ||
981 | 493 | % (oldvar, newvar)) | ||
982 | 494 | |||
983 | 495 | def test_substitutions(self): | ||
984 | 496 | test_text = ("UNIT TESTING %(real_name)s mailing list\n" | ||
985 | 497 | "%(real_name)s@%(host_name)s\n" | ||
986 | 498 | "%(web_page_url)slistinfo%(cgiext)s/%(_internal_name)s") | ||
987 | 499 | expected_text = ("UNIT TESTING $display_name mailing list\n" | ||
988 | 500 | "$fqdn_listname\n" | ||
989 | 501 | "$listinfo_uri") | ||
990 | 502 | for oldvar, newvar in self._conf_mapping.iteritems(): | ||
991 | 503 | self._pckdict[str(oldvar)] = str(test_text) | ||
992 | 504 | import_config_pck(self._mlist, self._pckdict) | ||
993 | 505 | newattr = getattr(self._mlist, newvar) | ||
994 | 506 | template_uri = expand(newattr, dict( | ||
995 | 507 | listname=self._mlist.fqdn_listname, | ||
996 | 508 | language=self._mlist.preferred_language.code, | ||
997 | 509 | )) | ||
998 | 510 | loader = getUtility(ITemplateLoader) | ||
999 | 511 | text = loader.get(template_uri) | ||
1000 | 512 | self.assertEqual(text, expected_text, | ||
1001 | 513 | "Old variables were not converted for %s" % newvar) | ||
1002 | 514 | |||
1003 | 515 | def test_keep_default(self): | ||
1004 | 516 | # If the value was not changed from MM2.1's default, don't import it | ||
1005 | 517 | default_msg_footer = ( | ||
1006 | 518 | "_______________________________________________\n" | ||
1007 | 519 | "%(real_name)s mailing list\n" | ||
1008 | 520 | "%(real_name)s@%(host_name)s\n" | ||
1009 | 521 | "%(web_page_url)slistinfo%(cgiext)s/%(_internal_name)s" | ||
1010 | 522 | ) | ||
1011 | 523 | for oldvar in ("msg_footer", "digest_footer"): | ||
1012 | 524 | newvar = self._conf_mapping[oldvar] | ||
1013 | 525 | self._pckdict[str(oldvar)] = str(default_msg_footer) | ||
1014 | 526 | old_value = getattr(self._mlist, newvar) | ||
1015 | 527 | import_config_pck(self._mlist, self._pckdict) | ||
1016 | 528 | new_value = getattr(self._mlist, newvar) | ||
1017 | 529 | self.assertEqual(old_value, new_value, | ||
1018 | 530 | "Default value was not preserved for %s" % newvar) | ||
1019 | 531 | |||
1020 | 532 | def test_keep_default_if_fqdn_changed(self): | ||
1021 | 533 | # Use case: importing the old a@ex.com into b@ex.com | ||
1022 | 534 | # We can't check if it changed from the default | ||
1023 | 535 | # -> don't import, we may do more harm than good and it's easy to | ||
1024 | 536 | # change if needed | ||
1025 | 537 | test_value = b"TEST-VALUE" | ||
1026 | 538 | for oldvar, newvar in self._conf_mapping.iteritems(): | ||
1027 | 539 | self._mlist.mail_host = "example.com" | ||
1028 | 540 | self._pckdict[b"mail_host"] = b"test.example.com" | ||
1029 | 541 | self._pckdict[str(oldvar)] = test_value | ||
1030 | 542 | old_value = getattr(self._mlist, newvar) | ||
1031 | 543 | import_config_pck(self._mlist, self._pckdict) | ||
1032 | 544 | new_value = getattr(self._mlist, newvar) | ||
1033 | 545 | self.assertEqual(old_value, new_value, | ||
1034 | 546 | "Default value was not preserved for %s" % newvar) | ||
1035 | 547 | |||
1036 | 548 | def test_unicode(self): | ||
1037 | 549 | # non-ascii templates | ||
1038 | 550 | for oldvar in self._conf_mapping: | ||
1039 | 551 | self._pckdict[str(oldvar)] = b"Ol\xe1!" | ||
1040 | 552 | try: | ||
1041 | 553 | import_config_pck(self._mlist, self._pckdict) | ||
1042 | 554 | except UnicodeDecodeError, e: | ||
1043 | 555 | print(format_exc()) | ||
1044 | 556 | self.fail(e) | ||
1045 | 557 | for oldvar, newvar in self._conf_mapping.iteritems(): | ||
1046 | 558 | newattr = getattr(self._mlist, newvar) | ||
1047 | 559 | text = decorate(self._mlist, newattr) | ||
1048 | 560 | expected = u'Ol\ufffd!' | ||
1049 | 561 | self.assertEqual(text, expected) | ||
1050 | 562 | |||
1051 | 563 | def test_unicode_in_default(self): | ||
1052 | 564 | # What if the default template is already in UTF-8? (like if you import twice) | ||
1053 | 565 | footer = b'\xe4\xb8\xad $listinfo_uri' | ||
1054 | 566 | footer_path = os.path.join(config.VAR_DIR, "templates", "lists", | ||
1055 | 567 | "blank@example.com", "en", "footer-generic.txt") | ||
1056 | 568 | try: | ||
1057 | 569 | os.makedirs(os.path.dirname(footer_path)) | ||
1058 | 570 | except OSError: | ||
1059 | 571 | pass | ||
1060 | 572 | with open(footer_path, "w") as footer_file: | ||
1061 | 573 | footer_file.write(footer) | ||
1062 | 574 | self._pckdict[b"msg_footer"] = b"NEW-VALUE" | ||
1063 | 575 | import_config_pck(self._mlist, self._pckdict) | ||
1064 | 576 | text = decorate(self._mlist, self._mlist.footer_uri) | ||
1065 | 577 | self.assertEqual(text, 'NEW-VALUE') | ||
1066 | 578 | |||
1067 | 579 | |||
1068 | 129 | 580 | ||
1069 | 581 | class TestRosterImport(unittest.TestCase): | ||
1070 | 582 | |||
1071 | 583 | layer = ConfigLayer | ||
1072 | 584 | |||
1073 | 585 | def setUp(self): | ||
1074 | 586 | self._mlist = create_list('blank@example.com') | ||
1075 | 587 | self._pckdict = { | ||
1076 | 588 | "members": { | ||
1077 | 589 | "anne@example.com": 0, | ||
1078 | 590 | "bob@example.com": b"bob@ExampLe.Com", | ||
1079 | 591 | }, | ||
1080 | 592 | "digest_members": { | ||
1081 | 593 | "cindy@example.com": 0, | ||
1082 | 594 | "dave@example.com": b"dave@ExampLe.Com", | ||
1083 | 595 | }, | ||
1084 | 596 | "passwords": { | ||
1085 | 597 | "anne@example.com" : b"annepass", | ||
1086 | 598 | "bob@example.com" : b"bobpass", | ||
1087 | 599 | "cindy@example.com": b"cindypass", | ||
1088 | 600 | "dave@example.com" : b"davepass", | ||
1089 | 601 | }, | ||
1090 | 602 | "language": { | ||
1091 | 603 | "anne@example.com" : b"fr", | ||
1092 | 604 | "bob@example.com" : b"de", | ||
1093 | 605 | "cindy@example.com": b"es", | ||
1094 | 606 | "dave@example.com" : b"it", | ||
1095 | 607 | }, | ||
1096 | 608 | "usernames": { # Usernames are unicode strings in the pickle | ||
1097 | 609 | "anne@example.com" : "Anne", | ||
1098 | 610 | "bob@example.com" : "Bob", | ||
1099 | 611 | "cindy@example.com": "Cindy", | ||
1100 | 612 | "dave@example.com" : "Dave", | ||
1101 | 613 | }, | ||
1102 | 614 | "owner": [ | ||
1103 | 615 | "anne@example.com", | ||
1104 | 616 | "emily@example.com", | ||
1105 | 617 | ], | ||
1106 | 618 | "moderator": [ | ||
1107 | 619 | "bob@example.com", | ||
1108 | 620 | "fred@example.com", | ||
1109 | 621 | ], | ||
1110 | 622 | } | ||
1111 | 623 | self._usermanager = getUtility(IUserManager) | ||
1112 | 624 | language_manager = getUtility(ILanguageManager) | ||
1113 | 625 | for code in self._pckdict["language"].values(): | ||
1114 | 626 | if code not in language_manager.codes: | ||
1115 | 627 | language_manager.add(code, 'utf-8', code) | ||
1116 | 628 | |||
1117 | 629 | def tearDown(self): | ||
1118 | 630 | remove_list(self._mlist) | ||
1119 | 631 | |||
1120 | 632 | def test_member(self): | ||
1121 | 633 | import_config_pck(self._mlist, self._pckdict) | ||
1122 | 634 | for name in ("anne", "bob", "cindy", "dave"): | ||
1123 | 635 | addr = "%s@example.com" % name | ||
1124 | 636 | self.assertTrue( | ||
1125 | 637 | addr in [ a.email for a in self._mlist.members.addresses], | ||
1126 | 638 | "Address %s was not imported" % addr) | ||
1127 | 639 | self.assertTrue("anne@example.com" in [ a.email | ||
1128 | 640 | for a in self._mlist.regular_members.addresses]) | ||
1129 | 641 | self.assertTrue("bob@example.com" in [ a.email | ||
1130 | 642 | for a in self._mlist.regular_members.addresses]) | ||
1131 | 643 | self.assertTrue("cindy@example.com" in [ a.email | ||
1132 | 644 | for a in self._mlist.digest_members.addresses]) | ||
1133 | 645 | self.assertTrue("dave@example.com" in [ a.email | ||
1134 | 646 | for a in self._mlist.digest_members.addresses]) | ||
1135 | 647 | |||
1136 | 648 | def test_original_email(self): | ||
1137 | 649 | import_config_pck(self._mlist, self._pckdict) | ||
1138 | 650 | bob = self._usermanager.get_address("bob@example.com") | ||
1139 | 651 | self.assertEqual(bob.original_email, "bob@ExampLe.Com") | ||
1140 | 652 | dave = self._usermanager.get_address("dave@example.com") | ||
1141 | 653 | self.assertEqual(dave.original_email, "dave@ExampLe.Com") | ||
1142 | 654 | |||
1143 | 655 | def test_language(self): | ||
1144 | 656 | import_config_pck(self._mlist, self._pckdict) | ||
1145 | 657 | for name in ("anne", "bob", "cindy", "dave"): | ||
1146 | 658 | addr = "%s@example.com" % name | ||
1147 | 659 | member = self._mlist.members.get_member(addr) | ||
1148 | 660 | self.assertTrue(member is not None, | ||
1149 | 661 | "Address %s was not imported" % addr) | ||
1150 | 662 | print(self._pckdict["language"]) | ||
1151 | 663 | print(member.preferred_language, member.preferred_language.code) | ||
1152 | 664 | self.assertEqual(member.preferred_language.code, | ||
1153 | 665 | self._pckdict["language"][addr]) | ||
1154 | 666 | |||
1155 | 667 | def test_new_language(self): | ||
1156 | 668 | self._pckdict[b"language"]["anne@example.com"] = b'xx_XX' | ||
1157 | 669 | try: | ||
1158 | 670 | import_config_pck(self._mlist, self._pckdict) | ||
1159 | 671 | except Import21Error, e: | ||
1160 | 672 | # check the message | ||
1161 | 673 | self.assertTrue("[language.xx_XX]" in str(e)) | ||
1162 | 674 | else: | ||
1163 | 675 | self.fail("Import21Error was not raised") | ||
1164 | 676 | |||
1165 | 677 | def test_username(self): | ||
1166 | 678 | import_config_pck(self._mlist, self._pckdict) | ||
1167 | 679 | for name in ("anne", "bob", "cindy", "dave"): | ||
1168 | 680 | addr = "%s@example.com" % name | ||
1169 | 681 | user = self._usermanager.get_user(addr) | ||
1170 | 682 | address = self._usermanager.get_address(addr) | ||
1171 | 683 | self.assertTrue(user is not None, | ||
1172 | 684 | "User %s was not imported" % addr) | ||
1173 | 685 | self.assertTrue(address is not None, | ||
1174 | 686 | "Address %s was not imported" % addr) | ||
1175 | 687 | display_name = self._pckdict["usernames"][addr] | ||
1176 | 688 | self.assertEqual(user.display_name, display_name, | ||
1177 | 689 | "The display name was not set for User %s" % addr) | ||
1178 | 690 | self.assertEqual(address.display_name, display_name, | ||
1179 | 691 | "The display name was not set for Address %s" % addr) | ||
1180 | 692 | |||
1181 | 693 | def test_owner(self): | ||
1182 | 694 | import_config_pck(self._mlist, self._pckdict) | ||
1183 | 695 | for name in ("anne", "emily"): | ||
1184 | 696 | addr = "%s@example.com" % name | ||
1185 | 697 | self.assertTrue( | ||
1186 | 698 | addr in [ a.email for a in self._mlist.owners.addresses ], | ||
1187 | 699 | "Address %s was not imported as owner" % addr) | ||
1188 | 700 | self.assertFalse("emily@example.com" in | ||
1189 | 701 | [ a.email for a in self._mlist.members.addresses ], | ||
1190 | 702 | "Address emily@ was wrongly added to the members list") | ||
1191 | 703 | |||
1192 | 704 | def test_moderator(self): | ||
1193 | 705 | import_config_pck(self._mlist, self._pckdict) | ||
1194 | 706 | for name in ("bob", "fred"): | ||
1195 | 707 | addr = "%s@example.com" % name | ||
1196 | 708 | self.assertTrue( | ||
1197 | 709 | addr in [ a.email for a in self._mlist.moderators.addresses ], | ||
1198 | 710 | "Address %s was not imported as moderator" % addr) | ||
1199 | 711 | self.assertFalse("fred@example.com" in | ||
1200 | 712 | [ a.email for a in self._mlist.members.addresses ], | ||
1201 | 713 | "Address fred@ was wrongly added to the members list") | ||
1202 | 714 | |||
1203 | 715 | def test_password(self): | ||
1204 | 716 | #self.anne.password = config.password_context.encrypt('abc123') | ||
1205 | 717 | import_config_pck(self._mlist, self._pckdict) | ||
1206 | 718 | for name in ("anne", "bob", "cindy", "dave"): | ||
1207 | 719 | addr = "%s@example.com" % name | ||
1208 | 720 | user = self._usermanager.get_user(addr) | ||
1209 | 721 | self.assertTrue(user is not None, | ||
1210 | 722 | "Address %s was not imported" % addr) | ||
1211 | 723 | self.assertEqual(user.password, b'{plaintext}%spass' % name, | ||
1212 | 724 | "Password for %s was not imported" % addr) | ||
1213 | 725 | |||
1214 | 726 | def test_same_user(self): | ||
1215 | 727 | # Adding the address of an existing User must not create another user | ||
1216 | 728 | user = self._usermanager.create_user('anne@example.com', 'Anne') | ||
1217 | 729 | user.register("bob@example.com") # secondary email | ||
1218 | 730 | import_config_pck(self._mlist, self._pckdict) | ||
1219 | 731 | member = self._mlist.members.get_member('bob@example.com') | ||
1220 | 732 | self.assertEqual(member.user, user) | ||
1221 | 733 | |||
1222 | 734 | def test_owner_and_moderator_not_lowercase(self): | ||
1223 | 735 | # In the v2.1 pickled dict, the owner and moderator lists are not | ||
1224 | 736 | # necessarily lowercased already | ||
1225 | 737 | self._pckdict["owner"] = [b"Anne@example.com"] | ||
1226 | 738 | self._pckdict["moderator"] = [b"Anne@example.com"] | ||
1227 | 739 | try: | ||
1228 | 740 | import_config_pck(self._mlist, self._pckdict) | ||
1229 | 741 | except AssertionError: | ||
1230 | 742 | print(format_exc()) | ||
1231 | 743 | self.fail("The address was not lowercased") | ||
1232 | 744 | self.assertTrue("anne@example.com" in | ||
1233 | 745 | [ a.email for a in self._mlist.owners.addresses ]) | ||
1234 | 746 | self.assertTrue("anne@example.com" in | ||
1235 | 747 | [ a.email for a in self._mlist.moderators.addresses]) | ||
1236 | 748 | |||
1237 | 749 | def test_address_already_exists_but_no_user(self): | ||
1238 | 750 | # An address already exists, but it is not linked to a user nor | ||
1239 | 751 | # subscribed | ||
1240 | 752 | anne_addr = Address("anne@example.com", "Anne") | ||
1241 | 753 | Store.of(self._mlist).add(anne_addr) | ||
1242 | 754 | try: | ||
1243 | 755 | import_config_pck(self._mlist, self._pckdict) | ||
1244 | 756 | except ExistingAddressError: | ||
1245 | 757 | print(format_exc()) | ||
1246 | 758 | self.fail("existing address was not checked") | ||
1247 | 759 | anne = self._usermanager.get_user("anne@example.com") | ||
1248 | 760 | self.assertTrue(anne.controls("anne@example.com")) | ||
1249 | 761 | self.assertTrue(anne_addr in self._mlist.regular_members.addresses) | ||
1250 | 762 | |||
1251 | 763 | def test_address_already_subscribed_but_no_user(self): | ||
1252 | 764 | # An address is already subscribed, but it is not linked to a user | ||
1253 | 765 | anne_addr = Address("anne@example.com", "Anne") | ||
1254 | 766 | self._mlist.subscribe(anne_addr) | ||
1255 | 767 | try: | ||
1256 | 768 | import_config_pck(self._mlist, self._pckdict) | ||
1257 | 769 | except ExistingAddressError: | ||
1258 | 770 | print(format_exc()) | ||
1259 | 771 | self.fail("existing address was not checked") | ||
1260 | 772 | anne = self._usermanager.get_user("anne@example.com") | ||
1261 | 773 | self.assertTrue(anne.controls("anne@example.com")) | ||
1262 | 774 | |||
1263 | 775 | |||
1264 | 776 | |||
1265 | 777 | |||
1266 | 130 | 778 | ||
1267 | 779 | class TestPreferencesImport(unittest.TestCase): | ||
1268 | 780 | |||
1269 | 781 | layer = ConfigLayer | ||
1270 | 782 | |||
1271 | 783 | def setUp(self): | ||
1272 | 784 | self._mlist = create_list('blank@example.com') | ||
1273 | 785 | self._pckdict = dict( | ||
1274 | 786 | members={ "anne@example.com": 0 }, | ||
1275 | 787 | user_options=dict(), | ||
1276 | 788 | delivery_status=dict(), | ||
1277 | 789 | ) | ||
1278 | 790 | self._usermanager = getUtility(IUserManager) | ||
1279 | 791 | |||
1280 | 792 | def tearDown(self): | ||
1281 | 793 | remove_list(self._mlist) | ||
1282 | 794 | |||
1283 | 795 | def _do_test(self, oldvalue, expected): | ||
1284 | 796 | self._pckdict["user_options"]["anne@example.com"] = oldvalue | ||
1285 | 797 | import_config_pck(self._mlist, self._pckdict) | ||
1286 | 798 | user = self._usermanager.get_user("anne@example.com") | ||
1287 | 799 | self.assertTrue(user is not None, "User was not imported") | ||
1288 | 800 | member = self._mlist.members.get_member("anne@example.com") | ||
1289 | 801 | self.assertTrue(member is not None, "Address was not subscribed") | ||
1290 | 802 | for exp_name, exp_val in expected.iteritems(): | ||
1291 | 803 | try: | ||
1292 | 804 | currentval = getattr(member, exp_name) | ||
1293 | 805 | except AttributeError: | ||
1294 | 806 | # hide_address has no direct getter | ||
1295 | 807 | currentval = getattr(member.preferences, exp_name) | ||
1296 | 808 | self.assertEqual(currentval, exp_val, | ||
1297 | 809 | "Preference %s was not imported" % exp_name) | ||
1298 | 810 | # XXX: should I check that other params are still equal to | ||
1299 | 811 | # mailman.core.constants.system_preferences ? | ||
1300 | 812 | |||
1301 | 813 | def test_acknowledge_posts(self): | ||
1302 | 814 | # AcknowledgePosts | ||
1303 | 815 | self._do_test(4, dict(acknowledge_posts=True)) | ||
1304 | 816 | |||
1305 | 817 | def test_hide_address(self): | ||
1306 | 818 | # ConcealSubscription | ||
1307 | 819 | self._do_test(16, dict(hide_address=True)) | ||
1308 | 820 | |||
1309 | 821 | def test_receive_own_postings(self): | ||
1310 | 822 | # DontReceiveOwnPosts | ||
1311 | 823 | self._do_test(2, dict(receive_own_postings=False)) | ||
1312 | 824 | |||
1313 | 825 | def test_receive_list_copy(self): | ||
1314 | 826 | # DontReceiveDuplicates | ||
1315 | 827 | self._do_test(256, dict(receive_list_copy=False)) | ||
1316 | 828 | |||
1317 | 829 | def test_digest_plain(self): | ||
1318 | 830 | # Digests & DisableMime | ||
1319 | 831 | self._pckdict["digest_members"] = self._pckdict["members"].copy() | ||
1320 | 832 | self._pckdict["members"] = dict() | ||
1321 | 833 | self._do_test(8, dict(delivery_mode=DeliveryMode.plaintext_digests)) | ||
1322 | 834 | |||
1323 | 835 | def test_digest_mime(self): | ||
1324 | 836 | # Digests & not DisableMime | ||
1325 | 837 | self._pckdict["digest_members"] = self._pckdict["members"].copy() | ||
1326 | 838 | self._pckdict["members"] = dict() | ||
1327 | 839 | self._do_test(0, dict(delivery_mode=DeliveryMode.mime_digests)) | ||
1328 | 840 | |||
1329 | 841 | def test_delivery_status(self): | ||
1330 | 842 | # look for the pckdict["delivery_status"] key which will look like | ||
1331 | 843 | # (status, time) where status is among the following: | ||
1332 | 844 | # ENABLED = 0 # enabled | ||
1333 | 845 | # UNKNOWN = 1 # legacy disabled | ||
1334 | 846 | # BYUSER = 2 # disabled by user choice | ||
1335 | 847 | # BYADMIN = 3 # disabled by admin choice | ||
1336 | 848 | # BYBOUNCE = 4 # disabled by bounces | ||
1337 | 849 | for oldval, expected in enumerate((DeliveryStatus.enabled, | ||
1338 | 850 | DeliveryStatus.unknown, DeliveryStatus.by_user, | ||
1339 | 851 | DeliveryStatus.by_moderator, DeliveryStatus.by_bounces)): | ||
1340 | 852 | self._pckdict["delivery_status"]["anne@example.com"] = (oldval, 0) | ||
1341 | 853 | import_config_pck(self._mlist, self._pckdict) | ||
1342 | 854 | member = self._mlist.members.get_member("anne@example.com") | ||
1343 | 855 | self.assertTrue(member is not None, "Address was not subscribed") | ||
1344 | 856 | self.assertEqual(member.delivery_status, expected) | ||
1345 | 857 | member.unsubscribe() | ||
1346 | 858 | |||
1347 | 859 | def test_moderate(self): | ||
1348 | 860 | # Option flag Moderate is translated to | ||
1349 | 861 | # member.moderation_action = Action.hold | ||
1350 | 862 | self._do_test(128, dict(moderation_action=Action.hold)) | ||
1351 | 863 | |||
1352 | 864 | def test_multiple_options(self): | ||
1353 | 865 | # DontReceiveDuplicates & DisableMime & SuppressPasswordReminder | ||
1354 | 866 | self._pckdict[b"digest_members"] = self._pckdict[b"members"].copy() | ||
1355 | 867 | self._pckdict[b"members"] = dict() | ||
1356 | 868 | self._do_test(296, dict( | ||
1357 | 869 | receive_list_copy=False, | ||
1358 | 870 | delivery_mode=DeliveryMode.plaintext_digests, | ||
1359 | 871 | )) |
Oh by the way, I'm not very at ease with Bazaar yet, so I may have made a mistake with the merging, please make sure I did not do something funny there... Sorry.