Merge lp:~sinzui/launchpad/contact-team into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: 14780
Proposed branch: lp:~sinzui/launchpad/contact-team
Merge into: lp:launchpad
Diff against target: 488 lines (+59/-213)
6 files modified
lib/lp/registry/browser/person.py (+25/-54)
lib/lp/registry/browser/tests/person-views.txt (+1/-22)
lib/lp/registry/browser/tests/team-views.txt (+2/-2)
lib/lp/registry/browser/tests/user-to-user-views.txt (+20/-116)
lib/lp/registry/stories/team/xx-team-home.txt (+10/-18)
lib/lp/registry/templates/contact-user.pt (+1/-1)
To merge this branch: bzr merge lp:~sinzui/launchpad/contact-team
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+92589@code.launchpad.net

Commit message

[r=bac][bug=246022] Allow team members to contact all the team members, non-members contact the team admins.

Description of the change

Allow team members to contact all the team members, non-members contact the
team admins.

    Launchpad bug: https://bugs.launchpad.net/bugs/246022
    Pre-implementation: lifeless

A team administrator cannot contact all members at once if, for example,
the contact address for the team is a mailing list, and not all members
are subscribed to it.

This issues has mutated over the years. When bug mail was out of control,
we decided to honour a team's choice to set a contact address to send
all email to a black hole. Bug mail is no longer an issue. When a member
choose the contact-this-team form, Launchpad will send email to all members
so that important messages can be sent. contact-this-team is still not
substitute for mailing lists because we limit its use to 3 time a day.

While discussing the rules recently, we discovered that we did not update
contact-this-team to contact team admins instead of the owner. We allow
the owner to leave the team, delegating all responsibility to the admins.

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

RULES

    * Delete the rules for when the team has a contact address, allowing
      the team member rules to be the only rule in play.
    * Change the contact owner rule to contact admins.
    * ADDENDUM: I cannot 'hit' the cancel link. I do not think the cancel
      is a verb and suffices.

QA

    * Visit https://qastaging.launchpad.net/~registry
    * Set the contact address to the mailing list.
    * Choose contact this team's members.
    * Verify the page says
      You are contacting nn members of the Registry Administrators (registry)
      team directly.
    * Verify the page says
       If you do not want Registry Administrators to know your email address,
       _cancel_ now.

    * Visit https://qastaging.launchpad.net/~bzr (or a
      team you are not a member of)
    * Choose contact this team's admins.
    * Verify the page says
      You are contacting the Bazaar Developers (bzr) team admins.

LINT

    lib/lp/registry/browser/person.py
    lib/lp/registry/browser/tests/person-views.txt
    lib/lp/registry/browser/tests/team-views.txt
    lib/lp/registry/browser/tests/user-to-user-views.txt
    lib/lp/registry/stories/team/xx-team-home.txt
    lib/lp/registry/templates/contact-user.pt

TEST

    ./bin/test -vcc -t user-to-user-views -t person-views -t team-views \
        -t xx-team-home lp.registry

IMPLEMENTATION

Removed the TO_TEAM state of ContactViaWebNotificationRecipientSet, deleted
all callsites that accessed it, deleted tests that check that the team
email address was contacted.

Renamed TO_OWNER to TO_ADMINS and revised the descriptive test. Updated
tests to verify team admins are contacted. Replaced
_getPrimaryRecipient() with a block to get the team admins (make the
tests pass). While there was no method that returned person and email
address, my implementation uses the get_recipients helper that caches
the user/team email addresses.

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

This code looks good Curtis and is a welcome improvement.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/person.py'
2--- lib/lp/registry/browser/person.py 2012-02-03 22:03:43 +0000
3+++ lib/lp/registry/browser/person.py 2012-02-10 21:51:21 +0000
4@@ -217,6 +217,7 @@
5 Milestone,
6 milestone_sort_key,
7 )
8+from lp.registry.model.person import get_recipients
9 from lp.services.config import config
10 from lp.services.database.sqlbase import flush_database_updates
11 from lp.services.feeds.browser import FeedsMixin
12@@ -2581,8 +2582,7 @@
13 :return: the recipients of the message.
14 :rtype: `ContactViaWebNotificationRecipientSet` constant:
15 TO_USER
16- TO_TEAM (Send to team's preferredemail)
17- TO_OWNER
18+ TO_ADMINS
19 TO_MEMBERS
20 """
21 return ContactViaWebNotificationRecipientSet(
22@@ -2597,13 +2597,10 @@
23 return 'Send an email to yourself through Launchpad'
24 else:
25 return 'Send an email to this user through Launchpad'
26- elif self.group_to_contact == ContactViaWeb.TO_TEAM:
27- return ("Send an email to your team's contact email address "
28- "through Launchpad")
29 elif self.group_to_contact == ContactViaWeb.TO_MEMBERS:
30 return "Send an email to your team's members through Launchpad"
31- elif self.group_to_contact == ContactViaWeb.TO_OWNER:
32- return "Send an email to this team's owner through Launchpad"
33+ elif self.group_to_contact == ContactViaWeb.TO_ADMINS:
34+ return "Send an email to this team's admins through Launchpad"
35 else:
36 raise AssertionError('Unknown group to contact.')
37
38@@ -2615,12 +2612,10 @@
39 # Note that we explicitly do not change the text to "Contact
40 # yourself" when viewing your own page.
41 return 'Contact this user'
42- elif self.group_to_contact == ContactViaWeb.TO_TEAM:
43- return "Contact this team's email address"
44 elif self.group_to_contact == ContactViaWeb.TO_MEMBERS:
45 return "Contact this team's members"
46- elif self.group_to_contact == ContactViaWeb.TO_OWNER:
47- return "Contact this team's owner"
48+ elif self.group_to_contact == ContactViaWeb.TO_ADMINS:
49+ return "Contact this team's admins"
50 else:
51 raise AssertionError('Unknown group to contact.')
52
53@@ -4823,9 +4818,8 @@
54
55 # Primary reason enumerations.
56 TO_USER = object()
57- TO_TEAM = object()
58 TO_MEMBERS = object()
59- TO_OWNER = object()
60+ TO_ADMINS = object()
61
62 def __init__(self, user, person_or_team):
63 """Initialize the state based on the context and the user.
64@@ -4861,36 +4855,16 @@
65 """
66 if person_or_team.is_team:
67 if self.user.inTeam(person_or_team):
68- if removeSecurityProxy(person_or_team).preferredemail is None:
69- # Send to each team member.
70- return self.TO_MEMBERS
71- else:
72- # Send to the team's contact address.
73- return self.TO_TEAM
74+ return self.TO_MEMBERS
75 else:
76 # A non-member can only send emails to a single person to
77 # hinder spam and to prevent leaking membership
78 # information for private teams when the members reply.
79- return self.TO_OWNER
80+ return self.TO_ADMINS
81 else:
82 # Send to the user
83 return self.TO_USER
84
85- def _getPrimaryRecipient(self, person_or_team):
86- """Return the primary recipient.
87-
88- The primary recipient is the ``person_or_team`` in all cases
89- except for when the email is restricted to a team owner.
90-
91- :param person_or_team: The party that is the context of the email.
92- :type person_or_team: `IPerson`.
93- """
94- if self._primary_reason is self.TO_OWNER:
95- person_or_team = person_or_team.teamowner
96- while person_or_team.is_team:
97- person_or_team = person_or_team.teamowner
98- return person_or_team
99-
100 def _getReasonAndHeader(self, person_or_team):
101 """Return the reason and header why the email was received.
102
103@@ -4902,20 +4876,13 @@
104 'using the "Contact this user" link on your profile page\n'
105 '(%s)' % canonical_url(person_or_team))
106 header = 'ContactViaWeb user'
107- elif self._primary_reason is self.TO_OWNER:
108+ elif self._primary_reason is self.TO_ADMINS:
109 reason = (
110- 'using the "Contact this team\'s owner" link on the '
111+ 'using the "Contact this team\'s admins" link on the '
112 '%s team page\n(%s)' % (
113 person_or_team.displayname,
114 canonical_url(person_or_team)))
115 header = 'ContactViaWeb owner (%s team)' % person_or_team.name
116- elif self._primary_reason is self.TO_TEAM:
117- reason = (
118- 'using the "Contact this team" link on the '
119- '%s team page\n(%s)' % (
120- person_or_team.displayname,
121- canonical_url(person_or_team)))
122- header = 'ContactViaWeb member (%s team)' % person_or_team.name
123 else:
124 # self._primary_reason is self.TO_MEMBERS.
125 reason = (
126@@ -4937,15 +4904,9 @@
127 return (
128 'You are contacting %s (%s).' %
129 (person_or_team.displayname, person_or_team.name))
130- elif self._primary_reason is self.TO_OWNER:
131- return (
132- 'You are contacting the %s (%s) team owner, %s (%s).' %
133- (person_or_team.displayname, person_or_team.name,
134- self._primary_recipient.displayname,
135- self._primary_recipient.name))
136- elif self._primary_reason is self.TO_TEAM:
137- return (
138- 'You are contacting the %s (%s) team.' %
139+ elif self._primary_reason is self.TO_ADMINS:
140+ return (
141+ 'You are contacting the %s (%s) team admins.' %
142 (person_or_team.displayname, person_or_team.name))
143 else:
144 # This is a team without a contact address (self.TO_MEMBERS).
145@@ -4968,6 +4929,16 @@
146 for recipient in team.getMembersWithPreferredEmails():
147 email = removeSecurityProxy(recipient).preferredemail.email
148 all_recipients[email] = recipient
149+ elif self._primary_reason is self.TO_ADMINS:
150+ team = self._primary_recipient
151+ for admin in team.adminmembers:
152+ # This method is similar to getTeamAdminsEmailAddresses, but
153+ # this case needs to know the user. Since both methods
154+ # ultimately iterate over get_recipients, this case is not
155+ # in a different performance class.
156+ for recipient in get_recipients(admin):
157+ email = removeSecurityProxy(recipient).preferredemail.email
158+ all_recipients[email] = recipient
159 elif self._primary_recipient.is_valid_person_or_team:
160 email = removeSecurityProxy(
161 self._primary_recipient).preferredemail.email
162@@ -5040,7 +5011,7 @@
163 """
164 self._reset_state()
165 self._primary_reason = self._getPrimaryReason(person)
166- self._primary_recipient = self._getPrimaryRecipient(person)
167+ self._primary_recipient = person
168 if reason is None:
169 reason, header = self._getReasonAndHeader(person)
170 self._reason = reason
171
172=== modified file 'lib/lp/registry/browser/tests/person-views.txt'
173--- lib/lp/registry/browser/tests/person-views.txt 2012-01-15 13:32:27 +0000
174+++ lib/lp/registry/browser/tests/person-views.txt 2012-02-10 21:51:21 +0000
175@@ -525,14 +525,6 @@
176 Non-member contacting a Team
177 ----------------------------
178
179-Users can contact teams, but the behaviour depends upon whether the user
180-is a member of the team. No Privileges Person is not a member of the
181-Landscape Developers team.
182-
183- >>> view = create_initialized_view(landscape_developers, '+index')
184- >>> print view.contact_link_title
185- Send an email to this team's owner through Launchpad
186-
187 The EmailToPersonView can be used by non-members to contact the team
188 owner.
189
190@@ -545,7 +537,7 @@
191
192 >>> print view.recipients.description
193 You are contacting the Landscape Developers (landscape-developers) team
194- owner, Sample Person (name12).
195+ admins.
196
197 >>> [recipient.name for recipient in view.recipients]
198 [u'name12']
199@@ -608,19 +600,6 @@
200 >>> [recipient.name for recipient in view.recipients]
201 [u'name12']
202
203-EmailToPersonView will use the contact address when the team has one.
204-
205- >>> email_address = factory.makeEmail(
206- ... 'landscapers@canonical.com', landscape_developers)
207- >>> landscape_developers.setContactAddress(email_address)
208-
209- >>> view = create_initialized_view(landscape_developers, '+contactuser')
210- >>> print view.recipients.description
211- You are contacting the Landscape Developers (landscape-developers) team.
212-
213- >>> [recipient.name for recipient in view.recipients]
214- [u'landscape-developers']
215-
216
217 Contact this user/team valid addresses and quotas
218 -------------------------------------------------
219
220=== modified file 'lib/lp/registry/browser/tests/team-views.txt'
221--- lib/lp/registry/browser/tests/team-views.txt 2012-01-09 12:39:11 +0000
222+++ lib/lp/registry/browser/tests/team-views.txt 2012-02-10 21:51:21 +0000
223@@ -213,9 +213,9 @@
224
225 >>> view = create_initialized_view(guadamen, '+index')
226 >>> print view.contact_link_title
227- Send an email to this team's owner through Launchpad
228+ Send an email to this team's admins through Launchpad
229 >>> print view.specific_contact_text
230- Contact this team's owner
231+ Contact this team's admins
232
233
234 Mugshots
235
236=== modified file 'lib/lp/registry/browser/tests/user-to-user-views.txt'
237--- lib/lp/registry/browser/tests/user-to-user-views.txt 2011-12-29 05:29:36 +0000
238+++ lib/lp/registry/browser/tests/user-to-user-views.txt 2012-02-10 21:51:21 +0000
239@@ -305,13 +305,14 @@
240 --
241 This message was sent from Launchpad by
242 Bart (http://launchpad.dev/~bart)
243- using the "Contact this team's owner" link on the GuadaMen team page
244+ using the "Contact this team's admins" link on the GuadaMen team page
245 (http://launchpad.dev/~guadamen).
246 For more information see
247 https://help.launchpad.net/YourAccount/ContactingPeople
248- # of Messages: 1
249+ # of Messages: 2
250 Recipients:
251 Foo Bar <foo.bar@canonical.com>
252+ Ubuntu Team <support@ubuntu.com>
253
254
255 Member to team
256@@ -363,110 +364,7 @@
257 Steve Alexander <steve.alexander@ubuntulinux.com>
258 Ubuntu Team <support@ubuntu.com>
259
260-The Guadamen team creates a mailing list but does not set it to be the
261-contact address. The mailing list will not be used.
262-
263- >>> team, mailing_list = factory.makeTeamAndMailingList(
264- ... guadamen.name, guadamen.teamowner.name)
265-
266- # Ignore the 'new mailing list message'
267-
268- >>> transaction.commit()
269- >>> del stub.test_emails[:]
270-
271-Foo Bar now contacts them again, which he can do because his quota is
272-still not met. This message includes a "%s" combination; it is not a
273-interpolation instruction.
274-
275- >>> view = create_view(
276- ... foo_bar, guadamen, {
277- ... 'field.field.from_': 'foo.bar@canonical.com',
278- ... 'field.subject': 'My last question for Guadamen',
279- ... 'field.message': 'Can one of you help me with "%s" usage!',
280- ... 'field.actions.send': 'Send',
281- ... })
282- >>> print_notifications(view)
283- Message sent to GuadaMen
284-
285-Foo Bar's message gets sent to each individual member of he team.
286-
287- >>> transaction.commit()
288- >>> print_messages()
289- Senders: set(['Foo Bar <foo.bar@canonical.com>'])
290- Subjects: set(['My last question for Guadamen'])
291- Bodies:
292- Can one of you help me with "%s" usage!
293- --
294- This message was sent from Launchpad by
295- Foo Bar (http://launchpad.dev/~name16)
296- to each member of the GuadaMen team
297- using the "Contact this team" link on the GuadaMen
298- team page (http://launchpad.dev/~guadamen).
299- For more information see
300- https://help.launchpad.net/YourAccount/ContactingPeople
301- # of Messages: 10
302- Recipients:
303- Alexander Limi <limi@plone.org>
304- Celso Providelo <celso.providelo@canonical.com>
305- Colin Watson <colin.watson@ubuntulinux.com>
306- Daniel Silverstone <daniel.silverstone@canonical.com>
307- Edgar Bursic <edgar@monteparadiso.hr>
308- Foo Bar <foo.bar@canonical.com>
309- Jeff Waugh <jeff.waugh@ubuntulinux.com>
310- Mark Shuttleworth <mark@example.com>
311- Steve Alexander <steve.alexander@ubuntulinux.com>
312- Ubuntu Team <support@ubuntu.com>
313-
314-The Guadamen team now registers an external contact address (they could
315-have also used their Launchpad mailing list address).
316-
317- >>> from lp.services.identity.interfaces.emailaddress import (
318- ... IEmailAddressSet)
319- >>> email_set = getUtility(IEmailAddressSet)
320- >>> address = email_set.new('guadamen@example.com', guadamen)
321- >>> guadamen.setContactAddress(address)
322-
323-Foo Bar contacts the Guadamen team again, which is allowed because his
324-quota was not met by his first message. This time only one message is
325-sent, and that to the new contact address.
326-
327- >>> view = create_view(
328- ... foo_bar, guadamen, {
329- ... 'field.field.from_': 'foo.bar@canonical.com',
330- ... 'field.subject': 'Hello again Guadamen',
331- ... 'field.message': 'Can one of you help me?',
332- ... 'field.actions.send': 'Send',
333- ... })
334- >>> print_notifications(view)
335- Message sent to GuadaMen
336-
337- >>> transaction.commit()
338- >>> len(stub.test_emails)
339- 1
340-
341- >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
342- >>> print from_addr, to_addrs
343- bounces@canonical.com [u'GuadaMen <guadamen@example.com>']
344-
345- >>> print raw_msg
346- Content-Type: text/plain; charset="us-ascii"
347- ...
348- From: Foo Bar <foo.bar@canonical.com>
349- To: GuadaMen <guadamen@example.com>
350- Subject: Hello again Guadamen
351- ...
352- X-Launchpad-Message-Rationale: ContactViaWeb member (guadamen team)
353- ...
354- <BLANKLINE>
355- Can one of you help me?
356- --
357- This message was sent from Launchpad by
358- Foo Bar (http://launchpad.dev/~name16)
359- using the "Contact this team" link on the GuadaMen team page
360- (http://launchpad.dev/~guadamen).
361- For more information see
362- https://help.launchpad.net/YourAccount/ContactingPeople
363-
364+ >>> transaction.commit()
365
366 Message quota
367 -------------
368@@ -477,6 +375,22 @@
369 emails again. The is_possible property will be False if is_allowed is
370 False.
371
372+ >>> view = create_view(
373+ ... foo_bar, guadamen, {
374+ ... 'field.field.from_': 'foo.bar@canonical.com',
375+ ... 'field.subject': 'Hello Guadamen',
376+ ... 'field.message': 'Can one of you help me?',
377+ ... 'field.actions.send': 'Send',
378+ ... })
379+ >>> view.contact_is_allowed
380+ True
381+
382+ >>> view.contact_is_possible
383+ True
384+
385+ >>> view.initialize()
386+ >>> transaction.commit()
387+
388 Foo Bar has now reached his quota and can send no more contact messages
389 today.
390
391@@ -500,16 +414,6 @@
392 Your message was not sent because you have exceeded your daily quota of
393 3 messages to contact users. Try again in ...
394
395-Bart has sent 1 message, he may send more messages.
396-
397- >>> login_person(bart)
398- >>> view = create_view(bart, guadamen)
399- >>> view.contact_is_allowed
400- True
401-
402- >>> view.contact_is_possible
403- True
404-
405
406 Identifying information
407 -----------------------
408
409=== modified file 'lib/lp/registry/stories/team/xx-team-home.txt'
410--- lib/lp/registry/stories/team/xx-team-home.txt 2011-01-04 16:08:57 +0000
411+++ lib/lp/registry/stories/team/xx-team-home.txt 2012-02-10 21:51:21 +0000
412@@ -1,4 +1,5 @@
413-= A team's home page =
414+A team's home page
415+==================
416
417 The home page of a public team is visible to everyone.
418
419@@ -133,18 +134,6 @@
420 ... find_tag_by_id(browser.contents, 'your-involvement'))
421 You are a member of this team...
422
423-He can also see the contact email address and a link to contact the team.
424-
425- >>> browser.open('http://launchpad.dev/~ubuntu-team')
426- >>> print extract_text(
427- ... find_tag_by_id(browser.contents, 'contact-email'))
428- Email:
429- support@ubuntu.com
430- Set contact address
431- >>> print extract_text(
432- ... find_tag_by_id(browser.contents, 'contact-user'))
433- Contact this team's email address
434-
435 Member can contact their team even if the team does not have a contact
436 address:
437
438@@ -190,7 +179,8 @@
439 ...
440
441
442-== Team admins ==
443+Team admins
444+-----------
445
446 Team owners and admins can see a link to approve and decline applicants.
447
448@@ -207,7 +197,8 @@
449 <Link text='Approve or decline members' url='.../+editproposedmembers'>
450
451
452-== Non members ==
453+Non members
454+-----------
455
456 No Privileges Person is not a member of the Ubuntu team.
457
458@@ -220,13 +211,14 @@
459 He can see the contact address, and the link explains the email
460 will actually go to the team's administrators.
461
462- >>> print extract_text(find_tag_by_id(user_browser.contents, 'contact-email'))
463+ >>> print extract_text(find_tag_by_id(
464+ ... user_browser.contents, 'contact-email'))
465 Email:
466 support@ubuntu.com
467 >>> content = find_tag_by_id(user_browser.contents, 'contact-user')
468 >>> print extract_text(content)
469- Contact this team's owner
470+ Contact this team's admins
471
472 >>> content.a
473 <a href="+contactuser"...
474- title="Send an email to this team's owner through Launchpad">...
475+ title="Send an email to this team's admins through Launchpad">...
476
477=== modified file 'lib/lp/registry/templates/contact-user.pt'
478--- lib/lp/registry/templates/contact-user.pt 2011-10-13 05:42:45 +0000
479+++ lib/lp/registry/templates/contact-user.pt 2012-02-10 21:51:21 +0000
480@@ -17,7 +17,7 @@
481 specified below. If you do not want
482 <tal:person
483 tal:content="context/displayname">Anne Person</tal:person>
484- to know your email address, hit
485+ to know your email address,
486 <a href="" tal:attributes="href view/cancel_url">Cancel</a> now.
487 </p>
488 </div>