diff options
| author | Aurélien Bompard | 2015-07-29 16:46:34 +0200 |
|---|---|---|
| committer | Barry Warsaw | 2016-05-04 21:05:49 -0500 |
| commit | 7216f6366ff51b034e420efb1206d1676dce78ef (patch) | |
| tree | 4a7a3081494dae197a2a0b9e247e6373e90710ec /src | |
| parent | 3ea95814ac6eb10445bce46f0cc33489efd122d1 (diff) | |
| download | mailman-7216f6366ff51b034e420efb1206d1676dce78ef.tar.gz mailman-7216f6366ff51b034e420efb1206d1676dce78ef.tar.zst mailman-7216f6366ff51b034e420efb1206d1676dce78ef.zip | |
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/interfaces/preferences.py | 6 | ||||
| -rw-r--r-- | src/mailman/interfaces/user.py | 8 | ||||
| -rw-r--r-- | src/mailman/model/preferences.py | 14 | ||||
| -rw-r--r-- | src/mailman/model/tests/test_preferences.py | 89 | ||||
| -rw-r--r-- | src/mailman/model/tests/test_user.py | 114 | ||||
| -rw-r--r-- | src/mailman/model/user.py | 31 |
6 files changed, 262 insertions, 0 deletions
diff --git a/src/mailman/interfaces/preferences.py b/src/mailman/interfaces/preferences.py index 4b5b490cb..84f85820a 100644 --- a/src/mailman/interfaces/preferences.py +++ b/src/mailman/interfaces/preferences.py @@ -68,3 +68,9 @@ class IPreferences(Interface): which means that no preference is specified. XXX I'm not sure this is the right place to put this.""") + + def absorb(preferences): + """Merge these preferences, and then delete them. + + Only this instance's unset preferences (None/NULL) will be imported. + """ diff --git a/src/mailman/interfaces/user.py b/src/mailman/interfaces/user.py index f1932fd7f..bdeccce3f 100644 --- a/src/mailman/interfaces/user.py +++ b/src/mailman/interfaces/user.py @@ -109,3 +109,11 @@ class IUser(Interface): preferences = Attribute( """This user's preferences.""") + + def absorb(user): + """Merge this user's attributes and memberships, and then delete it. + + In case of conflict, the current user's properties are preserved. + If an IAddress is given, the merge will be performed on the addresses' + linked user. + """ diff --git a/src/mailman/model/preferences.py b/src/mailman/model/preferences.py index 53bfe6975..22d22ed94 100644 --- a/src/mailman/model/preferences.py +++ b/src/mailman/model/preferences.py @@ -20,6 +20,7 @@ from mailman import public from mailman.database.model import Model from mailman.database.types import Enum +from mailman.database.transaction import dbconnection from mailman.interfaces.languages import ILanguageManager from mailman.interfaces.member import DeliveryMode, DeliveryStatus from mailman.interfaces.preferences import IPreferences @@ -43,6 +44,8 @@ class Preferences(Model): receive_own_postings = Column(Boolean) delivery_mode = Column(Enum(DeliveryMode)) delivery_status = Column(Enum(DeliveryStatus)) + # When adding new columns, also add them to + # mailman.model.tests.test_preferences.TestPreferences.test_absorb_all_attributes() def __repr__(self): return '<Preferences object at {:#x}>'.format(id(self)) @@ -62,3 +65,14 @@ class Preferences(Model): self._preferred_language = language.code except AttributeError: self._preferred_language = language + + @dbconnection + def absorb(self, store, preferences): + """See `IPreferences`.""" + column_names = [ c.name for c in self.__table__.columns + if not c.primary_key ] + for cname in column_names: + if (getattr(self, cname) is None and + getattr(preferences, cname) is not None): + setattr(self, cname, getattr(preferences, cname)) + store.delete(preferences) diff --git a/src/mailman/model/tests/test_preferences.py b/src/mailman/model/tests/test_preferences.py new file mode 100644 index 000000000..7b9d25356 --- /dev/null +++ b/src/mailman/model/tests/test_preferences.py @@ -0,0 +1,89 @@ +# Copyright (C) 2015 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Test preferences.""" + +__all__ = [ + 'TestPreferences', + ] + + +import unittest + +from mailman.app.lifecycle import create_list +from mailman.database.transaction import transaction +from mailman.interfaces.usermanager import IUserManager +from mailman.interfaces.languages import ILanguageManager +from mailman.interfaces.member import DeliveryMode, DeliveryStatus +from mailman.model.preferences import Preferences +from mailman.testing.layers import ConfigLayer +from zope.component import getUtility + + + +class TestPreferences(unittest.TestCase): + """Test preferences.""" + + layer = ConfigLayer + + def setUp(self): + self._manager = getUtility(IUserManager) + with transaction(): + # This has to happen in a transaction so that both the user and + # the preferences objects get valid ids. + self._mlist = create_list('test@example.com') + self._anne = self._manager.create_user( + 'anne@example.com', 'Anne Person') + self._bill = self._manager.create_user( + 'bill@example.com', 'Bill Person') + + def test_absorb_all_attributes(self): + attributes = { + 'acknowledge_posts': True, + 'hide_address': True, + 'preferred_language': + getUtility(ILanguageManager)['fr'], + 'receive_list_copy': True, + 'receive_own_postings': True, + 'delivery_mode': DeliveryMode.mime_digests, + 'delivery_status': DeliveryStatus.by_user, + } + bill_prefs = self._bill.preferences + for name, value in attributes.items(): + setattr(bill_prefs, name, value) + self._anne.preferences.absorb(self._bill.preferences) + for name, value in attributes.items(): + self.assertEqual(getattr(self._anne.preferences, name), value) + + def test_absorb_overwrite(self): + # Only overwrite the pref if it is unset in the absorber + anne_prefs = self._anne.preferences + bill_prefs = self._bill.preferences + self.assertEqual(self._anne.preferences.acknowledge_posts, None) + self.assertEqual(self._anne.preferences.hide_address, None) + self.assertEqual(self._anne.preferences.receive_list_copy, None) + anne_prefs.acknowledge_posts = False + bill_prefs.acknowledge_posts = True + anne_prefs.hide_address = True + bill_prefs.receive_list_copy = True + self._anne.preferences.absorb(self._bill.preferences) + # set for both anne and bill, don't overwrite + self.assertEqual(self._anne.preferences.acknowledge_posts, False) + # set only for anne + self.assertEqual(self._anne.preferences.hide_address, True) + # set only for bill, overwrite anne's default value + self.assertEqual(self._anne.preferences.receive_list_copy, True) diff --git a/src/mailman/model/tests/test_user.py b/src/mailman/model/tests/test_user.py index 7c8a3581e..7aaf78343 100644 --- a/src/mailman/model/tests/test_user.py +++ b/src/mailman/model/tests/test_user.py @@ -26,6 +26,7 @@ from mailman.interfaces.address import ( AddressAlreadyLinkedError, AddressNotLinkedError) from mailman.interfaces.user import UnverifiedAddressError from mailman.interfaces.usermanager import IUserManager +from mailman.interfaces.member import MemberRole from mailman.model.preferences import Preferences from mailman.testing.helpers import set_preferred from mailman.testing.layers import ConfigLayer @@ -120,3 +121,116 @@ class TestUser(unittest.TestCase): preferences = config.db.store.query(Preferences).filter_by( id=user.preferences.id) self.assertEqual(preferences.count(), 0) + + def test_absorb_addresses(self): + anne_addr = self._anne.preferred_address + with transaction(): + # This has to happen in a transaction so that both the user and + # the preferences objects get valid ids. + bill = self._manager.create_user( + 'bill@example.com', 'Bill Person') + bill_addr_2 = self._manager.create_address('bill2@example.com') + bill.link(bill_addr_2) + self._anne.absorb(bill) + self.assertIn('bill@example.com', + list(a.email for a in self._anne.addresses)) + self.assertIn('bill2@example.com', + list(a.email for a in self._anne.addresses)) + # the preferred address shouldn't change + self.assertEqual(self._anne.preferred_address, anne_addr) + + def test_absorb_memberships(self): + mlist2 = create_list('test2@example.com') + mlist3 = create_list('test3@example.com') + with transaction(): + # This has to happen in a transaction so that both the user and + # the preferences objects get valid ids. + bill = self._manager.create_user( + 'bill@example.com', 'Bill Person') + bill_address = list(bill.addresses)[0] + bill_address.verified_on = now() + bill.preferred_address = bill_address + # Subscribe both users to self._mlist + self._mlist.subscribe(self._anne, MemberRole.member) + self._mlist.subscribe(bill, MemberRole.moderator) + # Subscribe only bill to mlist2 + mlist2.subscribe(bill, MemberRole.owner) + # Subscribe only bill's address to mlist3 + mlist3.subscribe(bill.preferred_address, MemberRole.moderator) + self._anne.absorb(bill) + # check that anne is subscribed to all lists + self.assertEqual(self._anne.memberships.member_count, 3) + memberships = {} + for member in self._anne.memberships.members: + memberships[member.list_id] = member + self.assertEqual( + set(memberships.keys()), + set(['test.example.com', 'test2.example.com', 'test3.example.com'])) + # The subscription to test@example.com already existed, it must not be + # overwritten. + self.assertEqual( + memberships['test.example.com'].role, MemberRole.member) + # Check that the subscription roles were imported + self.assertEqual( + memberships['test2.example.com'].role, MemberRole.owner) + self.assertEqual( + memberships['test3.example.com'].role, MemberRole.moderator) + # The user bill was subscribed, the subscription must thus be + # transferred to anne's primary address. + self.assertEqual( + memberships['test2.example.com'].address, + self._anne.preferred_address) + # The address was subscribed, it must not be changed + self.assertEqual( + memberships['test3.example.com'].address.email, + 'bill@example.com') + + def test_absorb_preferences(self): + with transaction(): + # This has to happen in a transaction so that both the user and + # the preferences objects get valid ids. + bill = self._manager.create_user( + 'bill@example.com', 'Bill Person') + bill.preferences.acknowledge_posts = True + self.assertIsNone(self._anne.preferences.acknowledge_posts) + self._anne.absorb(bill) + self.assertEqual(self._anne.preferences.acknowledge_posts, True) + + def test_absorb_properties(self): + props = { + 'password': 'dummy', + 'is_server_owner': True + } + with transaction(): + # This has to happen in a transaction so that both the user and + # the preferences objects get valid ids. + bill = self._manager.create_user( + 'bill@example.com', 'Bill Person') + for prop, value in props.items(): + setattr(bill, prop, value) + self._anne.absorb(bill) + for prop, value in props.items(): + self.assertEqual(getattr(self._anne, prop), value) + # This was not empty so it must not be overwritten + self.assertEqual(self._anne.display_name, 'Anne Person') + + def test_absorb_delete_user(self): + # Make sure the user was deleted + with transaction(): + # This has to happen in a transaction so that both the user and + # the preferences objects get valid ids. + bill = self._manager.create_user( + 'bill@example.com', 'Bill Person') + bill_user_id = bill.user_id + self._anne.absorb(bill) + self.assertIsNone(self._manager.get_user_by_id(bill_user_id)) + + def test_absorb_self(self): + # Absorbing oneself should be a no-op (it must not delete the user) + self._mlist.subscribe(self._anne) + self._anne.absorb(self._anne) + new_anne = self._manager.get_user_by_id(self._anne.user_id) + self.assertIsNotNone(new_anne) + self.assertEqual( + [a.email for a in new_anne.addresses], ['anne@example.com']) + self.assertEqual(new_anne.memberships.member_count, 1) diff --git a/src/mailman/model/user.py b/src/mailman/model/user.py index 767600bdc..3d02b2777 100644 --- a/src/mailman/model/user.py +++ b/src/mailman/model/user.py @@ -26,10 +26,12 @@ from mailman.interfaces.address import ( from mailman.interfaces.user import ( IUser, PasswordChangeEvent, UnverifiedAddressError) from mailman.model.address import Address +from mailman.model.member import Member from mailman.model.preferences import Preferences from mailman.model.roster import Memberships from mailman.utilities.datetime import factory as date_factory from mailman.utilities.uid import UIDFactory +from sqlalchemy import not_ from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, Unicode from sqlalchemy.orm import backref, relationship from zope.event import notify @@ -174,6 +176,35 @@ class User(Model): def memberships(self): return Memberships(self) + @dbconnection + def absorb(self, store, user): + """See `IUser`.""" + assert user is not None + if user.id == self.id: + return # Protect against absorbing oneself. + # Relink addresses. + for address in list(user.addresses): + # convert to list because we'll mutate the result + address.user = self + # Merge memberships. + other_members = store.query(Member).filter( + Member.user_id == user.id) + # (only import memberships of lists I'm not subscribed to yet) + subscribed_lists = [ m.list_id for m in self.memberships.members ] + if subscribed_lists: + other_members = other_members.filter( + not_(Member.list_id.in_(subscribed_lists))) + for member in other_members: + member.user_id = self.id + # Merge the user preferences + self.preferences.absorb(user.preferences) + # Merge display_name, password and is_server_owner attributes. + for prop in ('display_name', 'password', 'is_server_owner'): + if getattr(user, prop) and not getattr(self, prop): + setattr(self, prop, getattr(user, prop)) + # Delete the other user. + store.delete(user) + @public class DomainOwner(Model): |
