Merge lp:~sinzui/launchpad/team-without-admin into lp:launchpad
- team-without-admin
- Merge into devel
| Status: | Merged | ||||
|---|---|---|---|---|---|
| Approved by: | Curtis Hovey on 2012-11-27 | ||||
| Approved revision: | no longer in the source branch. | ||||
| Merged at revision: | 16317 | ||||
| Proposed branch: | lp:~sinzui/launchpad/team-without-admin | ||||
| Merge into: | lp:launchpad | ||||
| Diff against target: |
1546 lines (+629/-781) 7 files modified
lib/lp/registry/browser/person.py (+72/-58) lib/lp/registry/browser/tests/person-views.txt (+0/-219) lib/lp/registry/browser/tests/test_person_contact.py (+438/-0) lib/lp/registry/browser/tests/user-to-user-views.txt (+0/-491) lib/lp/registry/mail/notification.py (+4/-12) lib/lp/registry/templates/contact-user.pt (+1/-1) lib/lp/registry/tests/test_notification.py (+114/-0) |
||||
| To merge this branch: | bzr merge lp:~sinzui/launchpad/team-without-admin | ||||
| Related bugs: |
|
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| Benji York (community) | code | 2012-11-26 | Approve on 2012-11-26 |
|
Review via email:
|
|||
Commit Message
Explain when a team has no admins to contact.
Description of the Change
The "Contact this team's admins" form oopses when the team does not
have any admins. This can happen when the team owner leaves the team,
but fails to delegate the admin responsibility to others.
-------
RULES
Pre-
* Update send_direct_
was sent if there was a message.
* Update page to explain that the team team does not have any
admins and advise the team owner to delegate one or more
people to be admins.
ADDENDUM
* The view and the recipient set do check if there are people to send
the email to, but the rule is broken. The recipient set used to
send to the owner, now it sends to admins. The owner rules were
removed, but the admin case needed to be handled.
QA
* Visit https:/
* Verify the page explains that the team has no admins so the
owner should be contacted instead.
LoC
I have more than 16,000 lines of credit.
LINT
lib/
lib/
lib/
lib/
lib/
TEST
./bin/test -vvc lp.registry.
./bin/test -vvc lp.registry.
./bin/test -vvc -t user-to-user -t person-views lp.registry.
^ These last tests have lot over overlap. I think we want to move
all the overlap into user-to-user.txt, then rewrite the tests as
unit tests to horrendous reduce the duplication the doctests.
IMPLEMENTATION
I updated the send_direct_
sent. I created a new test module for this code. It duplicates quota testing
in doctests. I will try to remove the duplication, or move the doctests into
the unit test before I land this branch.
lib/
lib/
I fixed the len() check on the recipient set. I choose to use a common
cached property that is already confirmed to select the correct recipients
instead of adding duplicate code to determine the length of the set for
TO_ADMIN and TO_MEMBER cases. The text shown when the there is no-one to
contact must have been wrong, it was just the same message shown in the form.
I added a property to the view to explain why the user or team could not
be contacted. I made the recipient set's primary_reason attribute public
so that the view did not need to duplicate code.
lib/
lib/
lib/
| Curtis Hovey (sinzui) wrote : | # |
On 11/26/2012 04:47 PM, Benji York wrote:
> Review: Approve code
>
> This branch looks good. I had a couple of small questions/
>
> On line 83 of the diff, I don't understand why the outer parenthesis
> exist on the right hand side of the equals:
>
> self._count_
The outer parens are cruft from the old code. I will remove them.
> It would be nice if the tests in
> lib/lp/
> smaller test cases instead of one big test case per class.
I will break these down.
--
Curtis Hovey
http://
Preview Diff
| 1 | === modified file 'lib/lp/registry/browser/person.py' |
| 2 | --- lib/lp/registry/browser/person.py 2012-11-19 21:59:03 +0000 |
| 3 | +++ lib/lp/registry/browser/person.py 2012-11-27 20:11:21 +0000 |
| 4 | @@ -1504,7 +1504,54 @@ |
| 5 | return self.context.latestKarma().count() > 0 |
| 6 | |
| 7 | |
| 8 | -class PersonView(LaunchpadView, FeedsMixin): |
| 9 | +class ContactViaWebLinksMixin: |
| 10 | + |
| 11 | + @cachedproperty |
| 12 | + def group_to_contact(self): |
| 13 | + """Contacting a team may contact different email addresses. |
| 14 | + |
| 15 | + :return: the recipients of the message. |
| 16 | + :rtype: `ContactViaWebNotificationRecipientSet` constant: |
| 17 | + TO_USER |
| 18 | + TO_ADMINS |
| 19 | + TO_MEMBERS |
| 20 | + """ |
| 21 | + return ContactViaWebNotificationRecipientSet( |
| 22 | + self.user, self.context).primary_reason |
| 23 | + |
| 24 | + @property |
| 25 | + def contact_link_title(self): |
| 26 | + """Return the appropriate +contactuser link title for the tooltip.""" |
| 27 | + ContactViaWeb = ContactViaWebNotificationRecipientSet |
| 28 | + if self.group_to_contact == ContactViaWeb.TO_USER: |
| 29 | + if self.viewing_own_page: |
| 30 | + return 'Send an email to yourself through Launchpad' |
| 31 | + else: |
| 32 | + return 'Send an email to this user through Launchpad' |
| 33 | + elif self.group_to_contact == ContactViaWeb.TO_MEMBERS: |
| 34 | + return "Send an email to your team's members through Launchpad" |
| 35 | + elif self.group_to_contact == ContactViaWeb.TO_ADMINS: |
| 36 | + return "Send an email to this team's admins through Launchpad" |
| 37 | + else: |
| 38 | + raise AssertionError('Unknown group to contact.') |
| 39 | + |
| 40 | + @property |
| 41 | + def specific_contact_text(self): |
| 42 | + """Return the appropriate link text.""" |
| 43 | + ContactViaWeb = ContactViaWebNotificationRecipientSet |
| 44 | + if self.group_to_contact == ContactViaWeb.TO_USER: |
| 45 | + # Note that we explicitly do not change the text to "Contact |
| 46 | + # yourself" when viewing your own page. |
| 47 | + return 'Contact this user' |
| 48 | + elif self.group_to_contact == ContactViaWeb.TO_MEMBERS: |
| 49 | + return "Contact this team's members" |
| 50 | + elif self.group_to_contact == ContactViaWeb.TO_ADMINS: |
| 51 | + return "Contact this team's admins" |
| 52 | + else: |
| 53 | + raise AssertionError('Unknown group to contact.') |
| 54 | + |
| 55 | + |
| 56 | +class PersonView(LaunchpadView, FeedsMixin, ContactViaWebLinksMixin): |
| 57 | """A View class used in almost all Person's pages.""" |
| 58 | |
| 59 | @property |
| 60 | @@ -1739,50 +1786,6 @@ |
| 61 | return ( |
| 62 | self.user is not None and self.context.is_valid_person_or_team) |
| 63 | |
| 64 | - @cachedproperty |
| 65 | - def group_to_contact(self): |
| 66 | - """Contacting a team may contact different email addresses. |
| 67 | - |
| 68 | - :return: the recipients of the message. |
| 69 | - :rtype: `ContactViaWebNotificationRecipientSet` constant: |
| 70 | - TO_USER |
| 71 | - TO_ADMINS |
| 72 | - TO_MEMBERS |
| 73 | - """ |
| 74 | - return ContactViaWebNotificationRecipientSet( |
| 75 | - self.user, self.context)._primary_reason |
| 76 | - |
| 77 | - @property |
| 78 | - def contact_link_title(self): |
| 79 | - """Return the appropriate +contactuser link title for the tooltip.""" |
| 80 | - ContactViaWeb = ContactViaWebNotificationRecipientSet |
| 81 | - if self.group_to_contact == ContactViaWeb.TO_USER: |
| 82 | - if self.viewing_own_page: |
| 83 | - return 'Send an email to yourself through Launchpad' |
| 84 | - else: |
| 85 | - return 'Send an email to this user through Launchpad' |
| 86 | - elif self.group_to_contact == ContactViaWeb.TO_MEMBERS: |
| 87 | - return "Send an email to your team's members through Launchpad" |
| 88 | - elif self.group_to_contact == ContactViaWeb.TO_ADMINS: |
| 89 | - return "Send an email to this team's admins through Launchpad" |
| 90 | - else: |
| 91 | - raise AssertionError('Unknown group to contact.') |
| 92 | - |
| 93 | - @property |
| 94 | - def specific_contact_text(self): |
| 95 | - """Return the appropriate link text.""" |
| 96 | - ContactViaWeb = ContactViaWebNotificationRecipientSet |
| 97 | - if self.group_to_contact == ContactViaWeb.TO_USER: |
| 98 | - # Note that we explicitly do not change the text to "Contact |
| 99 | - # yourself" when viewing your own page. |
| 100 | - return 'Contact this user' |
| 101 | - elif self.group_to_contact == ContactViaWeb.TO_MEMBERS: |
| 102 | - return "Contact this team's members" |
| 103 | - elif self.group_to_contact == ContactViaWeb.TO_ADMINS: |
| 104 | - return "Contact this team's admins" |
| 105 | - else: |
| 106 | - raise AssertionError('Unknown group to contact.') |
| 107 | - |
| 108 | @property |
| 109 | def should_show_polls_portlet(self): |
| 110 | # Circular imports. |
| 111 | @@ -3919,7 +3922,7 @@ |
| 112 | """ |
| 113 | self.user = user |
| 114 | self.description = None |
| 115 | - self._primary_reason = None |
| 116 | + self.primary_reason = None |
| 117 | self._primary_recipient = None |
| 118 | self._reason = None |
| 119 | self._header = None |
| 120 | @@ -3955,12 +3958,12 @@ |
| 121 | :param person_or_team: The party that is the context of the email. |
| 122 | :type person_or_team: `IPerson`. |
| 123 | """ |
| 124 | - if self._primary_reason is self.TO_USER: |
| 125 | + if self.primary_reason is self.TO_USER: |
| 126 | reason = ( |
| 127 | 'using the "Contact this user" link on your profile page\n' |
| 128 | '(%s)' % canonical_url(person_or_team)) |
| 129 | header = 'ContactViaWeb user' |
| 130 | - elif self._primary_reason is self.TO_ADMINS: |
| 131 | + elif self.primary_reason is self.TO_ADMINS: |
| 132 | reason = ( |
| 133 | 'using the "Contact this team\'s admins" link on the ' |
| 134 | '%s team page\n(%s)' % ( |
| 135 | @@ -3968,7 +3971,7 @@ |
| 136 | canonical_url(person_or_team))) |
| 137 | header = 'ContactViaWeb owner (%s team)' % person_or_team.name |
| 138 | else: |
| 139 | - # self._primary_reason is self.TO_MEMBERS. |
| 140 | + # self.primary_reason is self.TO_MEMBERS. |
| 141 | reason = ( |
| 142 | 'to each member of the %s team using the ' |
| 143 | '"Contact this team" link on the %s team page\n(%s)' % ( |
| 144 | @@ -3984,11 +3987,11 @@ |
| 145 | :param person_or_team: The party that is the context of the email. |
| 146 | :type person_or_team: `IPerson`. |
| 147 | """ |
| 148 | - if self._primary_reason is self.TO_USER: |
| 149 | + if self.primary_reason is self.TO_USER: |
| 150 | return ( |
| 151 | 'You are contacting %s (%s).' % |
| 152 | (person_or_team.displayname, person_or_team.name)) |
| 153 | - elif self._primary_reason is self.TO_ADMINS: |
| 154 | + elif self.primary_reason is self.TO_ADMINS: |
| 155 | return ( |
| 156 | 'You are contacting the %s (%s) team admins.' % |
| 157 | (person_or_team.displayname, person_or_team.name)) |
| 158 | @@ -4008,12 +4011,12 @@ |
| 159 | def _all_recipients(self): |
| 160 | """Set the cache of all recipients.""" |
| 161 | all_recipients = {} |
| 162 | - if self._primary_reason is self.TO_MEMBERS: |
| 163 | + if self.primary_reason is self.TO_MEMBERS: |
| 164 | team = self._primary_recipient |
| 165 | for recipient in team.getMembersWithPreferredEmails(): |
| 166 | email = removeSecurityProxy(recipient).preferredemail.email |
| 167 | all_recipients[email] = recipient |
| 168 | - elif self._primary_reason is self.TO_ADMINS: |
| 169 | + elif self.primary_reason is self.TO_ADMINS: |
| 170 | team = self._primary_recipient |
| 171 | for admin in team.adminmembers: |
| 172 | # This method is similar to getTeamAdminsEmailAddresses, but |
| 173 | @@ -4063,13 +4066,12 @@ |
| 174 | """The number of recipients in the set.""" |
| 175 | if self._count_recipients is None: |
| 176 | recipient = self._primary_recipient |
| 177 | - if self._primary_reason is self.TO_MEMBERS: |
| 178 | - self._count_recipients = ( |
| 179 | - recipient.getMembersWithPreferredEmailsCount()) |
| 180 | + if self.primary_reason in (self.TO_MEMBERS, self.TO_ADMINS): |
| 181 | + self._count_recipients = len(self._all_recipients) |
| 182 | elif recipient.is_valid_person_or_team: |
| 183 | self._count_recipients = 1 |
| 184 | else: |
| 185 | - # The user or team owner is deactivated. |
| 186 | + # The user is deactivated. |
| 187 | self._count_recipients = 0 |
| 188 | return self._count_recipients |
| 189 | |
| 190 | @@ -4094,7 +4096,7 @@ |
| 191 | recipients. |
| 192 | """ |
| 193 | self._reset_state() |
| 194 | - self._primary_reason = self._getPrimaryReason(person) |
| 195 | + self.primary_reason = self._getPrimaryReason(person) |
| 196 | self._primary_recipient = person |
| 197 | if reason is None: |
| 198 | reason, header = self._getReasonAndHeader(person) |
| 199 | @@ -4216,6 +4218,18 @@ |
| 200 | return throttle_date + interval |
| 201 | |
| 202 | @property |
| 203 | + def contact_not_possible_reason(self): |
| 204 | + """The reason the person cannot be contacted.""" |
| 205 | + if self.has_valid_email_address: |
| 206 | + return None |
| 207 | + elif self.recipients.primary_reason is self.recipients.TO_USER: |
| 208 | + return "The user is not active." |
| 209 | + elif self.recipients.primary_reason is self.recipients.TO_ADMINS: |
| 210 | + return "The team has no admins. Contact the team owner instead." |
| 211 | + else: |
| 212 | + return "The team has no members." |
| 213 | + |
| 214 | + @property |
| 215 | def page_title(self): |
| 216 | """Return the appropriate pagetitle.""" |
| 217 | if self.context.is_team: |
| 218 | |
| 219 | === modified file 'lib/lp/registry/browser/tests/person-views.txt' |
| 220 | --- lib/lp/registry/browser/tests/person-views.txt 2012-08-15 17:10:57 +0000 |
| 221 | +++ lib/lp/registry/browser/tests/person-views.txt 2012-11-27 20:11:21 +0000 |
| 222 | @@ -374,223 +374,6 @@ |
| 223 | 1 |
| 224 | |
| 225 | |
| 226 | -Person contacting another person |
| 227 | --------------------------------- |
| 228 | - |
| 229 | -The PersonView provides information to make the link to contact a user. |
| 230 | -No Privileges Person can send a message to Sample Person, even though |
| 231 | -Sample Person has hidden his email addresses. |
| 232 | - |
| 233 | - >>> login('no-priv@canonical.com') |
| 234 | - >>> sample_person.hide_email_addresses |
| 235 | - True |
| 236 | - |
| 237 | - >>> view = create_initialized_view(sample_person, '+index') |
| 238 | - >>> print view.contact_link_title |
| 239 | - Send an email to this user through Launchpad |
| 240 | - |
| 241 | -The EmailToPersonView provides many properties to the page template to |
| 242 | -explain exactly who is being contacted. |
| 243 | - |
| 244 | - >>> view = create_initialized_view(sample_person, '+contactuser') |
| 245 | - >>> print view.label |
| 246 | - Contact user |
| 247 | - |
| 248 | - >>> print view.page_title |
| 249 | - Contact this user |
| 250 | - |
| 251 | - >>> print view.recipients.description |
| 252 | - You are contacting Sample Person (name12). |
| 253 | - |
| 254 | - >>> [recipient.name for recipient in view.recipients] |
| 255 | - [u'name12'] |
| 256 | - |
| 257 | - |
| 258 | -Person contacting himself |
| 259 | -------------------------- |
| 260 | - |
| 261 | -For consistency and testing purposes, the "+contactuser" page is |
| 262 | -available even when someone is looking at his own profile page. The |
| 263 | -wording on the tooltip is different though. No Privileges Person can |
| 264 | -send a message to himself. |
| 265 | - |
| 266 | - >>> no_priv = person_set.getByEmail('no-priv@canonical.com') |
| 267 | - >>> view = create_initialized_view(no_priv, '+index') |
| 268 | - >>> print view.contact_link_title |
| 269 | - Send an email to yourself through Launchpad |
| 270 | - |
| 271 | -The EmailToPersonView provides the explanation about who is being |
| 272 | -contacted. |
| 273 | - |
| 274 | - >>> view = create_initialized_view(no_priv, '+contactuser') |
| 275 | - >>> print view.label |
| 276 | - Contact user |
| 277 | - |
| 278 | - >>> print view.page_title |
| 279 | - Contact yourself |
| 280 | - |
| 281 | - >>> print view.recipients.description |
| 282 | - You are contacting No Privileges Person (no-priv). |
| 283 | - |
| 284 | - >>> [recipient.name for recipient in view.recipients] |
| 285 | - [u'no-priv'] |
| 286 | - |
| 287 | - |
| 288 | -Non-team-admin contacting a Team |
| 289 | --------------------------------- |
| 290 | - |
| 291 | -The EmailToPersonView can be used by non-team-admins to contact the team |
| 292 | -admins. |
| 293 | - |
| 294 | - >>> view = create_initialized_view(landscape_developers, '+contactuser') |
| 295 | - >>> print view.label |
| 296 | - Contact user |
| 297 | - |
| 298 | - >>> print view.page_title |
| 299 | - Contact this team |
| 300 | - |
| 301 | - >>> print view.recipients.description |
| 302 | - You are contacting the Landscape Developers (landscape-developers) team |
| 303 | - admins. |
| 304 | - |
| 305 | - >>> [recipient.name for recipient in view.recipients] |
| 306 | - [u'name12'] |
| 307 | - |
| 308 | - |
| 309 | -Team admin contacting a Team |
| 310 | ----------------------------- |
| 311 | - |
| 312 | -Team admins can contact their team to broadcast a message to all members. |
| 313 | -Sample Person can contact his team, Landscape developers, even though they do |
| 314 | -not have a contact address. |
| 315 | - |
| 316 | - >>> team_admin = login_person(landscape_developers.adminmembers[0]) |
| 317 | - >>> view = create_initialized_view(landscape_developers, '+index') |
| 318 | - >>> print view.contact_link_title |
| 319 | - Send an email to your team's members through Launchpad |
| 320 | - |
| 321 | -The EmailToPersonView can be used by admins to contact their team. |
| 322 | - |
| 323 | - >>> view = create_initialized_view(landscape_developers, '+contactuser') |
| 324 | - >>> print view.label |
| 325 | - Contact user |
| 326 | - |
| 327 | - >>> print view.page_title |
| 328 | - Contact your team |
| 329 | - |
| 330 | - >>> print view.recipients.description |
| 331 | - You are contacting 2 members of the Landscape Developers |
| 332 | - (landscape-developers) team directly. |
| 333 | - |
| 334 | - >>> [recipient.name for recipient in view.recipients] |
| 335 | - [u'salgado', u'name12'] |
| 336 | - |
| 337 | -There are 2 recipients, so the object is treats as True. |
| 338 | - |
| 339 | - >>> recipients = view.recipients |
| 340 | - >>> len(recipients) |
| 341 | - 2 |
| 342 | - |
| 343 | - >>> bool(recipients) |
| 344 | - True |
| 345 | - |
| 346 | -If there is only one member of the team, who must therefore be the user |
| 347 | -sending the email, and also be the team owner, The view provides a |
| 348 | -special message just for him. |
| 349 | - |
| 350 | - >>> vanity_team = factory.makeTeam( |
| 351 | - ... sample_person, displayname='Vanity', name='vanity') |
| 352 | - >>> view = create_initialized_view(vanity_team, '+contactuser') |
| 353 | - >>> print view.label |
| 354 | - Contact user |
| 355 | - |
| 356 | - >>> print view.page_title |
| 357 | - Contact your team |
| 358 | - |
| 359 | - >>> print view.recipients.description |
| 360 | - You are contacting 1 member of the Vanity (vanity) team directly. |
| 361 | - |
| 362 | - >>> [recipient.name for recipient in view.recipients] |
| 363 | - [u'name12'] |
| 364 | - |
| 365 | - |
| 366 | -Contact this user/team valid addresses and quotas |
| 367 | -------------------------------------------------- |
| 368 | - |
| 369 | -The EmailToPersonView has_valid_email_address property is normally True. |
| 370 | -The is_possible property is True when contact_is_allowed and |
| 371 | -has_valid_email_address are both True. |
| 372 | - |
| 373 | - >>> view = create_initialized_view(landscape_developers, '+contactuser') |
| 374 | - >>> view.has_valid_email_address |
| 375 | - True |
| 376 | - |
| 377 | - >>> view.contact_is_possible |
| 378 | - True |
| 379 | - |
| 380 | -The EmailToPersonView provides two properties that check that the user |
| 381 | -is_allowed to send emails because he has not exceeded the daily quota. |
| 382 | -The next_try property is the date when the user will be allowed to send |
| 383 | -emails again. The is_possible property is True when both |
| 384 | -contact_is_allowed and as_valid_email_address are True. |
| 385 | - |
| 386 | -The daily quota is set to 3 emails per day. See the "Message quota" in |
| 387 | -`doc/user-to-user.txt` to see how these two attributes are used. |
| 388 | - |
| 389 | - |
| 390 | -Invalid users and anonymous contacters |
| 391 | --------------------------------------- |
| 392 | - |
| 393 | -Inactive users and users without a preferred email address are invalid |
| 394 | -and cannot be contacted. |
| 395 | - |
| 396 | - >>> former_user = person_set.getByEmail('former-user@canonical.com', |
| 397 | - ... filter_status=False) |
| 398 | - >>> view = create_initialized_view(former_user, '+contactuser') |
| 399 | - >>> view.request.response.getStatus() |
| 400 | - 302 |
| 401 | - |
| 402 | - >>> print view.request.response.getHeader('Location') |
| 403 | - http://launchpad.dev/~former-user-deactivatedaccount |
| 404 | - |
| 405 | - >>> recipients = view.recipients |
| 406 | - >>> len(recipients) |
| 407 | - 0 |
| 408 | - |
| 409 | - >>> bool(recipients) |
| 410 | - False |
| 411 | - |
| 412 | -Anonymous users cannot contact anyone, they are redirected to the person |
| 413 | -or team's profile page. This can happen when off-site links point to a |
| 414 | -person or team's contact page. |
| 415 | - |
| 416 | - >>> login(ANONYMOUS) |
| 417 | - >>> view = create_initialized_view(landscape_developers, '+contactuser') |
| 418 | - >>> view.request.response.getStatus() |
| 419 | - 302 |
| 420 | - |
| 421 | - >>> print view.request.response.getHeader('Location') |
| 422 | - http://launchpad.dev/~landscape-developers |
| 423 | - |
| 424 | - |
| 425 | -Messages and subjects cannot be empty |
| 426 | -------------------------------------- |
| 427 | - |
| 428 | -Messages or subjects that contain only whitespace are treated as an |
| 429 | -error that the user must fix. |
| 430 | - |
| 431 | - >>> login('test@canonical.com') |
| 432 | - >>> view = create_initialized_view( |
| 433 | - ... landscape_developers, '+contactuser', form={ |
| 434 | - ... 'field.field.from_': 'test@canonical.com', |
| 435 | - ... 'field.subject': ' ', |
| 436 | - ... 'field.message': ' ', |
| 437 | - ... 'field.actions.send': 'Send', |
| 438 | - ... }) |
| 439 | - >>> view.errors |
| 440 | - [u'You must provide a subject and a message.'] |
| 441 | - |
| 442 | - |
| 443 | Person +index "Personal package archives" section |
| 444 | ------------------------------------------------- |
| 445 | |
| 446 | @@ -685,5 +468,3 @@ |
| 447 | >>> view = create_initialized_view(sample_person, "+index") |
| 448 | >>> view.should_show_ppa_section |
| 449 | False |
| 450 | - |
| 451 | - |
| 452 | |
| 453 | === added file 'lib/lp/registry/browser/tests/test_person_contact.py' |
| 454 | --- lib/lp/registry/browser/tests/test_person_contact.py 1970-01-01 00:00:00 +0000 |
| 455 | +++ lib/lp/registry/browser/tests/test_person_contact.py 2012-11-27 20:11:21 +0000 |
| 456 | @@ -0,0 +1,438 @@ |
| 457 | +# Copyright 2012 Canonical Ltd. This software is licensed under the |
| 458 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
| 459 | +"""Test views and helpers related to the contact person feature.""" |
| 460 | + |
| 461 | +__metaclass__ = type |
| 462 | + |
| 463 | +from lp.app.browser.tales import DateTimeFormatterAPI |
| 464 | +from lp.registry.browser.person import ( |
| 465 | + ContactViaWebLinksMixin, |
| 466 | + ContactViaWebNotificationRecipientSet, |
| 467 | + ) |
| 468 | +from lp.services.identity.interfaces.emailaddress import EmailAddressStatus |
| 469 | +from lp.services.messages.interfaces.message import IDirectEmailAuthorization |
| 470 | +from lp.testing import ( |
| 471 | + person_logged_in, |
| 472 | + TestCaseWithFactory, |
| 473 | + ) |
| 474 | +from lp.testing.layers import DatabaseFunctionalLayer |
| 475 | +from lp.testing.views import create_initialized_view |
| 476 | + |
| 477 | + |
| 478 | +class ContactViaWebNotificationRecipientSetTestCase(TestCaseWithFactory): |
| 479 | + """Tests the behaviour of ContactViaWebNotificationRecipientSet.""" |
| 480 | + |
| 481 | + layer = DatabaseFunctionalLayer |
| 482 | + |
| 483 | + def test_len_to_user(self): |
| 484 | + # The recipient set length is based on the user activity. |
| 485 | + sender = self.factory.makePerson() |
| 486 | + user = self.factory.makePerson(email='him@eg.dom') |
| 487 | + self.assertEqual( |
| 488 | + 1, len(ContactViaWebNotificationRecipientSet(sender, user))) |
| 489 | + inactive_user = self.factory.makePerson( |
| 490 | + email_address_status=EmailAddressStatus.NEW) |
| 491 | + self.assertEqual( |
| 492 | + 0, len( |
| 493 | + ContactViaWebNotificationRecipientSet(sender, inactive_user))) |
| 494 | + |
| 495 | + def test_len_to_admins(self): |
| 496 | + # The recipient set length is based on the number of admins. |
| 497 | + sender = self.factory.makePerson() |
| 498 | + team = self.factory.makeTeam() |
| 499 | + self.assertEqual( |
| 500 | + 1, len(ContactViaWebNotificationRecipientSet(sender, team))) |
| 501 | + with person_logged_in(team.teamowner): |
| 502 | + team.teamowner.leave(team) |
| 503 | + self.assertEqual( |
| 504 | + 0, len(ContactViaWebNotificationRecipientSet(sender, team))) |
| 505 | + |
| 506 | + def test_len_to_members(self): |
| 507 | + # The recipient set length is based on the number members. |
| 508 | + member = self.factory.makePerson() |
| 509 | + sender_team = self.factory.makeTeam(members=[member]) |
| 510 | + owner = sender_team.teamowner |
| 511 | + self.assertEqual( |
| 512 | + 2, len(ContactViaWebNotificationRecipientSet(owner, sender_team))) |
| 513 | + with person_logged_in(owner): |
| 514 | + owner.leave(sender_team) |
| 515 | + self.assertEqual( |
| 516 | + 1, len(ContactViaWebNotificationRecipientSet(owner, sender_team))) |
| 517 | + |
| 518 | + def test_nonzero(self): |
| 519 | + # The recipient set can be used in boolean conditions. |
| 520 | + sender = self.factory.makePerson() |
| 521 | + user = self.factory.makePerson(email='him@eg.dom') |
| 522 | + self.assertTrue( |
| 523 | + bool(ContactViaWebNotificationRecipientSet(sender, user))) |
| 524 | + inactive_user = self.factory.makePerson( |
| 525 | + email_address_status=EmailAddressStatus.NEW) |
| 526 | + self.assertFalse( |
| 527 | + bool(ContactViaWebNotificationRecipientSet(sender, inactive_user))) |
| 528 | + |
| 529 | + def test_getRecipientPersons_to_user(self): |
| 530 | + # The recipient set only contains the user. |
| 531 | + sender = self.factory.makePerson() |
| 532 | + user = self.factory.makePerson(email='him@eg.dom') |
| 533 | + recipient_set = ContactViaWebNotificationRecipientSet(sender, user) |
| 534 | + self.assertContentEqual( |
| 535 | + [('him@eg.dom', user)], |
| 536 | + list(recipient_set.getRecipientPersons())) |
| 537 | + |
| 538 | + def test_getRecipientPersons_to_admins(self): |
| 539 | + # The recipient set only contains the team admins when the user |
| 540 | + # is not an admin of the team the user is contacting |
| 541 | + admin = self.factory.makePerson(email='admin@eg.dom') |
| 542 | + member = self.factory.makePerson(email='member@eg.dom') |
| 543 | + team = self.factory.makeTeam(owner=admin, members=[member]) |
| 544 | + recipient_set = ContactViaWebNotificationRecipientSet(member, team) |
| 545 | + self.assertContentEqual( |
| 546 | + [('admin@eg.dom', admin)], |
| 547 | + list(recipient_set.getRecipientPersons())) |
| 548 | + |
| 549 | + def test_getRecipientPersons_to_members(self): |
| 550 | + # The recipient set contains all the team members when the admin |
| 551 | + # is contacting the team. |
| 552 | + admin = self.factory.makePerson(email='admin@eg.dom') |
| 553 | + member = self.factory.makePerson(email='member@eg.dom') |
| 554 | + team = self.factory.makeTeam(owner=admin, members=[member]) |
| 555 | + recipient_set = ContactViaWebNotificationRecipientSet(admin, team) |
| 556 | + self.assertContentEqual( |
| 557 | + [('admin@eg.dom', admin), ('member@eg.dom', member)], |
| 558 | + list(recipient_set.getRecipientPersons())) |
| 559 | + |
| 560 | + def test_description_to_user(self): |
| 561 | + sender = self.factory.makePerson() |
| 562 | + user = self.factory.makePerson(name='pting') |
| 563 | + recipient_set = ContactViaWebNotificationRecipientSet(sender, user) |
| 564 | + self.assertEqual( |
| 565 | + 'You are contacting Pting (pting).', |
| 566 | + recipient_set.description) |
| 567 | + |
| 568 | + def test_description_to_admin(self): |
| 569 | + member = self.factory.makePerson() |
| 570 | + team = self.factory.makeTeam(name='pting', members=[member]) |
| 571 | + recipient_set = ContactViaWebNotificationRecipientSet(member, team) |
| 572 | + self.assertEqual( |
| 573 | + 'You are contacting the Pting (pting) team admins.', |
| 574 | + recipient_set.description) |
| 575 | + |
| 576 | + def test_description_to_members(self): |
| 577 | + member = self.factory.makePerson() |
| 578 | + team = self.factory.makeTeam(name='pting', members=[member]) |
| 579 | + admin = team.teamowner |
| 580 | + recipient_set = ContactViaWebNotificationRecipientSet(admin, team) |
| 581 | + self.assertEqual( |
| 582 | + 'You are contacting 2 members of the Pting (pting) team directly.', |
| 583 | + recipient_set.description) |
| 584 | + |
| 585 | + def test_rationale_and_reason_user(self): |
| 586 | + sender = self.factory.makePerson() |
| 587 | + user = self.factory.makePerson(name='pting') |
| 588 | + recipient_set = ContactViaWebNotificationRecipientSet(sender, user) |
| 589 | + for email, recipient in recipient_set.getRecipientPersons(): |
| 590 | + reason, rationale = recipient_set.getReason(email) |
| 591 | + self.assertEqual( |
| 592 | + 'using the "Contact this user" link on your profile page\n' |
| 593 | + '(http://launchpad.dev/~pting)', |
| 594 | + reason) |
| 595 | + self.assertEqual('ContactViaWeb user', rationale) |
| 596 | + |
| 597 | + def test_rationale_and_reason_admin(self): |
| 598 | + sender = self.factory.makePerson() |
| 599 | + team = self.factory.makeTeam(name='pting') |
| 600 | + recipient_set = ContactViaWebNotificationRecipientSet(sender, team) |
| 601 | + for email, recipient in recipient_set.getRecipientPersons(): |
| 602 | + reason, rationale = recipient_set.getReason(email) |
| 603 | + self.assertEqual( |
| 604 | + 'using the "Contact this team\'s admins" link ' |
| 605 | + 'on the Pting team page\n' |
| 606 | + '(http://launchpad.dev/~pting)', |
| 607 | + reason) |
| 608 | + self.assertEqual('ContactViaWeb owner (pting team)', rationale) |
| 609 | + |
| 610 | + def test_rationale_and_reason_members(self): |
| 611 | + team = self.factory.makeTeam(name='pting') |
| 612 | + sender = team.teamowner |
| 613 | + recipient_set = ContactViaWebNotificationRecipientSet(sender, team) |
| 614 | + for email, recipient in recipient_set.getRecipientPersons(): |
| 615 | + reason, rationale = recipient_set.getReason(email) |
| 616 | + self.assertEqual( |
| 617 | + 'to each member of the Pting team using the ' |
| 618 | + '"Contact this team" link on the Pting team page\n' |
| 619 | + '(http://launchpad.dev/~pting)', |
| 620 | + reason) |
| 621 | + self.assertEqual('ContactViaWeb member (pting team)', rationale) |
| 622 | + |
| 623 | + |
| 624 | +class ContactViaWebLinksMixinTestCase(TestCaseWithFactory): |
| 625 | + """Tests the behaviour of ContactViaWebLinksMixin.""" |
| 626 | + |
| 627 | + layer = DatabaseFunctionalLayer |
| 628 | + |
| 629 | + def test_PersonView_composition(self): |
| 630 | + # PersonView uses the mixin. |
| 631 | + sender = self.factory.makePerson() |
| 632 | + user = self.factory.makePerson(name='pting') |
| 633 | + with person_logged_in(sender): |
| 634 | + view = create_initialized_view(user, '+index') |
| 635 | + self.assertTrue(issubclass(view.__class__, ContactViaWebLinksMixin)) |
| 636 | + |
| 637 | + def test_contact_self(self): |
| 638 | + sender = self.factory.makePerson() |
| 639 | + with person_logged_in(sender): |
| 640 | + view = create_initialized_view(sender, '+index') |
| 641 | + self.assertEqual( |
| 642 | + 'Send an email to yourself through Launchpad', |
| 643 | + view.contact_link_title) |
| 644 | + self.assertIs( |
| 645 | + ContactViaWebNotificationRecipientSet.TO_USER, |
| 646 | + view.group_to_contact) |
| 647 | + self.assertEqual('Contact this user', view.specific_contact_text) |
| 648 | + |
| 649 | + def test_contact_user(self): |
| 650 | + sender = self.factory.makePerson() |
| 651 | + user = self.factory.makePerson() |
| 652 | + with person_logged_in(sender): |
| 653 | + view = create_initialized_view(user, '+index') |
| 654 | + self.assertIs( |
| 655 | + ContactViaWebNotificationRecipientSet.TO_USER, |
| 656 | + view.group_to_contact) |
| 657 | + self.assertEqual('Contact this user', view.specific_contact_text) |
| 658 | + self.assertEqual( |
| 659 | + 'Send an email to this user through Launchpad', |
| 660 | + view.contact_link_title) |
| 661 | + |
| 662 | + def test_contact_admins(self): |
| 663 | + sender = self.factory.makePerson() |
| 664 | + team = self.factory.makeTeam() |
| 665 | + with person_logged_in(sender): |
| 666 | + view = create_initialized_view(team, '+index') |
| 667 | + self.assertIs( |
| 668 | + ContactViaWebNotificationRecipientSet.TO_ADMINS, |
| 669 | + view.group_to_contact) |
| 670 | + self.assertEqual( |
| 671 | + "Contact this team's admins", view.specific_contact_text) |
| 672 | + self.assertEqual( |
| 673 | + "Send an email to this team's admins through Launchpad", |
| 674 | + view.contact_link_title) |
| 675 | + |
| 676 | + def test_contact_members(self): |
| 677 | + team = self.factory.makeTeam() |
| 678 | + admin = team.teamowner |
| 679 | + with person_logged_in(admin): |
| 680 | + view = create_initialized_view(team, '+index') |
| 681 | + self.assertIs( |
| 682 | + ContactViaWebNotificationRecipientSet.TO_MEMBERS, |
| 683 | + view.group_to_contact) |
| 684 | + self.assertEqual( |
| 685 | + "Contact this team's members", view.specific_contact_text) |
| 686 | + self.assertEqual( |
| 687 | + "Send an email to your team's members through Launchpad", |
| 688 | + view.contact_link_title) |
| 689 | + |
| 690 | + |
| 691 | +class EmailToPersonViewTestCase(TestCaseWithFactory): |
| 692 | + """Tests the behaviour of EmailToPersonView.""" |
| 693 | + |
| 694 | + layer = DatabaseFunctionalLayer |
| 695 | + |
| 696 | + def makeForm(self, email, subject='subject', message='body'): |
| 697 | + return { |
| 698 | + 'field.field.from_': email, |
| 699 | + 'field.subject': subject, |
| 700 | + 'field.message': message, |
| 701 | + 'field.actions.send': 'Send', |
| 702 | + } |
| 703 | + |
| 704 | + def makeThrottledSender(self): |
| 705 | + sender = self.factory.makePerson(email='me@eg.dom') |
| 706 | + old_message = self.factory.makeSignedMessage(email_address='me@eg.dom') |
| 707 | + authorization = IDirectEmailAuthorization(sender) |
| 708 | + for action in xrange(authorization.message_quota): |
| 709 | + authorization.record(old_message) |
| 710 | + return sender |
| 711 | + |
| 712 | + def test_anonymous_redirected(self): |
| 713 | + # Anonymous users cannot use the form. |
| 714 | + user = self.factory.makePerson(name='him') |
| 715 | + view = create_initialized_view(user, '+contactuser') |
| 716 | + response = view.request.response |
| 717 | + self.assertEqual(302, response.getStatus()) |
| 718 | + self.assertEqual( |
| 719 | + 'http://launchpad.dev/~him', response.getHeader('Location')) |
| 720 | + |
| 721 | + def test_inactive_user_redirects(self): |
| 722 | + # The view explains that the user is inactive. |
| 723 | + sender = self.factory.makePerson() |
| 724 | + inactive_user = self.factory.makePerson( |
| 725 | + name='him', email_address_status=EmailAddressStatus.NEW) |
| 726 | + with person_logged_in(sender): |
| 727 | + view = create_initialized_view(inactive_user, '+contactuser') |
| 728 | + response = view.request.response |
| 729 | + self.assertEqual(302, response.getStatus()) |
| 730 | + self.assertEqual( |
| 731 | + 'http://launchpad.dev/~him', response.getHeader('Location')) |
| 732 | + |
| 733 | + def test_contact_not_possible_reason_to_user(self): |
| 734 | + # The view explains that the user is inactive. |
| 735 | + inactive_user = self.factory.makePerson( |
| 736 | + email_address_status=EmailAddressStatus.NEW) |
| 737 | + user = self.factory.makePerson() |
| 738 | + with person_logged_in(user): |
| 739 | + view = create_initialized_view(inactive_user, '+contactuser') |
| 740 | + self.assertEqual( |
| 741 | + "The user is not active.", view.contact_not_possible_reason) |
| 742 | + |
| 743 | + def test_contact_not_possible_reason_to_admins(self): |
| 744 | + # The view explains that the team has no admins. |
| 745 | + team = self.factory.makeTeam() |
| 746 | + with person_logged_in(team.teamowner): |
| 747 | + team.teamowner.leave(team) |
| 748 | + user = self.factory.makePerson() |
| 749 | + with person_logged_in(user): |
| 750 | + view = create_initialized_view(team, '+contactuser') |
| 751 | + self.assertEqual( |
| 752 | + "The team has no admins. Contact the team owner instead.", |
| 753 | + view.contact_not_possible_reason) |
| 754 | + |
| 755 | + def test_contact_not_possible_reason_to_members(self): |
| 756 | + # The view explains the team has no members. |
| 757 | + team = self.factory.makeTeam() |
| 758 | + with person_logged_in(team.teamowner): |
| 759 | + team.teamowner.leave(team) |
| 760 | + with person_logged_in(team.teamowner): |
| 761 | + view = create_initialized_view(team, '+contactuser') |
| 762 | + self.assertEqual( |
| 763 | + "The team has no members.", view.contact_not_possible_reason) |
| 764 | + |
| 765 | + def test_has_valid_email_address(self): |
| 766 | + # The has_valid_email_address property checks the len of the |
| 767 | + # recipient set. |
| 768 | + team = self.factory.makeTeam() |
| 769 | + sender = self.factory.makePerson() |
| 770 | + with person_logged_in(sender): |
| 771 | + view = create_initialized_view(team, '+contactuser') |
| 772 | + self.assertTrue(view.has_valid_email_address) |
| 773 | + with person_logged_in(team.teamowner): |
| 774 | + team.teamowner.leave(team) |
| 775 | + with person_logged_in(sender): |
| 776 | + view = create_initialized_view(team, '+contactuser') |
| 777 | + self.assertFalse(view.has_valid_email_address) |
| 778 | + |
| 779 | + def test_contact_is_allowed(self): |
| 780 | + # The contact_is_allowed property checks if the user has not exceeded |
| 781 | + # the quota.. |
| 782 | + team = self.factory.makeTeam() |
| 783 | + sender = self.factory.makePerson() |
| 784 | + with person_logged_in(sender): |
| 785 | + view = create_initialized_view(team, '+contactuser') |
| 786 | + self.assertTrue(view.contact_is_allowed) |
| 787 | + |
| 788 | + other_sender = self.makeThrottledSender() |
| 789 | + with person_logged_in(other_sender): |
| 790 | + view = create_initialized_view(team, '+contactuser') |
| 791 | + self.assertFalse(view.contact_is_allowed) |
| 792 | + |
| 793 | + def test_contact_is_possible(self): |
| 794 | + # The contact_is_possible property checks has_valid_email_address |
| 795 | + # and contact_is_allowed. |
| 796 | + team = self.factory.makeTeam() |
| 797 | + sender = self.factory.makePerson() |
| 798 | + with person_logged_in(sender): |
| 799 | + view = create_initialized_view(team, '+contactuser') |
| 800 | + self.assertTrue(view.has_valid_email_address) |
| 801 | + self.assertTrue(view.contact_is_allowed) |
| 802 | + self.assertTrue(view.contact_is_possible) |
| 803 | + |
| 804 | + other_sender = self.makeThrottledSender() |
| 805 | + with person_logged_in(other_sender): |
| 806 | + view = create_initialized_view(team, '+contactuser') |
| 807 | + self.assertTrue(view.has_valid_email_address) |
| 808 | + self.assertFalse(view.contact_is_allowed) |
| 809 | + self.assertFalse(view.contact_is_possible) |
| 810 | + |
| 811 | + with person_logged_in(team.teamowner): |
| 812 | + team.teamowner.leave(team) |
| 813 | + with person_logged_in(sender): |
| 814 | + view = create_initialized_view(team, '+contactuser') |
| 815 | + self.assertTrue(view.contact_is_allowed) |
| 816 | + self.assertFalse(view.has_valid_email_address) |
| 817 | + self.assertFalse(view.contact_is_possible) |
| 818 | + |
| 819 | + def test_user_contacting_user(self): |
| 820 | + sender = self.factory.makePerson() |
| 821 | + user = self.factory.makePerson(name='pting') |
| 822 | + with person_logged_in(sender): |
| 823 | + view = create_initialized_view(user, '+contactuser') |
| 824 | + self.assertEqual('Contact user', view.label) |
| 825 | + self.assertEqual('Contact this user', view.page_title) |
| 826 | + |
| 827 | + def test_user_contacting_self(self): |
| 828 | + sender = self.factory.makePerson() |
| 829 | + with person_logged_in(sender): |
| 830 | + view = create_initialized_view(sender, '+contactuser') |
| 831 | + self.assertEqual('Contact user', view.label) |
| 832 | + self.assertEqual('Contact yourself', view.page_title) |
| 833 | + |
| 834 | + def test_user_contacting_team(self): |
| 835 | + sender = self.factory.makePerson() |
| 836 | + team = self.factory.makeTeam(name='pting') |
| 837 | + with person_logged_in(sender): |
| 838 | + view = create_initialized_view(team, '+contactuser') |
| 839 | + self.assertEqual('Contact user', view.label) |
| 840 | + self.assertEqual('Contact this team', view.page_title) |
| 841 | + |
| 842 | + def test_member_contacting_team(self): |
| 843 | + member = self.factory.makePerson() |
| 844 | + team = self.factory.makeTeam(name='pting', members=[member]) |
| 845 | + with person_logged_in(member): |
| 846 | + view = create_initialized_view(team, '+contactuser') |
| 847 | + self.assertEqual('Contact user', view.label) |
| 848 | + self.assertEqual('Contact your team', view.page_title) |
| 849 | + |
| 850 | + def test_admin_contacting_team(self): |
| 851 | + member = self.factory.makePerson() |
| 852 | + team = self.factory.makeTeam(name='pting', members=[member]) |
| 853 | + admin = team.teamowner |
| 854 | + with person_logged_in(admin): |
| 855 | + view = create_initialized_view(team, '+contactuser') |
| 856 | + self.assertEqual('Contact user', view.label) |
| 857 | + self.assertEqual('Contact your team', view.page_title) |
| 858 | + |
| 859 | + def test_submit(self): |
| 860 | + # The subject and message fields are required. |
| 861 | + sender = self.factory.makePerson(email='me@eg.dom') |
| 862 | + user = self.factory.makePerson(name='pting') |
| 863 | + form = self.makeForm('me@eg.dom', 'subject', 'body') |
| 864 | + with person_logged_in(sender): |
| 865 | + view = create_initialized_view(user, '+contactuser', form=form) |
| 866 | + self.assertEqual([], view.errors) |
| 867 | + notes = [n.message for n in view.request.response.notifications] |
| 868 | + self.assertEqual(['Message sent to Pting'], notes) |
| 869 | + |
| 870 | + def test_missing_subject_and_message(self): |
| 871 | + # The subject and message fields are required. |
| 872 | + sender = self.factory.makePerson(email='me@eg.dom') |
| 873 | + user = self.factory.makePerson() |
| 874 | + form = self.makeForm('me@eg.dom', ' ', ' ') |
| 875 | + with person_logged_in(sender): |
| 876 | + view = create_initialized_view(user, '+contactuser', form=form) |
| 877 | + self.assertEqual( |
| 878 | + [u'You must provide a subject and a message.'], view.errors) |
| 879 | + |
| 880 | + def test_submitted_after_quota(self): |
| 881 | + # The view explains when a message was not sent because the quota |
| 882 | + # was exceeded. |
| 883 | + user = self.factory.makePerson() |
| 884 | + sender = self.makeThrottledSender() |
| 885 | + form = self.makeForm('me@eg.dom') |
| 886 | + with person_logged_in(sender): |
| 887 | + view = create_initialized_view(user, '+contactuser', form=form) |
| 888 | + notification = [ |
| 889 | + 'Your message was not sent because you have exceeded your daily ' |
| 890 | + 'quota of 3 messages to contact users. Try again %s.' % |
| 891 | + DateTimeFormatterAPI(view.next_try).approximatedate() |
| 892 | + ] |
| 893 | + notes = [n.message for n in view.request.response.notifications] |
| 894 | + self.assertContentEqual(notification, notes) |
| 895 | |
| 896 | === removed file 'lib/lp/registry/browser/tests/user-to-user-views.txt' |
| 897 | --- lib/lp/registry/browser/tests/user-to-user-views.txt 2012-04-10 14:01:17 +0000 |
| 898 | +++ lib/lp/registry/browser/tests/user-to-user-views.txt 1970-01-01 00:00:00 +0000 |
| 899 | @@ -1,491 +0,0 @@ |
| 900 | -User-to-user direct email contact |
| 901 | -================================= |
| 902 | - |
| 903 | -A Launchpad user can contact another Launchpad user directly, even if |
| 904 | -the recipient is hiding their email addresses. |
| 905 | - |
| 906 | - >>> def create_view(sender, recipient, form=None): |
| 907 | - ... return create_initialized_view( |
| 908 | - ... recipient, '+contactuser', |
| 909 | - ... form=form, principal=sender) |
| 910 | - |
| 911 | - >>> def print_notifications(view): |
| 912 | - ... for notification in view.request.notifications: |
| 913 | - ... print notification.message |
| 914 | - |
| 915 | -For example, let's say No Privileges Person wants to contact Salgado... |
| 916 | - |
| 917 | - >>> from zope.component import getUtility |
| 918 | - >>> from lp.registry.interfaces.person import IPersonSet |
| 919 | - >>> person_set = getUtility(IPersonSet) |
| 920 | - >>> no_priv = person_set.getByName('no-priv') |
| 921 | - >>> salgado = person_set.getByName('salgado') |
| 922 | - |
| 923 | -...No Priv would start by going to Salgado's +contactuser page. |
| 924 | - |
| 925 | - >>> from lp.testing import login |
| 926 | - >>> ignored = login_person(no_priv) |
| 927 | - >>> view = create_view(no_priv, salgado) |
| 928 | - |
| 929 | -This contact is allowed. |
| 930 | - |
| 931 | - >>> print view.label |
| 932 | - Contact user |
| 933 | - |
| 934 | - >>> view.contact_is_allowed |
| 935 | - True |
| 936 | - |
| 937 | -No Priv changes her mind though. |
| 938 | - |
| 939 | - >>> print view.cancel_url |
| 940 | - http://launchpad.dev/~salgado |
| 941 | - |
| 942 | -No Priv decides, what the heck, let's contact Salgado after all. |
| 943 | - |
| 944 | - >>> view = create_view( |
| 945 | - ... no_priv, salgado, { |
| 946 | - ... 'field.field.from_': 'no-priv@canonical.com', |
| 947 | - ... 'field.subject': 'Hello Salgado', |
| 948 | - ... 'field.message': 'Can you tell me about your project?', |
| 949 | - ... 'field.actions.send': 'Send', |
| 950 | - ... }) |
| 951 | - >>> print_notifications(view) |
| 952 | - Message sent to Guilherme Salgado |
| 953 | - |
| 954 | - # Capture the date of the last contact for later. |
| 955 | - |
| 956 | - >>> from lp.services.config import config |
| 957 | - >>> from lp.services.messages.model.message import UserToUserEmail |
| 958 | - >>> from lazr.config import as_timedelta |
| 959 | - >>> from storm.locals import Store |
| 960 | - >>> first_contact = Store.of(no_priv).find( |
| 961 | - ... UserToUserEmail, |
| 962 | - ... UserToUserEmail.sender == no_priv).one() |
| 963 | - >>> expires = first_contact.date_sent + as_timedelta( |
| 964 | - ... config.launchpad.user_to_user_throttle_interval) |
| 965 | - |
| 966 | -No Priv sends two more messages to Salgado. Each of these are allowed |
| 967 | -too. |
| 968 | - |
| 969 | - >>> view = create_view( |
| 970 | - ... no_priv, salgado, { |
| 971 | - ... 'field.field.from_': 'no-priv@canonical.com', |
| 972 | - ... 'field.subject': 'Hello Salgado', |
| 973 | - ... 'field.message': 'Can you tell me about your project?', |
| 974 | - ... 'field.actions.send': 'Send', |
| 975 | - ... }) |
| 976 | - >>> print_notifications(view) |
| 977 | - Message sent to Guilherme Salgado |
| 978 | - |
| 979 | - >>> view = create_view( |
| 980 | - ... no_priv, salgado, { |
| 981 | - ... 'field.field.from_': 'no-priv@canonical.com', |
| 982 | - ... 'field.subject': 'Hello Salgado', |
| 983 | - ... 'field.message': 'Can you tell me about your project?', |
| 984 | - ... 'field.actions.send': 'Send', |
| 985 | - ... }) |
| 986 | - >>> print_notifications(view) |
| 987 | - Message sent to Guilherme Salgado |
| 988 | - |
| 989 | -Now however, No Priv had reached her quota for direct user-to-user |
| 990 | -contact and is not allowed to send a fourth message today. |
| 991 | - |
| 992 | - >>> view = create_view(no_priv, salgado) |
| 993 | - >>> view.contact_is_allowed |
| 994 | - False |
| 995 | - |
| 996 | -No Priv can try again later. |
| 997 | - |
| 998 | - >>> view.next_try == expires |
| 999 | - True |
| 1000 | - |
| 1001 | -As a corner case, let's say the number of notifications allowed was |
| 1002 | -greater yesterday than it was today. |
| 1003 | - |
| 1004 | - >>> config.push('seven_allowed', """\ |
| 1005 | - ... [launchpad] |
| 1006 | - ... user_to_user_max_messages: 7 |
| 1007 | - ... """) |
| 1008 | - |
| 1009 | -No Priv can actually try again right now. |
| 1010 | - |
| 1011 | - >>> from datetime import datetime |
| 1012 | - >>> import pytz |
| 1013 | - >>> view.next_try <= datetime.now(pytz.timezone('UTC')) |
| 1014 | - True |
| 1015 | - |
| 1016 | -So, No Priv sends four more emails. |
| 1017 | - |
| 1018 | - >>> for i in range(4): |
| 1019 | - ... assert create_view(no_priv, salgado).contact_is_allowed, ( |
| 1020 | - ... 'Contact was not allowed? %s' % i) |
| 1021 | - ... view = create_view( |
| 1022 | - ... no_priv, salgado, { |
| 1023 | - ... 'field.field.from_': 'no-priv@canonical.com', |
| 1024 | - ... 'field.subject': 'Hello Salgado', |
| 1025 | - ... 'field.message': 'Can you tell me about your project?', |
| 1026 | - ... 'field.actions.send': 'Send', |
| 1027 | - ... }) |
| 1028 | - ... print_notifications(view) |
| 1029 | - Message sent to Guilherme Salgado |
| 1030 | - Message sent to Guilherme Salgado |
| 1031 | - Message sent to Guilherme Salgado |
| 1032 | - Message sent to Guilherme Salgado |
| 1033 | - |
| 1034 | -No Priv has once again reached her limit of emails. |
| 1035 | - |
| 1036 | - >>> view = create_view(no_priv, salgado) |
| 1037 | - >>> view.contact_is_allowed |
| 1038 | - False |
| 1039 | - |
| 1040 | - >>> view.next_try == expires |
| 1041 | - True |
| 1042 | - |
| 1043 | -The configuration changes back to allow only three emails. |
| 1044 | - |
| 1045 | - >>> config.pop('seven_allowed') |
| 1046 | - (...) |
| 1047 | - |
| 1048 | - >>> contacts = Store.of(no_priv).find( |
| 1049 | - ... UserToUserEmail, |
| 1050 | - ... UserToUserEmail.sender == no_priv) |
| 1051 | - >>> contact = list(contacts)[4] |
| 1052 | - >>> expires = contact.date_sent + as_timedelta( |
| 1053 | - ... config.launchpad.user_to_user_throttle_interval) |
| 1054 | - |
| 1055 | - |
| 1056 | -Non-ASCII names |
| 1057 | ---------------- |
| 1058 | - |
| 1059 | -Carlos has non-ASCII characters in his name. When he sends a message to |
| 1060 | -a user, his real name will be properly RFC 2047 encoded. |
| 1061 | - |
| 1062 | - >>> transaction.abort() |
| 1063 | - >>> from lp.services.mail import stub |
| 1064 | - >>> del stub.test_emails[:] |
| 1065 | - >>> len(stub.test_emails) |
| 1066 | - 0 |
| 1067 | - |
| 1068 | - >>> carlos = person_set.getByName('carlos') |
| 1069 | - >>> login('carlos@canonical.com') |
| 1070 | - >>> view = create_view( |
| 1071 | - ... carlos, no_priv, { |
| 1072 | - ... 'field.field.from_': 'carlos@canonical.com', |
| 1073 | - ... 'field.subject': 'Hello No Priv', |
| 1074 | - ... 'field.message': 'I see funny characters', |
| 1075 | - ... 'field.actions.send': 'Send', |
| 1076 | - ... }) |
| 1077 | - >>> transaction.commit() |
| 1078 | - |
| 1079 | - >>> len(stub.test_emails) |
| 1080 | - 1 |
| 1081 | - |
| 1082 | - >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop() |
| 1083 | - >>> print raw_msg |
| 1084 | - Content-Type: text/plain; charset="us-ascii" |
| 1085 | - ... |
| 1086 | - From: =?utf-8?q?Carlos_Perell=C3=B3_Mar=C3=ADn?= <carlos@canonical.com> |
| 1087 | - To: No Privileges Person <no-priv@canonical.com> |
| 1088 | - ... |
| 1089 | - |
| 1090 | -Similarly, if Carlos is the recipient of a message, his real name will |
| 1091 | -be properly RFC 2047 encoded as well. |
| 1092 | - |
| 1093 | - >>> del stub.test_emails[:] |
| 1094 | - |
| 1095 | - >>> login('no-priv@canonical.com') |
| 1096 | - >>> view = create_view( |
| 1097 | - ... no_priv, carlos, { |
| 1098 | - ... 'field.field.from_': 'no-priv@canonical.com', |
| 1099 | - ... 'field.subject': 'Hello Carlos', |
| 1100 | - ... 'field.message': 'I see funny characters', |
| 1101 | - ... 'field.actions.send': 'Send', |
| 1102 | - ... }) |
| 1103 | - >>> transaction.commit() |
| 1104 | - |
| 1105 | - >>> len(stub.test_emails) |
| 1106 | - 1 |
| 1107 | - |
| 1108 | - >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop() |
| 1109 | - >>> print raw_msg |
| 1110 | - Content-Type: text/plain; charset="us-ascii" |
| 1111 | - ... |
| 1112 | - From: No Privileges Person <no-priv@canonical.com> |
| 1113 | - To: =?utf-8?q?Carlos_Perell=C3=B3_Mar=C3=ADn?= <carlos@canonical.com> |
| 1114 | - ... |
| 1115 | - |
| 1116 | - |
| 1117 | -Hidden addresses |
| 1118 | ----------------- |
| 1119 | - |
| 1120 | -Salgado decides to hide his email addresses. |
| 1121 | - |
| 1122 | - >>> ignored = login_person(salgado) |
| 1123 | - >>> salgado.hide_email_addresses = True |
| 1124 | - |
| 1125 | -Anne contacts Salgado even though his email addresses are hidden. |
| 1126 | - |
| 1127 | - >>> anne = factory.makePerson(email='anne@example.com', name='anne') |
| 1128 | - >>> ignored = login_person(anne) |
| 1129 | - |
| 1130 | - >>> view = create_view( |
| 1131 | - ... anne, salgado, { |
| 1132 | - ... 'field.field.from_': 'anne@example.com', |
| 1133 | - ... 'field.subject': 'Hello Salgado', |
| 1134 | - ... 'field.message': 'It is nice to meet you', |
| 1135 | - ... 'field.actions.send': 'Send', |
| 1136 | - ... }) |
| 1137 | - >>> print_notifications(view) |
| 1138 | - Message sent to Guilherme Salgado |
| 1139 | - |
| 1140 | - |
| 1141 | -Contacting teams |
| 1142 | ----------------- |
| 1143 | - |
| 1144 | -Teams can also be contacted directly, regardless of whether they have no |
| 1145 | -official contact address, use a Launchpad mailing list, or have the |
| 1146 | -contact address set to an explicit address. |
| 1147 | - |
| 1148 | - # Clear out left over crud. |
| 1149 | - |
| 1150 | - >>> transaction.commit() |
| 1151 | - >>> del stub.test_emails[:] |
| 1152 | - |
| 1153 | - >>> from email import message_from_string |
| 1154 | - >>> def print_messages(): |
| 1155 | - ... message_count = 0 |
| 1156 | - ... message_subjects = set() |
| 1157 | - ... message_senders = set() |
| 1158 | - ... message_recipients = set() |
| 1159 | - ... message_bodies = set() |
| 1160 | - ... while stub.test_emails: |
| 1161 | - ... from_addr, to_addrs, raw_msg = stub.test_emails.pop() |
| 1162 | - ... message = message_from_string(raw_msg) |
| 1163 | - ... message_count += 1 |
| 1164 | - ... message_subjects.add(message['subject']) |
| 1165 | - ... message_senders.add(message['from']) |
| 1166 | - ... message_recipients.add(message['to']) |
| 1167 | - ... message_bodies.add(message.get_payload()) |
| 1168 | - ... print 'Senders:', message_senders |
| 1169 | - ... print 'Subjects:', message_subjects |
| 1170 | - ... print 'Bodies:' |
| 1171 | - ... for body in sorted(message_bodies): |
| 1172 | - ... print body |
| 1173 | - ... print '# of Messages:', message_count |
| 1174 | - ... print 'Recipients:' |
| 1175 | - ... for recipient in sorted(message_recipients): |
| 1176 | - ... print ' ', recipient |
| 1177 | - |
| 1178 | - |
| 1179 | -Non-member to team |
| 1180 | -.................. |
| 1181 | - |
| 1182 | -Non-members may only contact the team owner. |
| 1183 | - |
| 1184 | - >>> guadamen = person_set.getByName('guadamen') |
| 1185 | - >>> bart = factory.makePerson(email='bart@example.com', name='bart') |
| 1186 | - >>> ignored = login_person(bart) |
| 1187 | - |
| 1188 | - >>> view = create_view( |
| 1189 | - ... bart, guadamen, { |
| 1190 | - ... 'field.field.from_': 'bart@example.com', |
| 1191 | - ... 'field.subject': 'Hello Guadamen', |
| 1192 | - ... 'field.message': 'Can one of you help me?', |
| 1193 | - ... 'field.actions.send': 'Send', |
| 1194 | - ... }) |
| 1195 | - >>> print_notifications(view) |
| 1196 | - Message sent to GuadaMen |
| 1197 | - |
| 1198 | - >>> transaction.commit() |
| 1199 | - >>> print_messages() |
| 1200 | - Senders: set(['Bart <bart@example.com>']) |
| 1201 | - Subjects: set(['Hello Guadamen']) |
| 1202 | - Bodies: |
| 1203 | - Can one of you help me? |
| 1204 | - -- |
| 1205 | - This message was sent from Launchpad by |
| 1206 | - Bart (http://launchpad.dev/~bart) |
| 1207 | - using the "Contact this team's admins" link on the GuadaMen team page |
| 1208 | - (http://launchpad.dev/~guadamen). |
| 1209 | - For more information see |
| 1210 | - https://help.launchpad.net/YourAccount/ContactingPeople |
| 1211 | - # of Messages: 2 |
| 1212 | - Recipients: |
| 1213 | - Foo Bar <foo.bar@canonical.com> |
| 1214 | - Ubuntu Team <support@ubuntu.com> |
| 1215 | - |
| 1216 | - |
| 1217 | -Member to team |
| 1218 | -.............. |
| 1219 | - |
| 1220 | -Foo Bar is a member of Guadamen team, he is not restricted to contacting |
| 1221 | -the team owner. The Guadamen team has no contact address, so contacting |
| 1222 | -them contacts all its members directly. |
| 1223 | - |
| 1224 | - >>> login('foo.bar@canonical.com') |
| 1225 | - >>> foo_bar = person_set.getByName('name16') |
| 1226 | - >>> view = create_view( |
| 1227 | - ... foo_bar, guadamen, { |
| 1228 | - ... 'field.field.from_': 'foo.bar@canonical.com', |
| 1229 | - ... 'field.subject': 'Hello Guadamen', |
| 1230 | - ... 'field.message': 'Can one of you help me?', |
| 1231 | - ... 'field.actions.send': 'Send', |
| 1232 | - ... }) |
| 1233 | - >>> print_notifications(view) |
| 1234 | - Message sent to GuadaMen |
| 1235 | - |
| 1236 | -There are 10 members of the team, so exactly 10 unique copies of the |
| 1237 | -message are sent, one to each team member. Everyone gets a message with |
| 1238 | -the same subject and body from the same sender. |
| 1239 | - |
| 1240 | - >>> transaction.commit() |
| 1241 | - >>> print_messages() |
| 1242 | - Senders: set(['Foo Bar <foo.bar@canonical.com>']) |
| 1243 | - Subjects: set(['Hello Guadamen']) |
| 1244 | - Bodies: |
| 1245 | - Can one of you help me? |
| 1246 | - -- |
| 1247 | - This message was sent from Launchpad by |
| 1248 | - Foo Bar (http://launchpad.dev/~name16) |
| 1249 | - to each member of the GuadaMen team using the "Contact this team" link on |
| 1250 | - the GuadaMen team page (http://launchpad.dev/~guadamen). |
| 1251 | - For more information see |
| 1252 | - https://help.launchpad.net/YourAccount/ContactingPeople |
| 1253 | - # of Messages: 10 |
| 1254 | - Recipients: |
| 1255 | - Alexander Limi <limi@plone.org> |
| 1256 | - Celso Providelo <celso.providelo@canonical.com> |
| 1257 | - Colin Watson <colin.watson@ubuntulinux.com> |
| 1258 | - Daniel Silverstone <daniel.silverstone@canonical.com> |
| 1259 | - Edgar Bursic <edgar@monteparadiso.hr> |
| 1260 | - Foo Bar <foo.bar@canonical.com> |
| 1261 | - Jeff Waugh <jeff.waugh@ubuntulinux.com> |
| 1262 | - Mark Shuttleworth <mark@example.com> |
| 1263 | - Steve Alexander <steve.alexander@ubuntulinux.com> |
| 1264 | - Ubuntu Team <support@ubuntu.com> |
| 1265 | - |
| 1266 | - >>> transaction.commit() |
| 1267 | - |
| 1268 | -Message quota |
| 1269 | -------------- |
| 1270 | - |
| 1271 | -The EmailToPersonView provides two properties that check that the user |
| 1272 | -is_allowed to send emails because he has not exceeded the daily quota. |
| 1273 | -The next_try property is the date when the user will be allowed to send |
| 1274 | -emails again. The is_possible property will be False if is_allowed is |
| 1275 | -False. |
| 1276 | - |
| 1277 | - >>> view = create_view( |
| 1278 | - ... foo_bar, guadamen, { |
| 1279 | - ... 'field.field.from_': 'foo.bar@canonical.com', |
| 1280 | - ... 'field.subject': 'Hello Guadamen', |
| 1281 | - ... 'field.message': 'Can one of you help me?', |
| 1282 | - ... 'field.actions.send': 'Send', |
| 1283 | - ... }) |
| 1284 | - >>> view.contact_is_allowed |
| 1285 | - True |
| 1286 | - |
| 1287 | - >>> view.contact_is_possible |
| 1288 | - True |
| 1289 | - |
| 1290 | - >>> view.initialize() |
| 1291 | - >>> transaction.commit() |
| 1292 | - |
| 1293 | -Foo Bar has now reached his quota and can send no more contact messages |
| 1294 | -today. |
| 1295 | - |
| 1296 | - >>> view = create_view( |
| 1297 | - ... foo_bar, guadamen, { |
| 1298 | - ... 'field.field.from_': 'foo.bar@canonical.com', |
| 1299 | - ... 'field.subject': 'My last question for Guadamen', |
| 1300 | - ... 'field.message': 'Really, can one of you help me!', |
| 1301 | - ... 'field.actions.send': 'Send', |
| 1302 | - ... }) |
| 1303 | - >>> view.contact_is_allowed |
| 1304 | - False |
| 1305 | - |
| 1306 | - >>> view.next_try |
| 1307 | - datetime.datetime... |
| 1308 | - |
| 1309 | - >>> view.contact_is_possible |
| 1310 | - False |
| 1311 | - |
| 1312 | - >>> print_notifications(view) |
| 1313 | - Your message was not sent because you have exceeded your daily quota of |
| 1314 | - 3 messages to contact users. Try again in ... |
| 1315 | - |
| 1316 | - |
| 1317 | -Identifying information |
| 1318 | ------------------------ |
| 1319 | - |
| 1320 | -Every contact message has a special Launchpad header so that people can |
| 1321 | -tell that the message came to them through Launchpad. It has a footer |
| 1322 | -that contains an explanation as well. |
| 1323 | - |
| 1324 | - >>> cris = factory.makePerson(email='cris@example.com', name='cris') |
| 1325 | - >>> dave = factory.makePerson(email='dave@example.com', name='dave') |
| 1326 | - >>> ignored = login_person(cris) |
| 1327 | - |
| 1328 | - >>> view = create_view( |
| 1329 | - ... cris, dave, { |
| 1330 | - ... 'field.field.from_': 'cris@example.com', |
| 1331 | - ... 'field.subject': 'Hi Dave', |
| 1332 | - ... 'field.message': 'Can you help me?' , |
| 1333 | - ... 'field.actions.send': 'Send', |
| 1334 | - ... }) |
| 1335 | - >>> transaction.commit() |
| 1336 | - >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop() |
| 1337 | - >>> print raw_msg |
| 1338 | - Content-Type: text/plain; charset="us-ascii" |
| 1339 | - ... |
| 1340 | - From: Cris <cris@example.com> |
| 1341 | - To: Dave <dave@example.com> |
| 1342 | - Subject: Hi Dave |
| 1343 | - ... |
| 1344 | - X-Launchpad-Message-Rationale: ContactViaWeb user |
| 1345 | - ... |
| 1346 | - <BLANKLINE> |
| 1347 | - Can you help me? |
| 1348 | - -- |
| 1349 | - This message was sent from Launchpad by |
| 1350 | - Cris (http://launchpad.dev/~cris) |
| 1351 | - using the "Contact this user" link on your profile page |
| 1352 | - (http://launchpad.dev/~dave). |
| 1353 | - For more information see |
| 1354 | - https://help.launchpad.net/YourAccount/ContactingPeople |
| 1355 | - |
| 1356 | - |
| 1357 | -Message wrapping |
| 1358 | ----------------- |
| 1359 | - |
| 1360 | -The message body is wrapped at 72 characters. The footer is not wrapped, |
| 1361 | -but a new line is started after the names of the sender and the recient |
| 1362 | -to minimise long lines. |
| 1363 | - |
| 1364 | - >>> login('test@canonical.com') |
| 1365 | - >>> sample_person = person_set.getByEmail('test@canonical.com') |
| 1366 | - >>> landscape_developers = person_set.getByName('landscape-developers') |
| 1367 | - >>> view = create_view( |
| 1368 | - ... sample_person, landscape_developers, { |
| 1369 | - ... 'field.field.from_': 'test@canonical.com', |
| 1370 | - ... 'field.subject': 'Wrapping test ', |
| 1371 | - ... 'field.message': 'Can you help me? ' * 8, |
| 1372 | - ... 'field.actions.send': 'Send', |
| 1373 | - ... }) |
| 1374 | - >>> transaction.commit() |
| 1375 | - >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop() |
| 1376 | - >>> header, body = raw_msg.split('\n\n') |
| 1377 | - >>> for line in body.split('\n'): |
| 1378 | - ... print "^%s$" % line |
| 1379 | - ^Can you help me? Can you help me? Can you help me? Can you help me? Can$ |
| 1380 | - ^you help me? Can you help me? Can you help me? Can you help me?$ |
| 1381 | - ^-- $ |
| 1382 | - ^This message was sent from Launchpad by$ |
| 1383 | - ^Sample Person (http://launchpad.dev/~name12)$ |
| 1384 | - ^to each member of the Landscape Developers team using the "Contact this$ |
| 1385 | - ^team" link on the Landscape Developers team page$ |
| 1386 | - ^(http://launchpad.dev/~landscape-developers).$ |
| 1387 | - ^For more information see$ |
| 1388 | - ^https://help.launchpad.net/YourAccount/ContactingPeople$ |
| 1389 | - |
| 1390 | - |
| 1391 | |
| 1392 | === modified file 'lib/lp/registry/mail/notification.py' |
| 1393 | --- lib/lp/registry/mail/notification.py 2012-08-14 23:27:07 +0000 |
| 1394 | +++ lib/lp/registry/mail/notification.py 2012-11-27 20:11:21 +0000 |
| 1395 | @@ -396,15 +396,7 @@ |
| 1396 | message['X-Launchpad-Message-Rationale'] = rational_header |
| 1397 | # Send the message. |
| 1398 | sendmail(message, bulk=False) |
| 1399 | - # BarryWarsaw 19-Nov-2008: If any messages were sent, record the fact that |
| 1400 | - # the sender contacted the team. This is not perfect though because we're |
| 1401 | - # really recording the fact that the person contacted the last member of |
| 1402 | - # the team. There's little we can do better though because the team has |
| 1403 | - # no contact address, and so there isn't actually an address to record as |
| 1404 | - # the team's recipient. It currently doesn't matter though because we |
| 1405 | - # don't actually do anything with the recipient information yet. All we |
| 1406 | - # care about is the sender, for quota purposes. We definitely want to |
| 1407 | - # record the contact outside the above loop though, because if there are |
| 1408 | - # 10 members of the team with no contact address, one message should not |
| 1409 | - # consume the sender's entire quota. |
| 1410 | - authorization.record(message) |
| 1411 | + # Use the information from the last message sent to record the action |
| 1412 | + # taken. The record will be used to throttle user-to-user emails. |
| 1413 | + if message is not None: |
| 1414 | + authorization.record(message) |
| 1415 | |
| 1416 | === modified file 'lib/lp/registry/templates/contact-user.pt' |
| 1417 | --- lib/lp/registry/templates/contact-user.pt 2012-02-10 19:52:27 +0000 |
| 1418 | +++ lib/lp/registry/templates/contact-user.pt 2012-11-27 20:11:21 +0000 |
| 1419 | @@ -31,7 +31,7 @@ |
| 1420 | </p> |
| 1421 | </tal:disallowed> |
| 1422 | <tal:disallowed tal:condition="not:view/has_valid_email_address"> |
| 1423 | - <p tal:content="view/recipients/description"/> |
| 1424 | + <p tal:content="view/contact_not_possible_reason"/> |
| 1425 | </tal:disallowed> |
| 1426 | </div> |
| 1427 | </body> |
| 1428 | |
| 1429 | === added file 'lib/lp/registry/tests/test_notification.py' |
| 1430 | --- lib/lp/registry/tests/test_notification.py 1970-01-01 00:00:00 +0000 |
| 1431 | +++ lib/lp/registry/tests/test_notification.py 2012-11-27 20:11:21 +0000 |
| 1432 | @@ -0,0 +1,114 @@ |
| 1433 | +# Copyright 2012 Canonical Ltd. This software is licensed under the |
| 1434 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
| 1435 | + |
| 1436 | +"""Test notification classes and functions.""" |
| 1437 | + |
| 1438 | +__metaclass__ = type |
| 1439 | + |
| 1440 | +from lp.registry.mail.notification import send_direct_contact_email |
| 1441 | +from lp.services.mail.notificationrecipientset import NotificationRecipientSet |
| 1442 | +from lp.services.messages.interfaces.message import ( |
| 1443 | + IDirectEmailAuthorization, |
| 1444 | + QuotaReachedError, |
| 1445 | + ) |
| 1446 | +from lp.testing import TestCaseWithFactory |
| 1447 | +from lp.testing.layers import DatabaseFunctionalLayer |
| 1448 | +from lp.testing.mail_helpers import pop_notifications |
| 1449 | + |
| 1450 | + |
| 1451 | +class SendDirectContactEmailTestCase(TestCaseWithFactory): |
| 1452 | + |
| 1453 | + layer = DatabaseFunctionalLayer |
| 1454 | + |
| 1455 | + def test_send_message(self): |
| 1456 | + self.factory.makePerson(email='me@eg.dom', name='me') |
| 1457 | + user = self.factory.makePerson(email='him@eg.dom', name='him') |
| 1458 | + subject = 'test subject' |
| 1459 | + body = 'test body' |
| 1460 | + recipients_set = NotificationRecipientSet() |
| 1461 | + recipients_set.add(user, 'test reason', 'test rationale') |
| 1462 | + pop_notifications() |
| 1463 | + send_direct_contact_email('me@eg.dom', recipients_set, subject, body) |
| 1464 | + notifications = pop_notifications() |
| 1465 | + notification = notifications[0] |
| 1466 | + self.assertEqual(1, len(notifications)) |
| 1467 | + self.assertEqual('Me <me@eg.dom>', notification['From']) |
| 1468 | + self.assertEqual('Him <him@eg.dom>', notification['To']) |
| 1469 | + self.assertEqual(subject, notification['Subject']) |
| 1470 | + self.assertEqual( |
| 1471 | + 'test rationale', notification['X-Launchpad-Message-Rationale']) |
| 1472 | + self.assertIs(None, notification['Precedence']) |
| 1473 | + self.assertTrue('launchpad' in notification['Message-ID']) |
| 1474 | + self.assertEqual( |
| 1475 | + '\n'.join([ |
| 1476 | + '%s' % body, |
| 1477 | + '-- ', |
| 1478 | + 'This message was sent from Launchpad by', |
| 1479 | + 'Me (http://launchpad.dev/~me)', |
| 1480 | + 'test reason.', |
| 1481 | + 'For more information see', |
| 1482 | + 'https://help.launchpad.net/YourAccount/ContactingPeople']), |
| 1483 | + notification.get_payload()) |
| 1484 | + |
| 1485 | + def test_quota_reached_error(self): |
| 1486 | + # An error is raised if the user has reached the daily quota. |
| 1487 | + self.factory.makePerson(email='me@eg.dom', name='me') |
| 1488 | + user = self.factory.makePerson(email='him@eg.dom', name='him') |
| 1489 | + recipients_set = NotificationRecipientSet() |
| 1490 | + old_message = self.factory.makeSignedMessage(email_address='me@eg.dom') |
| 1491 | + authorization = IDirectEmailAuthorization(user) |
| 1492 | + for action in xrange(authorization.message_quota): |
| 1493 | + authorization.record(old_message) |
| 1494 | + self.assertRaises( |
| 1495 | + QuotaReachedError, send_direct_contact_email, |
| 1496 | + 'me@eg.dom', recipients_set, 'subject', 'body') |
| 1497 | + |
| 1498 | + def test_empty_recipient_set(self): |
| 1499 | + # The recipient set can be empty. No messages are sent and the |
| 1500 | + # action does not count toward the daily quota. |
| 1501 | + self.factory.makePerson(email='me@eg.dom', name='me') |
| 1502 | + user = self.factory.makePerson(email='him@eg.dom', name='him') |
| 1503 | + recipients_set = NotificationRecipientSet() |
| 1504 | + old_message = self.factory.makeSignedMessage(email_address='me@eg.dom') |
| 1505 | + authorization = IDirectEmailAuthorization(user) |
| 1506 | + for action in xrange(authorization.message_quota - 1): |
| 1507 | + authorization.record(old_message) |
| 1508 | + pop_notifications() |
| 1509 | + send_direct_contact_email( |
| 1510 | + 'me@eg.dom', recipients_set, 'subject', 'body') |
| 1511 | + notifications = pop_notifications() |
| 1512 | + self.assertEqual(0, len(notifications)) |
| 1513 | + self.assertTrue(authorization.is_allowed) |
| 1514 | + |
| 1515 | + def test_wrapping(self): |
| 1516 | + self.factory.makePerson(email='me@eg.dom') |
| 1517 | + user = self.factory.makePerson() |
| 1518 | + recipients_set = NotificationRecipientSet() |
| 1519 | + recipients_set.add(user, 'test reason', 'test rationale') |
| 1520 | + pop_notifications() |
| 1521 | + body = 'Can you help me? ' * 8 |
| 1522 | + send_direct_contact_email('me@eg.dom', recipients_set, 'subject', body) |
| 1523 | + notifications = pop_notifications() |
| 1524 | + body, footer = notifications[0].get_payload().split('-- ') |
| 1525 | + self.assertEqual( |
| 1526 | + 'Can you help me? Can you help me? Can you help me? ' |
| 1527 | + 'Can you help me? Can\n' |
| 1528 | + 'you help me? Can you help me? Can you help me? ' |
| 1529 | + 'Can you help me?\n', |
| 1530 | + body) |
| 1531 | + |
| 1532 | + def test_name_utf8_encoding(self): |
| 1533 | + # Names are encoded in the From and To headers. |
| 1534 | + self.factory.makePerson(email='me@eg.dom', displayname=u'sn\xefrf') |
| 1535 | + user = self.factory.makePerson( |
| 1536 | + email='him@eg.dom', displayname=u'pti\xedng') |
| 1537 | + recipients_set = NotificationRecipientSet() |
| 1538 | + recipients_set.add(user, 'test reason', 'test rationale') |
| 1539 | + pop_notifications() |
| 1540 | + send_direct_contact_email('me@eg.dom', recipients_set, 'test', 'test') |
| 1541 | + notifications = pop_notifications() |
| 1542 | + notification = notifications[0] |
| 1543 | + self.assertEqual( |
| 1544 | + '=?utf-8?b?c27Dr3Jm?= <me@eg.dom>', notification['From']) |
| 1545 | + self.assertEqual( |
| 1546 | + '=?utf-8?q?pti=C3=ADng?= <him@eg.dom>', notification['To']) |

This branch looks good. I had a couple of small questions/ suggestions:
On line 83 of the diff, I don't understand why the outer parenthesis
exist on the right hand side of the equals:
self. _count_ recipients = (len(self. _all_recipients ))
It would be nice if the tests in registry/ browser/ tests/test_ person_ contact. py were arranged in
lib/lp/
smaller test cases instead of one big test case per class.