Merge lp:~sinzui/launchpad/person-merge-job-1 into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Approved by: Robert Collins
Approved revision: no longer in the source branch.
Merged at revision: 12783
Proposed branch: lp:~sinzui/launchpad/person-merge-job-1
Merge into: lp:launchpad
Diff against target: 385 lines (+187/-23)
10 files modified
lib/lp/registry/emailtemplates/team-merged.txt (+8/-0)
lib/lp/registry/interfaces/personnotification.py (+9/-1)
lib/lp/registry/interfaces/persontransferjob.py (+3/-0)
lib/lp/registry/model/person.py (+13/-9)
lib/lp/registry/model/personnotification.py (+21/-6)
lib/lp/registry/model/persontransferjob.py (+8/-1)
lib/lp/registry/scripts/personnotification.py (+3/-2)
lib/lp/registry/tests/test_person_merge_job.py (+18/-4)
lib/lp/registry/tests/test_personnotification.py (+38/-0)
lib/lp/registry/tests/test_team.py (+66/-0)
To merge this branch: bzr merge lp:~sinzui/launchpad/person-merge-job-1
Reviewer Review Type Date Requested Status
Robert Collins (community) Approve
Review via email: mp+54440@code.launchpad.net

Description of the change

PersonNotification supports teams.

    Launchpad bug: https://bugs.launchpad.net/bugs/728471
    Pre-implementation: jcsackett
    Test command: ./bin/test -vv \
      -t person-merge -t test_person_merge_job
      -t test_personnotification -t TestTeamGetTeamAdminsEmailAddresses

Before bug 736421 can be addressed, there are several infrastructure issues
that must be solved first:

    * Person.merge needs send success emails to teams.
    * PersonNotification.send() must support teams.
    * Team.getTeamAdminsEmailAddresses() must guarantee for admins.
    * The job.getErrorRecipients must return the team admin addresses.

--------------------------------------------------------------------

RULES

    * Update Team.getTeamAdminsEmailAddresses() to recurse through
      team owner to locate team admins with email addresses.
    * Update PersonNotification.send() to send to team admins if the
      person is a team.
      * Consider a property that can be tested because the method
        and PersonNotificationManager are testing person.preferredemail
    * Update Person.merge to send to teams
      * Add a team email template.
    * Refine PersonMergeJob.getErrorRecipients to provide the user or
      team email addresses.

QA

    * Merge a team with a team you admin on qastaging.
    * Run cronscripts/send-person-notifications.py on qastaging.
    * Verify a message to you is in the staging sent folder.

LINT

    lib/lp/registry/doc/person-merge.txt
    lib/lp/registry/emailtemplates/team-merged.txt
    lib/lp/registry/interfaces/personnotification.py
    lib/lp/registry/interfaces/persontransferjob.py
    lib/lp/registry/model/person.py
    lib/lp/registry/model/personnotification.py
    lib/lp/registry/model/persontransferjob.py
    lib/lp/registry/scripts/personnotification.py
    lib/lp/registry/tests/test_person_merge_job.py
    lib/lp/registry/tests/test_personnotification.py
    lib/lp/registry/tests/test_team.py

IMPLEMENTATION

Updated Team.getTeamAdminsEmailAddresses() to recurse through
team owner to locate team admins with email addresses. This applies the
same kind of loop used in contact-this-teams-owner, but uses the
getDirectTeamAdmins() method instead. Then solves cases where the immediate
admin is a team without a contact address.
    lib/lp/registry/model/person.py
    lib/lp/registry/tests/test_team.py

Updated PersonNotification.send() to send to team admins when the
person is a team. Added to_addresses and can_send properties so that
the send method and PersonNotificationManager can handles teams as easily
as users. The to_addresses property uses the revised
Team.getTeamAdminsEmailAddresses() method.
    lib/lp/registry/interfaces/personnotification.py
    lib/lp/registry/model/personnotification.py
    lib/lp/registry/scripts/personnotification.py
    lib/lp/registry/tests/test_personnotification.py

Refined PersonMergeJob.getErrorRecipients to provide the user or
team email addresses. I had to explicitly define the interfaces because
of some nonsense in decorates() that is used to construct the PersonMergeJob.
The method uses the revised Team.getTeamAdminsEmailAddresses() method.
    lib/lp/registry/interfaces/persontransferjob.py
    lib/lp/registry/model/persontransferjob.py
    lib/lp/registry/tests/test_person_merge_job.py

Send merge success emails to the team. I added an email template and updated
the existing test to show it was sent. This implementation is not ideal; I
know that ~registry admins do not care to get team delete emails. I will
address this in my next branch which deal with integrating premerge/delete
logic into the model.
    lib/lp/registry/emailtemplates/team-merged.txt
    lib/lp/registry/model/person.py
    lib/lp/registry/doc/person-merge.txt

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

75 + def getErrorRecipients(self):
76 + """See `BaseRunnableJob`."""
77 +
78

thats a little odd - if the method is on the base interface, its redundant, and if its not on the base interface, a reference to that is pointless.

+ team_or_person = self
92 + # Recurse through team ownership until an email address is found.
93 + while len(to_addrs) == 0:
94 + if team_or_person.is_team:
95 + for admin in team_or_person.getDirectAdministrators():
96 + to_addrs.update(get_contact_email_addresses(admin))
97 + team_or_person = team_or_person.teamowner
98 + else:
99 + to_addrs.update(get_contact_email_addresses(team_or_person))

confused me a little. And once I wasn't confused I concluded it was buggy. Its buggy because it will only notify one administrator. (consider this: the owner is a team with two members. one member is a person, one member is another team with people in it.

The for loop will on the first pass, get one email address in to_addrs, and then go to the *owning team* of the owning team, and terminate. What should happen is that all the *members* of the owning team get mailed - we don't want to traverse owner of owner, just owner. (I think. Am I wrong?)

Perhaps I am wrong. Perhaps it actually says 'if the owning team is totally uncontactable, try harder'. Which seems like a pathological case - you'd need an empty team with only owners as an owning team. But we should handle it... perhaps by fixing emailPeople ? ['if you contact a team with no members the owner is contacted']

In fact, the more I think about it, the more that makes sense: emailPeople is wrong.

The rest seems uncontentious.

review: Needs Information
Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi Robert.

The believe oddness of the getErrorRecipients() interface is caused by PersonTransferJobDerived which breaks the permission chain.

{{{
class PersonTransferJobDerived(BaseRunnableJob):
    delegates(IPersonTransferJob)
}}}

Thank you for looking at the loop to get the the admin email addresses. I see it is repeating the problems of "contact this team" that I wanted to avoid. We want to send an email to the most immediate members who know about the team, the action, and can do something about it. I think there are two issues in the loop.

1 getDirectAdministrators() does not (get direct administrators). I own several teams that I am not a member of. I am *not* an admin, but this method is injecting the team owner into the list. I can use team.adminmembers to get just the members with the status ADMIN. This method is repeating the mistakes of the past where a user want to contact a team, but we walk the hierarchy of owner instead. Mark gets email about teams that are 3 and 4 teams removed from him :( This implementation was chosen because send emails to the individual members of the owner team fails when the owners do not have email address (team). This is a rare case, but it affects prominent teams.

2 If I had written a test where one of the admins was a team without an email address, I would have noticed that the rules do not traverse admin teams as I want. The code needs to locate an email address for the admin team. Choosing to get the contact address of the members will fail for the same reason as owner described above. The code needs to traverse a hierarchy to get a person or team with an email address.

Lets discuss this when you have time later today. I will work on the next branch which is independent of this branch.

Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi Robert
On Wed, 2011-03-23 at 01:55 +0000, Robert Collins wrote:
> + team_or_person = self
> 92 + # Recurse through team ownership until an email
> address is found.
> 93 + while len(to_addrs) == 0:
> 94 + if team_or_person.is_team:
> 95 + for admin in
> team_or_person.getDirectAdministrators():
> 96 +
> to_addrs.update(get_contact_email_addresses(admin))
> 97 + team_or_person = team_or_person.teamowner
> 98 + else:
> 99 +
> to_addrs.update(get_contact_email_addresses(team_or_person))

I switch to team.adminmembers to avoid special casing the team owner.
I removed the recursive loop to find an email address.
The code is very simple now, mostly because
get_contact_email_addresses(team) knows how to handle a team without a
contact address.

I updated the tests. Notably, I added a test to show there can be no
contact address when there are no admins. I added a test to verify the
case of an admining team without a contact address. I think this case
was missing in my first effort. There were no surprises that it worked
since get_contact_email_addresses is smart.

I have attached a diff, but you might want to read def
getTeamAdminsEmailAddresses and TestTeamGetTeamAdminsEmailAddresses to
verify the whole method is consistent.

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2011-04-01 20:26:29 +0000
+++ lib/lp/registry/model/person.py 2011-04-06 16:46:14 +0000
@@ -1423,15 +1423,8 @@
1423 """See `IPerson`."""1423 """See `IPerson`."""
1424 assert self.is_team1424 assert self.is_team
1425 to_addrs = set()1425 to_addrs = set()
1426 team_or_person = self1426 for admin in self.adminmembers:
1427 # Recurse through team ownership until an email address is found.1427 to_addrs.update(get_contact_email_addresses(admin))
1428 while len(to_addrs) == 0:
1429 if team_or_person.is_team:
1430 for admin in team_or_person.getDirectAdministrators():
1431 to_addrs.update(get_contact_email_addresses(admin))
1432 team_or_person = team_or_person.teamowner
1433 else:
1434 to_addrs.update(get_contact_email_addresses(team_or_person))
1435 return sorted(to_addrs)1428 return sorted(to_addrs)
14361429
1437 def addMember(self, person, reviewer, comment=None, force_team_add=False,1430 def addMember(self, person, reviewer, comment=None, force_team_add=False,
14381431
=== modified file 'lib/lp/registry/tests/test_team.py'
--- lib/lp/registry/tests/test_team.py 2011-03-25 17:28:15 +0000
+++ lib/lp/registry/tests/test_team.py 2011-04-06 17:04:26 +0000
@@ -133,23 +133,20 @@
133 def setUp(self):133 def setUp(self):
134 super(TestTeamGetTeamAdminsEmailAddresses, self).setUp()134 super(TestTeamGetTeamAdminsEmailAddresses, self).setUp()
135 self.team = self.factory.makeTeam(name='finch')135 self.team = self.factory.makeTeam(name='finch')
136 self.address = self.factory.makeEmail('team@eg.dom', self.team)
137 login_celebrity('admin')136 login_celebrity('admin')
138137
139 def test_team_direct_owner(self):138 def test_admin_is_user(self):
140 # The team owner's email address is only email address.139 # The team owner is a user and admin who provides the email address.
141 email = self.team.teamowner.preferredemail.email140 email = self.team.teamowner.preferredemail.email
142 self.assertEqual([email], self.team.getTeamAdminsEmailAddresses())141 self.assertEqual([email], self.team.getTeamAdminsEmailAddresses())
143142
144 def test_team_direct_owner_not_member(self):143 def test_no_admins(self):
145 # The team owner's email address is only email address, even when144 # A team without admins has no email addresses.
146 # the owner is not a team member.
147 email = self.team.teamowner.preferredemail.email
148 self.team.teamowner.leave(self.team)145 self.team.teamowner.leave(self.team)
149 self.assertEqual([email], self.team.getTeamAdminsEmailAddresses())146 self.assertEqual([], self.team.getTeamAdminsEmailAddresses())
150147
151 def test_team_direct_admin(self):148 def test_admins_are_users_with_preferred_email_addresses(self):
152 # The team's owner and direct admins provide the email addresses.149 # The team's admins are users, and they provide the email addresses.
153 admin = self.factory.makePerson()150 admin = self.factory.makePerson()
154 self.team.addMember(admin, self.team.teamowner)151 self.team.addMember(admin, self.team.teamowner)
155 for membership in self.team.member_memberships:152 for membership in self.team.member_memberships:
@@ -160,35 +157,37 @@
160 self.assertEqual(157 self.assertEqual(
161 [email_1, email_2], self.team.getTeamAdminsEmailAddresses())158 [email_1, email_2], self.team.getTeamAdminsEmailAddresses())
162159
163 def test_team_indirect_owner(self):160 def setUpAdminingTeam(self, team):
164 # The team owner's of the owning team is only email address.161 """Return a new team set as the admin of the provided team."""
165 owning_team = self.factory.makeTeam()162 admin_team = self.factory.makeTeam()
166 member = self.team.teamowner163 admin_member = self.factory.makePerson()
167 self.team.teamowner = owning_team164 admin_team.addMember(admin_member, admin_team.teamowner)
168 for membership in self.team.member_memberships:165 team.addMember(
169 membership.setStatus(166 admin_team, team.teamowner, force_team_add=True)
170 TeamMembershipStatus.APPROVED, owning_team.teamowner)167 for membership in team.member_memberships:
171 email = owning_team.teamowner.preferredemail.email168 membership.setStatus(
172 self.assertEqual([email], self.team.getTeamAdminsEmailAddresses())169 TeamMembershipStatus.ADMIN, admin_team.teamowner)
173170 approved_member = self.factory.makePerson()
174 def test_team_indirect_admin(self):171 team.addMember(approved_member, team.teamowner)
175 # The team owner and admin of the owning team provide the172 team.teamowner.leave(team)
176 # email addresses.173 return admin_team
177 owning_team = self.factory.makeTeam()174
178 member = self.team.teamowner175 def test_admins_are_teams_with_preferred_email_addresses(self):
179 self.team.teamowner = owning_team176 # The team's admin is a team without a contact address.
180 for membership in self.team.member_memberships:177 # The admin team members provide the email addresses.
181 membership.setStatus(178 admin_team = self.setUpAdminingTeam(self.team)
182 TeamMembershipStatus.APPROVED, owning_team.teamowner)179 admin_team.setContactAddress(
183 admin = self.factory.makePerson()180 self.factory.makeEmail('team@eg.dom', admin_team))
184 owning_team.addMember(admin, owning_team.teamowner)181 self.assertEqual(
185 for membership in owning_team.member_memberships:182 ['team@eg.dom'], self.team.getTeamAdminsEmailAddresses())
186 membership.setStatus(183
187 TeamMembershipStatus.ADMIN, owning_team.teamowner)184 def test_admins_are_teams_without_preferred_email_addresses(self):
188 email_1 = owning_team.teamowner.preferredemail.email185 # The team's admin is a team with a contact address.
189 email_2 = admin.preferredemail.email186 admin_team = self.setUpAdminingTeam(self.team)
190 self.assertEqual(187 emails = sorted(
191 [email_1, email_2], self.team.getTeamAdminsEmailAddresses())188 m.preferredemail.email for m in admin_team.activemembers)
189 self.assertEqual(
190 emails, self.team.getTeamAdminsEmailAddresses())
192191
193192
194class TestDefaultRenewalPeriodIsRequiredForSomeTeams(TestCaseWithFactory):193class TestDefaultRenewalPeriodIsRequiredForSomeTeams(TestCaseWithFactory):
195194
Revision history for this message
Robert Collins (lifeless) wrote :

 def test_admins_are_teams_with_preferred_email_addresses(self):
106 + # The team's admin is a team without a contact address.
107 + # The admin team members provide the email addresses.
108 + admin_team = self.setUpAdminingTeam(self.team)
109 + admin_team.setContactAddress(
110 + self.factory.makeEmail('<email address hidden>', admin_team))
111 + self.assertEqual(
112 + ['<email address hidden>'], self.team.getTeamAdminsEmailAddresses())
113 +
114 + def test_admins_are_teams_without_preferred_email_addresses(self):

looks like you have them inverted: that is that the name and comment of the former test describes the behaviour of the latter test, and vice versa.

Other than that this looks good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'lib/lp/registry/emailtemplates/team-merged.txt'
--- lib/lp/registry/emailtemplates/team-merged.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/emailtemplates/team-merged.txt 2011-04-08 02:56:00 +0000
@@ -0,0 +1,8 @@
1
2The Launchpad team named '%(dupename)s' was merged into the team
3named '%(person)s'. You can review the changes at:
4
5 https://launchpad.net/~%(person)s
6
7Regards,
8Launchpad
09
=== modified file 'lib/lp/registry/interfaces/personnotification.py'
--- lib/lp/registry/interfaces/personnotification.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/interfaces/personnotification.py 2011-04-08 02:56:00 +0000
@@ -11,7 +11,10 @@
11 'IPersonNotificationSet',11 'IPersonNotificationSet',
12 ]12 ]
1313
14from zope.interface import Interface14from zope.interface import (
15 Attribute,
16 Interface,
17 )
15from zope.schema import (18from zope.schema import (
16 Datetime,19 Datetime,
17 Object,20 Object,
@@ -38,6 +41,11 @@
38 body = Text(title=_("Notification body."))41 body = Text(title=_("Notification body."))
39 subject = TextLine(title=_("Notification subject."))42 subject = TextLine(title=_("Notification subject."))
4043
44 can_send = Attribute("Can the notification be sent?")
45
46 to_addresses = Attribute(
47 "The list of addresses to send the notification to.")
48
41 def destroySelf():49 def destroySelf():
42 """Delete this notification."""50 """Delete this notification."""
4351
4452
=== modified file 'lib/lp/registry/interfaces/persontransferjob.py'
--- lib/lp/registry/interfaces/persontransferjob.py 2011-03-25 23:42:33 +0000
+++ lib/lp/registry/interfaces/persontransferjob.py 2011-04-08 02:56:00 +0000
@@ -95,6 +95,9 @@
95 vocabulary='ValidPersonOrTeam',95 vocabulary='ValidPersonOrTeam',
96 required=True)96 required=True)
9797
98 def getErrorRecipients(self):
99 """See `BaseRunnableJob`."""
100
98101
99class IPersonMergeJobSource(IJobSource):102class IPersonMergeJobSource(IJobSource):
100 """An interface for acquiring IPersonMergeJobs."""103 """An interface for acquiring IPersonMergeJobs."""
101104
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2011-03-31 19:10:35 +0000
+++ lib/lp/registry/model/person.py 2011-04-08 02:56:00 +0000
@@ -1423,8 +1423,8 @@
1423 """See `IPerson`."""1423 """See `IPerson`."""
1424 assert self.is_team1424 assert self.is_team
1425 to_addrs = set()1425 to_addrs = set()
1426 for person in self.getDirectAdministrators():1426 for admin in self.adminmembers:
1427 to_addrs.update(get_contact_email_addresses(person))1427 to_addrs.update(get_contact_email_addresses(admin))
1428 return sorted(to_addrs)1428 return sorted(to_addrs)
14291429
1430 def addMember(self, person, reviewer, comment=None, force_team_add=False,1430 def addMember(self, person, reviewer, comment=None, force_team_add=False,
@@ -4065,16 +4065,20 @@
4065 """ % sqlvalues(to_person.accountID, from_person.accountID))4065 """ % sqlvalues(to_person.accountID, from_person.accountID))
40664066
4067 # Inform the user of the merge changes.4067 # Inform the user of the merge changes.
4068 if not to_person.isTeam():4068 if to_person.isTeam():
4069 mail_text = get_email_template(
4070 'team-merged.txt', app='registry')
4071 subject = 'Launchpad teams merged'
4072 else:
4069 mail_text = get_email_template(4073 mail_text = get_email_template(
4070 'person-merged.txt', app='registry')4074 'person-merged.txt', app='registry')
4071 mail_text = mail_text % {
4072 'dupename': from_person.name,
4073 'person': to_person.name,
4074 }
4075 subject = 'Launchpad accounts merged'4075 subject = 'Launchpad accounts merged'
4076 getUtility(IPersonNotificationSet).addNotification(4076 mail_text = mail_text % {
4077 to_person, subject, mail_text)4077 'dupename': from_person.name,
4078 'person': to_person.name,
4079 }
4080 getUtility(IPersonNotificationSet).addNotification(
4081 to_person, subject, mail_text)
40784082
4079 def getValidPersons(self, persons):4083 def getValidPersons(self, persons):
4080 """See `IPersonSet.`"""4084 """See `IPersonSet.`"""
40814085
=== modified file 'lib/lp/registry/model/personnotification.py'
--- lib/lp/registry/model/personnotification.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/model/personnotification.py 2011-04-08 02:56:00 +0000
@@ -29,6 +29,7 @@
29 IPersonNotification,29 IPersonNotification,
30 IPersonNotificationSet,30 IPersonNotificationSet,
31 )31 )
32from lp.services.propertycache import cachedproperty
32from lp.services.mail.sendmail import (33from lp.services.mail.sendmail import (
33 format_address,34 format_address,
34 simple_sendmail,35 simple_sendmail,
@@ -45,14 +46,29 @@
45 body = StringCol(notNull=True)46 body = StringCol(notNull=True)
46 subject = StringCol(notNull=True)47 subject = StringCol(notNull=True)
4748
49 @cachedproperty
50 def to_addresses(self):
51 """See `IPersonNotification`."""
52 if self.person.is_team:
53 return self.person.getTeamAdminsEmailAddresses()
54 elif self.person.preferredemail is None:
55 return []
56 else:
57 return [format_address(
58 self.person.displayname, self.person.preferredemail.email)]
59
60 @property
61 def can_send(self):
62 """See `IPersonNotification`."""
63 return len(self.to_addresses) > 0
64
48 def send(self):65 def send(self):
49 """See `IPersonNotification`."""66 """See `IPersonNotification`."""
50 assert self.person.preferredemail is not None, (67 if not self.can_send:
51 "Can't send a notification to a person without an email.")68 raise AssertionError(
69 "Can't send a notification to a person without an email.")
52 from_addr = config.canonical.bounce_address70 from_addr = config.canonical.bounce_address
53 to_addr = format_address(71 simple_sendmail(from_addr, self.to_addresses, self.subject, self.body)
54 self.person.displayname, self.person.preferredemail.email)
55 simple_sendmail(from_addr, to_addr, self.subject, self.body)
56 self.date_emailed = datetime.now(pytz.timezone('UTC'))72 self.date_emailed = datetime.now(pytz.timezone('UTC'))
5773
5874
@@ -73,4 +89,3 @@
73 """See `IPersonNotificationSet`."""89 """See `IPersonNotificationSet`."""
74 return PersonNotification.select(90 return PersonNotification.select(
75 'date_created < %s' % sqlvalues(time_limit))91 'date_created < %s' % sqlvalues(time_limit))
76
7792
=== modified file 'lib/lp/registry/model/persontransferjob.py'
--- lib/lp/registry/model/persontransferjob.py 2011-03-25 23:42:33 +0000
+++ lib/lp/registry/model/persontransferjob.py 2011-04-08 02:56:00 +0000
@@ -64,6 +64,7 @@
64from lp.services.database.stormbase import StormBase64from lp.services.database.stormbase import StormBase
65from lp.services.job.model.job import Job65from lp.services.job.model.job import Job
66from lp.services.job.runner import BaseRunnableJob66from lp.services.job.runner import BaseRunnableJob
67from lp.services.mail.sendmail import format_address_for_person
6768
6869
69class PersonTransferJob(StormBase):70class PersonTransferJob(StormBase):
@@ -393,6 +394,13 @@
393 def log_name(self):394 def log_name(self):
394 return self.__class__.__name__395 return self.__class__.__name__
395396
397 def getErrorRecipients(self):
398 """See `IPersonMergeJob`."""
399 if self.to_person.is_team:
400 return self.to_person.getTeamAdminsEmailAddresses()
401 else:
402 return [format_address_for_person(self.to_person)]
403
396 def run(self):404 def run(self):
397 """Perform the merge."""405 """Perform the merge."""
398 from_person_name = self.from_person.name406 from_person_name = self.from_person.name
@@ -402,7 +410,6 @@
402 log.debug(410 log.debug(
403 "%s is about to merge ~%s into ~%s", self.log_name,411 "%s is about to merge ~%s into ~%s", self.log_name,
404 from_person_name, to_person_name)412 from_person_name, to_person_name)
405
406 getUtility(IPersonSet).merge(413 getUtility(IPersonSet).merge(
407 from_person=self.from_person, to_person=self.to_person,414 from_person=self.from_person, to_person=self.to_person,
408 reviewer=self.reviewer)415 reviewer=self.reviewer)
409416
=== modified file 'lib/lp/registry/scripts/personnotification.py'
--- lib/lp/registry/scripts/personnotification.py 2010-12-08 17:22:23 +0000
+++ lib/lp/registry/scripts/personnotification.py 2011-04-08 02:56:00 +0000
@@ -38,14 +38,15 @@
38 '%d notification(s) to send.' % pending_notifications.count())38 '%d notification(s) to send.' % pending_notifications.count())
39 for notification in pending_notifications:39 for notification in pending_notifications:
40 person = notification.person40 person = notification.person
41 if person.preferredemail is None:41 if not notification.can_send:
42 unsent_notifications.append(notification)42 unsent_notifications.append(notification)
43 self.logger.info(43 self.logger.info(
44 "%s has no email address." % person.name)44 "%s has no email address." % person.name)
45 continue45 continue
46 self.logger.info(46 self.logger.info(
47 "Sending notification to %s <%s>."47 "Sending notification to %s <%s>."
48 % (person.name, removeSecurityProxy(person).preferredemail.email))48 % (person.name,
49 removeSecurityProxy(person).preferredemail.email))
49 notification.send()50 notification.send()
50 notifications_sent = True51 notifications_sent = True
51 # Commit after each email sent, so that we won't re-mail the52 # Commit after each email sent, so that we won't re-mail the
5253
=== modified file 'lib/lp/registry/tests/test_person_merge_job.py'
--- lib/lp/registry/tests/test_person_merge_job.py 2011-03-25 23:42:33 +0000
+++ lib/lp/registry/tests/test_person_merge_job.py 2011-04-08 02:56:00 +0000
@@ -26,6 +26,7 @@
26from lp.services.job.interfaces.job import JobStatus26from lp.services.job.interfaces.job import JobStatus
27from lp.services.job.model.job import Job27from lp.services.job.model.job import Job
28from lp.services.log.logger import BufferLogger28from lp.services.log.logger import BufferLogger
29from lp.services.mail.sendmail import format_address_for_person
29from lp.testing import (30from lp.testing import (
30 run_script,31 run_script,
31 person_logged_in,32 person_logged_in,
@@ -39,10 +40,8 @@
3940
40 def setUp(self):41 def setUp(self):
41 super(TestPersonMergeJob, self).setUp()42 super(TestPersonMergeJob, self).setUp()
42 self.from_person = self.factory.makePerson(43 self.from_person = self.factory.makePerson(name='void')
43 name='void', email='void@eg.dom')44 self.to_person = self.factory.makePerson(name='gestalt')
44 self.to_person = self.factory.makePerson(
45 name='gestalt', email='gestalt@eg.dom')
46 self.job_source = getUtility(IPersonMergeJobSource)45 self.job_source = getUtility(IPersonMergeJobSource)
47 self.job = self.job_source.create(46 self.job = self.job_source.create(
48 from_person=self.from_person, to_person=self.to_person)47 from_person=self.from_person, to_person=self.to_person)
@@ -59,6 +58,21 @@
59 self.assertEqual(self.to_person, self.job.major_person)58 self.assertEqual(self.to_person, self.job.major_person)
60 self.assertEqual({}, self.job.metadata)59 self.assertEqual({}, self.job.metadata)
6160
61 def test_getErrorRecipients_user(self):
62 # The to_person is the recipient.
63 email_id = format_address_for_person(self.to_person)
64 self.assertEqual([email_id], self.job.getErrorRecipients())
65
66 def test_getErrorRecipients_team(self):
67 # The to_person admins are the recipients.
68 to_team = self.factory.makeTeam()
69 from_team = self.factory.makeTeam()
70 job = self.job_source.create(
71 from_person=from_team, to_person=to_team,
72 reviewer=to_team.teamowner)
73 self.assertEqual(
74 to_team.getTeamAdminsEmailAddresses(), job.getErrorRecipients())
75
62 def test_enqueue(self):76 def test_enqueue(self):
63 # Newly created jobs are enqueued.77 # Newly created jobs are enqueued.
64 self.assertEqual([self.job], list(self.job_source.iterReady()))78 self.assertEqual([self.job], list(self.job_source.iterReady()))
6579
=== modified file 'lib/lp/registry/tests/test_personnotification.py'
--- lib/lp/registry/tests/test_personnotification.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/tests/test_personnotification.py 2011-04-08 02:56:00 +0000
@@ -27,6 +27,44 @@
27from lp.testing import TestCaseWithFactory27from lp.testing import TestCaseWithFactory
2828
2929
30class TestPersonNotification(TestCaseWithFactory):
31 """Tests for PersonNotification."""
32
33 layer = DatabaseFunctionalLayer
34
35 def setUp(self):
36 super(TestPersonNotification, self).setUp()
37 self.notification_set = getUtility(IPersonNotificationSet)
38
39 def test_to_addresses_user(self):
40 # The to_addresses list is the user's preferred email address.
41 user = self.factory.makePerson()
42 notification = self.notification_set.addNotification(
43 user, 'subject', 'body')
44 email = '%s <%s>' % (
45 user.displayname, removeSecurityProxy(user.preferredemail).email)
46 self.assertEqual([email], notification.to_addresses)
47 self.assertTrue(notification.can_send)
48
49 def test_to_addresses_user_without_address(self):
50 # The to_addresses list is empty and the notification cannot be sent.
51 user = self.factory.makePerson()
52 user.setPreferredEmail(None)
53 notification = self.notification_set.addNotification(
54 user, 'subject', 'body')
55 self.assertEqual([], notification.to_addresses)
56 self.assertFalse(notification.can_send)
57
58 def test_to_addresses_team(self):
59 # The to_addresses list is the team admin addresses.
60 team = self.factory.makeTeam()
61 notification = self.notification_set.addNotification(
62 team, 'subject', 'body')
63 email = removeSecurityProxy(team.teamowner.preferredemail).email
64 self.assertEqual([email], notification.to_addresses)
65 self.assertTrue(notification.can_send)
66
67
30class TestPersonNotificationManager(TestCaseWithFactory):68class TestPersonNotificationManager(TestCaseWithFactory):
31 """Tests for the PersonNotificationManager use in scripts."""69 """Tests for the PersonNotificationManager use in scripts."""
3270
3371
=== modified file 'lib/lp/registry/tests/test_team.py'
--- lib/lp/registry/tests/test_team.py 2011-03-23 16:28:51 +0000
+++ lib/lp/registry/tests/test_team.py 2011-04-08 02:56:00 +0000
@@ -30,6 +30,7 @@
30 TeamMembershipRenewalPolicy,30 TeamMembershipRenewalPolicy,
31 TeamSubscriptionPolicy,31 TeamSubscriptionPolicy,
32 )32 )
33from lp.registry.interfaces.teammembership import TeamMembershipStatus
33from lp.registry.model.persontransferjob import PersonTransferJob34from lp.registry.model.persontransferjob import PersonTransferJob
34from lp.soyuz.enums import ArchiveStatus35from lp.soyuz.enums import ArchiveStatus
35from lp.testing import (36from lp.testing import (
@@ -124,6 +125,71 @@
124 self.assertEqual([], self.getAllEmailAddresses())125 self.assertEqual([], self.getAllEmailAddresses())
125126
126127
128class TestTeamGetTeamAdminsEmailAddresses(TestCaseWithFactory):
129 """Test the rules of IPerson.getTeamAdminsEmailAddresses()."""
130
131 layer = DatabaseFunctionalLayer
132
133 def setUp(self):
134 super(TestTeamGetTeamAdminsEmailAddresses, self).setUp()
135 self.team = self.factory.makeTeam(name='finch')
136 login_celebrity('admin')
137
138 def test_admin_is_user(self):
139 # The team owner is a user and admin who provides the email address.
140 email = self.team.teamowner.preferredemail.email
141 self.assertEqual([email], self.team.getTeamAdminsEmailAddresses())
142
143 def test_no_admins(self):
144 # A team without admins has no email addresses.
145 self.team.teamowner.leave(self.team)
146 self.assertEqual([], self.team.getTeamAdminsEmailAddresses())
147
148 def test_admins_are_users_with_preferred_email_addresses(self):
149 # The team's admins are users, and they provide the email addresses.
150 admin = self.factory.makePerson()
151 self.team.addMember(admin, self.team.teamowner)
152 for membership in self.team.member_memberships:
153 membership.setStatus(
154 TeamMembershipStatus.ADMIN, self.team.teamowner)
155 email_1 = self.team.teamowner.preferredemail.email
156 email_2 = admin.preferredemail.email
157 self.assertEqual(
158 [email_1, email_2], self.team.getTeamAdminsEmailAddresses())
159
160 def setUpAdminingTeam(self, team):
161 """Return a new team set as the admin of the provided team."""
162 admin_team = self.factory.makeTeam()
163 admin_member = self.factory.makePerson()
164 admin_team.addMember(admin_member, admin_team.teamowner)
165 team.addMember(
166 admin_team, team.teamowner, force_team_add=True)
167 for membership in team.member_memberships:
168 membership.setStatus(
169 TeamMembershipStatus.ADMIN, admin_team.teamowner)
170 approved_member = self.factory.makePerson()
171 team.addMember(approved_member, team.teamowner)
172 team.teamowner.leave(team)
173 return admin_team
174
175 def test_admins_are_teams_with_preferred_email_addresses(self):
176 # The team's admin is a team with a contact address.
177 admin_team = self.setUpAdminingTeam(self.team)
178 admin_team.setContactAddress(
179 self.factory.makeEmail('team@eg.dom', admin_team))
180 self.assertEqual(
181 ['team@eg.dom'], self.team.getTeamAdminsEmailAddresses())
182
183 def test_admins_are_teams_without_preferred_email_addresses(self):
184 # The team's admin is a team without a contact address.
185 # The admin team members provide the email addresses.
186 admin_team = self.setUpAdminingTeam(self.team)
187 emails = sorted(
188 m.preferredemail.email for m in admin_team.activemembers)
189 self.assertEqual(
190 emails, self.team.getTeamAdminsEmailAddresses())
191
192
127class TestDefaultRenewalPeriodIsRequiredForSomeTeams(TestCaseWithFactory):193class TestDefaultRenewalPeriodIsRequiredForSomeTeams(TestCaseWithFactory):
128194
129 layer = DatabaseFunctionalLayer195 layer = DatabaseFunctionalLayer