Merge lp:~barry/mailman/abhilash into lp:mailman

Proposed by Barry Warsaw
Status: Merged
Merged at revision: 7253
Proposed branch: lp:~barry/mailman/abhilash
Merge into: lp:mailman
Diff against target: 7096 lines (+1329/-3682)
78 files modified
MANIFEST.in (+1/-2)
setup.py (+2/-1)
src/mailman/app/docs/moderator.rst (+34/-43)
src/mailman/app/subscriptions.py (+7/-9)
src/mailman/bin/tests/test_master.py (+1/-1)
src/mailman/commands/docs/conf.rst (+1/-0)
src/mailman/commands/docs/withlist.rst (+2/-2)
src/mailman/config/alembic.cfg (+20/-0)
src/mailman/config/config.py (+4/-9)
src/mailman/config/configure.zcml (+0/-20)
src/mailman/config/schema.cfg (+7/-4)
src/mailman/core/docs/runner.rst (+4/-0)
src/mailman/core/logging.py (+30/-19)
src/mailman/database/alembic/__init__.py (+32/-0)
src/mailman/database/alembic/env.py (+75/-0)
src/mailman/database/alembic/script.py.mako (+22/-0)
src/mailman/database/alembic/versions/51b7f92bd06c_initial.py (+66/-0)
src/mailman/database/base.py (+19/-126)
src/mailman/database/docs/migration.rst (+0/-207)
src/mailman/database/factory.py (+65/-23)
src/mailman/database/model.py (+30/-40)
src/mailman/database/postgresql.py (+6/-59)
src/mailman/database/schema/helpers.py (+0/-43)
src/mailman/database/schema/mm_00000000000000_base.py (+0/-35)
src/mailman/database/schema/mm_20120407000000.py (+0/-212)
src/mailman/database/schema/mm_20121015000000.py (+0/-95)
src/mailman/database/schema/mm_20130406000000.py (+0/-65)
src/mailman/database/schema/postgres.sql (+0/-349)
src/mailman/database/schema/sqlite.sql (+0/-327)
src/mailman/database/schema/sqlite_20120407000000_01.sql (+0/-280)
src/mailman/database/schema/sqlite_20121015000000_01.sql (+0/-230)
src/mailman/database/schema/sqlite_20130406000000_01.sql (+0/-46)
src/mailman/database/sqlite.py (+5/-41)
src/mailman/database/tests/data/migration_postgres_1.sql (+0/-133)
src/mailman/database/tests/data/migration_sqlite_1.sql (+0/-133)
src/mailman/database/tests/test_factory.py (+160/-0)
src/mailman/database/tests/test_migrations.py (+0/-506)
src/mailman/database/types.py (+57/-30)
src/mailman/handlers/docs/owner-recips.rst (+2/-2)
src/mailman/interfaces/database.py (+1/-7)
src/mailman/interfaces/domain.py (+6/-1)
src/mailman/interfaces/listmanager.py (+4/-2)
src/mailman/interfaces/messages.py (+1/-1)
src/mailman/model/address.py (+17/-12)
src/mailman/model/autorespond.py (+25/-23)
src/mailman/model/bans.py (+16/-13)
src/mailman/model/bounce.py (+13/-10)
src/mailman/model/digests.py (+13/-10)
src/mailman/model/docs/autorespond.rst (+11/-11)
src/mailman/model/docs/mailinglist.rst (+9/-6)
src/mailman/model/docs/messagestore.rst (+3/-2)
src/mailman/model/docs/requests.rst (+10/-10)
src/mailman/model/domain.py (+17/-15)
src/mailman/model/language.py (+7/-5)
src/mailman/model/listmanager.py (+13/-13)
src/mailman/model/mailinglist.py (+231/-216)
src/mailman/model/member.py (+19/-17)
src/mailman/model/message.py (+7/-5)
src/mailman/model/messagestore.py (+15/-14)
src/mailman/model/mime.py (+11/-8)
src/mailman/model/pending.py (+34/-27)
src/mailman/model/preferences.py (+11/-9)
src/mailman/model/requests.py (+26/-17)
src/mailman/model/roster.py (+19/-26)
src/mailman/model/tests/test_listmanager.py (+12/-4)
src/mailman/model/tests/test_requests.py (+2/-2)
src/mailman/model/uid.py (+9/-5)
src/mailman/model/user.py (+32/-17)
src/mailman/model/usermanager.py (+9/-9)
src/mailman/model/version.py (+0/-44)
src/mailman/rest/docs/moderation.rst (+15/-17)
src/mailman/rest/validator.py (+1/-1)
src/mailman/styles/base.py (+2/-6)
src/mailman/testing/layers.py (+5/-0)
src/mailman/testing/testing.cfg (+3/-3)
src/mailman/utilities/importer.py (+24/-1)
src/mailman/utilities/modules.py (+14/-1)
src/mailman/utilities/tests/test_import.py (+10/-0)
To merge this branch: bzr merge lp:~barry/mailman/abhilash
Reviewer Review Type Date Requested Status
Aurélien Bompard Pending
Abhilash Raj Pending
Mailman Coders Pending
Review via email: mp+238222@code.launchpad.net

Description of the change

Merge branch of work primarily done by Abhilash and Aurelien for porting MM3 core from Storm to SQLAlchemy and Alembic. This is very nearly ready for merging to trunk.

To post a comment you must log in.
lp:~barry/mailman/abhilash updated
7285. By Barry Warsaw

Use print() to smooth over the SA return of Python longs in PostgreSQL.

Revision history for this message
Aurélien Bompard (abompard) wrote :

A short first comment: in MANIFEST.in, according to the python docs, the "include" directive is not recursive, so we can't just include "*.mako" and "*.py", we have to keep the full path to the files' directories. You can check that by running sdist, the database/alembic/version/*.py files and the mako script will not be included.

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

I don't know how to comment inline in lp, so the sorry for the last comment that went empty.
In sqlite.py the persmissions should be `0666` like before I suppose? It(`0o666`) is probably just a typo in the code.

Revision history for this message
Mark Sapiro (msapiro) wrote :

On 10/14/2014 05:22 AM, Abhilash Raj wrote:
> I don't know how to comment inline in lp, so the sorry for the last comment that went empty.
> In sqlite.py the persmissions should be `0666` like before I suppose? It(`0o666`) is probably just a typo in the code.

Actually, your inline comment was there. It was just very hard to pick
out from the 6593 lines of quoted diff that surrounded it.

--
Mark Sapiro <email address hidden> The highway is for gamblers,
San Francisco Bay Area, California better use your sense - B. Dylan

lp:~barry/mailman/abhilash updated
7286. By Barry Warsaw

Merge abompard's fixes to the importer for encode_ascii_prefixes.

7287. By Barry Warsaw

Merge abompard's fixes to the Postgres test suite.

7288. By Barry Warsaw

Merge in the last of Aurelien's changes, and make the test suite pass with
PostgreSQL.

7289. By Barry Warsaw

SQLite by default

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'MANIFEST.in'
2--- MANIFEST.in 2011-06-11 18:16:04 +0000
3+++ MANIFEST.in 2014-11-01 03:48:29 +0000
4@@ -1,4 +1,4 @@
5-include *.py *.rc
6+include *.py *.rc *.mako
7 include COPYING
8 recursive-include .buildout *
9 recursive-include contrib *
10@@ -6,7 +6,6 @@
11 recursive-include data *
12 global-include *.txt *.rst *.po *.mo *.cfg *.sql *.zcml *.html
13 global-exclude *.egg-info
14-exclude MANIFEST.in
15 prune src/attic
16 prune src/web
17 prune eggs
18
19=== modified file 'setup.py'
20--- setup.py 2014-04-15 16:06:01 +0000
21+++ setup.py 2014-11-01 03:48:29 +0000
22@@ -93,6 +93,7 @@
23 'console_scripts' : list(scripts),
24 },
25 install_requires = [
26+ 'alembic',
27 'enum34',
28 'flufl.bounce',
29 'flufl.i18n',
30@@ -104,7 +105,7 @@
31 'nose2',
32 'passlib',
33 'restish',
34- 'storm',
35+ 'sqlalchemy',
36 'zope.component',
37 'zope.configuration',
38 'zope.event',
39
40=== modified file 'src/mailman/app/docs/moderator.rst'
41--- src/mailman/app/docs/moderator.rst 2014-04-28 15:23:35 +0000
42+++ src/mailman/app/docs/moderator.rst 2014-11-01 03:48:29 +0000
43@@ -211,9 +211,9 @@
44 ...
45 ... Here's something important about our mailing list.
46 ... """)
47- >>> hold_message(mlist, msg, {}, 'Needs approval')
48- 2
49- >>> handle_message(mlist, 2, Action.discard, forward=['zack@example.com'])
50+ >>> req_id = hold_message(mlist, msg, {}, 'Needs approval')
51+ >>> handle_message(mlist, req_id, Action.discard,
52+ ... forward=['zack@example.com'])
53
54 The forwarded message is in the virgin queue, destined for the moderator.
55 ::
56@@ -243,10 +243,9 @@
57
58 >>> from mailman.app.moderator import hold_subscription
59 >>> from mailman.interfaces.member import DeliveryMode
60- >>> hold_subscription(mlist,
61- ... 'fred@example.org', 'Fred Person',
62+ >>> req_id = hold_subscription(
63+ ... mlist, 'fred@example.org', 'Fred Person',
64 ... '{NONE}abcxyz', DeliveryMode.regular, 'en')
65- 2
66
67
68 Disposing of membership change requests
69@@ -257,26 +256,27 @@
70 simply defer a decision for now.
71
72 >>> from mailman.app.moderator import handle_subscription
73- >>> handle_subscription(mlist, 2, Action.defer)
74- >>> requests.get_request(2) is not None
75+ >>> handle_subscription(mlist, req_id, Action.defer)
76+ >>> requests.get_request(req_id) is not None
77 True
78
79 The held subscription can also be discarded.
80
81- >>> handle_subscription(mlist, 2, Action.discard)
82- >>> print(requests.get_request(2))
83+ >>> handle_subscription(mlist, req_id, Action.discard)
84+ >>> print(requests.get_request(req_id))
85 None
86
87 Gwen tries to subscribe to the mailing list, but...
88
89- >>> hold_subscription(mlist,
90- ... 'gwen@example.org', 'Gwen Person',
91+ >>> req_id = hold_subscription(
92+ ... mlist, 'gwen@example.org', 'Gwen Person',
93 ... '{NONE}zyxcba', DeliveryMode.regular, 'en')
94- 2
95+
96
97 ...her request is rejected...
98
99- >>> handle_subscription(mlist, 2, Action.reject, 'This is a closed list')
100+ >>> handle_subscription(
101+ ... mlist, req_id, Action.reject, 'This is a closed list')
102 >>> messages = get_queue_messages('virgin')
103 >>> len(messages)
104 1
105@@ -304,14 +304,13 @@
106 mailing list.
107
108 >>> mlist.send_welcome_message = False
109- >>> hold_subscription(mlist,
110- ... 'herb@example.org', 'Herb Person',
111+ >>> req_id = hold_subscription(
112+ ... mlist, 'herb@example.org', 'Herb Person',
113 ... 'abcxyz', DeliveryMode.regular, 'en')
114- 2
115
116 The moderators accept the subscription request.
117
118- >>> handle_subscription(mlist, 2, Action.accept)
119+ >>> handle_subscription(mlist, req_id, Action.accept)
120
121 And now Herb is a member of the mailing list.
122
123@@ -328,29 +327,27 @@
124 Herb now wants to leave the mailing list, but his request must be approved.
125
126 >>> from mailman.app.moderator import hold_unsubscription
127- >>> hold_unsubscription(mlist, 'herb@example.org')
128- 2
129+ >>> req_id = hold_unsubscription(mlist, 'herb@example.org')
130
131 As with subscription requests, the unsubscription request can be deferred.
132
133 >>> from mailman.app.moderator import handle_unsubscription
134- >>> handle_unsubscription(mlist, 2, Action.defer)
135+ >>> handle_unsubscription(mlist, req_id, Action.defer)
136 >>> print(mlist.members.get_member('herb@example.org').address)
137 Herb Person <herb@example.org>
138
139 The held unsubscription can also be discarded, and the member will remain
140 subscribed.
141
142- >>> handle_unsubscription(mlist, 2, Action.discard)
143+ >>> handle_unsubscription(mlist, req_id, Action.discard)
144 >>> print(mlist.members.get_member('herb@example.org').address)
145 Herb Person <herb@example.org>
146
147 The request can be rejected, in which case a message is sent to the member,
148 and the person remains a member of the mailing list.
149
150- >>> hold_unsubscription(mlist, 'herb@example.org')
151- 2
152- >>> handle_unsubscription(mlist, 2, Action.reject, 'No can do')
153+ >>> req_id = hold_unsubscription(mlist, 'herb@example.org')
154+ >>> handle_unsubscription(mlist, req_id, Action.reject, 'No can do')
155 >>> print(mlist.members.get_member('herb@example.org').address)
156 Herb Person <herb@example.org>
157
158@@ -381,10 +378,9 @@
159 The unsubscription request can also be accepted. This removes the member from
160 the mailing list.
161
162- >>> hold_unsubscription(mlist, 'herb@example.org')
163- 2
164+ >>> req_id = hold_unsubscription(mlist, 'herb@example.org')
165 >>> mlist.send_goodbye_message = False
166- >>> handle_unsubscription(mlist, 2, Action.accept)
167+ >>> handle_unsubscription(mlist, req_id, Action.accept)
168 >>> print(mlist.members.get_member('herb@example.org'))
169 None
170
171@@ -403,9 +399,8 @@
172
173 Iris tries to subscribe to the mailing list.
174
175- >>> hold_subscription(mlist, 'iris@example.org', 'Iris Person',
176+ >>> req_id = hold_subscription(mlist, 'iris@example.org', 'Iris Person',
177 ... 'password', DeliveryMode.regular, 'en')
178- 2
179
180 There's now a message in the virgin queue, destined for the list owner.
181
182@@ -429,8 +424,7 @@
183 Similarly, the administrator gets notifications on unsubscription requests.
184 Jeff is a member of the mailing list, and chooses to unsubscribe.
185
186- >>> hold_unsubscription(mlist, 'jeff@example.org')
187- 3
188+ >>> unsub_req_id = hold_unsubscription(mlist, 'jeff@example.org')
189 >>> messages = get_queue_messages('virgin')
190 >>> len(messages)
191 1
192@@ -457,7 +451,7 @@
193
194 >>> mlist.admin_notify_mchanges = True
195 >>> mlist.admin_immed_notify = False
196- >>> handle_subscription(mlist, 2, Action.accept)
197+ >>> handle_subscription(mlist, req_id, Action.accept)
198 >>> messages = get_queue_messages('virgin')
199 >>> len(messages)
200 1
201@@ -474,9 +468,8 @@
202 Similarly when an unsubscription request is accepted, the administrators can
203 get a notification.
204
205- >>> hold_unsubscription(mlist, 'iris@example.org')
206- 4
207- >>> handle_unsubscription(mlist, 4, Action.accept)
208+ >>> req_id = hold_unsubscription(mlist, 'iris@example.org')
209+ >>> handle_unsubscription(mlist, req_id, Action.accept)
210 >>> messages = get_queue_messages('virgin')
211 >>> len(messages)
212 1
213@@ -498,10 +491,9 @@
214
215 >>> mlist.admin_notify_mchanges = False
216 >>> mlist.send_welcome_message = True
217- >>> hold_subscription(mlist, 'kate@example.org', 'Kate Person',
218- ... 'password', DeliveryMode.regular, 'en')
219- 4
220- >>> handle_subscription(mlist, 4, Action.accept)
221+ >>> req_id = hold_subscription(mlist, 'kate@example.org', 'Kate Person',
222+ ... 'password', DeliveryMode.regular, 'en')
223+ >>> handle_subscription(mlist, req_id, Action.accept)
224 >>> messages = get_queue_messages('virgin')
225 >>> len(messages)
226 1
227@@ -523,9 +515,8 @@
228 goodbye message.
229
230 >>> mlist.send_goodbye_message = True
231- >>> hold_unsubscription(mlist, 'kate@example.org')
232- 4
233- >>> handle_unsubscription(mlist, 4, Action.accept)
234+ >>> req_id = hold_unsubscription(mlist, 'kate@example.org')
235+ >>> handle_unsubscription(mlist, req_id, Action.accept)
236 >>> messages = get_queue_messages('virgin')
237 >>> len(messages)
238 1
239
240=== modified file 'src/mailman/app/subscriptions.py'
241--- src/mailman/app/subscriptions.py 2014-04-15 14:03:39 +0000
242+++ src/mailman/app/subscriptions.py 2014-11-01 03:48:29 +0000
243@@ -28,7 +28,7 @@
244
245 from operator import attrgetter
246 from passlib.utils import generate_password as generate
247-from storm.expr import And, Or
248+from sqlalchemy import and_, or_
249 from uuid import UUID
250 from zope.component import getUtility
251 from zope.interface import implementer
252@@ -88,9 +88,7 @@
253 @dbconnection
254 def get_member(self, store, member_id):
255 """See `ISubscriptionService`."""
256- members = store.find(
257- Member,
258- Member._member_id == member_id)
259+ members = store.query(Member).filter(Member._member_id == member_id)
260 if members.count() == 0:
261 return None
262 else:
263@@ -117,8 +115,8 @@
264 # This probably could be made more efficient.
265 if address is None or user is None:
266 return []
267- query.append(Or(Member.address_id == address.id,
268- Member.user_id == user.id))
269+ query.append(or_(Member.address_id == address.id,
270+ Member.user_id == user.id))
271 else:
272 # subscriber is a user id.
273 user = user_manager.get_user_by_id(subscriber)
274@@ -126,15 +124,15 @@
275 if address.id is not None)
276 if len(address_ids) == 0 or user is None:
277 return []
278- query.append(Or(Member.user_id == user.id,
279- Member.address_id.is_in(address_ids)))
280+ query.append(or_(Member.user_id == user.id,
281+ Member.address_id.in_(address_ids)))
282 # Calculate the rest of the query expression, which will get And'd
283 # with the Or clause above (if there is one).
284 if list_id is not None:
285 query.append(Member.list_id == list_id)
286 if role is not None:
287 query.append(Member.role == role)
288- results = store.find(Member, And(*query))
289+ results = store.query(Member).filter(and_(*query))
290 return sorted(results, key=_membership_sort_key)
291
292 def __iter__(self):
293
294=== modified file 'src/mailman/bin/tests/test_master.py'
295--- src/mailman/bin/tests/test_master.py 2014-04-28 15:23:35 +0000
296+++ src/mailman/bin/tests/test_master.py 2014-11-01 03:48:29 +0000
297@@ -55,7 +55,7 @@
298 lock = master.acquire_lock_1(False, self.lock_file)
299 is_locked = lock.is_locked
300 lock.unlock()
301- self.failUnless(is_locked)
302+ self.assertTrue(is_locked)
303
304 def test_master_state(self):
305 my_lock = Lock(self.lock_file)
306
307=== modified file 'src/mailman/commands/docs/conf.rst'
308--- src/mailman/commands/docs/conf.rst 2013-09-01 15:08:46 +0000
309+++ src/mailman/commands/docs/conf.rst 2014-11-01 03:48:29 +0000
310@@ -49,6 +49,7 @@
311 [logging.config] path: mailman.log
312 [logging.error] path: mailman.log
313 [logging.smtp] path: smtp.log
314+ [logging.database] path: mailman.log
315 [logging.http] path: mailman.log
316 [logging.root] path: mailman.log
317 [logging.fromusenet] path: mailman.log
318
319=== modified file 'src/mailman/commands/docs/withlist.rst'
320--- src/mailman/commands/docs/withlist.rst 2014-04-28 15:23:35 +0000
321+++ src/mailman/commands/docs/withlist.rst 2014-11-01 03:48:29 +0000
322@@ -90,13 +90,13 @@
323 >>> args.listname = '^.*example.com'
324 >>> command.process(args)
325 The list's display name is Aardvark
326+ The list's display name is Badboys
327 The list's display name is Badger
328- The list's display name is Badboys
329
330 >>> args.listname = '^bad.*'
331 >>> command.process(args)
332+ The list's display name is Badboys
333 The list's display name is Badger
334- The list's display name is Badboys
335
336 >>> args.listname = '^foo'
337 >>> command.process(args)
338
339=== added file 'src/mailman/config/alembic.cfg'
340--- src/mailman/config/alembic.cfg 1970-01-01 00:00:00 +0000
341+++ src/mailman/config/alembic.cfg 2014-11-01 03:48:29 +0000
342@@ -0,0 +1,20 @@
343+# Copyright (C) 2014 by the Free Software Foundation, Inc.
344+#
345+# This file is part of GNU Mailman.
346+#
347+# GNU Mailman is free software: you can redistribute it and/or modify it under
348+# the terms of the GNU General Public License as published by the Free
349+# Software Foundation, either version 3 of the License, or (at your option)
350+# any later version.
351+#
352+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
353+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
354+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
355+# more details.
356+#
357+# You should have received a copy of the GNU General Public License along with
358+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
359+
360+[alembic]
361+# Path to Alembic migration scripts.
362+script_location: mailman.database:alembic
363
364=== modified file 'src/mailman/config/config.py'
365--- src/mailman/config/config.py 2014-03-02 22:59:30 +0000
366+++ src/mailman/config/config.py 2014-11-01 03:48:29 +0000
367@@ -33,7 +33,7 @@
368 from ConfigParser import SafeConfigParser
369 from flufl.lock import Lock
370 from lazr.config import ConfigSchema, as_boolean
371-from pkg_resources import resource_filename, resource_stream, resource_string
372+from pkg_resources import resource_stream, resource_string
373 from string import Template
374 from zope.component import getUtility
375 from zope.event import notify
376@@ -46,7 +46,7 @@
377 ConfigurationUpdatedEvent, IConfiguration, MissingConfigurationFileError)
378 from mailman.interfaces.languages import ILanguageManager
379 from mailman.utilities.filesystem import makedirs
380-from mailman.utilities.modules import call_name
381+from mailman.utilities.modules import call_name, expand_path
382
383
384 SPACE = ' '
385@@ -141,7 +141,7 @@
386 def _expand_paths(self):
387 """Expand all configuration paths."""
388 # Set up directories.
389- bin_dir = os.path.abspath(os.path.dirname(sys.argv[0]))
390+ bin_dir = os.path.abspath(os.path.dirname(sys.executable))
391 # Now that we've loaded all the configuration files we're going to
392 # load, set up some useful directories based on the settings in the
393 # configuration file.
394@@ -304,12 +304,7 @@
395 :return: A `ConfigParser` instance.
396 """
397 # Is the context coming from a file system or Python path?
398- if path.startswith('python:'):
399- resource_path = path[7:]
400- package, dot, resource = resource_path.rpartition('.')
401- cfg_path = resource_filename(package, resource + '.cfg')
402- else:
403- cfg_path = path
404+ cfg_path = expand_path(path)
405 parser = SafeConfigParser()
406 files = parser.read(cfg_path)
407 if files != [cfg_path]:
408
409=== modified file 'src/mailman/config/configure.zcml'
410--- src/mailman/config/configure.zcml 2013-11-26 02:26:15 +0000
411+++ src/mailman/config/configure.zcml 2014-11-01 03:48:29 +0000
412@@ -40,20 +40,6 @@
413 factory="mailman.model.requests.ListRequests"
414 />
415
416- <adapter
417- for="mailman.interfaces.database.IDatabase"
418- provides="mailman.interfaces.database.ITemporaryDatabase"
419- factory="mailman.database.sqlite.make_temporary"
420- name="sqlite"
421- />
422-
423- <adapter
424- for="mailman.interfaces.database.IDatabase"
425- provides="mailman.interfaces.database.ITemporaryDatabase"
426- factory="mailman.database.postgresql.make_temporary"
427- name="postgres"
428- />
429-
430 <utility
431 provides="mailman.interfaces.bounce.IBounceProcessor"
432 factory="mailman.model.bounce.BounceProcessor"
433@@ -72,12 +58,6 @@
434 />
435
436 <utility
437- provides="mailman.interfaces.database.IDatabaseFactory"
438- factory="mailman.database.factory.DatabaseTemporaryFactory"
439- name="temporary"
440- />
441-
442- <utility
443 provides="mailman.interfaces.domain.IDomainManager"
444 factory="mailman.model.domain.DomainManager"
445 />
446
447=== modified file 'src/mailman/config/schema.cfg'
448--- src/mailman/config/schema.cfg 2014-01-01 14:59:42 +0000
449+++ src/mailman/config/schema.cfg 2014-11-01 03:48:29 +0000
450@@ -204,9 +204,6 @@
451 url: sqlite:///$DATA_DIR/mailman.db
452 debug: no
453
454-# The module path to the migrations modules.
455-migrations_path: mailman.database.schema
456-
457 [logging.template]
458 # This defines various log settings. The options available are:
459 #
460@@ -229,6 +226,7 @@
461 # - archiver -- All archiver output
462 # - bounce -- All bounce processing logs go here
463 # - config -- Configuration issues
464+# - database -- Database logging (SQLAlchemy and Alembic)
465 # - debug -- Only used for development
466 # - error -- All exceptions go to this log
467 # - fromusenet -- Information related to the Usenet to Mailman gateway
468@@ -255,6 +253,8 @@
469
470 [logging.config]
471
472+[logging.database]
473+
474 [logging.debug]
475 path: debug.log
476 level: info
477@@ -304,6 +304,9 @@
478
479 [logging.vette]
480
481+[logging.database]
482+level: warn
483+
484
485 [webservice]
486 # The hostname at which admin web service resources are exposed.
487@@ -532,7 +535,7 @@
488 # following values.
489
490 # The class implementing the IArchiver interface.
491-class:
492+class:
493
494 # Set this to 'yes' to enable the archiver.
495 enable: no
496
497=== modified file 'src/mailman/core/docs/runner.rst'
498--- src/mailman/core/docs/runner.rst 2014-04-28 15:23:35 +0000
499+++ src/mailman/core/docs/runner.rst 2014-11-01 03:48:29 +0000
500@@ -73,3 +73,7 @@
501 version : 3
502
503 XXX More of the Runner API should be tested.
504+
505+..
506+ Clean up.
507+ >>> config.pop('test-runner')
508
509=== modified file 'src/mailman/core/logging.py'
510--- src/mailman/core/logging.py 2014-04-28 15:23:35 +0000
511+++ src/mailman/core/logging.py 2014-11-01 03:48:29 +0000
512@@ -104,6 +104,27 @@
513
514
515
516
517+def _init_logger(propagate, sub_name, log, logger_config):
518+ # Get settings from log configuration file (or defaults).
519+ log_format = logger_config.format
520+ log_datefmt = logger_config.datefmt
521+ # Propagation to the root logger is how we handle logging to stderr
522+ # when the runners are not run as a subprocess of 'bin/mailman start'.
523+ log.propagate = (as_boolean(logger_config.propagate)
524+ if propagate is None else propagate)
525+ # Set the logger's level.
526+ log.setLevel(as_log_level(logger_config.level))
527+ # Create a formatter for this logger, then a handler, and link the
528+ # formatter to the handler.
529+ formatter = logging.Formatter(fmt=log_format, datefmt=log_datefmt)
530+ path_str = logger_config.path
531+ path_abs = os.path.normpath(os.path.join(config.LOG_DIR, path_str))
532+ handler = ReopenableFileHandler(sub_name, path_abs)
533+ _handlers[sub_name] = handler
534+ handler.setFormatter(formatter)
535+ log.addHandler(handler)
536+
537+
538 def initialize(propagate=None):
539 """Initialize all logs.
540
541@@ -126,28 +147,18 @@
542 continue
543 if sub_name == 'locks':
544 log = logging.getLogger('flufl.lock')
545+ if sub_name == 'database':
546+ # Set both the SQLAlchemy and Alembic logs to the mailman.database
547+ # log configuration, essentially ignoring the alembic.cfg
548+ # settings. Do the SQLAlchemy one first, then let the Alembic one
549+ # fall through to the common code path.
550+ log = logging.getLogger('sqlalchemy')
551+ _init_logger(propagate, sub_name, log, logger_config)
552+ log = logging.getLogger('alembic')
553 else:
554 logger_name = 'mailman.' + sub_name
555 log = logging.getLogger(logger_name)
556- # Get settings from log configuration file (or defaults).
557- log_format = logger_config.format
558- log_datefmt = logger_config.datefmt
559- # Propagation to the root logger is how we handle logging to stderr
560- # when the runners are not run as a subprocess of 'bin/mailman start'.
561- log.propagate = (as_boolean(logger_config.propagate)
562- if propagate is None else propagate)
563- # Set the logger's level.
564- log.setLevel(as_log_level(logger_config.level))
565- # Create a formatter for this logger, then a handler, and link the
566- # formatter to the handler.
567- formatter = logging.Formatter(fmt=log_format, datefmt=log_datefmt)
568- path_str = logger_config.path
569- path_abs = os.path.normpath(os.path.join(config.LOG_DIR, path_str))
570- handler = ReopenableFileHandler(sub_name, path_abs)
571- _handlers[sub_name] = handler
572- handler.setFormatter(formatter)
573- log.addHandler(handler)
574-
575+ _init_logger(propagate, sub_name, log, logger_config)
576
577
578
579 def reopen():
580
581=== added directory 'src/mailman/database/alembic'
582=== added file 'src/mailman/database/alembic/__init__.py'
583--- src/mailman/database/alembic/__init__.py 1970-01-01 00:00:00 +0000
584+++ src/mailman/database/alembic/__init__.py 2014-11-01 03:48:29 +0000
585@@ -0,0 +1,32 @@
586+# Copyright (C) 2014 by the Free Software Foundation, Inc.
587+#
588+# This file is part of GNU Mailman.
589+#
590+# GNU Mailman is free software: you can redistribute it and/or modify it under
591+# the terms of the GNU General Public License as published by the Free
592+# Software Foundation, either version 3 of the License, or (at your option)
593+# any later version.
594+#
595+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
596+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
597+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
598+# more details.
599+#
600+# You should have received a copy of the GNU General Public License along with
601+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
602+
603+"""Alembic configuration initization."""
604+
605+from __future__ import absolute_import, print_function, unicode_literals
606+
607+__metaclass__ = type
608+__all__ = [
609+ 'alembic_cfg',
610+ ]
611+
612+
613+from alembic.config import Config
614+from mailman.utilities.modules import expand_path
615+
616+
617+alembic_cfg = Config(expand_path('python:mailman.config.alembic'))
618
619=== added file 'src/mailman/database/alembic/env.py'
620--- src/mailman/database/alembic/env.py 1970-01-01 00:00:00 +0000
621+++ src/mailman/database/alembic/env.py 2014-11-01 03:48:29 +0000
622@@ -0,0 +1,75 @@
623+# Copyright (C) 2014 by the Free Software Foundation, Inc.
624+#
625+# This file is part of GNU Mailman.
626+#
627+# GNU Mailman is free software: you can redistribute it and/or modify it under
628+# the terms of the GNU General Public License as published by the Free
629+# Software Foundation, either version 3 of the License, or (at your option)
630+# any later version.
631+#
632+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
633+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
634+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
635+# more details.
636+#
637+# You should have received a copy of the GNU General Public License along with
638+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
639+
640+"""Alembic migration environment."""
641+
642+from __future__ import absolute_import, print_function, unicode_literals
643+
644+__metaclass__ = type
645+__all__ = [
646+ 'run_migrations_offline',
647+ 'run_migrations_online',
648+ ]
649+
650+
651+from alembic import context
652+from contextlib import closing
653+from sqlalchemy import create_engine
654+
655+from mailman.config import config
656+from mailman.database.model import Model
657+from mailman.utilities.string import expand
658+
659+
660+
661
662+def run_migrations_offline():
663+ """Run migrations in 'offline' mode.
664+
665+ This configures the context with just a URL and not an Engine,
666+ though an Engine is acceptable here as well. By skipping the Engine
667+ creation we don't even need a DBAPI to be available.
668+
669+ Calls to context.execute() here emit the given string to the script
670+ output.
671+ """
672+ url = expand(config.database.url, config.paths)
673+ context.configure(url=url, target_metadata=Model.metadata)
674+ with context.begin_transaction():
675+ context.run_migrations()
676+
677+
678+def run_migrations_online():
679+ """Run migrations in 'online' mode.
680+
681+ In this scenario we need to create an Engine and associate a
682+ connection with the context.
683+ """
684+ url = expand(config.database.url, config.paths)
685+ engine = create_engine(url)
686+
687+ connection = engine.connect()
688+ with closing(connection):
689+ context.configure(
690+ connection=connection, target_metadata=Model.metadata)
691+ with context.begin_transaction():
692+ context.run_migrations()
693+
694+
695+if context.is_offline_mode():
696+ run_migrations_offline()
697+else:
698+ run_migrations_online()
699
700=== added file 'src/mailman/database/alembic/script.py.mako'
701--- src/mailman/database/alembic/script.py.mako 1970-01-01 00:00:00 +0000
702+++ src/mailman/database/alembic/script.py.mako 2014-11-01 03:48:29 +0000
703@@ -0,0 +1,22 @@
704+"""${message}
705+
706+Revision ID: ${up_revision}
707+Revises: ${down_revision}
708+Create Date: ${create_date}
709+
710+"""
711+
712+# revision identifiers, used by Alembic.
713+revision = ${repr(up_revision)}
714+down_revision = ${repr(down_revision)}
715+
716+from alembic import op
717+import sqlalchemy as sa
718+${imports if imports else ""}
719+
720+def upgrade():
721+ ${upgrades if upgrades else "pass"}
722+
723+
724+def downgrade():
725+ ${downgrades if downgrades else "pass"}
726
727=== added directory 'src/mailman/database/alembic/versions'
728=== added file 'src/mailman/database/alembic/versions/51b7f92bd06c_initial.py'
729--- src/mailman/database/alembic/versions/51b7f92bd06c_initial.py 1970-01-01 00:00:00 +0000
730+++ src/mailman/database/alembic/versions/51b7f92bd06c_initial.py 2014-11-01 03:48:29 +0000
731@@ -0,0 +1,66 @@
732+# Copyright (C) 2014 by the Free Software Foundation, Inc.
733+#
734+# This file is part of GNU Mailman.
735+#
736+# GNU Mailman is free software: you can redistribute it and/or modify it under
737+# the terms of the GNU General Public License as published by the Free
738+# Software Foundation, either version 3 of the License, or (at your option)
739+# any later version.
740+#
741+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
742+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
743+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
744+# more details.
745+#
746+# You should have received a copy of the GNU General Public License along with
747+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
748+
749+"""Initial migration.
750+
751+This empty migration file makes sure there is always an alembic_version
752+in the database. As a consequence, if the database version is reported
753+as None, it means the database needs to be created from scratch with
754+SQLAlchemy itself.
755+
756+It also removes schema items left over from Storm.
757+
758+Revision ID: 51b7f92bd06c
759+Revises: None
760+Create Date: 2014-10-10 09:53:35.624472
761+"""
762+
763+from __future__ import absolute_import, print_function, unicode_literals
764+
765+__metaclass__ = type
766+__all__ = [
767+ 'downgrade',
768+ 'upgrade',
769+ ]
770+
771+
772+from alembic import op
773+import sqlalchemy as sa
774+
775+
776+# Revision identifiers, used by Alembic.
777+revision = '51b7f92bd06c'
778+down_revision = None
779+
780+
781+def upgrade():
782+ op.drop_table('version')
783+ if op.get_bind().dialect.name != 'sqlite':
784+ # SQLite does not support dropping columns.
785+ op.drop_column('mailinglist', 'acceptable_aliases_id')
786+ op.create_index(op.f('ix_user__user_id'), 'user',
787+ ['_user_id'], unique=False)
788+ op.drop_index('ix_user_user_id', table_name='user')
789+
790+
791+def downgrade():
792+ op.create_table('version')
793+ op.create_index('ix_user_user_id', 'user', ['_user_id'], unique=False)
794+ op.drop_index(op.f('ix_user__user_id'), table_name='user')
795+ op.add_column(
796+ 'mailinglist',
797+ sa.Column('acceptable_aliases_id', sa.INTEGER(), nullable=True))
798
799=== modified file 'src/mailman/database/base.py'
800--- src/mailman/database/base.py 2014-01-01 14:59:42 +0000
801+++ src/mailman/database/base.py 2014-11-01 03:48:29 +0000
802@@ -19,49 +19,39 @@
803
804 __metaclass__ = type
805 __all__ = [
806- 'StormBaseDatabase',
807+ 'SABaseDatabase',
808 ]
809
810
811-import os
812-import sys
813 import logging
814
815-from lazr.config import as_boolean
816-from pkg_resources import resource_listdir, resource_string
817-from storm.cache import GenerationalCache
818-from storm.locals import create_database, Store
819+from sqlalchemy import create_engine
820+from sqlalchemy.orm import sessionmaker
821 from zope.interface import implementer
822
823 from mailman.config import config
824 from mailman.interfaces.database import IDatabase
825-from mailman.model.version import Version
826 from mailman.utilities.string import expand
827
828+
829 log = logging.getLogger('mailman.config')
830-
831 NL = '\n'
832
833
834
835
836 @implementer(IDatabase)
837-class StormBaseDatabase:
838- """The database base class for use with the Storm ORM.
839+class SABaseDatabase:
840+ """The database base class for use with SQLAlchemy.
841
842- Use this as a base class for your DB-specific derived classes.
843+ Use this as a base class for your DB-Specific derived classes.
844 """
845-
846- # Tag used to distinguish the database being used. Override this in base
847- # classes.
848- TAG = ''
849-
850 def __init__(self):
851 self.url = None
852 self.store = None
853
854 def begin(self):
855 """See `IDatabase`."""
856- # Storm takes care of this for us.
857+ # SQLAlchemy does this for us.
858 pass
859
860 def commit(self):
861@@ -72,16 +62,6 @@
862 """See `IDatabase`."""
863 self.store.rollback()
864
865- def _database_exists(self):
866- """Return True if the database exists and is initialized.
867-
868- Return False when Mailman needs to create and initialize the
869- underlying database schema.
870-
871- Base classes *must* override this.
872- """
873- raise NotImplementedError
874-
875 def _pre_reset(self, store):
876 """Clean up method for testing.
877
878@@ -113,6 +93,7 @@
879 """See `IDatabase`."""
880 # Calculate the engine url.
881 url = expand(config.database.url, config.paths)
882+ self._prepare(url)
883 log.debug('Database url: %s', url)
884 # XXX By design of SQLite, database file creation does not honor
885 # umask. See their ticket #1193:
886@@ -129,101 +110,13 @@
887 # engines, and yes, we could have chmod'd the file after the fact, but
888 # half dozen and all...
889 self.url = url
890- self._prepare(url)
891- database = create_database(url)
892- store = Store(database, GenerationalCache())
893- database.DEBUG = (as_boolean(config.database.debug)
894- if debug is None else debug)
895- self.store = store
896- store.commit()
897-
898- def load_migrations(self, until=None):
899- """Load schema migrations.
900-
901- :param until: Load only the migrations up to the specified timestamp.
902- With default value of None, load all migrations.
903- :type until: string
904- """
905- migrations_path = config.database.migrations_path
906- if '.' in migrations_path:
907- parent, dot, child = migrations_path.rpartition('.')
908- else:
909- parent = migrations_path
910- child = ''
911- # If the database does not yet exist, load the base schema.
912- filenames = sorted(resource_listdir(parent, child))
913- # Find out which schema migrations have already been loaded.
914- if self._database_exists(self.store):
915- versions = set(version.version for version in
916- self.store.find(Version, component='schema'))
917- else:
918- versions = set()
919- for filename in filenames:
920- module_fn, extension = os.path.splitext(filename)
921- if extension != '.py':
922- continue
923- parts = module_fn.split('_')
924- if len(parts) < 2:
925- continue
926- version = parts[1].strip()
927- if len(version) == 0:
928- # Not a schema migration file.
929- continue
930- if version in versions:
931- log.debug('already migrated to %s', version)
932- continue
933- if until is not None and version > until:
934- # We're done.
935- break
936- module_path = migrations_path + '.' + module_fn
937- __import__(module_path)
938- upgrade = getattr(sys.modules[module_path], 'upgrade', None)
939- if upgrade is None:
940- continue
941- log.debug('migrating db to %s: %s', version, module_path)
942- upgrade(self, self.store, version, module_path)
943- self.commit()
944-
945- def load_sql(self, store, sql):
946- """Load the given SQL into the store.
947-
948- :param store: The Storm store to load the schema into.
949- :type store: storm.locals.Store`
950- :param sql: The possibly multi-line SQL to load.
951- :type sql: string
952- """
953- # Discard all blank and comment lines.
954- lines = (line for line in sql.splitlines()
955- if line.strip() != '' and line.strip()[:2] != '--')
956- sql = NL.join(lines)
957- for statement in sql.split(';'):
958- if statement.strip() != '':
959- store.execute(statement + ';')
960-
961- def load_schema(self, store, version, filename, module_path):
962- """Load the schema from a file.
963-
964- This is a helper method for migration classes to call.
965-
966- :param store: The Storm store to load the schema into.
967- :type store: storm.locals.Store`
968- :param version: The schema version identifier of the form
969- YYYYMMDDHHMMSS.
970- :type version: string
971- :param filename: The file name containing the schema to load. Pass
972- `None` if there is no schema file to load.
973- :type filename: string
974- :param module_path: The fully qualified Python module path to the
975- migration module being loaded. This is used to record information
976- for use by the test suite.
977- :type module_path: string
978- """
979- if filename is not None:
980- contents = resource_string('mailman.database.schema', filename)
981- self.load_sql(store, contents)
982- # Add a marker that indicates the migration version being applied.
983- store.add(Version(component='schema', version=version))
984-
985- @staticmethod
986- def _make_temporary():
987- raise NotImplementedError
988+ self.engine = create_engine(url)
989+ session = sessionmaker(bind=self.engine)
990+ self.store = session()
991+ self.store.commit()
992+
993+ # XXX BAW Why doesn't model.py _reset() do this?
994+ def destroy(self):
995+ """Drop all database tables"""
996+ from mailman.database.model import Model
997+ Model.metadata.drop_all(self.engine)
998
999=== removed directory 'src/mailman/database/docs'
1000=== removed file 'src/mailman/database/docs/__init__.py'
1001=== removed file 'src/mailman/database/docs/migration.rst'
1002--- src/mailman/database/docs/migration.rst 2014-04-28 15:23:35 +0000
1003+++ src/mailman/database/docs/migration.rst 1970-01-01 00:00:00 +0000
1004@@ -1,207 +0,0 @@
1005-=================
1006-Schema migrations
1007-=================
1008-
1009-The SQL database schema will over time require upgrading to support new
1010-features. This is supported via schema migration.
1011-
1012-Migrations are embodied in individual Python classes, which themselves may
1013-load SQL into the database. The naming scheme for migration files is:
1014-
1015- mm_YYYYMMDDHHMMSS_comment.py
1016-
1017-where `YYYYMMDDHHMMSS` is a required numeric year, month, day, hour, minute,
1018-and second specifier providing unique ordering for processing. Only this
1019-component of the file name is used to determine the ordering. The prefix is
1020-required due to Python module naming requirements, but it is actually
1021-ignored. `mm_` is reserved for Mailman's own use.
1022-
1023-The optional `comment` part of the file name can be used as a short
1024-description for the migration, although comments and docstrings in the
1025-migration files should be used for more detailed descriptions.
1026-
1027-Migrations are applied automatically when Mailman starts up, but can also be
1028-applied at any time by calling in the API directly. Once applied, a
1029-migration's version string is registered so it will not be applied again.
1030-
1031-We see that the base migration, as well as subsequent standard migrations, are
1032-already applied.
1033-
1034- >>> from mailman.model.version import Version
1035- >>> results = config.db.store.find(Version, component='schema')
1036- >>> results.count()
1037- 4
1038- >>> versions = sorted(result.version for result in results)
1039- >>> for version in versions:
1040- ... print(version)
1041- 00000000000000
1042- 20120407000000
1043- 20121015000000
1044- 20130406000000
1045-
1046-
1047-Migrations
1048-==========
1049-
1050-Migrations can be loaded at any time, and can be found in the migrations path
1051-specified in the configuration file.
1052-
1053-.. Create a temporary directory for the migrations::
1054-
1055- >>> import os, sys, tempfile
1056- >>> tempdir = tempfile.mkdtemp()
1057- >>> path = os.path.join(tempdir, 'migrations')
1058- >>> os.makedirs(path)
1059- >>> sys.path.append(tempdir)
1060- >>> config.push('migrations', """
1061- ... [database]
1062- ... migrations_path: migrations
1063- ... """)
1064-
1065-.. Clean this up at the end of the doctest.
1066- >>> def cleanup():
1067- ... import shutil
1068- ... from mailman.config import config
1069- ... config.pop('migrations')
1070- ... shutil.rmtree(tempdir)
1071- >>> cleanups.append(cleanup)
1072-
1073-Here is an example migrations module. The key part of this interface is the
1074-``upgrade()`` method, which takes four arguments:
1075-
1076- * `database` - The database class, as derived from `StormBaseDatabase`
1077- * `store` - The Storm `Store` object.
1078- * `version` - The version string as derived from the migrations module's file
1079- name. This will include only the `YYYYMMDDHHMMSS` string.
1080- * `module_path` - The dotted module path to the migrations module, suitable
1081- for lookup in `sys.modules`.
1082-
1083-This migration module just adds a marker to the `version` table.
1084-
1085- >>> with open(os.path.join(path, '__init__.py'), 'w') as fp:
1086- ... pass
1087- >>> with open(os.path.join(path, 'mm_20159999000000.py'), 'w') as fp:
1088- ... print("""
1089- ... from __future__ import unicode_literals
1090- ... from mailman.model.version import Version
1091- ... def upgrade(database, store, version, module_path):
1092- ... v = Version(component='test', version=version)
1093- ... store.add(v)
1094- ... database.load_schema(store, version, None, module_path)
1095- ... """, file=fp)
1096-
1097-This will load the new migration, since it hasn't been loaded before.
1098-
1099- >>> config.db.load_migrations()
1100- >>> results = config.db.store.find(Version, component='schema')
1101- >>> for result in sorted(result.version for result in results):
1102- ... print(result)
1103- 00000000000000
1104- 20120407000000
1105- 20121015000000
1106- 20130406000000
1107- 20159999000000
1108- >>> test = config.db.store.find(Version, component='test').one()
1109- >>> print(test.version)
1110- 20159999000000
1111-
1112-Migrations will only be loaded once.
1113-
1114- >>> with open(os.path.join(path, 'mm_20159999000001.py'), 'w') as fp:
1115- ... print("""
1116- ... from __future__ import unicode_literals
1117- ... from mailman.model.version import Version
1118- ... _marker = 801
1119- ... def upgrade(database, store, version, module_path):
1120- ... global _marker
1121- ... # Pad enough zeros on the left to reach 14 characters wide.
1122- ... marker = '{0:=#014d}'.format(_marker)
1123- ... _marker += 1
1124- ... v = Version(component='test', version=marker)
1125- ... store.add(v)
1126- ... database.load_schema(store, version, None, module_path)
1127- ... """, file=fp)
1128-
1129-The first time we load this new migration, we'll get the 801 marker.
1130-
1131- >>> config.db.load_migrations()
1132- >>> results = config.db.store.find(Version, component='schema')
1133- >>> for result in sorted(result.version for result in results):
1134- ... print(result)
1135- 00000000000000
1136- 20120407000000
1137- 20121015000000
1138- 20130406000000
1139- 20159999000000
1140- 20159999000001
1141- >>> test = config.db.store.find(Version, component='test')
1142- >>> for marker in sorted(marker.version for marker in test):
1143- ... print(marker)
1144- 00000000000801
1145- 20159999000000
1146-
1147-We do not get an 802 marker because the migration has already been loaded.
1148-
1149- >>> config.db.load_migrations()
1150- >>> results = config.db.store.find(Version, component='schema')
1151- >>> for result in sorted(result.version for result in results):
1152- ... print(result)
1153- 00000000000000
1154- 20120407000000
1155- 20121015000000
1156- 20130406000000
1157- 20159999000000
1158- 20159999000001
1159- >>> test = config.db.store.find(Version, component='test')
1160- >>> for marker in sorted(marker.version for marker in test):
1161- ... print(marker)
1162- 00000000000801
1163- 20159999000000
1164-
1165-
1166-Partial upgrades
1167-================
1168-
1169-It's possible (mostly for testing purposes) to only do a partial upgrade, by
1170-providing a timestamp to `load_migrations()`. To demonstrate this, we add two
1171-additional migrations, intended to be applied in sequential order.
1172-
1173- >>> from shutil import copyfile
1174- >>> from mailman.testing.helpers import chdir
1175- >>> with chdir(path):
1176- ... copyfile('mm_20159999000000.py', 'mm_20159999000002.py')
1177- ... copyfile('mm_20159999000000.py', 'mm_20159999000003.py')
1178- ... copyfile('mm_20159999000000.py', 'mm_20159999000004.py')
1179-
1180-Now, only migrate to the ...03 timestamp.
1181-
1182- >>> config.db.load_migrations('20159999000003')
1183-
1184-You'll notice that the ...04 version is not present.
1185-
1186- >>> results = config.db.store.find(Version, component='schema')
1187- >>> for result in sorted(result.version for result in results):
1188- ... print(result)
1189- 00000000000000
1190- 20120407000000
1191- 20121015000000
1192- 20130406000000
1193- 20159999000000
1194- 20159999000001
1195- 20159999000002
1196- 20159999000003
1197-
1198-
1199-.. cleanup:
1200- Because the Version table holds schema migration data, it will not be
1201- cleaned up by the standard test suite. This is generally not a problem
1202- for SQLite since each test gets a new database file, but for PostgreSQL,
1203- this will cause migration.rst to fail on subsequent runs. So let's just
1204- clean up the database explicitly.
1205-
1206- >>> if config.db.TAG != 'sqlite':
1207- ... results = config.db.store.execute("""
1208- ... DELETE FROM version WHERE version.version >= '201299990000'
1209- ... OR version.component = 'test';
1210- ... """)
1211- ... config.db.commit()
1212
1213=== modified file 'src/mailman/database/factory.py'
1214--- src/mailman/database/factory.py 2014-01-01 14:59:42 +0000
1215+++ src/mailman/database/factory.py 2014-11-01 03:48:29 +0000
1216@@ -22,25 +22,32 @@
1217 __metaclass__ = type
1218 __all__ = [
1219 'DatabaseFactory',
1220- 'DatabaseTemporaryFactory',
1221 'DatabaseTestingFactory',
1222 ]
1223
1224
1225 import os
1226 import types
1227+import alembic.command
1228
1229+from alembic.migration import MigrationContext
1230+from alembic.script import ScriptDirectory
1231 from flufl.lock import Lock
1232-from zope.component import getAdapter
1233+from sqlalchemy import MetaData
1234 from zope.interface import implementer
1235 from zope.interface.verify import verifyObject
1236
1237 from mailman.config import config
1238+from mailman.database.alembic import alembic_cfg
1239+from mailman.database.model import Model
1240 from mailman.interfaces.database import (
1241- IDatabase, IDatabaseFactory, ITemporaryDatabase)
1242+ DatabaseError, IDatabase, IDatabaseFactory)
1243 from mailman.utilities.modules import call_name
1244
1245
1246+LAST_STORM_SCHEMA_VERSION = '20130406000000'
1247+
1248+
1249
1250
1251 @implementer(IDatabaseFactory)
1252 class DatabaseFactory:
1253@@ -54,18 +61,69 @@
1254 database = call_name(database_class)
1255 verifyObject(IDatabase, database)
1256 database.initialize()
1257- database.load_migrations()
1258+ SchemaManager(database).setup_database()
1259 database.commit()
1260 return database
1261
1262
1263
1264
1265+class SchemaManager:
1266+ "Manage schema migrations."""
1267+
1268+ def __init__(self, database):
1269+ self._database = database
1270+ self._script = ScriptDirectory.from_config(alembic_cfg)
1271+
1272+ def _get_storm_schema_version(self):
1273+ metadata = MetaData()
1274+ metadata.reflect(bind=self._database.engine)
1275+ if 'version' not in metadata.tables:
1276+ # There are no Storm artifacts left.
1277+ return None
1278+ Version = metadata.tables['version']
1279+ last_version = self._database.store.query(Version.c.version).filter(
1280+ Version.c.component == 'schema'
1281+ ).order_by(Version.c.version.desc()).first()
1282+ # Don't leave open transactions or they will block any schema change.
1283+ self._database.commit()
1284+ return last_version
1285+
1286+ def setup_database(self):
1287+ context = MigrationContext.configure(self._database.store.connection())
1288+ current_rev = context.get_current_revision()
1289+ head_rev = self._script.get_current_head()
1290+ if current_rev == head_rev:
1291+ # We're already at the latest revision so there's nothing to do.
1292+ return head_rev
1293+ if current_rev is None:
1294+ # No Alembic information is available.
1295+ storm_version = self._get_storm_schema_version()
1296+ if storm_version is None:
1297+ # Initial database creation.
1298+ Model.metadata.create_all(self._database.engine)
1299+ self._database.commit()
1300+ alembic.command.stamp(alembic_cfg, 'head')
1301+ else:
1302+ # The database was previously managed by Storm.
1303+ if storm_version.version < LAST_STORM_SCHEMA_VERSION:
1304+ raise DatabaseError(
1305+ 'Upgrades skipping beta versions is not supported.')
1306+ # Run migrations to remove the Storm-specific table and upgrade
1307+ # to SQLAlchemy and Alembic.
1308+ alembic.command.upgrade(alembic_cfg, 'head')
1309+ elif current_rev != head_rev:
1310+ alembic.command.upgrade(alembic_cfg, 'head')
1311+ return head_rev
1312+
1313+
1314+
1315
1316 def _reset(self):
1317 """See `IDatabase`."""
1318- from mailman.database.model import ModelMeta
1319+ # Avoid a circular import at module level.
1320+ from mailman.database.model import Model
1321 self.store.rollback()
1322 self._pre_reset(self.store)
1323- ModelMeta._reset(self.store)
1324+ Model._reset(self)
1325 self._post_reset(self.store)
1326 self.store.commit()
1327
1328@@ -81,24 +139,8 @@
1329 database = call_name(database_class)
1330 verifyObject(IDatabase, database)
1331 database.initialize()
1332- database.load_migrations()
1333+ Model.metadata.create_all(database.engine)
1334 database.commit()
1335 # Make _reset() a bound method of the database instance.
1336 database._reset = types.MethodType(_reset, database)
1337 return database
1338-
1339-
1340-
1341
1342-@implementer(IDatabaseFactory)
1343-class DatabaseTemporaryFactory:
1344- """Create a temporary database for some of the migration tests."""
1345-
1346- @staticmethod
1347- def create():
1348- """See `IDatabaseFactory`."""
1349- database_class_name = config.database['class']
1350- database = call_name(database_class_name)
1351- verifyObject(IDatabase, database)
1352- adapted_database = getAdapter(
1353- database, ITemporaryDatabase, database.TAG)
1354- return adapted_database
1355
1356=== modified file 'src/mailman/database/model.py'
1357--- src/mailman/database/model.py 2014-01-01 14:59:42 +0000
1358+++ src/mailman/database/model.py 2014-11-01 03:48:29 +0000
1359@@ -25,44 +25,34 @@
1360 ]
1361
1362
1363-from operator import attrgetter
1364-
1365-from storm.properties import PropertyPublisherMeta
1366-
1367-
1368-
1369
1370-class ModelMeta(PropertyPublisherMeta):
1371- """Do more magic on table classes."""
1372-
1373- _class_registry = set()
1374-
1375- def __init__(self, name, bases, dict):
1376- # Before we let the base class do it's thing, force an __storm_table__
1377- # property to enforce our table naming convention.
1378- self.__storm_table__ = name.lower()
1379- super(ModelMeta, self).__init__(name, bases, dict)
1380- # Register the model class so that it can be more easily cleared.
1381- # This is required by the test framework so that the corresponding
1382- # table can be reset between tests.
1383- #
1384- # The PRESERVE flag indicates whether the table should be reset or
1385- # not. We have to handle the actual Model base class explicitly
1386- # because it does not correspond to a table in the database.
1387- if not getattr(self, 'PRESERVE', False) and name != 'Model':
1388- ModelMeta._class_registry.add(self)
1389-
1390+import contextlib
1391+
1392+from sqlalchemy.ext.declarative import declarative_base
1393+
1394+from mailman.config import config
1395+
1396+
1397+class ModelMeta:
1398+ """The custom metaclass for all model base classes.
1399+
1400+ This is used in the test suite to quickly reset the database after each
1401+ test. It works by iterating over all the tables, deleting each. The test
1402+ suite will then recreate the tables before each test.
1403+ """
1404 @staticmethod
1405- def _reset(store):
1406- from mailman.config import config
1407- config.db._pre_reset(store)
1408- # Make sure this is deterministic, by sorting on the storm table name.
1409- classes = sorted(ModelMeta._class_registry,
1410- key=attrgetter('__storm_table__'))
1411- for model_class in classes:
1412- store.find(model_class).remove()
1413-
1414-
1415-
1416
1417-class Model:
1418- """Like Storm's `Storm` subclass, but with a bit extra."""
1419- __metaclass__ = ModelMeta
1420+ def _reset(db):
1421+ with contextlib.closing(config.db.engine.connect()) as connection:
1422+ transaction = connection.begin()
1423+ try:
1424+ # Delete all the tables in reverse foreign key dependency
1425+ # order. http://tinyurl.com/on8dy6f
1426+ for table in reversed(Model.metadata.sorted_tables):
1427+ connection.execute(table.delete())
1428+ except:
1429+ transaction.rollback()
1430+ raise
1431+ else:
1432+ transaction.commit()
1433+
1434+
1435+Model = declarative_base(cls=ModelMeta)
1436
1437=== modified file 'src/mailman/database/postgresql.py'
1438--- src/mailman/database/postgresql.py 2014-01-01 14:59:42 +0000
1439+++ src/mailman/database/postgresql.py 2014-11-01 03:48:29 +0000
1440@@ -22,34 +22,17 @@
1441 __metaclass__ = type
1442 __all__ = [
1443 'PostgreSQLDatabase',
1444- 'make_temporary',
1445 ]
1446
1447
1448-import types
1449-
1450-from functools import partial
1451-from operator import attrgetter
1452-from urlparse import urlsplit, urlunsplit
1453-
1454-from mailman.database.base import StormBaseDatabase
1455-from mailman.testing.helpers import configuration
1456+from mailman.database.base import SABaseDatabase
1457+from mailman.database.model import Model
1458
1459
1460
1461
1462-class PostgreSQLDatabase(StormBaseDatabase):
1463+class PostgreSQLDatabase(SABaseDatabase):
1464 """Database class for PostgreSQL."""
1465
1466- TAG = 'postgres'
1467-
1468- def _database_exists(self, store):
1469- """See `BaseDatabase`."""
1470- table_query = ('SELECT table_name FROM information_schema.tables '
1471- "WHERE table_schema = 'public'")
1472- results = store.execute(table_query)
1473- table_names = set(item[0] for item in results)
1474- return 'version' in table_names
1475-
1476 def _post_reset(self, store):
1477 """PostgreSQL-specific test suite cleanup.
1478
1479@@ -57,49 +40,13 @@
1480 restart from zero for new tests.
1481 """
1482 super(PostgreSQLDatabase, self)._post_reset(store)
1483- from mailman.database.model import ModelMeta
1484- classes = sorted(ModelMeta._class_registry,
1485- key=attrgetter('__storm_table__'))
1486+ tables = reversed(Model.metadata.sorted_tables)
1487 # Recipe adapted from
1488 # http://stackoverflow.com/questions/544791/
1489 # django-postgresql-how-to-reset-primary-key
1490- for model_class in classes:
1491+ for table in tables:
1492 store.execute("""\
1493 SELECT setval('"{0}_id_seq"', coalesce(max("id"), 1),
1494 max("id") IS NOT null)
1495 FROM "{0}";
1496- """.format(model_class.__storm_table__))
1497-
1498-
1499-
1500
1501-# Test suite adapter for ITemporaryDatabase.
1502-
1503-def _cleanup(self, store, tempdb_name):
1504- from mailman.config import config
1505- store.rollback()
1506- store.close()
1507- # From the original database connection, drop the now unused database.
1508- config.db.store.execute('DROP DATABASE {0}'.format(tempdb_name))
1509-
1510-
1511-def make_temporary(database):
1512- """Adapts by monkey patching an existing PostgreSQL IDatabase."""
1513- from mailman.config import config
1514- parts = urlsplit(config.database.url)
1515- assert parts.scheme == 'postgres'
1516- new_parts = list(parts)
1517- new_parts[2] = '/mmtest'
1518- url = urlunsplit(new_parts)
1519- # Use the existing database connection to create a new testing
1520- # database.
1521- config.db.store.execute('ABORT;')
1522- config.db.store.execute('CREATE DATABASE mmtest;')
1523- with configuration('database', url=url):
1524- database.initialize()
1525- database._cleanup = types.MethodType(
1526- partial(_cleanup, store=database.store, tempdb_name='mmtest'),
1527- database)
1528- # bool column values in PostgreSQL.
1529- database.FALSE = 'False'
1530- database.TRUE = 'True'
1531- return database
1532+ """.format(table))
1533
1534=== removed directory 'src/mailman/database/schema'
1535=== removed file 'src/mailman/database/schema/__init__.py'
1536=== removed file 'src/mailman/database/schema/helpers.py'
1537--- src/mailman/database/schema/helpers.py 2014-01-01 14:59:42 +0000
1538+++ src/mailman/database/schema/helpers.py 1970-01-01 00:00:00 +0000
1539@@ -1,43 +0,0 @@
1540-# Copyright (C) 2013-2014 by the Free Software Foundation, Inc.
1541-#
1542-# This file is part of GNU Mailman.
1543-#
1544-# GNU Mailman is free software: you can redistribute it and/or modify it under
1545-# the terms of the GNU General Public License as published by the Free
1546-# Software Foundation, either version 3 of the License, or (at your option)
1547-# any later version.
1548-#
1549-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
1550-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
1551-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
1552-# more details.
1553-#
1554-# You should have received a copy of the GNU General Public License along with
1555-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
1556-
1557-"""Schema migration helpers."""
1558-
1559-from __future__ import absolute_import, print_function, unicode_literals
1560-
1561-__metaclass__ = type
1562-__all__ = [
1563- 'make_listid',
1564- ]
1565-
1566-
1567-
1568
1569-def make_listid(fqdn_listname):
1570- """Turn a FQDN list name into a List-ID."""
1571- list_name, at, mail_host = fqdn_listname.partition('@')
1572- if at == '':
1573- # If there is no @ sign in the value, assume it already contains the
1574- # list-id.
1575- return fqdn_listname
1576- return '{0}.{1}'.format(list_name, mail_host)
1577-
1578-
1579-
1580
1581-def pivot(store, table_name):
1582- """Pivot a backup table into the real table name."""
1583- store.execute('DROP TABLE {}'.format(table_name))
1584- store.execute('ALTER TABLE {0}_backup RENAME TO {0}'.format(table_name))
1585
1586=== removed file 'src/mailman/database/schema/mm_00000000000000_base.py'
1587--- src/mailman/database/schema/mm_00000000000000_base.py 2014-01-01 14:59:42 +0000
1588+++ src/mailman/database/schema/mm_00000000000000_base.py 1970-01-01 00:00:00 +0000
1589@@ -1,35 +0,0 @@
1590-# Copyright (C) 2012-2014 by the Free Software Foundation, Inc.
1591-#
1592-# This file is part of GNU Mailman.
1593-#
1594-# GNU Mailman is free software: you can redistribute it and/or modify it under
1595-# the terms of the GNU General Public License as published by the Free
1596-# Software Foundation, either version 3 of the License, or (at your option)
1597-# any later version.
1598-#
1599-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
1600-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
1601-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
1602-# more details.
1603-#
1604-# You should have received a copy of the GNU General Public License along with
1605-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
1606-
1607-"""Load the base schema."""
1608-
1609-from __future__ import absolute_import, print_function, unicode_literals
1610-
1611-__metaclass__ = type
1612-__all__ = [
1613- 'upgrade',
1614- ]
1615-
1616-
1617-VERSION = '00000000000000'
1618-_helper = None
1619-
1620-
1621-
1622
1623-def upgrade(database, store, version, module_path):
1624- filename = '{0}.sql'.format(database.TAG)
1625- database.load_schema(store, version, filename, module_path)
1626
1627=== removed file 'src/mailman/database/schema/mm_20120407000000.py'
1628--- src/mailman/database/schema/mm_20120407000000.py 2014-01-01 14:59:42 +0000
1629+++ src/mailman/database/schema/mm_20120407000000.py 1970-01-01 00:00:00 +0000
1630@@ -1,212 +0,0 @@
1631-# Copyright (C) 2012-2014 by the Free Software Foundation, Inc.
1632-#
1633-# This file is part of GNU Mailman.
1634-#
1635-# GNU Mailman is free software: you can redistribute it and/or modify it under
1636-# the terms of the GNU General Public License as published by the Free
1637-# Software Foundation, either version 3 of the License, or (at your option)
1638-# any later version.
1639-#
1640-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
1641-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
1642-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
1643-# more details.
1644-#
1645-# You should have received a copy of the GNU General Public License along with
1646-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
1647-
1648-"""3.0b1 -> 3.0b2 schema migrations.
1649-
1650-All column changes are in the `mailinglist` table.
1651-
1652-* Renames:
1653- - news_prefix_subject_too -> nntp_prefix_subject_too
1654- - news_moderation -> newsgroup_moderation
1655-
1656-* Collapsing:
1657- - archive, archive_private -> archive_policy
1658-
1659-* Remove:
1660- - archive_volume_frequency
1661- - generic_nonmember_action
1662- - nntp_host
1663-
1664-* Added:
1665- - list_id
1666-
1667-* Changes:
1668- member.mailing_list holds the list_id not the fqdn_listname
1669-
1670-See https://bugs.launchpad.net/mailman/+bug/971013 for details.
1671-"""
1672-
1673-from __future__ import absolute_import, print_function, unicode_literals
1674-
1675-__metaclass__ = type
1676-__all__ = [
1677- 'upgrade',
1678- ]
1679-
1680-
1681-from mailman.database.schema.helpers import pivot
1682-from mailman.interfaces.archiver import ArchivePolicy
1683-
1684-
1685-VERSION = '20120407000000'
1686-
1687-
1688-
1689
1690-def upgrade(database, store, version, module_path):
1691- if database.TAG == 'sqlite':
1692- upgrade_sqlite(database, store, version, module_path)
1693- else:
1694- upgrade_postgres(database, store, version, module_path)
1695-
1696-
1697-
1698
1699-def archive_policy(archive, archive_private):
1700- """Convert archive and archive_private to archive_policy."""
1701- if archive == 0:
1702- return ArchivePolicy.never.value
1703- elif archive_private == 1:
1704- return ArchivePolicy.private.value
1705- else:
1706- return ArchivePolicy.public.value
1707-
1708-
1709-
1710
1711-def upgrade_sqlite(database, store, version, module_path):
1712- # Load the first part of the migration. This creates a temporary table to
1713- # hold the new mailinglist table columns. The problem is that some of the
1714- # changes must be performed in Python, so after the first part is loaded,
1715- # we do the Python changes, drop the old mailing list table, and then
1716- # rename the temporary table to its place.
1717- database.load_schema(
1718- store, version, 'sqlite_{0}_01.sql'.format(version), module_path)
1719- results = store.execute("""
1720- SELECT id, include_list_post_header,
1721- news_prefix_subject_too, news_moderation,
1722- archive, archive_private, list_name, mail_host
1723- FROM mailinglist;
1724- """)
1725- for value in results:
1726- (id, list_post,
1727- news_prefix, news_moderation,
1728- archive, archive_private,
1729- list_name, mail_host) = value
1730- # Figure out what the new archive_policy column value should be.
1731- list_id = '{0}.{1}'.format(list_name, mail_host)
1732- fqdn_listname = '{0}@{1}'.format(list_name, mail_host)
1733- store.execute("""
1734- UPDATE mailinglist_backup SET
1735- allow_list_posts = {0},
1736- newsgroup_moderation = {1},
1737- nntp_prefix_subject_too = {2},
1738- archive_policy = {3},
1739- list_id = '{4}'
1740- WHERE id = {5};
1741- """.format(
1742- list_post,
1743- news_moderation,
1744- news_prefix,
1745- archive_policy(archive, archive_private),
1746- list_id,
1747- id))
1748- # Also update the member.mailing_list column to hold the list_id
1749- # instead of the fqdn_listname.
1750- store.execute("""
1751- UPDATE member SET
1752- mailing_list = '{0}'
1753- WHERE mailing_list = '{1}';
1754- """.format(list_id, fqdn_listname))
1755- # Pivot the backup table to the real thing.
1756- pivot(store, 'mailinglist')
1757- # Now add some indexes that were previously missing.
1758- store.execute(
1759- 'CREATE INDEX ix_mailinglist_list_id ON mailinglist (list_id);')
1760- store.execute(
1761- 'CREATE INDEX ix_mailinglist_fqdn_listname '
1762- 'ON mailinglist (list_name, mail_host);')
1763- # Now, do the member table.
1764- results = store.execute('SELECT id, mailing_list FROM member;')
1765- for id, mailing_list in results:
1766- list_name, at, mail_host = mailing_list.partition('@')
1767- if at == '':
1768- list_id = mailing_list
1769- else:
1770- list_id = '{0}.{1}'.format(list_name, mail_host)
1771- store.execute("""
1772- UPDATE member_backup SET list_id = '{0}'
1773- WHERE id = {1};
1774- """.format(list_id, id))
1775- # Pivot the backup table to the real thing.
1776- pivot(store, 'member')
1777-
1778-
1779-
1780
1781-def upgrade_postgres(database, store, version, module_path):
1782- # Get the old values from the mailinglist table.
1783- results = store.execute("""
1784- SELECT id, archive, archive_private, list_name, mail_host
1785- FROM mailinglist;
1786- """)
1787- # Do the simple renames first.
1788- store.execute("""
1789- ALTER TABLE mailinglist
1790- RENAME COLUMN news_prefix_subject_too TO nntp_prefix_subject_too;
1791- """)
1792- store.execute("""
1793- ALTER TABLE mailinglist
1794- RENAME COLUMN news_moderation TO newsgroup_moderation;
1795- """)
1796- store.execute("""
1797- ALTER TABLE mailinglist
1798- RENAME COLUMN include_list_post_header TO allow_list_posts;
1799- """)
1800- # Do the easy column drops next.
1801- for column in ('archive_volume_frequency',
1802- 'generic_nonmember_action',
1803- 'nntp_host'):
1804- store.execute(
1805- 'ALTER TABLE mailinglist DROP COLUMN {0};'.format(column))
1806- # Now do the trickier collapsing of values. Add the new columns.
1807- store.execute('ALTER TABLE mailinglist ADD COLUMN archive_policy INTEGER;')
1808- store.execute('ALTER TABLE mailinglist ADD COLUMN list_id TEXT;')
1809- # Query the database for the old values of archive and archive_private in
1810- # each column. Then loop through all the results and update the new
1811- # archive_policy from the old values.
1812- for value in results:
1813- id, archive, archive_private, list_name, mail_host = value
1814- list_id = '{0}.{1}'.format(list_name, mail_host)
1815- store.execute("""
1816- UPDATE mailinglist SET
1817- archive_policy = {0},
1818- list_id = '{1}'
1819- WHERE id = {2};
1820- """.format(archive_policy(archive, archive_private), list_id, id))
1821- # Now drop the old columns.
1822- for column in ('archive', 'archive_private'):
1823- store.execute(
1824- 'ALTER TABLE mailinglist DROP COLUMN {0};'.format(column))
1825- # Now add some indexes that were previously missing.
1826- store.execute(
1827- 'CREATE INDEX ix_mailinglist_list_id ON mailinglist (list_id);')
1828- store.execute(
1829- 'CREATE INDEX ix_mailinglist_fqdn_listname '
1830- 'ON mailinglist (list_name, mail_host);')
1831- # Now, do the member table.
1832- results = store.execute('SELECT id, mailing_list FROM member;')
1833- store.execute('ALTER TABLE member ADD COLUMN list_id TEXT;')
1834- for id, mailing_list in results:
1835- list_name, at, mail_host = mailing_list.partition('@')
1836- if at == '':
1837- list_id = mailing_list
1838- else:
1839- list_id = '{0}.{1}'.format(list_name, mail_host)
1840- store.execute("""
1841- UPDATE member SET list_id = '{0}'
1842- WHERE id = {1};
1843- """.format(list_id, id))
1844- store.execute('ALTER TABLE member DROP COLUMN mailing_list;')
1845- # Record the migration in the version table.
1846- database.load_schema(store, version, None, module_path)
1847
1848=== removed file 'src/mailman/database/schema/mm_20121015000000.py'
1849--- src/mailman/database/schema/mm_20121015000000.py 2014-01-01 14:59:42 +0000
1850+++ src/mailman/database/schema/mm_20121015000000.py 1970-01-01 00:00:00 +0000
1851@@ -1,95 +0,0 @@
1852-# Copyright (C) 2012-2014 by the Free Software Foundation, Inc.
1853-#
1854-# This file is part of GNU Mailman.
1855-#
1856-# GNU Mailman is free software: you can redistribute it and/or modify it under
1857-# the terms of the GNU General Public License as published by the Free
1858-# Software Foundation, either version 3 of the License, or (at your option)
1859-# any later version.
1860-#
1861-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
1862-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
1863-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
1864-# more details.
1865-#
1866-# You should have received a copy of the GNU General Public License along with
1867-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
1868-
1869-"""3.0b2 -> 3.0b3 schema migrations.
1870-
1871-Renamed:
1872- * bans.mailing_list -> bans.list_id
1873-
1874-Removed:
1875- * mailinglist.new_member_options
1876- * mailinglist.send_remindersn
1877-"""
1878-
1879-from __future__ import absolute_import, print_function, unicode_literals
1880-
1881-__metaclass__ = type
1882-__all__ = [
1883- 'upgrade',
1884- ]
1885-
1886-
1887-from mailman.database.schema.helpers import make_listid, pivot
1888-
1889-
1890-VERSION = '20121015000000'
1891-
1892-
1893-
1894
1895-def upgrade(database, store, version, module_path):
1896- if database.TAG == 'sqlite':
1897- upgrade_sqlite(database, store, version, module_path)
1898- else:
1899- upgrade_postgres(database, store, version, module_path)
1900-
1901-
1902-
1903
1904-def upgrade_sqlite(database, store, version, module_path):
1905- database.load_schema(
1906- store, version, 'sqlite_{}_01.sql'.format(version), module_path)
1907- results = store.execute("""
1908- SELECT id, mailing_list
1909- FROM ban;
1910- """)
1911- for id, mailing_list in results:
1912- # Skip global bans since there's nothing to update.
1913- if mailing_list is None:
1914- continue
1915- store.execute("""
1916- UPDATE ban_backup SET list_id = '{}'
1917- WHERE id = {};
1918- """.format(make_listid(mailing_list), id))
1919- # Pivot the bans backup table to the real thing.
1920- pivot(store, 'ban')
1921- pivot(store, 'mailinglist')
1922-
1923-
1924-
1925
1926-def upgrade_postgres(database, store, version, module_path):
1927- # Get the old values from the ban table.
1928- results = store.execute('SELECT id, mailing_list FROM ban;')
1929- store.execute('ALTER TABLE ban ADD COLUMN list_id TEXT;')
1930- for id, mailing_list in results:
1931- # Skip global bans since there's nothing to update.
1932- if mailing_list is None:
1933- continue
1934- store.execute("""
1935- UPDATE ban SET list_id = '{0}'
1936- WHERE id = {1};
1937- """.format(make_listid(mailing_list), id))
1938- store.execute('ALTER TABLE ban DROP COLUMN mailing_list;')
1939- store.execute('ALTER TABLE mailinglist DROP COLUMN new_member_options;')
1940- store.execute('ALTER TABLE mailinglist DROP COLUMN send_reminders;')
1941- store.execute('ALTER TABLE mailinglist DROP COLUMN subscribe_policy;')
1942- store.execute('ALTER TABLE mailinglist DROP COLUMN unsubscribe_policy;')
1943- store.execute(
1944- 'ALTER TABLE mailinglist DROP COLUMN subscribe_auto_approval;')
1945- store.execute('ALTER TABLE mailinglist DROP COLUMN private_roster;')
1946- store.execute(
1947- 'ALTER TABLE mailinglist DROP COLUMN admin_member_chunksize;')
1948- # Record the migration in the version table.
1949- database.load_schema(store, version, None, module_path)
1950
1951=== removed file 'src/mailman/database/schema/mm_20130406000000.py'
1952--- src/mailman/database/schema/mm_20130406000000.py 2014-01-01 14:59:42 +0000
1953+++ src/mailman/database/schema/mm_20130406000000.py 1970-01-01 00:00:00 +0000
1954@@ -1,65 +0,0 @@
1955-# Copyright (C) 2013-2014 by the Free Software Foundation, Inc.
1956-#
1957-# This file is part of GNU Mailman.
1958-#
1959-# GNU Mailman is free software: you can redistribute it and/or modify it under
1960-# the terms of the GNU General Public License as published by the Free
1961-# Software Foundation, either version 3 of the License, or (at your option)
1962-# any later version.
1963-#
1964-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
1965-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
1966-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
1967-# more details.
1968-#
1969-# You should have received a copy of the GNU General Public License along with
1970-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
1971-
1972-"""3.0b3 -> 3.0b4 schema migrations.
1973-
1974-Renamed:
1975- * bounceevent.list_name -> bounceevent.list_id
1976-"""
1977-
1978-
1979-from __future__ import absolute_import, print_function, unicode_literals
1980-
1981-__metaclass__ = type
1982-__all__ = [
1983- 'upgrade'
1984- ]
1985-
1986-
1987-from mailman.database.schema.helpers import make_listid, pivot
1988-
1989-
1990-VERSION = '20130406000000'
1991-
1992-
1993-
1994
1995-def upgrade(database, store, version, module_path):
1996- if database.TAG == 'sqlite':
1997- upgrade_sqlite(database, store, version, module_path)
1998- else:
1999- upgrade_postgres(database, store, version, module_path)
2000-
2001-
2002-
2003
2004-def upgrade_sqlite(database, store, version, module_path):
2005- database.load_schema(
2006- store, version, 'sqlite_{}_01.sql'.format(version), module_path)
2007- results = store.execute("""
2008- SELECT id, list_name
2009- FROM bounceevent;
2010- """)
2011- for id, list_name in results:
2012- store.execute("""
2013- UPDATE bounceevent_backup SET list_id = '{}'
2014- WHERE id = {};
2015- """.format(make_listid(list_name), id))
2016- pivot(store, 'bounceevent')
2017-
2018-
2019-
2020
2021-def upgrade_postgres(database, store, version, module_path):
2022- pass
2023
2024=== removed file 'src/mailman/database/schema/postgres.sql'
2025--- src/mailman/database/schema/postgres.sql 2012-07-23 14:40:53 +0000
2026+++ src/mailman/database/schema/postgres.sql 1970-01-01 00:00:00 +0000
2027@@ -1,349 +0,0 @@
2028-CREATE TABLE mailinglist (
2029- id SERIAL NOT NULL,
2030- -- List identity
2031- list_name TEXT,
2032- mail_host TEXT,
2033- include_list_post_header BOOLEAN,
2034- include_rfc2369_headers BOOLEAN,
2035- -- Attributes not directly modifiable via the web u/i
2036- created_at TIMESTAMP,
2037- admin_member_chunksize INTEGER,
2038- next_request_id INTEGER,
2039- next_digest_number INTEGER,
2040- digest_last_sent_at TIMESTAMP,
2041- volume INTEGER,
2042- last_post_at TIMESTAMP,
2043- accept_these_nonmembers BYTEA,
2044- acceptable_aliases_id INTEGER,
2045- admin_immed_notify BOOLEAN,
2046- admin_notify_mchanges BOOLEAN,
2047- administrivia BOOLEAN,
2048- advertised BOOLEAN,
2049- anonymous_list BOOLEAN,
2050- archive BOOLEAN,
2051- archive_private BOOLEAN,
2052- archive_volume_frequency INTEGER,
2053- -- Automatic responses.
2054- autorespond_owner INTEGER,
2055- autoresponse_owner_text TEXT,
2056- autorespond_postings INTEGER,
2057- autoresponse_postings_text TEXT,
2058- autorespond_requests INTEGER,
2059- autoresponse_request_text TEXT,
2060- autoresponse_grace_period TEXT,
2061- -- Bounces.
2062- forward_unrecognized_bounces_to INTEGER,
2063- process_bounces BOOLEAN,
2064- bounce_info_stale_after TEXT,
2065- bounce_matching_headers TEXT,
2066- bounce_notify_owner_on_disable BOOLEAN,
2067- bounce_notify_owner_on_removal BOOLEAN,
2068- bounce_score_threshold INTEGER,
2069- bounce_you_are_disabled_warnings INTEGER,
2070- bounce_you_are_disabled_warnings_interval TEXT,
2071- -- Content filtering.
2072- filter_action INTEGER,
2073- filter_content BOOLEAN,
2074- collapse_alternatives BOOLEAN,
2075- convert_html_to_plaintext BOOLEAN,
2076- default_member_action INTEGER,
2077- default_nonmember_action INTEGER,
2078- description TEXT,
2079- digest_footer_uri TEXT,
2080- digest_header_uri TEXT,
2081- digest_is_default BOOLEAN,
2082- digest_send_periodic BOOLEAN,
2083- digest_size_threshold REAL,
2084- digest_volume_frequency INTEGER,
2085- digestable BOOLEAN,
2086- discard_these_nonmembers BYTEA,
2087- emergency BOOLEAN,
2088- encode_ascii_prefixes BOOLEAN,
2089- first_strip_reply_to BOOLEAN,
2090- footer_uri TEXT,
2091- forward_auto_discards BOOLEAN,
2092- gateway_to_mail BOOLEAN,
2093- gateway_to_news BOOLEAN,
2094- generic_nonmember_action INTEGER,
2095- goodbye_message_uri TEXT,
2096- header_matches BYTEA,
2097- header_uri TEXT,
2098- hold_these_nonmembers BYTEA,
2099- info TEXT,
2100- linked_newsgroup TEXT,
2101- max_days_to_hold INTEGER,
2102- max_message_size INTEGER,
2103- max_num_recipients INTEGER,
2104- member_moderation_notice TEXT,
2105- mime_is_default_digest BOOLEAN,
2106- moderator_password TEXT,
2107- new_member_options INTEGER,
2108- news_moderation INTEGER,
2109- news_prefix_subject_too BOOLEAN,
2110- nntp_host TEXT,
2111- nondigestable BOOLEAN,
2112- nonmember_rejection_notice TEXT,
2113- obscure_addresses BOOLEAN,
2114- owner_chain TEXT,
2115- owner_pipeline TEXT,
2116- personalize INTEGER,
2117- post_id INTEGER,
2118- posting_chain TEXT,
2119- posting_pipeline TEXT,
2120- preferred_language TEXT,
2121- private_roster BOOLEAN,
2122- display_name TEXT,
2123- reject_these_nonmembers BYTEA,
2124- reply_goes_to_list INTEGER,
2125- reply_to_address TEXT,
2126- require_explicit_destination BOOLEAN,
2127- respond_to_post_requests BOOLEAN,
2128- scrub_nondigest BOOLEAN,
2129- send_goodbye_message BOOLEAN,
2130- send_reminders BOOLEAN,
2131- send_welcome_message BOOLEAN,
2132- subject_prefix TEXT,
2133- subscribe_auto_approval BYTEA,
2134- subscribe_policy INTEGER,
2135- topics BYTEA,
2136- topics_bodylines_limit INTEGER,
2137- topics_enabled BOOLEAN,
2138- unsubscribe_policy INTEGER,
2139- welcome_message_uri TEXT,
2140- -- This was accidentally added by the PostgreSQL porter.
2141- -- moderation_callback TEXT,
2142- PRIMARY KEY (id)
2143- );
2144-
2145-CREATE TABLE _request (
2146- id SERIAL NOT NULL,
2147- "key" TEXT,
2148- request_type INTEGER,
2149- data_hash BYTEA,
2150- mailing_list_id INTEGER,
2151- PRIMARY KEY (id)
2152- -- XXX: config.db_reset() triggers IntegrityError
2153- -- ,
2154- -- CONSTRAINT _request_mailing_list_id_fk
2155- -- FOREIGN KEY (mailing_list_id) REFERENCES mailinglist (id)
2156- );
2157-
2158-CREATE TABLE acceptablealias (
2159- id SERIAL NOT NULL,
2160- "alias" TEXT NOT NULL,
2161- mailing_list_id INTEGER NOT NULL,
2162- PRIMARY KEY (id)
2163- -- XXX: config.db_reset() triggers IntegrityError
2164- -- ,
2165- -- CONSTRAINT acceptablealias_mailing_list_id_fk
2166- -- FOREIGN KEY (mailing_list_id) REFERENCES mailinglist (id)
2167- );
2168-CREATE INDEX ix_acceptablealias_mailing_list_id
2169- ON acceptablealias (mailing_list_id);
2170-CREATE INDEX ix_acceptablealias_alias ON acceptablealias ("alias");
2171-
2172-CREATE TABLE preferences (
2173- id SERIAL NOT NULL,
2174- acknowledge_posts BOOLEAN,
2175- hide_address BOOLEAN,
2176- preferred_language TEXT,
2177- receive_list_copy BOOLEAN,
2178- receive_own_postings BOOLEAN,
2179- delivery_mode INTEGER,
2180- delivery_status INTEGER,
2181- PRIMARY KEY (id)
2182- );
2183-
2184-CREATE TABLE address (
2185- id SERIAL NOT NULL,
2186- email TEXT,
2187- _original TEXT,
2188- display_name TEXT,
2189- verified_on TIMESTAMP,
2190- registered_on TIMESTAMP,
2191- user_id INTEGER,
2192- preferences_id INTEGER,
2193- PRIMARY KEY (id)
2194- -- XXX: config.db_reset() triggers IntegrityError
2195- -- ,
2196- -- CONSTRAINT address_preferences_id_fk
2197- -- FOREIGN KEY (preferences_id) REFERENCES preferences (id)
2198- );
2199-
2200-CREATE TABLE "user" (
2201- id SERIAL NOT NULL,
2202- display_name TEXT,
2203- password BYTEA,
2204- _user_id UUID,
2205- _created_on TIMESTAMP,
2206- _preferred_address_id INTEGER,
2207- preferences_id INTEGER,
2208- PRIMARY KEY (id)
2209- -- XXX: config.db_reset() triggers IntegrityError
2210- -- ,
2211- -- CONSTRAINT user_preferences_id_fk
2212- -- FOREIGN KEY (preferences_id) REFERENCES preferences (id),
2213- -- XXX: config.db_reset() triggers IntegrityError
2214- -- CONSTRAINT _preferred_address_id_fk
2215- -- FOREIGN KEY (_preferred_address_id) REFERENCES address (id)
2216- );
2217-CREATE INDEX ix_user_user_id ON "user" (_user_id);
2218-
2219--- since user and address have circular foreign key refs, the
2220--- constraint on the address table has to be added after
2221--- the user table is created
2222---
2223--- XXX: users.rst triggers an IntegrityError
2224--- ALTER TABLE address ADD
2225--- CONSTRAINT address_user_id_fk
2226--- FOREIGN KEY (user_id) REFERENCES "user" (id);
2227-
2228-CREATE TABLE autoresponserecord (
2229- id SERIAL NOT NULL,
2230- address_id INTEGER,
2231- mailing_list_id INTEGER,
2232- response_type INTEGER,
2233- date_sent TIMESTAMP,
2234- PRIMARY KEY (id)
2235- -- XXX: config.db_reset() triggers IntegrityError
2236- -- ,
2237- -- CONSTRAINT autoresponserecord_address_id_fk
2238- -- FOREIGN KEY (address_id) REFERENCES address (id)
2239- -- XXX: config.db_reset() triggers IntegrityError
2240- -- ,
2241- -- CONSTRAINT autoresponserecord_mailing_list_id
2242- -- FOREIGN KEY (mailing_list_id) REFERENCES mailinglist (id)
2243- );
2244-CREATE INDEX ix_autoresponserecord_address_id
2245- ON autoresponserecord (address_id);
2246-CREATE INDEX ix_autoresponserecord_mailing_list_id
2247- ON autoresponserecord (mailing_list_id);
2248-
2249-CREATE TABLE bounceevent (
2250- id SERIAL NOT NULL,
2251- list_name TEXT,
2252- email TEXT,
2253- "timestamp" TIMESTAMP,
2254- message_id TEXT,
2255- context INTEGER,
2256- processed BOOLEAN,
2257- PRIMARY KEY (id)
2258- );
2259-
2260-CREATE TABLE contentfilter (
2261- id SERIAL NOT NULL,
2262- mailing_list_id INTEGER,
2263- filter_pattern TEXT,
2264- filter_type INTEGER,
2265- PRIMARY KEY (id),
2266- CONSTRAINT contentfilter_mailing_list_id
2267- FOREIGN KEY (mailing_list_id) REFERENCES mailinglist (id)
2268- );
2269-CREATE INDEX ix_contentfilter_mailing_list_id
2270- ON contentfilter (mailing_list_id);
2271-
2272-CREATE TABLE domain (
2273- id SERIAL NOT NULL,
2274- mail_host TEXT,
2275- base_url TEXT,
2276- description TEXT,
2277- contact_address TEXT,
2278- PRIMARY KEY (id)
2279- );
2280-
2281-CREATE TABLE language (
2282- id SERIAL NOT NULL,
2283- code TEXT,
2284- PRIMARY KEY (id)
2285- );
2286-
2287-CREATE TABLE member (
2288- id SERIAL NOT NULL,
2289- _member_id UUID,
2290- role INTEGER,
2291- mailing_list TEXT,
2292- moderation_action INTEGER,
2293- address_id INTEGER,
2294- preferences_id INTEGER,
2295- user_id INTEGER,
2296- PRIMARY KEY (id)
2297- -- XXX: config.db_reset() triggers IntegrityError
2298- -- ,
2299- -- CONSTRAINT member_address_id_fk
2300- -- FOREIGN KEY (address_id) REFERENCES address (id),
2301- -- XXX: config.db_reset() triggers IntegrityError
2302- -- CONSTRAINT member_preferences_id_fk
2303- -- FOREIGN KEY (preferences_id) REFERENCES preferences (id),
2304- -- CONSTRAINT member_user_id_fk
2305- -- FOREIGN KEY (user_id) REFERENCES "user" (id)
2306- );
2307-CREATE INDEX ix_member__member_id ON member (_member_id);
2308-CREATE INDEX ix_member_address_id ON member (address_id);
2309-CREATE INDEX ix_member_preferences_id ON member (preferences_id);
2310-
2311-CREATE TABLE message (
2312- id SERIAL NOT NULL,
2313- message_id_hash BYTEA,
2314- path BYTEA,
2315- message_id TEXT,
2316- PRIMARY KEY (id)
2317- );
2318-
2319-CREATE TABLE onelastdigest (
2320- id SERIAL NOT NULL,
2321- mailing_list_id INTEGER,
2322- address_id INTEGER,
2323- delivery_mode INTEGER,
2324- PRIMARY KEY (id),
2325- CONSTRAINT onelastdigest_mailing_list_id_fk
2326- FOREIGN KEY (mailing_list_id) REFERENCES mailinglist(id),
2327- CONSTRAINT onelastdigest_address_id_fk
2328- FOREIGN KEY (address_id) REFERENCES address(id)
2329- );
2330-
2331-CREATE TABLE pended (
2332- id SERIAL NOT NULL,
2333- token BYTEA,
2334- expiration_date TIMESTAMP,
2335- PRIMARY KEY (id)
2336- );
2337-
2338-CREATE TABLE pendedkeyvalue (
2339- id SERIAL NOT NULL,
2340- "key" TEXT,
2341- value TEXT,
2342- pended_id INTEGER,
2343- PRIMARY KEY (id)
2344- -- ,
2345- -- XXX: config.db_reset() triggers IntegrityError
2346- -- CONSTRAINT pendedkeyvalue_pended_id_fk
2347- -- FOREIGN KEY (pended_id) REFERENCES pended (id)
2348- );
2349-
2350-CREATE TABLE version (
2351- id SERIAL NOT NULL,
2352- component TEXT,
2353- version TEXT,
2354- PRIMARY KEY (id)
2355- );
2356-
2357-CREATE INDEX ix__request_mailing_list_id ON _request (mailing_list_id);
2358-CREATE INDEX ix_address_preferences_id ON address (preferences_id);
2359-CREATE INDEX ix_address_user_id ON address (user_id);
2360-CREATE INDEX ix_pendedkeyvalue_pended_id ON pendedkeyvalue (pended_id);
2361-CREATE INDEX ix_user_preferences_id ON "user" (preferences_id);
2362-
2363-CREATE TABLE ban (
2364- id SERIAL NOT NULL,
2365- email TEXT,
2366- mailing_list TEXT,
2367- PRIMARY KEY (id)
2368- );
2369-
2370-CREATE TABLE uid (
2371- -- Keep track of all assigned unique ids to prevent re-use.
2372- id SERIAL NOT NULL,
2373- uid UUID,
2374- PRIMARY KEY (id)
2375- );
2376-CREATE INDEX ix_uid_uid ON uid (uid);
2377
2378=== removed file 'src/mailman/database/schema/sqlite.sql'
2379--- src/mailman/database/schema/sqlite.sql 2012-04-08 16:15:29 +0000
2380+++ src/mailman/database/schema/sqlite.sql 1970-01-01 00:00:00 +0000
2381@@ -1,327 +0,0 @@
2382--- THIS FILE HAS BEEN FROZEN AS OF 3.0b1
2383--- SEE THE SCHEMA MIGRATIONS FOR DIFFERENCES.
2384-
2385-PRAGMA foreign_keys = ON;
2386-
2387-CREATE TABLE _request (
2388- id INTEGER NOT NULL,
2389- "key" TEXT,
2390- request_type INTEGER,
2391- data_hash TEXT,
2392- mailing_list_id INTEGER,
2393- PRIMARY KEY (id),
2394- CONSTRAINT _request_mailing_list_id_fk
2395- FOREIGN KEY (mailing_list_id) REFERENCES mailinglist (id)
2396- );
2397-
2398-CREATE TABLE acceptablealias (
2399- id INTEGER NOT NULL,
2400- "alias" TEXT NOT NULL,
2401- mailing_list_id INTEGER NOT NULL,
2402- PRIMARY KEY (id),
2403- CONSTRAINT acceptablealias_mailing_list_id_fk
2404- FOREIGN KEY (mailing_list_id) REFERENCES mailinglist (id)
2405- );
2406-CREATE INDEX ix_acceptablealias_mailing_list_id
2407- ON acceptablealias (mailing_list_id);
2408-CREATE INDEX ix_acceptablealias_alias ON acceptablealias ("alias");
2409-
2410-CREATE TABLE address (
2411- id INTEGER NOT NULL,
2412- email TEXT,
2413- _original TEXT,
2414- display_name TEXT,
2415- verified_on TIMESTAMP,
2416- registered_on TIMESTAMP,
2417- user_id INTEGER,
2418- preferences_id INTEGER,
2419- PRIMARY KEY (id),
2420- CONSTRAINT address_user_id_fk
2421- FOREIGN KEY (user_id) REFERENCES user (id),
2422- CONSTRAINT address_preferences_id_fk
2423- FOREIGN KEY (preferences_id) REFERENCES preferences (id)
2424- );
2425-
2426-CREATE TABLE autoresponserecord (
2427- id INTEGER NOT NULL,
2428- address_id INTEGER,
2429- mailing_list_id INTEGER,
2430- response_type INTEGER,
2431- date_sent TIMESTAMP,
2432- PRIMARY KEY (id),
2433- CONSTRAINT autoresponserecord_address_id_fk
2434- FOREIGN KEY (address_id) REFERENCES address (id),
2435- CONSTRAINT autoresponserecord_mailing_list_id
2436- FOREIGN KEY (mailing_list_id) REFERENCES mailinglist (id)
2437- );
2438-CREATE INDEX ix_autoresponserecord_address_id
2439- ON autoresponserecord (address_id);
2440-CREATE INDEX ix_autoresponserecord_mailing_list_id
2441- ON autoresponserecord (mailing_list_id);
2442-
2443-CREATE TABLE bounceevent (
2444- id INTEGER NOT NULL,
2445- list_name TEXT,
2446- email TEXT,
2447- 'timestamp' TIMESTAMP,
2448- message_id TEXT,
2449- context INTEGER,
2450- processed BOOLEAN,
2451- PRIMARY KEY (id)
2452- );
2453-
2454-CREATE TABLE contentfilter (
2455- id INTEGER NOT NULL,
2456- mailing_list_id INTEGER,
2457- filter_pattern TEXT,
2458- filter_type INTEGER,
2459- PRIMARY KEY (id),
2460- CONSTRAINT contentfilter_mailing_list_id
2461- FOREIGN KEY (mailing_list_id) REFERENCES mailinglist (id)
2462- );
2463-CREATE INDEX ix_contentfilter_mailing_list_id
2464- ON contentfilter (mailing_list_id);
2465-
2466-CREATE TABLE domain (
2467- id INTEGER NOT NULL,
2468- mail_host TEXT,
2469- base_url TEXT,
2470- description TEXT,
2471- contact_address TEXT,
2472- PRIMARY KEY (id)
2473- );
2474-
2475-CREATE TABLE language (
2476- id INTEGER NOT NULL,
2477- code TEXT,
2478- PRIMARY KEY (id)
2479- );
2480-
2481-CREATE TABLE mailinglist (
2482- id INTEGER NOT NULL,
2483- -- List identity
2484- list_name TEXT,
2485- mail_host TEXT,
2486- include_list_post_header BOOLEAN,
2487- include_rfc2369_headers BOOLEAN,
2488- -- Attributes not directly modifiable via the web u/i
2489- created_at TIMESTAMP,
2490- admin_member_chunksize INTEGER,
2491- next_request_id INTEGER,
2492- next_digest_number INTEGER,
2493- digest_last_sent_at TIMESTAMP,
2494- volume INTEGER,
2495- last_post_at TIMESTAMP,
2496- accept_these_nonmembers BLOB,
2497- acceptable_aliases_id INTEGER,
2498- admin_immed_notify BOOLEAN,
2499- admin_notify_mchanges BOOLEAN,
2500- administrivia BOOLEAN,
2501- advertised BOOLEAN,
2502- anonymous_list BOOLEAN,
2503- archive BOOLEAN,
2504- archive_private BOOLEAN,
2505- archive_volume_frequency INTEGER,
2506- -- Automatic responses.
2507- autorespond_owner INTEGER,
2508- autoresponse_owner_text TEXT,
2509- autorespond_postings INTEGER,
2510- autoresponse_postings_text TEXT,
2511- autorespond_requests INTEGER,
2512- autoresponse_request_text TEXT,
2513- autoresponse_grace_period TEXT,
2514- -- Bounces.
2515- forward_unrecognized_bounces_to INTEGER,
2516- process_bounces BOOLEAN,
2517- bounce_info_stale_after TEXT,
2518- bounce_matching_headers TEXT,
2519- bounce_notify_owner_on_disable BOOLEAN,
2520- bounce_notify_owner_on_removal BOOLEAN,
2521- bounce_score_threshold INTEGER,
2522- bounce_you_are_disabled_warnings INTEGER,
2523- bounce_you_are_disabled_warnings_interval TEXT,
2524- -- Content filtering.
2525- filter_action INTEGER,
2526- filter_content BOOLEAN,
2527- collapse_alternatives BOOLEAN,
2528- convert_html_to_plaintext BOOLEAN,
2529- default_member_action INTEGER,
2530- default_nonmember_action INTEGER,
2531- description TEXT,
2532- digest_footer_uri TEXT,
2533- digest_header_uri TEXT,
2534- digest_is_default BOOLEAN,
2535- digest_send_periodic BOOLEAN,
2536- digest_size_threshold FLOAT,
2537- digest_volume_frequency INTEGER,
2538- digestable BOOLEAN,
2539- discard_these_nonmembers BLOB,
2540- emergency BOOLEAN,
2541- encode_ascii_prefixes BOOLEAN,
2542- first_strip_reply_to BOOLEAN,
2543- footer_uri TEXT,
2544- forward_auto_discards BOOLEAN,
2545- gateway_to_mail BOOLEAN,
2546- gateway_to_news BOOLEAN,
2547- generic_nonmember_action INTEGER,
2548- goodbye_message_uri TEXT,
2549- header_matches BLOB,
2550- header_uri TEXT,
2551- hold_these_nonmembers BLOB,
2552- info TEXT,
2553- linked_newsgroup TEXT,
2554- max_days_to_hold INTEGER,
2555- max_message_size INTEGER,
2556- max_num_recipients INTEGER,
2557- member_moderation_notice TEXT,
2558- mime_is_default_digest BOOLEAN,
2559- moderator_password TEXT,
2560- new_member_options INTEGER,
2561- news_moderation INTEGER,
2562- news_prefix_subject_too BOOLEAN,
2563- nntp_host TEXT,
2564- nondigestable BOOLEAN,
2565- nonmember_rejection_notice TEXT,
2566- obscure_addresses BOOLEAN,
2567- owner_chain TEXT,
2568- owner_pipeline TEXT,
2569- personalize INTEGER,
2570- post_id INTEGER,
2571- posting_chain TEXT,
2572- posting_pipeline TEXT,
2573- preferred_language TEXT,
2574- private_roster BOOLEAN,
2575- display_name TEXT,
2576- reject_these_nonmembers BLOB,
2577- reply_goes_to_list INTEGER,
2578- reply_to_address TEXT,
2579- require_explicit_destination BOOLEAN,
2580- respond_to_post_requests BOOLEAN,
2581- scrub_nondigest BOOLEAN,
2582- send_goodbye_message BOOLEAN,
2583- send_reminders BOOLEAN,
2584- send_welcome_message BOOLEAN,
2585- subject_prefix TEXT,
2586- subscribe_auto_approval BLOB,
2587- subscribe_policy INTEGER,
2588- topics BLOB,
2589- topics_bodylines_limit INTEGER,
2590- topics_enabled BOOLEAN,
2591- unsubscribe_policy INTEGER,
2592- welcome_message_uri TEXT,
2593- PRIMARY KEY (id)
2594- );
2595-
2596-CREATE TABLE member (
2597- id INTEGER NOT NULL,
2598- _member_id TEXT,
2599- role INTEGER,
2600- mailing_list TEXT,
2601- moderation_action INTEGER,
2602- address_id INTEGER,
2603- preferences_id INTEGER,
2604- user_id INTEGER,
2605- PRIMARY KEY (id),
2606- CONSTRAINT member_address_id_fk
2607- FOREIGN KEY (address_id) REFERENCES address (id),
2608- CONSTRAINT member_preferences_id_fk
2609- FOREIGN KEY (preferences_id) REFERENCES preferences (id)
2610- CONSTRAINT member_user_id_fk
2611- FOREIGN KEY (user_id) REFERENCES user (id)
2612- );
2613-CREATE INDEX ix_member__member_id ON member (_member_id);
2614-CREATE INDEX ix_member_address_id ON member (address_id);
2615-CREATE INDEX ix_member_preferences_id ON member (preferences_id);
2616-
2617-CREATE TABLE message (
2618- id INTEGER NOT NULL,
2619- message_id_hash TEXT,
2620- path TEXT,
2621- message_id TEXT,
2622- PRIMARY KEY (id)
2623- );
2624-
2625-CREATE TABLE onelastdigest (
2626- id INTEGER NOT NULL,
2627- mailing_list_id INTEGER,
2628- address_id INTEGER,
2629- delivery_mode INTEGER,
2630- PRIMARY KEY (id),
2631- CONSTRAINT onelastdigest_mailing_list_id_fk
2632- FOREIGN KEY (mailing_list_id) REFERENCES mailinglist(id),
2633- CONSTRAINT onelastdigest_address_id_fk
2634- FOREIGN KEY (address_id) REFERENCES address(id)
2635- );
2636-
2637-CREATE TABLE pended (
2638- id INTEGER NOT NULL,
2639- token TEXT,
2640- expiration_date TIMESTAMP,
2641- PRIMARY KEY (id)
2642- );
2643-
2644-CREATE TABLE pendedkeyvalue (
2645- id INTEGER NOT NULL,
2646- "key" TEXT,
2647- value TEXT,
2648- pended_id INTEGER,
2649- PRIMARY KEY (id),
2650- CONSTRAINT pendedkeyvalue_pended_id_fk
2651- FOREIGN KEY (pended_id) REFERENCES pended (id)
2652- );
2653-
2654-CREATE TABLE preferences (
2655- id INTEGER NOT NULL,
2656- acknowledge_posts BOOLEAN,
2657- hide_address BOOLEAN,
2658- preferred_language TEXT,
2659- receive_list_copy BOOLEAN,
2660- receive_own_postings BOOLEAN,
2661- delivery_mode INTEGER,
2662- delivery_status INTEGER,
2663- PRIMARY KEY (id)
2664- );
2665-
2666-CREATE TABLE user (
2667- id INTEGER NOT NULL,
2668- display_name TEXT,
2669- password BINARY,
2670- _user_id TEXT,
2671- _created_on TIMESTAMP,
2672- _preferred_address_id INTEGER,
2673- preferences_id INTEGER,
2674- PRIMARY KEY (id),
2675- CONSTRAINT user_preferences_id_fk
2676- FOREIGN KEY (preferences_id) REFERENCES preferences (id),
2677- CONSTRAINT _preferred_address_id_fk
2678- FOREIGN KEY (_preferred_address_id) REFERENCES address (id)
2679- );
2680-CREATE INDEX ix_user_user_id ON user (_user_id);
2681-
2682-CREATE TABLE version (
2683- id INTEGER NOT NULL,
2684- component TEXT,
2685- version TEXT,
2686- PRIMARY KEY (id)
2687- );
2688-
2689-CREATE INDEX ix__request_mailing_list_id ON _request (mailing_list_id);
2690-CREATE INDEX ix_address_preferences_id ON address (preferences_id);
2691-CREATE INDEX ix_address_user_id ON address (user_id);
2692-CREATE INDEX ix_pendedkeyvalue_pended_id ON pendedkeyvalue (pended_id);
2693-CREATE INDEX ix_user_preferences_id ON user (preferences_id);
2694-
2695-CREATE TABLE ban (
2696- id INTEGER NOT NULL,
2697- email TEXT,
2698- mailing_list TEXT,
2699- PRIMARY KEY (id)
2700- );
2701-
2702-CREATE TABLE uid (
2703- -- Keep track of all assigned unique ids to prevent re-use.
2704- id INTEGER NOT NULL,
2705- uid TEXT,
2706- PRIMARY KEY (id)
2707- );
2708-CREATE INDEX ix_uid_uid ON uid (uid);
2709
2710=== removed file 'src/mailman/database/schema/sqlite_20120407000000_01.sql'
2711--- src/mailman/database/schema/sqlite_20120407000000_01.sql 2013-09-01 15:15:08 +0000
2712+++ src/mailman/database/schema/sqlite_20120407000000_01.sql 1970-01-01 00:00:00 +0000
2713@@ -1,280 +0,0 @@
2714--- This file contains the sqlite3 schema migration from
2715--- 3.0b1 TO 3.0b2
2716---
2717--- 3.0b2 has been released thus you MAY NOT edit this file.
2718-
2719--- For SQLite3 migration strategy, see
2720--- http://sqlite.org/faq.html#q11
2721-
2722--- REMOVALS from the mailinglist table:
2723--- REM archive
2724--- REM archive_private
2725--- REM archive_volume_frequency
2726--- REM include_list_post_header
2727--- REM news_moderation
2728--- REM news_prefix_subject_too
2729--- REM nntp_host
2730---
2731--- ADDS to the mailing list table:
2732--- ADD allow_list_posts
2733--- ADD archive_policy
2734--- ADD list_id
2735--- ADD newsgroup_moderation
2736--- ADD nntp_prefix_subject_too
2737-
2738--- LP: #971013
2739--- LP: #967238
2740-
2741--- REMOVALS from the member table:
2742--- REM mailing_list
2743-
2744--- ADDS to the member table:
2745--- ADD list_id
2746-
2747--- LP: #1024509
2748-
2749-
2750-CREATE TABLE mailinglist_backup (
2751- id INTEGER NOT NULL,
2752- -- List identity
2753- list_name TEXT,
2754- mail_host TEXT,
2755- allow_list_posts BOOLEAN,
2756- include_rfc2369_headers BOOLEAN,
2757- -- Attributes not directly modifiable via the web u/i
2758- created_at TIMESTAMP,
2759- admin_member_chunksize INTEGER,
2760- next_request_id INTEGER,
2761- next_digest_number INTEGER,
2762- digest_last_sent_at TIMESTAMP,
2763- volume INTEGER,
2764- last_post_at TIMESTAMP,
2765- accept_these_nonmembers BLOB,
2766- acceptable_aliases_id INTEGER,
2767- admin_immed_notify BOOLEAN,
2768- admin_notify_mchanges BOOLEAN,
2769- administrivia BOOLEAN,
2770- advertised BOOLEAN,
2771- anonymous_list BOOLEAN,
2772- -- Automatic responses.
2773- autorespond_owner INTEGER,
2774- autoresponse_owner_text TEXT,
2775- autorespond_postings INTEGER,
2776- autoresponse_postings_text TEXT,
2777- autorespond_requests INTEGER,
2778- autoresponse_request_text TEXT,
2779- autoresponse_grace_period TEXT,
2780- -- Bounces.
2781- forward_unrecognized_bounces_to INTEGER,
2782- process_bounces BOOLEAN,
2783- bounce_info_stale_after TEXT,
2784- bounce_matching_headers TEXT,
2785- bounce_notify_owner_on_disable BOOLEAN,
2786- bounce_notify_owner_on_removal BOOLEAN,
2787- bounce_score_threshold INTEGER,
2788- bounce_you_are_disabled_warnings INTEGER,
2789- bounce_you_are_disabled_warnings_interval TEXT,
2790- -- Content filtering.
2791- filter_action INTEGER,
2792- filter_content BOOLEAN,
2793- collapse_alternatives BOOLEAN,
2794- convert_html_to_plaintext BOOLEAN,
2795- default_member_action INTEGER,
2796- default_nonmember_action INTEGER,
2797- description TEXT,
2798- digest_footer_uri TEXT,
2799- digest_header_uri TEXT,
2800- digest_is_default BOOLEAN,
2801- digest_send_periodic BOOLEAN,
2802- digest_size_threshold FLOAT,
2803- digest_volume_frequency INTEGER,
2804- digestable BOOLEAN,
2805- discard_these_nonmembers BLOB,
2806- emergency BOOLEAN,
2807- encode_ascii_prefixes BOOLEAN,
2808- first_strip_reply_to BOOLEAN,
2809- footer_uri TEXT,
2810- forward_auto_discards BOOLEAN,
2811- gateway_to_mail BOOLEAN,
2812- gateway_to_news BOOLEAN,
2813- goodbye_message_uri TEXT,
2814- header_matches BLOB,
2815- header_uri TEXT,
2816- hold_these_nonmembers BLOB,
2817- info TEXT,
2818- linked_newsgroup TEXT,
2819- max_days_to_hold INTEGER,
2820- max_message_size INTEGER,
2821- max_num_recipients INTEGER,
2822- member_moderation_notice TEXT,
2823- mime_is_default_digest BOOLEAN,
2824- moderator_password TEXT,
2825- new_member_options INTEGER,
2826- nondigestable BOOLEAN,
2827- nonmember_rejection_notice TEXT,
2828- obscure_addresses BOOLEAN,
2829- owner_chain TEXT,
2830- owner_pipeline TEXT,
2831- personalize INTEGER,
2832- post_id INTEGER,
2833- posting_chain TEXT,
2834- posting_pipeline TEXT,
2835- preferred_language TEXT,
2836- private_roster BOOLEAN,
2837- display_name TEXT,
2838- reject_these_nonmembers BLOB,
2839- reply_goes_to_list INTEGER,
2840- reply_to_address TEXT,
2841- require_explicit_destination BOOLEAN,
2842- respond_to_post_requests BOOLEAN,
2843- scrub_nondigest BOOLEAN,
2844- send_goodbye_message BOOLEAN,
2845- send_reminders BOOLEAN,
2846- send_welcome_message BOOLEAN,
2847- subject_prefix TEXT,
2848- subscribe_auto_approval BLOB,
2849- subscribe_policy INTEGER,
2850- topics BLOB,
2851- topics_bodylines_limit INTEGER,
2852- topics_enabled BOOLEAN,
2853- unsubscribe_policy INTEGER,
2854- welcome_message_uri TEXT,
2855- PRIMARY KEY (id)
2856- );
2857-
2858-INSERT INTO mailinglist_backup SELECT
2859- id,
2860- -- List identity
2861- list_name,
2862- mail_host,
2863- include_list_post_header,
2864- include_rfc2369_headers,
2865- -- Attributes not directly modifiable via the web u/i
2866- created_at,
2867- admin_member_chunksize,
2868- next_request_id,
2869- next_digest_number,
2870- digest_last_sent_at,
2871- volume,
2872- last_post_at,
2873- accept_these_nonmembers,
2874- acceptable_aliases_id,
2875- admin_immed_notify,
2876- admin_notify_mchanges,
2877- administrivia,
2878- advertised,
2879- anonymous_list,
2880- -- Automatic responses.
2881- autorespond_owner,
2882- autoresponse_owner_text,
2883- autorespond_postings,
2884- autoresponse_postings_text,
2885- autorespond_requests,
2886- autoresponse_request_text,
2887- autoresponse_grace_period,
2888- -- Bounces.
2889- forward_unrecognized_bounces_to,
2890- process_bounces,
2891- bounce_info_stale_after,
2892- bounce_matching_headers,
2893- bounce_notify_owner_on_disable,
2894- bounce_notify_owner_on_removal,
2895- bounce_score_threshold,
2896- bounce_you_are_disabled_warnings,
2897- bounce_you_are_disabled_warnings_interval,
2898- -- Content filtering.
2899- filter_action,
2900- filter_content,
2901- collapse_alternatives,
2902- convert_html_to_plaintext,
2903- default_member_action,
2904- default_nonmember_action,
2905- description,
2906- digest_footer_uri,
2907- digest_header_uri,
2908- digest_is_default,
2909- digest_send_periodic,
2910- digest_size_threshold,
2911- digest_volume_frequency,
2912- digestable,
2913- discard_these_nonmembers,
2914- emergency,
2915- encode_ascii_prefixes,
2916- first_strip_reply_to,
2917- footer_uri,
2918- forward_auto_discards,
2919- gateway_to_mail,
2920- gateway_to_news,
2921- goodbye_message_uri,
2922- header_matches,
2923- header_uri,
2924- hold_these_nonmembers,
2925- info,
2926- linked_newsgroup,
2927- max_days_to_hold,
2928- max_message_size,
2929- max_num_recipients,
2930- member_moderation_notice,
2931- mime_is_default_digest,
2932- moderator_password,
2933- new_member_options,
2934- nondigestable,
2935- nonmember_rejection_notice,
2936- obscure_addresses,
2937- owner_chain,
2938- owner_pipeline,
2939- personalize,
2940- post_id,
2941- posting_chain,
2942- posting_pipeline,
2943- preferred_language,
2944- private_roster,
2945- display_name,
2946- reject_these_nonmembers,
2947- reply_goes_to_list,
2948- reply_to_address,
2949- require_explicit_destination,
2950- respond_to_post_requests,
2951- scrub_nondigest,
2952- send_goodbye_message,
2953- send_reminders,
2954- send_welcome_message,
2955- subject_prefix,
2956- subscribe_auto_approval,
2957- subscribe_policy,
2958- topics,
2959- topics_bodylines_limit,
2960- topics_enabled,
2961- unsubscribe_policy,
2962- welcome_message_uri
2963- FROM mailinglist;
2964-
2965-CREATE TABLE member_backup(
2966- id INTEGER NOT NULL,
2967- _member_id TEXT,
2968- role INTEGER,
2969- moderation_action INTEGER,
2970- address_id INTEGER,
2971- preferences_id INTEGER,
2972- user_id INTEGER,
2973- PRIMARY KEY (id)
2974- );
2975-
2976-INSERT INTO member_backup SELECT
2977- id,
2978- _member_id,
2979- role,
2980- moderation_action,
2981- address_id,
2982- preferences_id,
2983- user_id
2984- FROM member;
2985-
2986-
2987--- Add the new columns. They'll get inserted at the Python layer.
2988-ALTER TABLE mailinglist_backup ADD COLUMN archive_policy INTEGER;
2989-ALTER TABLE mailinglist_backup ADD COLUMN list_id TEXT;
2990-ALTER TABLE mailinglist_backup ADD COLUMN nntp_prefix_subject_too INTEGER;
2991-ALTER TABLE mailinglist_backup ADD COLUMN newsgroup_moderation INTEGER;
2992-
2993-ALTER TABLE member_backup ADD COLUMN list_id TEXT;
2994
2995=== removed file 'src/mailman/database/schema/sqlite_20121015000000_01.sql'
2996--- src/mailman/database/schema/sqlite_20121015000000_01.sql 2013-09-01 15:15:08 +0000
2997+++ src/mailman/database/schema/sqlite_20121015000000_01.sql 1970-01-01 00:00:00 +0000
2998@@ -1,230 +0,0 @@
2999--- This file contains the sqlite3 schema migration from
3000--- 3.0b2 TO 3.0b3
3001---
3002--- 3.0b3 has been released thus you MAY NOT edit this file.
3003-
3004--- REMOVALS from the ban table:
3005--- REM mailing_list
3006-
3007--- ADDS to the ban table:
3008--- ADD list_id
3009-
3010-CREATE TABLE ban_backup (
3011- id INTEGER NOT NULL,
3012- email TEXT,
3013- PRIMARY KEY (id)
3014- );
3015-
3016-INSERT INTO ban_backup SELECT
3017- id, email
3018- FROM ban;
3019-
3020-ALTER TABLE ban_backup ADD COLUMN list_id TEXT;
3021-
3022--- REMOVALS from the mailinglist table.
3023--- REM new_member_options
3024--- REM send_reminders
3025--- REM subscribe_policy
3026--- REM unsubscribe_policy
3027--- REM subscribe_auto_approval
3028--- REM private_roster
3029--- REM admin_member_chunksize
3030-
3031-CREATE TABLE mailinglist_backup (
3032- id INTEGER NOT NULL,
3033- list_name TEXT,
3034- mail_host TEXT,
3035- allow_list_posts BOOLEAN,
3036- include_rfc2369_headers BOOLEAN,
3037- created_at TIMESTAMP,
3038- next_request_id INTEGER,
3039- next_digest_number INTEGER,
3040- digest_last_sent_at TIMESTAMP,
3041- volume INTEGER,
3042- last_post_at TIMESTAMP,
3043- accept_these_nonmembers BLOB,
3044- acceptable_aliases_id INTEGER,
3045- admin_immed_notify BOOLEAN,
3046- admin_notify_mchanges BOOLEAN,
3047- administrivia BOOLEAN,
3048- advertised BOOLEAN,
3049- anonymous_list BOOLEAN,
3050- autorespond_owner INTEGER,
3051- autoresponse_owner_text TEXT,
3052- autorespond_postings INTEGER,
3053- autoresponse_postings_text TEXT,
3054- autorespond_requests INTEGER,
3055- autoresponse_request_text TEXT,
3056- autoresponse_grace_period TEXT,
3057- forward_unrecognized_bounces_to INTEGER,
3058- process_bounces BOOLEAN,
3059- bounce_info_stale_after TEXT,
3060- bounce_matching_headers TEXT,
3061- bounce_notify_owner_on_disable BOOLEAN,
3062- bounce_notify_owner_on_removal BOOLEAN,
3063- bounce_score_threshold INTEGER,
3064- bounce_you_are_disabled_warnings INTEGER,
3065- bounce_you_are_disabled_warnings_interval TEXT,
3066- filter_action INTEGER,
3067- filter_content BOOLEAN,
3068- collapse_alternatives BOOLEAN,
3069- convert_html_to_plaintext BOOLEAN,
3070- default_member_action INTEGER,
3071- default_nonmember_action INTEGER,
3072- description TEXT,
3073- digest_footer_uri TEXT,
3074- digest_header_uri TEXT,
3075- digest_is_default BOOLEAN,
3076- digest_send_periodic BOOLEAN,
3077- digest_size_threshold FLOAT,
3078- digest_volume_frequency INTEGER,
3079- digestable BOOLEAN,
3080- discard_these_nonmembers BLOB,
3081- emergency BOOLEAN,
3082- encode_ascii_prefixes BOOLEAN,
3083- first_strip_reply_to BOOLEAN,
3084- footer_uri TEXT,
3085- forward_auto_discards BOOLEAN,
3086- gateway_to_mail BOOLEAN,
3087- gateway_to_news BOOLEAN,
3088- goodbye_message_uri TEXT,
3089- header_matches BLOB,
3090- header_uri TEXT,
3091- hold_these_nonmembers BLOB,
3092- info TEXT,
3093- linked_newsgroup TEXT,
3094- max_days_to_hold INTEGER,
3095- max_message_size INTEGER,
3096- max_num_recipients INTEGER,
3097- member_moderation_notice TEXT,
3098- mime_is_default_digest BOOLEAN,
3099- moderator_password TEXT,
3100- nondigestable BOOLEAN,
3101- nonmember_rejection_notice TEXT,
3102- obscure_addresses BOOLEAN,
3103- owner_chain TEXT,
3104- owner_pipeline TEXT,
3105- personalize INTEGER,
3106- post_id INTEGER,
3107- posting_chain TEXT,
3108- posting_pipeline TEXT,
3109- preferred_language TEXT,
3110- display_name TEXT,
3111- reject_these_nonmembers BLOB,
3112- reply_goes_to_list INTEGER,
3113- reply_to_address TEXT,
3114- require_explicit_destination BOOLEAN,
3115- respond_to_post_requests BOOLEAN,
3116- scrub_nondigest BOOLEAN,
3117- send_goodbye_message BOOLEAN,
3118- send_welcome_message BOOLEAN,
3119- subject_prefix TEXT,
3120- topics BLOB,
3121- topics_bodylines_limit INTEGER,
3122- topics_enabled BOOLEAN,
3123- welcome_message_uri TEXT,
3124- archive_policy INTEGER,
3125- list_id TEXT,
3126- nntp_prefix_subject_too INTEGER,
3127- newsgroup_moderation INTEGER,
3128- PRIMARY KEY (id)
3129- );
3130-
3131-INSERT INTO mailinglist_backup SELECT
3132- id,
3133- list_name,
3134- mail_host,
3135- allow_list_posts,
3136- include_rfc2369_headers,
3137- created_at,
3138- next_request_id,
3139- next_digest_number,
3140- digest_last_sent_at,
3141- volume,
3142- last_post_at,
3143- accept_these_nonmembers,
3144- acceptable_aliases_id,
3145- admin_immed_notify,
3146- admin_notify_mchanges,
3147- administrivia,
3148- advertised,
3149- anonymous_list,
3150- autorespond_owner,
3151- autoresponse_owner_text,
3152- autorespond_postings,
3153- autoresponse_postings_text,
3154- autorespond_requests,
3155- autoresponse_request_text,
3156- autoresponse_grace_period,
3157- forward_unrecognized_bounces_to,
3158- process_bounces,
3159- bounce_info_stale_after,
3160- bounce_matching_headers,
3161- bounce_notify_owner_on_disable,
3162- bounce_notify_owner_on_removal,
3163- bounce_score_threshold,
3164- bounce_you_are_disabled_warnings,
3165- bounce_you_are_disabled_warnings_interval,
3166- filter_action,
3167- filter_content,
3168- collapse_alternatives,
3169- convert_html_to_plaintext,
3170- default_member_action,
3171- default_nonmember_action,
3172- description,
3173- digest_footer_uri,
3174- digest_header_uri,
3175- digest_is_default,
3176- digest_send_periodic,
3177- digest_size_threshold,
3178- digest_volume_frequency,
3179- digestable,
3180- discard_these_nonmembers,
3181- emergency,
3182- encode_ascii_prefixes,
3183- first_strip_reply_to,
3184- footer_uri,
3185- forward_auto_discards,
3186- gateway_to_mail,
3187- gateway_to_news,
3188- goodbye_message_uri,
3189- header_matches,
3190- header_uri,
3191- hold_these_nonmembers,
3192- info,
3193- linked_newsgroup,
3194- max_days_to_hold,
3195- max_message_size,
3196- max_num_recipients,
3197- member_moderation_notice,
3198- mime_is_default_digest,
3199- moderator_password,
3200- nondigestable,
3201- nonmember_rejection_notice,
3202- obscure_addresses,
3203- owner_chain,
3204- owner_pipeline,
3205- personalize,
3206- post_id,
3207- posting_chain,
3208- posting_pipeline,
3209- preferred_language,
3210- display_name,
3211- reject_these_nonmembers,
3212- reply_goes_to_list,
3213- reply_to_address,
3214- require_explicit_destination,
3215- respond_to_post_requests,
3216- scrub_nondigest,
3217- send_goodbye_message,
3218- send_welcome_message,
3219- subject_prefix,
3220- topics,
3221- topics_bodylines_limit,
3222- topics_enabled,
3223- welcome_message_uri,
3224- archive_policy,
3225- list_id,
3226- nntp_prefix_subject_too,
3227- newsgroup_moderation
3228- FROM mailinglist;
3229
3230=== removed file 'src/mailman/database/schema/sqlite_20130406000000_01.sql'
3231--- src/mailman/database/schema/sqlite_20130406000000_01.sql 2013-11-26 02:26:15 +0000
3232+++ src/mailman/database/schema/sqlite_20130406000000_01.sql 1970-01-01 00:00:00 +0000
3233@@ -1,46 +0,0 @@
3234--- This file contains the SQLite schema migration from
3235--- 3.0b3 to 3.0b4
3236---
3237--- After 3.0b4 is released you may not edit this file.
3238-
3239--- For SQLite3 migration strategy, see
3240--- http://sqlite.org/faq.html#q11
3241-
3242--- ADD listarchiver table.
3243-
3244--- REMOVALs from the bounceevent table:
3245--- REM list_name
3246-
3247--- ADDs to the bounceevent table:
3248--- ADD list_id
3249-
3250--- ADDs to the mailinglist table:
3251--- ADD archiver_id
3252-
3253-CREATE TABLE bounceevent_backup (
3254- id INTEGER NOT NULL,
3255- email TEXT,
3256- 'timestamp' TIMESTAMP,
3257- message_id TEXT,
3258- context INTEGER,
3259- processed BOOLEAN,
3260- PRIMARY KEY (id)
3261- );
3262-
3263-INSERT INTO bounceevent_backup SELECT
3264- id, email, "timestamp", message_id,
3265- context, processed
3266- FROM bounceevent;
3267-
3268-ALTER TABLE bounceevent_backup ADD COLUMN list_id TEXT;
3269-
3270-CREATE TABLE listarchiver (
3271- id INTEGER NOT NULL,
3272- mailing_list_id INTEGER NOT NULL,
3273- name TEXT NOT NULL,
3274- _is_enabled BOOLEAN,
3275- PRIMARY KEY (id)
3276- );
3277-
3278-CREATE INDEX ix_listarchiver_mailing_list_id
3279- ON listarchiver(mailing_list_id);
3280
3281=== modified file 'src/mailman/database/sqlite.py'
3282--- src/mailman/database/sqlite.py 2014-01-01 14:59:42 +0000
3283+++ src/mailman/database/sqlite.py 2014-11-01 03:48:29 +0000
3284@@ -22,63 +22,27 @@
3285 __metaclass__ = type
3286 __all__ = [
3287 'SQLiteDatabase',
3288- 'make_temporary',
3289 ]
3290
3291
3292 import os
3293-import types
3294-import shutil
3295-import tempfile
3296
3297-from functools import partial
3298+from mailman.database.base import SABaseDatabase
3299 from urlparse import urlparse
3300
3301-from mailman.database.base import StormBaseDatabase
3302-from mailman.testing.helpers import configuration
3303-
3304
3305
3306
3307-class SQLiteDatabase(StormBaseDatabase):
3308+class SQLiteDatabase(SABaseDatabase):
3309 """Database class for SQLite."""
3310
3311- TAG = 'sqlite'
3312-
3313- def _database_exists(self, store):
3314- """See `BaseDatabase`."""
3315- table_query = 'select tbl_name from sqlite_master;'
3316- table_names = set(item[0] for item in
3317- store.execute(table_query))
3318- return 'version' in table_names
3319-
3320 def _prepare(self, url):
3321 parts = urlparse(url)
3322 assert parts.scheme == 'sqlite', (
3323 'Database url mismatch (expected sqlite prefix): {0}'.format(url))
3324+ # Ensure that the SQLite database file has the proper permissions,
3325+ # since SQLite doesn't play nice with umask.
3326 path = os.path.normpath(parts.path)
3327- fd = os.open(path, os.O_WRONLY | os.O_NONBLOCK | os.O_CREAT, 0666)
3328+ fd = os.open(path, os.O_WRONLY | os.O_NONBLOCK | os.O_CREAT, 0o666)
3329 # Ignore errors
3330 if fd > 0:
3331 os.close(fd)
3332-
3333-
3334-
3335
3336-# Test suite adapter for ITemporaryDatabase.
3337-
3338-def _cleanup(self, tempdir):
3339- shutil.rmtree(tempdir)
3340-
3341-
3342-def make_temporary(database):
3343- """Adapts by monkey patching an existing SQLite IDatabase."""
3344- tempdir = tempfile.mkdtemp()
3345- url = 'sqlite:///' + os.path.join(tempdir, 'mailman.db')
3346- with configuration('database', url=url):
3347- database.initialize()
3348- database._cleanup = types.MethodType(
3349- partial(_cleanup, tempdir=tempdir),
3350- database)
3351- # bool column values in SQLite must be integers.
3352- database.FALSE = 0
3353- database.TRUE = 1
3354- return database
3355
3356=== added directory 'src/mailman/database/tests'
3357=== removed directory 'src/mailman/database/tests'
3358=== added file 'src/mailman/database/tests/__init__.py'
3359=== removed file 'src/mailman/database/tests/__init__.py'
3360=== removed directory 'src/mailman/database/tests/data'
3361=== removed file 'src/mailman/database/tests/data/__init__.py'
3362=== removed file 'src/mailman/database/tests/data/mailman_01.db'
3363Binary files src/mailman/database/tests/data/mailman_01.db 2012-04-20 21:32:27 +0000 and src/mailman/database/tests/data/mailman_01.db 1970-01-01 00:00:00 +0000 differ
3364=== removed file 'src/mailman/database/tests/data/migration_postgres_1.sql'
3365--- src/mailman/database/tests/data/migration_postgres_1.sql 2012-07-26 04:22:19 +0000
3366+++ src/mailman/database/tests/data/migration_postgres_1.sql 1970-01-01 00:00:00 +0000
3367@@ -1,133 +0,0 @@
3368-INSERT INTO "acceptablealias" VALUES(1,'foo@example.com',1);
3369-INSERT INTO "acceptablealias" VALUES(2,'bar@example.com',1);
3370-
3371-INSERT INTO "address" VALUES(
3372- 1,'anne@example.com',NULL,'Anne Person',
3373- '2012-04-19 00:52:24.826432','2012-04-19 00:49:42.373769',1,2);
3374-INSERT INTO "address" VALUES(
3375- 2,'bart@example.com',NULL,'Bart Person',
3376- '2012-04-19 00:53:25.878800','2012-04-19 00:49:52.882050',2,4);
3377-
3378-INSERT INTO "domain" VALUES(
3379- 1,'example.com','http://example.com',NULL,'postmaster@example.com');
3380-
3381-INSERT INTO "mailinglist" VALUES(
3382- -- id,list_name,mail_host,include_list_post_header,include_rfc2369_headers
3383- 1,'test','example.com',True,True,
3384- -- created_at,admin_member_chunksize,next_request_id,next_digest_number
3385- '2012-04-19 00:46:13.173844',30,1,1,
3386- -- digest_last_sent_at,volume,last_post_at,accept_these_nonmembers
3387- NULL,1,NULL,E'\\x80025D71012E',
3388- -- acceptable_aliases_id,admin_immed_notify,admin_notify_mchanges
3389- NULL,True,False,
3390- -- administrivia,advertised,anonymous_list,archive,archive_private
3391- True,True,False,True,False,
3392- -- archive_volume_frequency
3393- 1,
3394- --autorespond_owner,autoresponse_owner_text
3395- 0,'',
3396- -- autorespond_postings,autoresponse_postings_text
3397- 0,'',
3398- -- autorespond_requests,authoresponse_requests_text
3399- 0,'',
3400- -- autoresponse_grace_period
3401- '90 days, 0:00:00',
3402- -- forward_unrecognized_bounces_to,process_bounces
3403- 1,True,
3404- -- bounce_info_stale_after,bounce_matching_headers
3405- '7 days, 0:00:00','
3406-# Lines that *start* with a ''#'' are comments.
3407-to: friend@public.com
3408-message-id: relay.comanche.denmark.eu
3409-from: list@listme.com
3410-from: .*@uplinkpro.com
3411-',
3412- -- bounce_notify_owner_on_disable,bounce_notify_owner_on_removal
3413- True,True,
3414- -- bounce_score_threshold,bounce_you_are_disabled_warnings
3415- 5,3,
3416- -- bounce_you_are_disabled_warnings_interval
3417- '7 days, 0:00:00',
3418- -- filter_action,filter_content,collapse_alternatives
3419- 2,False,True,
3420- -- convert_html_to_plaintext,default_member_action,default_nonmember_action
3421- False,4,0,
3422- -- description
3423- '',
3424- -- digest_footer_uri
3425- 'mailman:///$listname/$language/footer-generic.txt',
3426- -- digest_header_uri
3427- NULL,
3428- -- digest_is_default,digest_send_periodic,digest_size_threshold
3429- False,True,30.0,
3430- -- digest_volume_frequency,digestable,discard_these_nonmembers
3431- 1,True,E'\\x80025D71012E',
3432- -- emergency,encode_ascii_prefixes,first_strip_reply_to
3433- False,False,False,
3434- -- footer_uri
3435- 'mailman:///$listname/$language/footer-generic.txt',
3436- -- forward_auto_discards,gateway_to_mail,gateway_to_news
3437- True,False,FAlse,
3438- -- generic_nonmember_action,goodby_message_uri
3439- 1,'',
3440- -- header_matches,header_uri,hold_these_nonmembers,info,linked_newsgroup
3441- E'\\x80025D71012E',NULL,E'\\x80025D71012E','','',
3442- -- max_days_to_hold,max_message_size,max_num_recipients
3443- 0,40,10,
3444- -- member_moderation_notice,mime_is_default_digest,moderator_password
3445- '',False,NULL,
3446- -- new_member_options,news_moderation,news_prefix_subject_too
3447- 256,0,True,
3448- -- nntp_host,nondigestable,nonmember_rejection_notice,obscure_addresses
3449- '',True,'',True,
3450- -- owner_chain,owner_pipeline,personalize,post_id
3451- 'default-owner-chain','default-owner-pipeline',0,1,
3452- -- posting_chain,posting_pipeline,preferred_language,private_roster
3453- 'default-posting-chain','default-posting-pipeline','en',True,
3454- -- display_name,reject_these_nonmembers
3455- 'Test',E'\\x80025D71012E',
3456- -- reply_goes_to_list,reply_to_address
3457- 0,'',
3458- -- require_explicit_destination,respond_to_post_requests
3459- True,True,
3460- -- scrub_nondigest,send_goodbye_message,send_reminders,send_welcome_message
3461- False,True,True,True,
3462- -- subject_prefix,subscribe_auto_approval
3463- '[Test] ',E'\\x80025D71012E',
3464- -- subscribe_policy,topics,topics_bodylines_limit,topics_enabled
3465- 1,E'\\x80025D71012E',5,False,
3466- -- unsubscribe_policy,welcome_message_uri
3467- 0,'mailman:///welcome.txt');
3468-
3469-INSERT INTO "member" VALUES(
3470- 1,'d1243f4d-e604-4f6b-af52-98d0a7bce0f1',1,'test@example.com',4,NULL,5,1);
3471-INSERT INTO "member" VALUES(
3472- 2,'dccc3851-fdfb-4afa-90cf-bdcbf80ad0fd',2,'test@example.com',3,NULL,6,1);
3473-INSERT INTO "member" VALUES(
3474- 3,'479be431-45f2-473d-bc3c-7eac614030ac',3,'test@example.com',3,NULL,7,2);
3475-INSERT INTO "member" VALUES(
3476- 4,'e2dc604c-d93a-4b91-b5a8-749e3caade36',1,'test@example.com',4,NULL,8,2);
3477-
3478-INSERT INTO "preferences" VALUES(1,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
3479-INSERT INTO "preferences" VALUES(2,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
3480-INSERT INTO "preferences" VALUES(3,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
3481-INSERT INTO "preferences" VALUES(4,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
3482-INSERT INTO "preferences" VALUES(5,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
3483-INSERT INTO "preferences" VALUES(6,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
3484-INSERT INTO "preferences" VALUES(7,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
3485-INSERT INTO "preferences" VALUES(8,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
3486-
3487-INSERT INTO "user" VALUES(
3488- 1,'Anne Person',NULL,'0adf3caa-6f26-46f8-a11d-5256c8148592',
3489- '2012-04-19 00:49:42.370493',1,1);
3490-INSERT INTO "user" VALUES(
3491- 2,'Bart Person',NULL,'63f5d1a2-e533-4055-afe4-475dec3b1163',
3492- '2012-04-19 00:49:52.868746',2,3);
3493-
3494-INSERT INTO "uid" VALUES(1,'8bf9a615-f23e-4980-b7d1-90ac0203c66f');
3495-INSERT INTO "uid" VALUES(2,'0adf3caa-6f26-46f8-a11d-5256c8148592');
3496-INSERT INTO "uid" VALUES(3,'63f5d1a2-e533-4055-afe4-475dec3b1163');
3497-INSERT INTO "uid" VALUES(4,'d1243f4d-e604-4f6b-af52-98d0a7bce0f1');
3498-INSERT INTO "uid" VALUES(5,'dccc3851-fdfb-4afa-90cf-bdcbf80ad0fd');
3499-INSERT INTO "uid" VALUES(6,'479be431-45f2-473d-bc3c-7eac614030ac');
3500-INSERT INTO "uid" VALUES(7,'e2dc604c-d93a-4b91-b5a8-749e3caade36');
3501
3502=== removed file 'src/mailman/database/tests/data/migration_sqlite_1.sql'
3503--- src/mailman/database/tests/data/migration_sqlite_1.sql 2012-07-26 04:22:19 +0000
3504+++ src/mailman/database/tests/data/migration_sqlite_1.sql 1970-01-01 00:00:00 +0000
3505@@ -1,133 +0,0 @@
3506-INSERT INTO "acceptablealias" VALUES(1,'foo@example.com',1);
3507-INSERT INTO "acceptablealias" VALUES(2,'bar@example.com',1);
3508-
3509-INSERT INTO "address" VALUES(
3510- 1,'anne@example.com',NULL,'Anne Person',
3511- '2012-04-19 00:52:24.826432','2012-04-19 00:49:42.373769',1,2);
3512-INSERT INTO "address" VALUES(
3513- 2,'bart@example.com',NULL,'Bart Person',
3514- '2012-04-19 00:53:25.878800','2012-04-19 00:49:52.882050',2,4);
3515-
3516-INSERT INTO "domain" VALUES(
3517- 1,'example.com','http://example.com',NULL,'postmaster@example.com');
3518-
3519-INSERT INTO "mailinglist" VALUES(
3520- -- id,list_name,mail_host,include_list_post_header,include_rfc2369_headers
3521- 1,'test','example.com',1,1,
3522- -- created_at,admin_member_chunksize,next_request_id,next_digest_number
3523- '2012-04-19 00:46:13.173844',30,1,1,
3524- -- digest_last_sent_at,volume,last_post_at,accept_these_nonmembers
3525- NULL,1,NULL,X'80025D71012E',
3526- -- acceptable_aliases_id,admin_immed_notify,admin_notify_mchanges
3527- NULL,1,0,
3528- -- administrivia,advertised,anonymous_list,archive,archive_private
3529- 1,1,0,1,0,
3530- -- archive_volume_frequency
3531- 1,
3532- --autorespond_owner,autoresponse_owner_text
3533- 0,'',
3534- -- autorespond_postings,autoresponse_postings_text
3535- 0,'',
3536- -- autorespond_requests,authoresponse_requests_text
3537- 0,'',
3538- -- autoresponse_grace_period
3539- '90 days, 0:00:00',
3540- -- forward_unrecognized_bounces_to,process_bounces
3541- 1,1,
3542- -- bounce_info_stale_after,bounce_matching_headers
3543- '7 days, 0:00:00','
3544-# Lines that *start* with a ''#'' are comments.
3545-to: friend@public.com
3546-message-id: relay.comanche.denmark.eu
3547-from: list@listme.com
3548-from: .*@uplinkpro.com
3549-',
3550- -- bounce_notify_owner_on_disable,bounce_notify_owner_on_removal
3551- 1,1,
3552- -- bounce_score_threshold,bounce_you_are_disabled_warnings
3553- 5,3,
3554- -- bounce_you_are_disabled_warnings_interval
3555- '7 days, 0:00:00',
3556- -- filter_action,filter_content,collapse_alternatives
3557- 2,0,1,
3558- -- convert_html_to_plaintext,default_member_action,default_nonmember_action
3559- 0,4,0,
3560- -- description
3561- '',
3562- -- digest_footer_uri
3563- 'mailman:///$listname/$language/footer-generic.txt',
3564- -- digest_header_uri
3565- NULL,
3566- -- digest_is_default,digest_send_periodic,digest_size_threshold
3567- 0,1,30.0,
3568- -- digest_volume_frequency,digestable,discard_these_nonmembers
3569- 1,1,X'80025D71012E',
3570- -- emergency,encode_ascii_prefixes,first_strip_reply_to
3571- 0,0,0,
3572- -- footer_uri
3573- 'mailman:///$listname/$language/footer-generic.txt',
3574- -- forward_auto_discards,gateway_to_mail,gateway_to_news
3575- 1,0,0,
3576- -- generic_nonmember_action,goodby_message_uri
3577- 1,'',
3578- -- header_matches,header_uri,hold_these_nonmembers,info,linked_newsgroup
3579- X'80025D71012E',NULL,X'80025D71012E','','',
3580- -- max_days_to_hold,max_message_size,max_num_recipients
3581- 0,40,10,
3582- -- member_moderation_notice,mime_is_default_digest,moderator_password
3583- '',0,NULL,
3584- -- new_member_options,news_moderation,news_prefix_subject_too
3585- 256,0,1,
3586- -- nntp_host,nondigestable,nonmember_rejection_notice,obscure_addresses
3587- '',1,'',1,
3588- -- owner_chain,owner_pipeline,personalize,post_id
3589- 'default-owner-chain','default-owner-pipeline',0,1,
3590- -- posting_chain,posting_pipeline,preferred_language,private_roster
3591- 'default-posting-chain','default-posting-pipeline','en',1,
3592- -- display_name,reject_these_nonmembers
3593- 'Test',X'80025D71012E',
3594- -- reply_goes_to_list,reply_to_address
3595- 0,'',
3596- -- require_explicit_destination,respond_to_post_requests
3597- 1,1,
3598- -- scrub_nondigest,send_goodbye_message,send_reminders,send_welcome_message
3599- 0,1,1,1,
3600- -- subject_prefix,subscribe_auto_approval
3601- '[Test] ',X'80025D71012E',
3602- -- subscribe_policy,topics,topics_bodylines_limit,topics_enabled
3603- 1,X'80025D71012E',5,0,
3604- -- unsubscribe_policy,welcome_message_uri
3605- 0,'mailman:///welcome.txt');
3606-
3607-INSERT INTO "member" VALUES(
3608- 1,'d1243f4d-e604-4f6b-af52-98d0a7bce0f1',1,'test@example.com',4,NULL,5,1);
3609-INSERT INTO "member" VALUES(
3610- 2,'dccc3851-fdfb-4afa-90cf-bdcbf80ad0fd',2,'test@example.com',3,NULL,6,1);
3611-INSERT INTO "member" VALUES(
3612- 3,'479be431-45f2-473d-bc3c-7eac614030ac',3,'test@example.com',3,NULL,7,2);
3613-INSERT INTO "member" VALUES(
3614- 4,'e2dc604c-d93a-4b91-b5a8-749e3caade36',1,'test@example.com',4,NULL,8,2);
3615-
3616-INSERT INTO "preferences" VALUES(1,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
3617-INSERT INTO "preferences" VALUES(2,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
3618-INSERT INTO "preferences" VALUES(3,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
3619-INSERT INTO "preferences" VALUES(4,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
3620-INSERT INTO "preferences" VALUES(5,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
3621-INSERT INTO "preferences" VALUES(6,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
3622-INSERT INTO "preferences" VALUES(7,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
3623-INSERT INTO "preferences" VALUES(8,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
3624-
3625-INSERT INTO "user" VALUES(
3626- 1,'Anne Person',NULL,'0adf3caa-6f26-46f8-a11d-5256c8148592',
3627- '2012-04-19 00:49:42.370493',1,1);
3628-INSERT INTO "user" VALUES(
3629- 2,'Bart Person',NULL,'63f5d1a2-e533-4055-afe4-475dec3b1163',
3630- '2012-04-19 00:49:52.868746',2,3);
3631-
3632-INSERT INTO "uid" VALUES(1,'8bf9a615-f23e-4980-b7d1-90ac0203c66f');
3633-INSERT INTO "uid" VALUES(2,'0adf3caa-6f26-46f8-a11d-5256c8148592');
3634-INSERT INTO "uid" VALUES(3,'63f5d1a2-e533-4055-afe4-475dec3b1163');
3635-INSERT INTO "uid" VALUES(4,'d1243f4d-e604-4f6b-af52-98d0a7bce0f1');
3636-INSERT INTO "uid" VALUES(5,'dccc3851-fdfb-4afa-90cf-bdcbf80ad0fd');
3637-INSERT INTO "uid" VALUES(6,'479be431-45f2-473d-bc3c-7eac614030ac');
3638-INSERT INTO "uid" VALUES(7,'e2dc604c-d93a-4b91-b5a8-749e3caade36');
3639
3640=== added file 'src/mailman/database/tests/test_factory.py'
3641--- src/mailman/database/tests/test_factory.py 1970-01-01 00:00:00 +0000
3642+++ src/mailman/database/tests/test_factory.py 2014-11-01 03:48:29 +0000
3643@@ -0,0 +1,160 @@
3644+# Copyright (C) 2013-2014 by the Free Software Foundation, Inc.
3645+#
3646+# This file is part of GNU Mailman.
3647+#
3648+# GNU Mailman is free software: you can redistribute it and/or modify it under
3649+# the terms of the GNU General Public License as published by the Free
3650+# Software Foundation, either version 3 of the License, or (at your option)
3651+# any later version.
3652+#
3653+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
3654+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
3655+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
3656+# more details.
3657+#
3658+# You should have received a copy of the GNU General Public License along with
3659+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
3660+
3661+"""Test database schema migrations"""
3662+
3663+from __future__ import absolute_import, print_function, unicode_literals
3664+
3665+__metaclass__ = type
3666+__all__ = [
3667+ 'TestSchemaManager',
3668+ ]
3669+
3670+
3671+import unittest
3672+import alembic.command
3673+
3674+from mock import patch
3675+from sqlalchemy import MetaData, Table, Column, Integer, Unicode
3676+from sqlalchemy.exc import ProgrammingError, OperationalError
3677+from sqlalchemy.schema import Index
3678+
3679+from mailman.config import config
3680+from mailman.database.alembic import alembic_cfg
3681+from mailman.database.factory import LAST_STORM_SCHEMA_VERSION, SchemaManager
3682+from mailman.database.model import Model
3683+from mailman.interfaces.database import DatabaseError
3684+from mailman.testing.layers import ConfigLayer
3685+
3686+
3687+
3688
3689+class TestSchemaManager(unittest.TestCase):
3690+
3691+ layer = ConfigLayer
3692+
3693+ def setUp(self):
3694+ # Drop the existing database.
3695+ Model.metadata.drop_all(config.db.engine)
3696+ md = MetaData()
3697+ md.reflect(bind=config.db.engine)
3698+ for tablename in ('alembic_version', 'version'):
3699+ if tablename in md.tables:
3700+ md.tables[tablename].drop(config.db.engine)
3701+ self.schema_mgr = SchemaManager(config.db)
3702+
3703+ def tearDown(self):
3704+ self._drop_storm_database()
3705+ # Restore a virgin database.
3706+ Model.metadata.create_all(config.db.engine)
3707+
3708+ def _table_exists(self, tablename):
3709+ md = MetaData()
3710+ md.reflect(bind=config.db.engine)
3711+ return tablename in md.tables
3712+
3713+ def _create_storm_database(self, revision):
3714+ version_table = Table(
3715+ 'version', Model.metadata,
3716+ Column('id', Integer, primary_key=True),
3717+ Column('component', Unicode),
3718+ Column('version', Unicode),
3719+ )
3720+ version_table.create(config.db.engine)
3721+ config.db.store.execute(version_table.insert().values(
3722+ component='schema', version=revision))
3723+ config.db.commit()
3724+ # Other Storm specific changes, those SQL statements hopefully work on
3725+ # all DB engines...
3726+ config.db.engine.execute(
3727+ 'ALTER TABLE mailinglist ADD COLUMN acceptable_aliases_id INT')
3728+ Index('ix_user__user_id').drop(bind=config.db.engine)
3729+ # Don't pollute our main metadata object, create a new one.
3730+ md = MetaData()
3731+ user_table = Model.metadata.tables['user'].tometadata(md)
3732+ Index('ix_user_user_id', user_table.c._user_id).create(
3733+ bind=config.db.engine)
3734+ config.db.commit()
3735+
3736+ def _drop_storm_database(self):
3737+ """Remove the leftovers from a Storm DB.
3738+
3739+ A drop_all() must be issued afterwards.
3740+ """
3741+ if 'version' in Model.metadata.tables:
3742+ version = Model.metadata.tables['version']
3743+ version.drop(config.db.engine, checkfirst=True)
3744+ Model.metadata.remove(version)
3745+ try:
3746+ Index('ix_user_user_id').drop(bind=config.db.engine)
3747+ except (ProgrammingError, OperationalError):
3748+ # Nonexistent. PostgreSQL raises a ProgrammingError, while SQLite
3749+ # raises an OperationalError.
3750+ pass
3751+ config.db.commit()
3752+
3753+ def test_current_database(self):
3754+ # The database is already at the latest version.
3755+ alembic.command.stamp(alembic_cfg, 'head')
3756+ with patch('alembic.command') as alembic_command:
3757+ self.schema_mgr.setup_database()
3758+ self.assertFalse(alembic_command.stamp.called)
3759+ self.assertFalse(alembic_command.upgrade.called)
3760+
3761+ @patch('alembic.command')
3762+ def test_initial(self, alembic_command):
3763+ # No existing database.
3764+ self.assertFalse(self._table_exists('mailinglist'))
3765+ self.assertFalse(self._table_exists('alembic_version'))
3766+ self.schema_mgr.setup_database()
3767+ self.assertFalse(alembic_command.upgrade.called)
3768+ self.assertTrue(self._table_exists('mailinglist'))
3769+ self.assertTrue(self._table_exists('alembic_version'))
3770+
3771+ @patch('alembic.command.stamp')
3772+ def test_storm(self, alembic_command_stamp):
3773+ # Existing Storm database.
3774+ Model.metadata.create_all(config.db.engine)
3775+ self._create_storm_database(LAST_STORM_SCHEMA_VERSION)
3776+ self.schema_mgr.setup_database()
3777+ self.assertFalse(alembic_command_stamp.called)
3778+ self.assertTrue(
3779+ self._table_exists('mailinglist')
3780+ and self._table_exists('alembic_version')
3781+ and not self._table_exists('version'))
3782+
3783+ @patch('alembic.command')
3784+ def test_old_storm(self, alembic_command):
3785+ # Existing Storm database in an old version.
3786+ Model.metadata.create_all(config.db.engine)
3787+ self._create_storm_database('001')
3788+ self.assertRaises(DatabaseError, self.schema_mgr.setup_database)
3789+ self.assertFalse(alembic_command.stamp.called)
3790+ self.assertFalse(alembic_command.upgrade.called)
3791+
3792+ def test_old_db(self):
3793+ # The database is in an old revision, must upgrade.
3794+ alembic.command.stamp(alembic_cfg, 'head')
3795+ md = MetaData()
3796+ md.reflect(bind=config.db.engine)
3797+ config.db.store.execute(md.tables['alembic_version'].delete())
3798+ config.db.store.execute(md.tables['alembic_version'].insert().values(
3799+ version_num='dummyrevision'))
3800+ config.db.commit()
3801+ with patch('alembic.command') as alembic_command:
3802+ self.schema_mgr.setup_database()
3803+ self.assertFalse(alembic_command.stamp.called)
3804+ self.assertTrue(alembic_command.upgrade.called)
3805
3806=== removed file 'src/mailman/database/tests/test_migrations.py'
3807--- src/mailman/database/tests/test_migrations.py 2014-01-01 14:59:42 +0000
3808+++ src/mailman/database/tests/test_migrations.py 1970-01-01 00:00:00 +0000
3809@@ -1,506 +0,0 @@
3810-# Copyright (C) 2012-2014 by the Free Software Foundation, Inc.
3811-#
3812-# This file is part of GNU Mailman.
3813-#
3814-# GNU Mailman is free software: you can redistribute it and/or modify it under
3815-# the terms of the GNU General Public License as published by the Free
3816-# Software Foundation, either version 3 of the License, or (at your option)
3817-# any later version.
3818-#
3819-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
3820-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
3821-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
3822-# more details.
3823-#
3824-# You should have received a copy of the GNU General Public License along with
3825-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
3826-
3827-"""Test schema migrations."""
3828-
3829-from __future__ import absolute_import, print_function, unicode_literals
3830-
3831-__metaclass__ = type
3832-__all__ = [
3833- 'TestMigration20120407MigratedData',
3834- 'TestMigration20120407Schema',
3835- 'TestMigration20120407UnchangedData',
3836- 'TestMigration20121015MigratedData',
3837- 'TestMigration20121015Schema',
3838- 'TestMigration20130406MigratedData',
3839- 'TestMigration20130406Schema',
3840- ]
3841-
3842-
3843-import unittest
3844-
3845-from datetime import datetime
3846-from operator import attrgetter
3847-from pkg_resources import resource_string
3848-from sqlite3 import OperationalError
3849-from storm.exceptions import DatabaseError
3850-from zope.component import getUtility
3851-
3852-from mailman.interfaces.database import IDatabaseFactory
3853-from mailman.interfaces.domain import IDomainManager
3854-from mailman.interfaces.archiver import ArchivePolicy
3855-from mailman.interfaces.bounce import BounceContext
3856-from mailman.interfaces.listmanager import IListManager
3857-from mailman.interfaces.mailinglist import IAcceptableAliasSet
3858-from mailman.interfaces.nntp import NewsgroupModeration
3859-from mailman.interfaces.subscriptions import ISubscriptionService
3860-from mailman.model.bans import Ban
3861-from mailman.model.bounce import BounceEvent
3862-from mailman.testing.helpers import temporary_db
3863-from mailman.testing.layers import ConfigLayer
3864-
3865-
3866-
3867
3868-class MigrationTestBase(unittest.TestCase):
3869- """Test database migrations."""
3870-
3871- layer = ConfigLayer
3872-
3873- def setUp(self):
3874- self._database = getUtility(IDatabaseFactory, 'temporary').create()
3875-
3876- def tearDown(self):
3877- self._database._cleanup()
3878-
3879- def _table_missing_present(self, migrations, missing, present):
3880- """The appropriate migrations leave some tables missing and present.
3881-
3882- :param migrations: Sequence of migrations to load.
3883- :param missing: Tables which should be missing.
3884- :param present: Tables which should be present.
3885- """
3886- for migration in migrations:
3887- self._database.load_migrations(migration)
3888- self._database.store.commit()
3889- for table in missing:
3890- self.assertRaises(OperationalError,
3891- self._database.store.execute,
3892- 'select * from {};'.format(table))
3893- for table in present:
3894- self._database.store.execute('select * from {};'.format(table))
3895-
3896- def _missing_present(self, table, migrations, missing, present):
3897- """The appropriate migrations leave columns missing and present.
3898-
3899- :param table: The table to test columns from.
3900- :param migrations: Sequence of migrations to load.
3901- :param missing: Set of columns which should be missing after the
3902- migrations are loaded.
3903- :param present: Set of columns which should be present after the
3904- migrations are loaded.
3905- """
3906- for migration in migrations:
3907- self._database.load_migrations(migration)
3908- self._database.store.commit()
3909- for column in missing:
3910- self.assertRaises(DatabaseError,
3911- self._database.store.execute,
3912- 'select {0} from {1};'.format(column, table))
3913- self._database.store.rollback()
3914- for column in present:
3915- # This should not produce an exception. Is there some better test
3916- # that we can perform?
3917- self._database.store.execute(
3918- 'select {0} from {1};'.format(column, table))
3919-
3920-
3921-
3922
3923-class TestMigration20120407Schema(MigrationTestBase):
3924- """Test column migrations."""
3925-
3926- def test_pre_upgrade_columns_migration(self):
3927- # Test that before the migration, the old table columns are present
3928- # and the new database columns are not.
3929- self._missing_present('mailinglist',
3930- ['20120406999999'],
3931- # New columns are missing.
3932- ('allow_list_posts',
3933- 'archive_policy',
3934- 'list_id',
3935- 'nntp_prefix_subject_too'),
3936- # Old columns are present.
3937- ('archive',
3938- 'archive_private',
3939- 'archive_volume_frequency',
3940- 'generic_nonmember_action',
3941- 'include_list_post_header',
3942- 'news_moderation',
3943- 'news_prefix_subject_too',
3944- 'nntp_host'))
3945- self._missing_present('member',
3946- ['20120406999999'],
3947- ('list_id',),
3948- ('mailing_list',))
3949-
3950- def test_post_upgrade_columns_migration(self):
3951- # Test that after the migration, the old table columns are missing
3952- # and the new database columns are present.
3953- self._missing_present('mailinglist',
3954- ['20120406999999',
3955- '20120407000000'],
3956- # The old columns are missing.
3957- ('archive',
3958- 'archive_private',
3959- 'archive_volume_frequency',
3960- 'generic_nonmember_action',
3961- 'include_list_post_header',
3962- 'news_moderation',
3963- 'news_prefix_subject_too',
3964- 'nntp_host'),
3965- # The new columns are present.
3966- ('allow_list_posts',
3967- 'archive_policy',
3968- 'list_id',
3969- 'nntp_prefix_subject_too'))
3970- self._missing_present('member',
3971- ['20120406999999',
3972- '20120407000000'],
3973- ('mailing_list',),
3974- ('list_id',))
3975-
3976-
3977-
3978
3979-class TestMigration20120407UnchangedData(MigrationTestBase):
3980- """Test non-migrated data."""
3981-
3982- def setUp(self):
3983- MigrationTestBase.setUp(self)
3984- # Load all the migrations to just before the one we're testing.
3985- self._database.load_migrations('20120406999999')
3986- # Load the previous schema's sample data.
3987- sample_data = resource_string(
3988- 'mailman.database.tests.data',
3989- 'migration_{0}_1.sql'.format(self._database.TAG))
3990- self._database.load_sql(self._database.store, sample_data)
3991- # XXX 2012-12-28: We have to load the last migration defined in the
3992- # system, otherwise the ORM model will not match the SQL table
3993- # definitions and we'll get OperationalErrors from SQLite.
3994- self._database.load_migrations('20121015000000')
3995-
3996- def test_migration_domains(self):
3997- # Test that the domains table, which isn't touched, doesn't change.
3998- with temporary_db(self._database):
3999- # Check that the domains survived the migration. This table
4000- # was not touched so it should be fine.
4001- domains = list(getUtility(IDomainManager))
4002- self.assertEqual(len(domains), 1)
4003- self.assertEqual(domains[0].mail_host, 'example.com')
4004-
4005- def test_migration_mailing_lists(self):
4006- # Test that the mailing lists survive migration.
4007- with temporary_db(self._database):
4008- # There should be exactly one mailing list defined.
4009- mlists = list(getUtility(IListManager).mailing_lists)
4010- self.assertEqual(len(mlists), 1)
4011- self.assertEqual(mlists[0].fqdn_listname, 'test@example.com')
4012-
4013- def test_migration_acceptable_aliases(self):
4014- # Test that the mailing list's acceptable aliases survive migration.
4015- # This proves that foreign key references are migrated properly.
4016- with temporary_db(self._database):
4017- mlist = getUtility(IListManager).get('test@example.com')
4018- aliases_set = IAcceptableAliasSet(mlist)
4019- self.assertEqual(set(aliases_set.aliases),
4020- set(['foo@example.com', 'bar@example.com']))
4021-
4022- def test_migration_members(self):
4023- # Test that the members of a mailing list all survive migration.
4024- with temporary_db(self._database):
4025- mlist = getUtility(IListManager).get('test@example.com')
4026- # Test that all the members we expect are still there. Start with
4027- # the two list delivery members.
4028- addresses = set(address.email
4029- for address in mlist.members.addresses)
4030- self.assertEqual(addresses,
4031- set(['anne@example.com', 'bart@example.com']))
4032- # There is one owner.
4033- owners = set(address.email for address in mlist.owners.addresses)
4034- self.assertEqual(len(owners), 1)
4035- self.assertEqual(owners.pop(), 'anne@example.com')
4036- # There is one moderator.
4037- moderators = set(address.email
4038- for address in mlist.moderators.addresses)
4039- self.assertEqual(len(moderators), 1)
4040- self.assertEqual(moderators.pop(), 'bart@example.com')
4041-
4042-
4043-
4044
4045-class TestMigration20120407MigratedData(MigrationTestBase):
4046- """Test affected migration data."""
4047-
4048- def setUp(self):
4049- MigrationTestBase.setUp(self)
4050- # Load all the migrations to just before the one we're testing.
4051- self._database.load_migrations('20120406999999')
4052- # Load the previous schema's sample data.
4053- sample_data = resource_string(
4054- 'mailman.database.tests.data',
4055- 'migration_{0}_1.sql'.format(self._database.TAG))
4056- self._database.load_sql(self._database.store, sample_data)
4057-
4058- def _upgrade(self):
4059- # XXX 2012-12-28: We have to load the last migration defined in the
4060- # system, otherwise the ORM model will not match the SQL table
4061- # definitions and we'll get OperationalErrors from SQLite.
4062- self._database.load_migrations('20121015000000')
4063-
4064- def test_migration_archive_policy_never_0(self):
4065- # Test that the new archive_policy value is updated correctly. In the
4066- # case of old column archive=0, the archive_private column is
4067- # ignored. This test sets it to 0 to ensure it's ignored.
4068- self._database.store.execute(
4069- 'UPDATE mailinglist SET archive = {0}, archive_private = {0} '
4070- 'WHERE id = 1;'.format(self._database.FALSE))
4071- # Complete the migration
4072- self._upgrade()
4073- with temporary_db(self._database):
4074- mlist = getUtility(IListManager).get('test@example.com')
4075- self.assertEqual(mlist.archive_policy, ArchivePolicy.never)
4076-
4077- def test_migration_archive_policy_never_1(self):
4078- # Test that the new archive_policy value is updated correctly. In the
4079- # case of old column archive=0, the archive_private column is
4080- # ignored. This test sets it to 1 to ensure it's ignored.
4081- self._database.store.execute(
4082- 'UPDATE mailinglist SET archive = {0}, archive_private = {1} '
4083- 'WHERE id = 1;'.format(self._database.FALSE,
4084- self._database.TRUE))
4085- # Complete the migration
4086- self._upgrade()
4087- with temporary_db(self._database):
4088- mlist = getUtility(IListManager).get('test@example.com')
4089- self.assertEqual(mlist.archive_policy, ArchivePolicy.never)
4090-
4091- def test_archive_policy_private(self):
4092- # Test that the new archive_policy value is updated correctly for
4093- # private archives.
4094- self._database.store.execute(
4095- 'UPDATE mailinglist SET archive = {0}, archive_private = {0} '
4096- 'WHERE id = 1;'.format(self._database.TRUE))
4097- # Complete the migration
4098- self._upgrade()
4099- with temporary_db(self._database):
4100- mlist = getUtility(IListManager).get('test@example.com')
4101- self.assertEqual(mlist.archive_policy, ArchivePolicy.private)
4102-
4103- def test_archive_policy_public(self):
4104- # Test that the new archive_policy value is updated correctly for
4105- # public archives.
4106- self._database.store.execute(
4107- 'UPDATE mailinglist SET archive = {1}, archive_private = {0} '
4108- 'WHERE id = 1;'.format(self._database.FALSE,
4109- self._database.TRUE))
4110- # Complete the migration
4111- self._upgrade()
4112- with temporary_db(self._database):
4113- mlist = getUtility(IListManager).get('test@example.com')
4114- self.assertEqual(mlist.archive_policy, ArchivePolicy.public)
4115-
4116- def test_list_id(self):
4117- # Test that the mailinglist table gets a list_id column.
4118- self._upgrade()
4119- with temporary_db(self._database):
4120- mlist = getUtility(IListManager).get('test@example.com')
4121- self.assertEqual(mlist.list_id, 'test.example.com')
4122-
4123- def test_list_id_member(self):
4124- # Test that the member table's mailing_list column becomes list_id.
4125- self._upgrade()
4126- with temporary_db(self._database):
4127- service = getUtility(ISubscriptionService)
4128- members = list(service.find_members(list_id='test.example.com'))
4129- self.assertEqual(len(members), 4)
4130-
4131- def test_news_moderation_none(self):
4132- # Test that news_moderation becomes newsgroup_moderation.
4133- self._database.store.execute(
4134- 'UPDATE mailinglist SET news_moderation = 0 '
4135- 'WHERE id = 1;')
4136- self._upgrade()
4137- with temporary_db(self._database):
4138- mlist = getUtility(IListManager).get('test@example.com')
4139- self.assertEqual(mlist.newsgroup_moderation,
4140- NewsgroupModeration.none)
4141-
4142- def test_news_moderation_open_moderated(self):
4143- # Test that news_moderation becomes newsgroup_moderation.
4144- self._database.store.execute(
4145- 'UPDATE mailinglist SET news_moderation = 1 '
4146- 'WHERE id = 1;')
4147- self._upgrade()
4148- with temporary_db(self._database):
4149- mlist = getUtility(IListManager).get('test@example.com')
4150- self.assertEqual(mlist.newsgroup_moderation,
4151- NewsgroupModeration.open_moderated)
4152-
4153- def test_news_moderation_moderated(self):
4154- # Test that news_moderation becomes newsgroup_moderation.
4155- self._database.store.execute(
4156- 'UPDATE mailinglist SET news_moderation = 2 '
4157- 'WHERE id = 1;')
4158- self._upgrade()
4159- with temporary_db(self._database):
4160- mlist = getUtility(IListManager).get('test@example.com')
4161- self.assertEqual(mlist.newsgroup_moderation,
4162- NewsgroupModeration.moderated)
4163-
4164- def test_nntp_prefix_subject_too_false(self):
4165- # Test that news_prefix_subject_too becomes nntp_prefix_subject_too.
4166- self._database.store.execute(
4167- 'UPDATE mailinglist SET news_prefix_subject_too = {0} '
4168- 'WHERE id = 1;'.format(self._database.FALSE))
4169- self._upgrade()
4170- with temporary_db(self._database):
4171- mlist = getUtility(IListManager).get('test@example.com')
4172- self.assertFalse(mlist.nntp_prefix_subject_too)
4173-
4174- def test_nntp_prefix_subject_too_true(self):
4175- # Test that news_prefix_subject_too becomes nntp_prefix_subject_too.
4176- self._database.store.execute(
4177- 'UPDATE mailinglist SET news_prefix_subject_too = {0} '
4178- 'WHERE id = 1;'.format(self._database.TRUE))
4179- self._upgrade()
4180- with temporary_db(self._database):
4181- mlist = getUtility(IListManager).get('test@example.com')
4182- self.assertTrue(mlist.nntp_prefix_subject_too)
4183-
4184- def test_allow_list_posts_false(self):
4185- # Test that include_list_post_header -> allow_list_posts.
4186- self._database.store.execute(
4187- 'UPDATE mailinglist SET include_list_post_header = {0} '
4188- 'WHERE id = 1;'.format(self._database.FALSE))
4189- self._upgrade()
4190- with temporary_db(self._database):
4191- mlist = getUtility(IListManager).get('test@example.com')
4192- self.assertFalse(mlist.allow_list_posts)
4193-
4194- def test_allow_list_posts_true(self):
4195- # Test that include_list_post_header -> allow_list_posts.
4196- self._database.store.execute(
4197- 'UPDATE mailinglist SET include_list_post_header = {0} '
4198- 'WHERE id = 1;'.format(self._database.TRUE))
4199- self._upgrade()
4200- with temporary_db(self._database):
4201- mlist = getUtility(IListManager).get('test@example.com')
4202- self.assertTrue(mlist.allow_list_posts)
4203-
4204-
4205-
4206
4207-class TestMigration20121015Schema(MigrationTestBase):
4208- """Test column migrations."""
4209-
4210- def test_pre_upgrade_column_migrations(self):
4211- self._missing_present('ban',
4212- ['20121014999999'],
4213- ('list_id',),
4214- ('mailing_list',))
4215- self._missing_present('mailinglist',
4216- ['20121014999999'],
4217- (),
4218- ('new_member_options', 'send_reminders',
4219- 'subscribe_policy', 'unsubscribe_policy',
4220- 'subscribe_auto_approval', 'private_roster',
4221- 'admin_member_chunksize'),
4222- )
4223-
4224- def test_post_upgrade_column_migrations(self):
4225- self._missing_present('ban',
4226- ['20121014999999',
4227- '20121015000000'],
4228- ('mailing_list',),
4229- ('list_id',))
4230- self._missing_present('mailinglist',
4231- ['20121014999999',
4232- '20121015000000'],
4233- ('new_member_options', 'send_reminders',
4234- 'subscribe_policy', 'unsubscribe_policy',
4235- 'subscribe_auto_approval', 'private_roster',
4236- 'admin_member_chunksize'),
4237- ())
4238-
4239-
4240-
4241
4242-class TestMigration20121015MigratedData(MigrationTestBase):
4243- """Test non-migrated data."""
4244-
4245- def test_migration_bans(self):
4246- # Load all the migrations to just before the one we're testing.
4247- self._database.load_migrations('20121014999999')
4248- # Insert a list-specific ban.
4249- self._database.store.execute("""
4250- INSERT INTO ban VALUES (
4251- 1, 'anne@example.com', 'test@example.com');
4252- """)
4253- # Insert a global ban.
4254- self._database.store.execute("""
4255- INSERT INTO ban VALUES (
4256- 2, 'bart@example.com', NULL);
4257- """)
4258- # Update to the current migration we're testing.
4259- self._database.load_migrations('20121015000000')
4260- # Now both the local and global bans should still be present.
4261- bans = sorted(self._database.store.find(Ban),
4262- key=attrgetter('email'))
4263- self.assertEqual(bans[0].email, 'anne@example.com')
4264- self.assertEqual(bans[0].list_id, 'test.example.com')
4265- self.assertEqual(bans[1].email, 'bart@example.com')
4266- self.assertEqual(bans[1].list_id, None)
4267-
4268-
4269-
4270
4271-class TestMigration20130406Schema(MigrationTestBase):
4272- """Test column migrations."""
4273-
4274- def test_pre_upgrade_column_migrations(self):
4275- self._missing_present('bounceevent',
4276- ['20130405999999'],
4277- ('list_id',),
4278- ('list_name',))
4279-
4280- def test_post_upgrade_column_migrations(self):
4281- self._missing_present('bounceevent',
4282- ['20130405999999',
4283- '20130406000000'],
4284- ('list_name',),
4285- ('list_id',))
4286-
4287- def test_pre_listarchiver_table(self):
4288- self._table_missing_present(['20130405999999'], ('listarchiver',), ())
4289-
4290- def test_post_listarchiver_table(self):
4291- self._table_missing_present(['20130405999999',
4292- '20130406000000'],
4293- (),
4294- ('listarchiver',))
4295-
4296-
4297-
4298
4299-class TestMigration20130406MigratedData(MigrationTestBase):
4300- """Test migrated data."""
4301-
4302- def test_migration_bounceevent(self):
4303- # Load all migrations to just before the one we're testing.
4304- self._database.load_migrations('20130405999999')
4305- # Insert a bounce event.
4306- self._database.store.execute("""
4307- INSERT INTO bounceevent VALUES (
4308- 1, 'test@example.com', 'anne@example.com',
4309- '2013-04-06 21:12:00', '<abc@example.com>',
4310- 1, 0);
4311- """)
4312- # Update to the current migration we're testing
4313- self._database.load_migrations('20130406000000')
4314- # The bounce event should exist, but with a list-id instead of a fqdn
4315- # list name.
4316- events = list(self._database.store.find(BounceEvent))
4317- self.assertEqual(len(events), 1)
4318- self.assertEqual(events[0].list_id, 'test.example.com')
4319- self.assertEqual(events[0].email, 'anne@example.com')
4320- self.assertEqual(events[0].timestamp, datetime(2013, 4, 6, 21, 12))
4321- self.assertEqual(events[0].message_id, '<abc@example.com>')
4322- self.assertEqual(events[0].context, BounceContext.normal)
4323- self.assertFalse(events[0].processed)
4324
4325=== modified file 'src/mailman/database/types.py'
4326--- src/mailman/database/types.py 2014-04-28 15:23:35 +0000
4327+++ src/mailman/database/types.py 2014-11-01 03:48:29 +0000
4328@@ -23,43 +23,70 @@
4329 __metaclass__ = type
4330 __all__ = [
4331 'Enum',
4332+ 'UUID',
4333 ]
4334
4335+import uuid
4336
4337-from storm.properties import SimpleProperty
4338-from storm.variables import Variable
4339+from sqlalchemy import Integer
4340+from sqlalchemy.dialects import postgresql
4341+from sqlalchemy.types import TypeDecorator, CHAR
4342
4343
4344
4345
4346-class _EnumVariable(Variable):
4347- """Storm variable for supporting enum types.
4348+class Enum(TypeDecorator):
4349+ """Handle Python 3.4 style enums.
4350
4351- To use this, make the database column a INTEGER.
4352+ Stores an integer-based Enum as an integer in the database, and
4353+ converts it on-the-fly.
4354 """
4355-
4356- def __init__(self, *args, **kws):
4357- self._enum = kws.pop('enum')
4358- super(_EnumVariable, self).__init__(*args, **kws)
4359-
4360- def parse_set(self, value, from_db):
4361- if value is None:
4362- return None
4363- if not from_db:
4364- return value
4365- return self._enum(value)
4366-
4367- def parse_get(self, value, to_db):
4368- if value is None:
4369- return None
4370- if not to_db:
4371- return value
4372+ impl = Integer
4373+
4374+ def __init__(self, enum, *args, **kw):
4375+ self.enum = enum
4376+ super(Enum, self).__init__(*args, **kw)
4377+
4378+ def process_bind_param(self, value, dialect):
4379+ if value is None:
4380+ return None
4381 return value.value
4382
4383-
4384-class Enum(SimpleProperty):
4385- """Custom type for Storm supporting enums."""
4386-
4387- variable_class = _EnumVariable
4388-
4389- def __init__(self, enum=None):
4390- super(Enum, self).__init__(enum=enum)
4391+ def process_result_value(self, value, dialect):
4392+ if value is None:
4393+ return None
4394+ return self.enum(value)
4395+
4396+
4397+
4398
4399+class UUID(TypeDecorator):
4400+ """Platform-independent GUID type.
4401+
4402+ Uses Postgresql's UUID type, otherwise uses
4403+ CHAR(32), storing as stringified hex values.
4404+
4405+ """
4406+ impl = CHAR
4407+
4408+ def load_dialect_impl(self, dialect):
4409+ if dialect.name == 'postgresql':
4410+ return dialect.type_descriptor(postgresql.UUID())
4411+ else:
4412+ return dialect.type_descriptor(CHAR(32))
4413+
4414+ def process_bind_param(self, value, dialect):
4415+ if value is None:
4416+ return value
4417+ elif dialect.name == 'postgresql':
4418+ return str(value)
4419+ else:
4420+ if not isinstance(value, uuid.UUID):
4421+ return "%.32x" % uuid.UUID(value)
4422+ else:
4423+ # hexstring
4424+ return "%.32x" % value
4425+
4426+ def process_result_value(self, value, dialect):
4427+ if value is None:
4428+ return value
4429+ else:
4430+ return uuid.UUID(value)
4431
4432=== modified file 'src/mailman/handlers/docs/owner-recips.rst'
4433--- src/mailman/handlers/docs/owner-recips.rst 2012-03-23 20:34:54 +0000
4434+++ src/mailman/handlers/docs/owner-recips.rst 2014-11-01 03:48:29 +0000
4435@@ -41,7 +41,7 @@
4436 >>> handler.process(mlist_1, msg, msgdata)
4437 >>> dump_list(msgdata['recipients'])
4438 bart@example.com
4439-
4440+
4441 If Bart also disables his owner delivery, then no one could contact the list's
4442 owners. Since this is unacceptable, the site owner is used as a fallback.
4443
4444@@ -55,7 +55,7 @@
4445 a fallback.
4446
4447 >>> mlist_2 = create_list('beta@example.com')
4448- >>> mlist_2.administrators.member_count
4449+ >>> print(mlist_2.administrators.member_count)
4450 0
4451 >>> msgdata = {}
4452 >>> handler.process(mlist_2, msg, msgdata)
4453
4454=== modified file 'src/mailman/interfaces/database.py'
4455--- src/mailman/interfaces/database.py 2014-01-01 14:59:42 +0000
4456+++ src/mailman/interfaces/database.py 2014-11-01 03:48:29 +0000
4457@@ -24,7 +24,6 @@
4458 'DatabaseError',
4459 'IDatabase',
4460 'IDatabaseFactory',
4461- 'ITemporaryDatabase',
4462 ]
4463
4464
4465@@ -61,12 +60,7 @@
4466 """Abort the current transaction."""
4467
4468 store = Attribute(
4469- """The underlying Storm store on which you can do queries.""")
4470-
4471-
4472-
4473
4474-class ITemporaryDatabase(Interface):
4475- """Marker interface for test suite adaptation."""
4476+ """The underlying database object on which you can do queries.""")
4477
4478
4479
4480
4481
4482=== modified file 'src/mailman/interfaces/domain.py'
4483--- src/mailman/interfaces/domain.py 2014-04-28 15:23:35 +0000
4484+++ src/mailman/interfaces/domain.py 2014-11-01 03:48:29 +0000
4485@@ -97,7 +97,10 @@
4486 E.g. postmaster@example.com""")
4487
4488 mailing_lists = Attribute(
4489- 'All mailing lists for this domain.')
4490+ """All mailing lists for this domain.
4491+
4492+ The mailing lists are returned in order sorted by list-id.
4493+ """)
4494
4495 def confirm_url(token=''):
4496 """The url used for various forms of confirmation.
4497@@ -166,6 +169,8 @@
4498 def __iter__():
4499 """An iterator over all the domains.
4500
4501+ Domains are returned sorted by `mail_host`.
4502+
4503 :return: iterator over `IDomain`.
4504 """
4505
4506
4507=== modified file 'src/mailman/interfaces/listmanager.py'
4508--- src/mailman/interfaces/listmanager.py 2014-04-28 15:23:35 +0000
4509+++ src/mailman/interfaces/listmanager.py 2014-11-01 03:48:29 +0000
4510@@ -130,8 +130,10 @@
4511 """
4512
4513 mailing_lists = Attribute(
4514- """An iterator over all the mailing list objects managed by this list
4515- manager.""")
4516+ """An iterator over all the mailing list objects.
4517+
4518+ The mailing lists are returned in order sorted by `list_id`.
4519+ """)
4520
4521 def __iter__():
4522 """An iterator over all the mailing lists.
4523
4524=== modified file 'src/mailman/interfaces/messages.py'
4525--- src/mailman/interfaces/messages.py 2014-04-28 15:23:35 +0000
4526+++ src/mailman/interfaces/messages.py 2014-11-01 03:48:29 +0000
4527@@ -83,7 +83,7 @@
4528
4529 def get_message_by_hash(message_id_hash):
4530 """Return the message with the matching X-Message-ID-Hash.
4531-
4532+
4533 :param message_id_hash: The X-Message-ID-Hash header contents to
4534 search for.
4535 :returns: The message, or None if no matching message was found.
4536
4537=== modified file 'src/mailman/model/address.py'
4538--- src/mailman/model/address.py 2014-04-15 03:00:41 +0000
4539+++ src/mailman/model/address.py 2014-11-01 03:48:29 +0000
4540@@ -26,7 +26,9 @@
4541
4542
4543 from email.utils import formataddr
4544-from storm.locals import DateTime, Int, Reference, Unicode
4545+from sqlalchemy import (
4546+ Column, DateTime, ForeignKey, Integer, Unicode)
4547+from sqlalchemy.orm import relationship, backref
4548 from zope.component import getUtility
4549 from zope.event import notify
4550 from zope.interface import implementer
4551@@ -42,17 +44,20 @@
4552 class Address(Model):
4553 """See `IAddress`."""
4554
4555- id = Int(primary=True)
4556- email = Unicode()
4557- _original = Unicode()
4558- display_name = Unicode()
4559- _verified_on = DateTime(name='verified_on')
4560- registered_on = DateTime()
4561-
4562- user_id = Int()
4563- user = Reference(user_id, 'User.id')
4564- preferences_id = Int()
4565- preferences = Reference(preferences_id, 'Preferences.id')
4566+ __tablename__ = 'address'
4567+
4568+ id = Column(Integer, primary_key=True)
4569+ email = Column(Unicode)
4570+ _original = Column(Unicode)
4571+ display_name = Column(Unicode)
4572+ _verified_on = Column('verified_on', DateTime)
4573+ registered_on = Column(DateTime)
4574+
4575+ user_id = Column(Integer, ForeignKey('user.id'), index=True)
4576+
4577+ preferences_id = Column(Integer, ForeignKey('preferences.id'), index=True)
4578+ preferences = relationship(
4579+ 'Preferences', backref=backref('address', uselist=False))
4580
4581 def __init__(self, email, display_name):
4582 super(Address, self).__init__()
4583
4584=== modified file 'src/mailman/model/autorespond.py'
4585--- src/mailman/model/autorespond.py 2014-01-01 14:59:42 +0000
4586+++ src/mailman/model/autorespond.py 2014-11-01 03:48:29 +0000
4587@@ -26,7 +26,9 @@
4588 ]
4589
4590
4591-from storm.locals import And, Date, Desc, Int, Reference
4592+from sqlalchemy import Column, Date, ForeignKey, Integer
4593+from sqlalchemy import desc
4594+from sqlalchemy.orm import relationship
4595 from zope.interface import implementer
4596
4597 from mailman.database.model import Model
4598@@ -42,16 +44,18 @@
4599 class AutoResponseRecord(Model):
4600 """See `IAutoResponseRecord`."""
4601
4602- id = Int(primary=True)
4603-
4604- address_id = Int()
4605- address = Reference(address_id, 'Address.id')
4606-
4607- mailing_list_id = Int()
4608- mailing_list = Reference(mailing_list_id, 'MailingList.id')
4609-
4610- response_type = Enum(Response)
4611- date_sent = Date()
4612+ __tablename__ = 'autoresponserecord'
4613+
4614+ id = Column(Integer, primary_key=True)
4615+
4616+ address_id = Column(Integer, ForeignKey('address.id'), index=True)
4617+ address = relationship('Address')
4618+
4619+ mailing_list_id = Column(Integer, ForeignKey('mailinglist.id'), index=True)
4620+ mailing_list = relationship('MailingList')
4621+
4622+ response_type = Column(Enum(Response))
4623+ date_sent = Column(Date)
4624
4625 def __init__(self, mailing_list, address, response_type):
4626 self.mailing_list = mailing_list
4627@@ -71,12 +75,11 @@
4628 @dbconnection
4629 def todays_count(self, store, address, response_type):
4630 """See `IAutoResponseSet`."""
4631- return store.find(
4632- AutoResponseRecord,
4633- And(AutoResponseRecord.address == address,
4634- AutoResponseRecord.mailing_list == self._mailing_list,
4635- AutoResponseRecord.response_type == response_type,
4636- AutoResponseRecord.date_sent == today())).count()
4637+ return store.query(AutoResponseRecord).filter_by(
4638+ address=address,
4639+ mailing_list=self._mailing_list,
4640+ response_type=response_type,
4641+ date_sent=today()).count()
4642
4643 @dbconnection
4644 def response_sent(self, store, address, response_type):
4645@@ -88,10 +91,9 @@
4646 @dbconnection
4647 def last_response(self, store, address, response_type):
4648 """See `IAutoResponseSet`."""
4649- results = store.find(
4650- AutoResponseRecord,
4651- And(AutoResponseRecord.address == address,
4652- AutoResponseRecord.mailing_list == self._mailing_list,
4653- AutoResponseRecord.response_type == response_type)
4654- ).order_by(Desc(AutoResponseRecord.date_sent))
4655+ results = store.query(AutoResponseRecord).filter_by(
4656+ address=address,
4657+ mailing_list=self._mailing_list,
4658+ response_type=response_type
4659+ ).order_by(desc(AutoResponseRecord.date_sent))
4660 return (None if results.count() == 0 else results.first())
4661
4662=== modified file 'src/mailman/model/bans.py'
4663--- src/mailman/model/bans.py 2014-01-01 14:59:42 +0000
4664+++ src/mailman/model/bans.py 2014-11-01 03:48:29 +0000
4665@@ -27,7 +27,7 @@
4666
4667 import re
4668
4669-from storm.locals import Int, Unicode
4670+from sqlalchemy import Column, Integer, Unicode
4671 from zope.interface import implementer
4672
4673 from mailman.database.model import Model
4674@@ -40,9 +40,11 @@
4675 class Ban(Model):
4676 """See `IBan`."""
4677
4678- id = Int(primary=True)
4679- email = Unicode()
4680- list_id = Unicode()
4681+ __tablename__ = 'ban'
4682+
4683+ id = Column(Integer, primary_key=True)
4684+ email = Column(Unicode)
4685+ list_id = Column(Unicode)
4686
4687 def __init__(self, email, list_id):
4688 super(Ban, self).__init__()
4689@@ -62,7 +64,7 @@
4690 @dbconnection
4691 def ban(self, store, email):
4692 """See `IBanManager`."""
4693- bans = store.find(Ban, email=email, list_id=self._list_id)
4694+ bans = store.query(Ban).filter_by(email=email, list_id=self._list_id)
4695 if bans.count() == 0:
4696 ban = Ban(email, self._list_id)
4697 store.add(ban)
4698@@ -70,9 +72,10 @@
4699 @dbconnection
4700 def unban(self, store, email):
4701 """See `IBanManager`."""
4702- ban = store.find(Ban, email=email, list_id=self._list_id).one()
4703+ ban = store.query(Ban).filter_by(
4704+ email=email, list_id=self._list_id).first()
4705 if ban is not None:
4706- store.remove(ban)
4707+ store.delete(ban)
4708
4709 @dbconnection
4710 def is_banned(self, store, email):
4711@@ -81,32 +84,32 @@
4712 if list_id is None:
4713 # The client is asking for global bans. Look up bans on the
4714 # specific email address first.
4715- bans = store.find(Ban, email=email, list_id=None)
4716+ bans = store.query(Ban).filter_by(email=email, list_id=None)
4717 if bans.count() > 0:
4718 return True
4719 # And now look for global pattern bans.
4720- bans = store.find(Ban, list_id=None)
4721+ bans = store.query(Ban).filter_by(list_id=None)
4722 for ban in bans:
4723 if (ban.email.startswith('^') and
4724 re.match(ban.email, email, re.IGNORECASE) is not None):
4725 return True
4726 else:
4727 # This is a list-specific ban.
4728- bans = store.find(Ban, email=email, list_id=list_id)
4729+ bans = store.query(Ban).filter_by(email=email, list_id=list_id)
4730 if bans.count() > 0:
4731 return True
4732 # Try global bans next.
4733- bans = store.find(Ban, email=email, list_id=None)
4734+ bans = store.query(Ban).filter_by(email=email, list_id=None)
4735 if bans.count() > 0:
4736 return True
4737 # Now try specific mailing list bans, but with a pattern.
4738- bans = store.find(Ban, list_id=list_id)
4739+ bans = store.query(Ban).filter_by(list_id=list_id)
4740 for ban in bans:
4741 if (ban.email.startswith('^') and
4742 re.match(ban.email, email, re.IGNORECASE) is not None):
4743 return True
4744 # And now try global pattern bans.
4745- bans = store.find(Ban, list_id=None)
4746+ bans = store.query(Ban).filter_by(list_id=None)
4747 for ban in bans:
4748 if (ban.email.startswith('^') and
4749 re.match(ban.email, email, re.IGNORECASE) is not None):
4750
4751=== modified file 'src/mailman/model/bounce.py'
4752--- src/mailman/model/bounce.py 2014-01-01 14:59:42 +0000
4753+++ src/mailman/model/bounce.py 2014-11-01 03:48:29 +0000
4754@@ -26,7 +26,8 @@
4755 ]
4756
4757
4758-from storm.locals import Bool, Int, DateTime, Unicode
4759+
4760+from sqlalchemy import Boolean, Column, DateTime, Integer, Unicode
4761 from zope.interface import implementer
4762
4763 from mailman.database.model import Model
4764@@ -42,13 +43,15 @@
4765 class BounceEvent(Model):
4766 """See `IBounceEvent`."""
4767
4768- id = Int(primary=True)
4769- list_id = Unicode()
4770- email = Unicode()
4771- timestamp = DateTime()
4772- message_id = Unicode()
4773- context = Enum(BounceContext)
4774- processed = Bool()
4775+ __tablename__ = 'bounceevent'
4776+
4777+ id = Column(Integer, primary_key=True)
4778+ list_id = Column(Unicode)
4779+ email = Column(Unicode)
4780+ timestamp = Column(DateTime)
4781+ message_id = Column(Unicode)
4782+ context = Column(Enum(BounceContext))
4783+ processed = Column(Boolean)
4784
4785 def __init__(self, list_id, email, msg, context=None):
4786 self.list_id = list_id
4787@@ -75,12 +78,12 @@
4788 @dbconnection
4789 def events(self, store):
4790 """See `IBounceProcessor`."""
4791- for event in store.find(BounceEvent):
4792+ for event in store.query(BounceEvent).all():
4793 yield event
4794
4795 @property
4796 @dbconnection
4797 def unprocessed(self, store):
4798 """See `IBounceProcessor`."""
4799- for event in store.find(BounceEvent, BounceEvent.processed == False):
4800+ for event in store.query(BounceEvent).filter_by(processed=False):
4801 yield event
4802
4803=== modified file 'src/mailman/model/digests.py'
4804--- src/mailman/model/digests.py 2014-01-01 14:59:42 +0000
4805+++ src/mailman/model/digests.py 2014-11-01 03:48:29 +0000
4806@@ -25,7 +25,8 @@
4807 ]
4808
4809
4810-from storm.locals import Int, Reference
4811+from sqlalchemy import Column, Integer, ForeignKey
4812+from sqlalchemy.orm import relationship
4813 from zope.interface import implementer
4814
4815 from mailman.database.model import Model
4816@@ -39,15 +40,17 @@
4817 class OneLastDigest(Model):
4818 """See `IOneLastDigest`."""
4819
4820- id = Int(primary=True)
4821-
4822- mailing_list_id = Int()
4823- mailing_list = Reference(mailing_list_id, 'MailingList.id')
4824-
4825- address_id = Int()
4826- address = Reference(address_id, 'Address.id')
4827-
4828- delivery_mode = Enum(DeliveryMode)
4829+ __tablename__ = 'onelastdigest'
4830+
4831+ id = Column(Integer, primary_key=True)
4832+
4833+ mailing_list_id = Column(Integer, ForeignKey('mailinglist.id'))
4834+ mailing_list = relationship('MailingList')
4835+
4836+ address_id = Column(Integer, ForeignKey('address.id'))
4837+ address = relationship('Address')
4838+
4839+ delivery_mode = Column(Enum(DeliveryMode))
4840
4841 def __init__(self, mailing_list, address, delivery_mode):
4842 self.mailing_list = mailing_list
4843
4844=== modified file 'src/mailman/model/docs/autorespond.rst'
4845--- src/mailman/model/docs/autorespond.rst 2014-04-28 15:23:35 +0000
4846+++ src/mailman/model/docs/autorespond.rst 2014-11-01 03:48:29 +0000
4847@@ -37,34 +37,34 @@
4848 ... 'aperson@example.com')
4849
4850 >>> from mailman.interfaces.autorespond import Response
4851- >>> response_set.todays_count(address, Response.hold)
4852+ >>> print(response_set.todays_count(address, Response.hold))
4853 0
4854- >>> response_set.todays_count(address, Response.command)
4855+ >>> print(response_set.todays_count(address, Response.command))
4856 0
4857
4858 Using the response set, we can record that a hold response is sent to the
4859 address.
4860
4861 >>> response_set.response_sent(address, Response.hold)
4862- >>> response_set.todays_count(address, Response.hold)
4863+ >>> print(response_set.todays_count(address, Response.hold))
4864 1
4865- >>> response_set.todays_count(address, Response.command)
4866+ >>> print(response_set.todays_count(address, Response.command))
4867 0
4868
4869 We can also record that a command response was sent.
4870
4871 >>> response_set.response_sent(address, Response.command)
4872- >>> response_set.todays_count(address, Response.hold)
4873+ >>> print(response_set.todays_count(address, Response.hold))
4874 1
4875- >>> response_set.todays_count(address, Response.command)
4876+ >>> print(response_set.todays_count(address, Response.command))
4877 1
4878
4879 Let's send one more.
4880
4881 >>> response_set.response_sent(address, Response.command)
4882- >>> response_set.todays_count(address, Response.hold)
4883+ >>> print(response_set.todays_count(address, Response.hold))
4884 1
4885- >>> response_set.todays_count(address, Response.command)
4886+ >>> print(response_set.todays_count(address, Response.command))
4887 2
4888
4889 Now the day flips over and all the counts reset.
4890@@ -73,9 +73,9 @@
4891 >>> from mailman.utilities.datetime import factory
4892 >>> factory.fast_forward()
4893
4894- >>> response_set.todays_count(address, Response.hold)
4895+ >>> print(response_set.todays_count(address, Response.hold))
4896 0
4897- >>> response_set.todays_count(address, Response.command)
4898+ >>> print(response_set.todays_count(address, Response.command))
4899 0
4900
4901
4902@@ -110,7 +110,7 @@
4903
4904 >>> address = getUtility(IUserManager).create_address(
4905 ... 'bperson@example.com')
4906- >>> response_set.todays_count(address, Response.command)
4907+ >>> print(response_set.todays_count(address, Response.command))
4908 0
4909 >>> print(response_set.last_response(address, Response.command))
4910 None
4911
4912=== modified file 'src/mailman/model/docs/mailinglist.rst'
4913--- src/mailman/model/docs/mailinglist.rst 2014-04-28 15:23:35 +0000
4914+++ src/mailman/model/docs/mailinglist.rst 2014-11-01 03:48:29 +0000
4915@@ -50,7 +50,10 @@
4916
4917 Both addresses appear on the roster of members.
4918
4919- >>> for member in mlist.members.members:
4920+ >>> from operator import attrgetter
4921+ >>> sort_key = attrgetter('address.email')
4922+
4923+ >>> for member in sorted(mlist.members.members, key=sort_key):
4924 ... print(member)
4925 <Member: aperson@example.com on aardvark@example.com as MemberRole.member>
4926 <Member: bperson@example.com on aardvark@example.com as MemberRole.member>
4927@@ -72,7 +75,7 @@
4928 an owner and a moderator.
4929 ::
4930
4931- >>> for member in mlist.owners.members:
4932+ >>> for member in sorted(mlist.owners.members, key=sort_key):
4933 ... print(member)
4934 <Member: aperson@example.com on aardvark@example.com as MemberRole.owner>
4935 <Member: cperson@example.com on aardvark@example.com as MemberRole.owner>
4936@@ -87,13 +90,13 @@
4937 ::
4938
4939 >>> roster = mlist.get_roster(MemberRole.member)
4940- >>> for member in roster.members:
4941+ >>> for member in sorted(roster.members, key=sort_key):
4942 ... print(member)
4943 <Member: aperson@example.com on aardvark@example.com as MemberRole.member>
4944 <Member: bperson@example.com on aardvark@example.com as MemberRole.member>
4945
4946 >>> roster = mlist.get_roster(MemberRole.owner)
4947- >>> for member in roster.members:
4948+ >>> for member in sorted(roster.members, key=sort_key):
4949 ... print(member)
4950 <Member: aperson@example.com on aardvark@example.com as MemberRole.owner>
4951 <Member: cperson@example.com on aardvark@example.com as MemberRole.owner>
4952@@ -122,7 +125,7 @@
4953 >>> mlist.subscribe(user)
4954 <Member: Dave Person <dperson@example.com> on aardvark@example.com
4955 as MemberRole.member>
4956- >>> for member in mlist.members.members:
4957+ >>> for member in sorted(mlist.members.members, key=sort_key):
4958 ... print(member)
4959 <Member: aperson@example.com on aardvark@example.com as MemberRole.member>
4960 <Member: bperson@example.com on aardvark@example.com as MemberRole.member>
4961@@ -133,7 +136,7 @@
4962 >>> new_address.verified_on = now()
4963 >>> user.preferred_address = new_address
4964
4965- >>> for member in mlist.members.members:
4966+ >>> for member in sorted(mlist.members.members, key=sort_key):
4967 ... print(member)
4968 <Member: aperson@example.com on aardvark@example.com as MemberRole.member>
4969 <Member: bperson@example.com on aardvark@example.com as MemberRole.member>
4970
4971=== modified file 'src/mailman/model/docs/messagestore.rst'
4972--- src/mailman/model/docs/messagestore.rst 2014-04-28 15:23:35 +0000
4973+++ src/mailman/model/docs/messagestore.rst 2014-11-01 03:48:29 +0000
4974@@ -28,8 +28,9 @@
4975 However, if the message has a ``Message-ID`` header, it can be stored.
4976
4977 >>> msg['Message-ID'] = '<87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp>'
4978- >>> message_store.add(msg)
4979- 'AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35'
4980+ >>> x_message_id_hash = message_store.add(msg)
4981+ >>> print(x_message_id_hash)
4982+ AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35
4983 >>> print(msg.as_string())
4984 Subject: An important message
4985 Message-ID: <87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp>
4986
4987=== modified file 'src/mailman/model/docs/requests.rst'
4988--- src/mailman/model/docs/requests.rst 2014-04-28 15:23:35 +0000
4989+++ src/mailman/model/docs/requests.rst 2014-11-01 03:48:29 +0000
4990@@ -35,7 +35,7 @@
4991
4992 The list's requests database starts out empty.
4993
4994- >>> requests.count
4995+ >>> print(requests.count)
4996 0
4997 >>> dump_list(requests.held_requests)
4998 *Empty*
4999@@ -68,21 +68,21 @@
5000
The diff has been truncated for viewing.