~launchpad-pqm/launchpad/devel

12156.1.3 by Brad Crittenden
Updated copyright
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
8687.15.17 by Karl Fogel
Add the copyright header block to the rest of the files under lib/lp/.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4962.5.8 by Guilherme Salgado
Lots of refactorings
3
4
"""People Merge related wiew classes."""
5
6
__metaclass__ = type
7
8
__all__ = [
9
    'AdminPeopleMergeView',
10
    'AdminTeamMergeView',
10085.5.8 by Curtis Hovey
Revised comments and documentation.
11
    'DeleteTeamView',
4962.5.8 by Guilherme Salgado
Lots of refactorings
12
    'FinishedPeopleMergeRequestView',
13
    'RequestPeopleMergeMultipleEmailsView',
10123.4.5 by Brad Crittenden
Removed breadcrumbs as they showed up on other /people pages. Simplified page template.
14
    'RequestPeopleMergeView',
15
    ]
4962.5.8 by Guilherme Salgado
Lots of refactorings
16
5653.2.1 by Maris Fogels
Changed occurrances of addError() so that the conform to the new API. Fixed a number of broken msgids.
17
4962.5.8 by Guilherme Salgado
Lots of refactorings
18
from zope.component import getUtility
12043.3.2 by Curtis Hovey
Use a single import for removeSecurityProxy.
19
from zope.security.proxy import removeSecurityProxy
4962.5.8 by Guilherme Salgado
Lots of refactorings
20
14600.1.12 by Curtis Hovey
Move i18n to lp.
21
from lp import _
11929.9.1 by Tim Penhey
Move launchpadform into lp.app.browser.
22
from lp.app.browser.launchpadform import (
23
    action,
24
    LaunchpadFormView,
25
    )
13130.1.12 by Curtis Hovey
Sorted imports.
26
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
11818.6.17 by Curtis Hovey
Restored the check for the purged state to AdminTeamMergeView (but placed it
27
from lp.registry.interfaces.mailinglist import (
28
    MailingListStatus,
29
    PURGE_STATES,
30
    )
7675.110.3 by Curtis Hovey
Ran the migration script to move registry code to lp.registry.
31
from lp.registry.interfaces.person import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
32
    IAdminPeopleMergeSchema,
33
    IAdminTeamMergeSchema,
34
    IPersonSet,
35
    IRequestPeopleMerge,
36
    )
14612.2.1 by William Grant
format-imports on lib/. So many imports.
37
from lp.services.database.lpstorm import IMasterObject
14550.1.1 by Steve Kowalik
Run format-imports over lib/lp and lib/canonical/launchpad
38
from lp.services.identity.interfaces.emailaddress import (
39
    EmailAddressStatus,
40
    IEmailAddressSet,
41
    )
11818.6.3 by Curtis Hovey
Moved the merge super team rule into the base view to ensure the right behaviour is honoured if the user uses merge instead of delete.
42
from lp.services.propertycache import cachedproperty
14612.2.1 by William Grant
format-imports on lib/. So many imports.
43
from lp.services.verification.interfaces.authtoken import LoginTokenType
44
from lp.services.verification.interfaces.logintoken import ILoginTokenSet
45
from lp.services.webapp import (
46
    canonical_url,
47
    LaunchpadView,
48
    )
49
from lp.services.webapp.interfaces import ILaunchBag
12156.1.1 by Brad Crittenden
Removed has_existing_ppa and has_ppa_with_published_packages from IPerson. Modified IArchiveSet.getPPAOwnedByPerson to replace those two ill-placed methods.
50
from lp.soyuz.enums import ArchiveStatus
51
from lp.soyuz.interfaces.archive import IArchiveSet
4962.5.8 by Guilherme Salgado
Lots of refactorings
52
53
12167.1.5 by j.c.sackett
Updated validator.
54
class ValidatingMergeView(LaunchpadFormView):
12167.1.6 by j.c.sackett
Lint fixes.
55
12167.1.5 by j.c.sackett
Updated validator.
56
    def validate(self, data):
57
        """Check that user is not attempting to merge a person into itself."""
58
        dupe_person = data.get('dupe_person')
12760.1.4 by Curtis Hovey
Added a test to verify an IPerson cannot merge into himself; fixed validator.
59
        target_person = data.get('target_person') or self.user
12760.1.11 by Curtis Hovey
Explain that the duplicate person/team must be active.
60
        if dupe_person is None:
61
            self.setFieldError(
62
                'dupe_person', 'The duplicate is not a valid person or team.')
63
        else:
64
            if dupe_person == target_person:
65
                self.addError(_("You can't merge ${name} into itself.",
66
                      mapping=dict(name=dupe_person.name)))
12167.1.5 by j.c.sackett
Updated validator.
67
            dupe_person_ppas = getUtility(IArchiveSet).getPPAOwnedByPerson(
68
                dupe_person, statuses=[ArchiveStatus.ACTIVE,
69
                                       ArchiveStatus.DELETING])
70
            if dupe_person_ppas is not None:
71
                self.addError(_(
72
                    "${name} has a PPA that must be deleted before it "
73
                    "can be merged. It may take ten minutes to remove the "
74
                    "deleted PPA's files.",
75
                    mapping=dict(name=dupe_person.name)))
12760.1.4 by Curtis Hovey
Added a test to verify an IPerson cannot merge into himself; fixed validator.
76
            if dupe_person.is_merge_pending:
77
                self.addError(_("${name} is already queued for merging.",
78
                      mapping=dict(name=dupe_person.name)))
79
        if target_person is not None and target_person.is_merge_pending:
80
            self.addError(_("${name} is already queued for merging.",
81
                  mapping=dict(name=target_person.name)))
82
12167.1.5 by j.c.sackett
Updated validator.
83
84
class AdminMergeBaseView(ValidatingMergeView):
4962.5.8 by Guilherme Salgado
Lots of refactorings
85
    """Base view for the pages where admins can merge people/teams."""
86
11118.3.7 by Curtis Hovey
Added missing page titles to malone index and admin merge. Removed obsolete test.
87
    page_title = 'Merge Launchpad accounts'
4962.5.8 by Guilherme Salgado
Lots of refactorings
88
    # Both subclasses share the same template so we need to define these
89
    # variables (which are used in the template) here rather than on
90
    # subclasses.
91
    should_confirm_email_reassignment = False
92
    should_confirm_member_deactivation = False
12760.1.9 by Curtis Hovey
Updated the merge views to queue a merge job.
93
    merge_message = _(
94
        'A merge is queued and is expected to complete in a few minutes.')
4962.5.8 by Guilherme Salgado
Lots of refactorings
95
96
    dupe_person_emails = ()
97
    dupe_person = None
98
    target_person = None
13555.6.1 by Robert Collins
Refactor team deletes to let the model code know that a delete is requested.
99
    delete = False
4962.5.8 by Guilherme Salgado
Lots of refactorings
100
9271.3.3 by Guilherme Salgado
convert merge pages
101
    @property
102
    def cancel_url(self):
103
        return canonical_url(getUtility(IPersonSet))
104
10085.5.4 by Curtis Hovey
Refactored merging to accommodate the team delete scenario.
105
    @property
10085.5.7 by Curtis Hovey
Restored the next_url behaviour to the base merge form.
106
    def success_url(self):
10085.5.4 by Curtis Hovey
Refactored merging to accommodate the team delete scenario.
107
        return canonical_url(self.target_person)
108
4962.5.8 by Guilherme Salgado
Lots of refactorings
109
    def render(self):
110
        # Subclasses may define other actions that they will render manually
111
        # only in certain circunstances, so don't include them in the list of
112
        # actions to be rendered.
113
        self.actions = [self.merge_action]
114
        return super(AdminMergeBaseView, self).render()
115
4962.5.10 by Guilherme Salgado
Add/improve a couple docstrings and rename a method.
116
    def setUpPeople(self, data):
117
        """Store the people to be merged in instance variables.
118
119
        Also store all emails associated with the dupe account in an
120
        instance variable.
121
        """
4962.5.8 by Guilherme Salgado
Lots of refactorings
122
        emailset = getUtility(IEmailAddressSet)
123
        self.dupe_person = data['dupe_person']
13555.6.1 by Robert Collins
Refactor team deletes to let the model code know that a delete is requested.
124
        self.target_person = data.get('target_person', None)
4962.5.8 by Guilherme Salgado
Lots of refactorings
125
        self.dupe_person_emails = emailset.getByPerson(self.dupe_person)
126
127
    def doMerge(self, data):
12394.1.1 by Gavin Panella
Improve the docs around some uses of PersonSet.merge().
128
        """Merge the two person/team entries specified in the form.
129
130
        Before merging this moves each email address of the duplicate person
131
        to the target person, and resets them to `NEW`.
132
        """
12651.1.10 by Curtis Hovey
Removed team purge rules from merge views.
133
        if not self.dupe_person.is_team:
134
            # Transfer user email addresses. Team addresses will be deleted.
135
            for email in self.dupe_person_emails:
136
                email = IMasterObject(email)
137
                # EmailAddress.person and EmailAddress.account are readonly
138
                # fields, so we need to remove the security proxy here.
139
                naked_email = removeSecurityProxy(email)
140
                naked_email.personID = self.target_person.id
141
                naked_email.accountID = self.target_person.accountID
142
                naked_email.status = EmailAddressStatus.NEW
13303.11.22 by Aaron Bentley
Fix lint.
143
        getUtility(IPersonSet).mergeAsync(
13555.6.1 by Robert Collins
Refactor team deletes to let the model code know that a delete is requested.
144
            self.dupe_person, self.target_person, reviewer=self.user,
145
            delete=self.delete)
10085.5.4 by Curtis Hovey
Refactored merging to accommodate the team delete scenario.
146
        self.request.response.addInfoNotification(self.merge_message)
10085.5.7 by Curtis Hovey
Restored the next_url behaviour to the base merge form.
147
        self.next_url = self.success_url
4962.5.8 by Guilherme Salgado
Lots of refactorings
148
149
150
class AdminPeopleMergeView(AdminMergeBaseView):
4962.5.10 by Guilherme Salgado
Add/improve a couple docstrings and rename a method.
151
    """A view for merging two Persons.
152
153
    If the duplicate person has any email addresses associated with we'll
154
    ask the user to confirm that it's okay to reassign these emails to the
155
    other account.  We do it because the fact that the dupe person still has
156
    email addresses is a possible indication that the admin may be merging
157
    the wrong person.
158
    """
4962.5.8 by Guilherme Salgado
Lots of refactorings
159
160
    label = "Merge Launchpad people"
161
    schema = IAdminPeopleMergeSchema
162
163
    @action('Merge', name='merge')
164
    def merge_action(self, action, data):
165
        """Merge the two person entries specified in the form.
166
167
        If we're merging a person which has email addresses associated with
168
        we'll ask for confirmation before actually performing the merge.
169
        """
4962.5.10 by Guilherme Salgado
Add/improve a couple docstrings and rename a method.
170
        self.setUpPeople(data)
4962.5.8 by Guilherme Salgado
Lots of refactorings
171
        if self.dupe_person_emails.count() > 0:
172
            # We're merging a person which has one or more email addresses,
173
            # so we better warn the admin doing the operation and have him
174
            # check the emails that will be reassigned to ensure he's not
175
            # doing anything stupid.
176
            self.should_confirm_email_reassignment = True
177
            return
178
        self.doMerge(data)
179
180
    @action('Reassign E-mails and Merge', name='reassign_emails_and_merge')
181
    def reassign_emails_and_merge_action(self, action, data):
182
        """Reassign emails of the person to be merged and merge them."""
4962.5.10 by Guilherme Salgado
Add/improve a couple docstrings and rename a method.
183
        self.setUpPeople(data)
4962.5.8 by Guilherme Salgado
Lots of refactorings
184
        self.doMerge(data)
185
186
187
class AdminTeamMergeView(AdminMergeBaseView):
188
    """A view for merging two Teams.
189
4962.5.10 by Guilherme Salgado
Add/improve a couple docstrings and rename a method.
190
    The duplicate team cannot be associated with a mailing list and if it
191
    has any active members we'll ask for confirmation from the user as we'll
192
    need to deactivate all members before we can do the merge.
4962.5.8 by Guilherme Salgado
Lots of refactorings
193
    """
194
195
    label = "Merge Launchpad teams"
196
    schema = IAdminTeamMergeSchema
197
10085.5.2 by Curtis Hovey
Added support for team owners and moderators to delete teams.
198
    def hasMailingList(self, team):
11818.6.17 by Curtis Hovey
Restored the check for the purged state to AdminTeamMergeView (but placed it
199
        unused_states = [state for state in PURGE_STATES]
200
        unused_states.append(MailingListStatus.PURGED)
10085.5.2 by Curtis Hovey
Added support for team owners and moderators to delete teams.
201
        return (
202
            team.mailing_list is not None
11818.6.17 by Curtis Hovey
Restored the check for the purged state to AdminTeamMergeView (but placed it
203
            and team.mailing_list.status not in unused_states)
11818.6.1 by Curtis Hovey
Allow team owner to purge their lists. Automatically purge a purgable list when deleting a team. Use bold text to inform the owner that he needs to purge the list first.
204
11818.6.3 by Curtis Hovey
Moved the merge super team rule into the base view to ensure the right behaviour is honoured if the user uses merge instead of delete.
205
    @cachedproperty
206
    def registry_experts(self):
207
        return getUtility(ILaunchpadCelebrities).registry_experts
208
4962.5.8 by Guilherme Salgado
Lots of refactorings
209
    def validate(self, data):
210
        """Check there are no mailing lists associated with the dupe team."""
8752.1.1 by Brad Crittenden
Properly handle invalid teams when merging.
211
        # If errors have already been discovered there is no need to continue,
212
        # especially since some of our expected data may be missing in the
213
        # case of user-entered invalid data.
214
        if len(self.errors) > 0:
215
            return
216
4962.5.8 by Guilherme Salgado
Lots of refactorings
217
        super(AdminTeamMergeView, self).validate(data)
7675.286.1 by Guilherme Salgado
Fix the bug by preventing teams with superteams from being merged
218
        dupe_team = data['dupe_person']
6821.5.19 by Barry Warsaw
Finish up team merges.
219
        # We cannot merge the teams if there is a mailing list on the
220
        # duplicate person, unless that mailing list is purged.
10085.5.2 by Curtis Hovey
Added support for team owners and moderators to delete teams.
221
        if self.hasMailingList(dupe_team):
4962.5.8 by Guilherme Salgado
Lots of refactorings
222
            self.addError(_(
5653.2.1 by Maris Fogels
Changed occurrances of addError() so that the conform to the new API. Fixed a number of broken msgids.
223
                "${name} is associated with a Launchpad mailing list; we "
7675.286.1 by Guilherme Salgado
Fix the bug by preventing teams with superteams from being merged
224
                "can't merge it.", mapping=dict(name=dupe_team.name)))
4962.5.8 by Guilherme Salgado
Lots of refactorings
225
226
    @action('Merge', name='merge')
227
    def merge_action(self, action, data):
228
        """Merge the two team entries specified in the form.
229
230
        A confirmation will be asked if the team we're merging from still
231
        has active members, as in that case we'll have to deactivate all
232
        members first.
233
        """
4962.5.10 by Guilherme Salgado
Add/improve a couple docstrings and rename a method.
234
        self.setUpPeople(data)
4962.5.8 by Guilherme Salgado
Lots of refactorings
235
        if self.dupe_person.activemembers.count() > 0:
236
            # Merging teams with active members is not possible, so we'll
237
            # ask the admin if he wants to deactivate all members and then
238
            # merge.
239
            self.should_confirm_member_deactivation = True
240
            return
12651.1.10 by Curtis Hovey
Removed team purge rules from merge views.
241
        super(AdminTeamMergeView, self).doMerge(data)
4962.5.8 by Guilherme Salgado
Lots of refactorings
242
243
    @action('Deactivate Members and Merge',
244
            name='deactivate_members_and_merge')
245
    def deactivate_members_and_merge_action(self, action, data):
246
        """Deactivate all members of the team to be merged and merge them."""
4962.5.10 by Guilherme Salgado
Add/improve a couple docstrings and rename a method.
247
        self.setUpPeople(data)
12651.1.10 by Curtis Hovey
Removed team purge rules from merge views.
248
        super(AdminTeamMergeView, self).doMerge(data)
4962.5.8 by Guilherme Salgado
Lots of refactorings
249
250
10085.5.2 by Curtis Hovey
Added support for team owners and moderators to delete teams.
251
class DeleteTeamView(AdminTeamMergeView):
252
    """A view that deletes a team by merging it with Registry experts."""
253
254
    page_title = 'Delete'
13555.6.1 by Robert Collins
Refactor team deletes to let the model code know that a delete is requested.
255
    field_names = ['dupe_person']
12760.1.9 by Curtis Hovey
Updated the merge views to queue a merge job.
256
    merge_message = _('The team is queued to be deleted.')
10085.5.2 by Curtis Hovey
Added support for team owners and moderators to delete teams.
257
258
    @property
259
    def label(self):
260
        return 'Delete %s' % self.context.displayname
261
262
    def __init__(self, context, request):
263
        super(DeleteTeamView, self).__init__(context, request)
13555.6.1 by Robert Collins
Refactor team deletes to let the model code know that a delete is requested.
264
        if ('field.dupe_person' in self.request.form):
10085.5.8 by Curtis Hovey
Revised comments and documentation.
265
            # These fields have fixed values and are managed by this method.
266
            # The user has crafted a request to gain ownership of the dupe
267
            # team's assets.
268
            self.addError('Unable to process submitted data.')
10085.5.5 by Curtis Hovey
Added tests to verify teams with lists cannot be deleted.
269
        elif 'field.actions.delete' in self.request.form:
10085.5.2 by Curtis Hovey
Added support for team owners and moderators to delete teams.
270
            # In the case of deleting a team, the form values are always
271
            # the context team, and the registry experts team. These values
272
            # are injected during __init__ because the base classes assume the
10085.5.8 by Curtis Hovey
Revised comments and documentation.
273
            # values are submitted. The validations performed by the base
10085.5.2 by Curtis Hovey
Added support for team owners and moderators to delete teams.
274
            # classes are still required to ensure the team can be deleted.
10085.5.4 by Curtis Hovey
Refactored merging to accommodate the team delete scenario.
275
            self.request.form.update(self.default_values)
10085.5.5 by Curtis Hovey
Added tests to verify teams with lists cannot be deleted.
276
        else:
277
            # Show the page explaining the action.
278
            pass
10085.5.4 by Curtis Hovey
Refactored merging to accommodate the team delete scenario.
279
280
    @property
281
    def default_values(self):
282
        return {
283
            'field.dupe_person': self.context.name,
13555.6.1 by Robert Collins
Refactor team deletes to let the model code know that a delete is requested.
284
            'field.delete': True,
10085.5.4 by Curtis Hovey
Refactored merging to accommodate the team delete scenario.
285
            }
10085.5.2 by Curtis Hovey
Added support for team owners and moderators to delete teams.
286
287
    @property
288
    def cancel_url(self):
289
        return canonical_url(self.context)
290
291
    @property
10085.5.7 by Curtis Hovey
Restored the next_url behaviour to the base merge form.
292
    def success_url(self):
10085.5.4 by Curtis Hovey
Refactored merging to accommodate the team delete scenario.
293
        return canonical_url(getUtility(IPersonSet))
294
295
    @property
10085.5.2 by Curtis Hovey
Added support for team owners and moderators to delete teams.
296
    def has_mailing_list(self):
297
        return self.hasMailingList(self.context)
298
299
    def canDelete(self, data):
300
        return not self.has_mailing_list
301
302
    @action('Delete', name='delete', condition=canDelete)
303
    def merge_action(self, action, data):
304
        base = super(DeleteTeamView, self)
13555.6.1 by Robert Collins
Refactor team deletes to let the model code know that a delete is requested.
305
        self.delete = True
10085.5.2 by Curtis Hovey
Added support for team owners and moderators to delete teams.
306
        base.deactivate_members_and_merge_action.success(data)
307
308
4962.5.8 by Guilherme Salgado
Lots of refactorings
309
class FinishedPeopleMergeRequestView(LaunchpadView):
310
    """A simple view for a page where we only tell the user that we sent the
311
    email with further instructions to complete the merge.
312
313
    This view is used only when the dupe account has a single email address.
314
    """
10085.5.2 by Curtis Hovey
Added support for team owners and moderators to delete teams.
315
13980.3.16 by Curtis Hovey
Moved person-mergerequest-sent page_title into view.
316
    page_title = 'Merge request sent'
317
4962.5.8 by Guilherme Salgado
Lots of refactorings
318
    def initialize(self):
319
        user = getUtility(ILaunchBag).user
320
        try:
321
            dupe_id = int(self.request.get('dupe'))
322
        except (ValueError, TypeError):
323
            self.request.response.redirect(canonical_url(user))
324
            return
325
326
        dupe_account = getUtility(IPersonSet).get(dupe_id)
327
        results = getUtility(IEmailAddressSet).getByPerson(dupe_account)
328
329
        result_count = results.count()
330
        if not result_count:
331
            # The user came back to visit this page with nothing to
332
            # merge, so we redirect him away to somewhere useful.
333
            self.request.response.redirect(canonical_url(user))
334
            return
335
        assert result_count == 1
6879.1.1 by Guilherme Salgado
Fix the bug
336
        # Need to remove the security proxy because the dupe account may have
337
        # hidden email addresses.
338
        self.dupe_email = removeSecurityProxy(results[0]).email
4962.5.8 by Guilherme Salgado
Lots of refactorings
339
340
    def render(self):
341
        if self.dupe_email:
342
            return LaunchpadView.render(self)
343
        else:
344
            return ''
345
346
13303.11.10 by Aaron Bentley
Fix pages broken by assuming LaunchpadView.
347
class RequestPeopleMergeMultipleEmailsView(LaunchpadView):
10123.4.1 by Brad Crittenden
Don't show hidden emails when merging accounts.
348
    """Merge request view when dupe account has multiple email addresses."""
4962.5.8 by Guilherme Salgado
Lots of refactorings
349
10123.4.5 by Brad Crittenden
Removed breadcrumbs as they showed up on other /people pages. Simplified page template.
350
    label = 'Merge Launchpad accounts'
351
    page_title = label
352
4962.5.8 by Guilherme Salgado
Lots of refactorings
353
    def __init__(self, context, request):
13303.11.10 by Aaron Bentley
Fix pages broken by assuming LaunchpadView.
354
        super(RequestPeopleMergeMultipleEmailsView, self).__init__(
355
            context, request)
4962.5.8 by Guilherme Salgado
Lots of refactorings
356
        self.form_processed = False
357
        self.dupe = None
358
        self.notified_addresses = []
359
360
    def processForm(self):
361
        dupe = self.request.form.get('dupe')
362
        if dupe is None:
363
            # We just got redirected to this page and we don't have the dupe
364
            # hidden field in request.form.
365
            dupe = self.request.get('dupe')
366
            if dupe is None:
367
                return
368
369
        self.dupe = getUtility(IPersonSet).get(int(dupe))
370
        emailaddrset = getUtility(IEmailAddressSet)
371
        self.dupeemails = emailaddrset.getByPerson(self.dupe)
372
373
        if self.request.method != "POST":
374
            return
375
376
        login = getUtility(ILaunchBag).login
377
        logintokenset = getUtility(ILoginTokenSet)
378
10123.4.1 by Brad Crittenden
Don't show hidden emails when merging accounts.
379
        email_addresses = []
380
        if self.email_hidden:
381
            # If the email addresses are hidden we must send a merge request
382
            # to each of them.  But first we've got to remove the security
383
            # proxy so we can get to them.
384
            email_addresses = [removeSecurityProxy(email).email
385
                               for email in self.dupeemails]
386
        else:
387
            # Otherwise we send a merge request only to the ones the user
388
            # selected.
389
            emails = self.request.form.get("selected")
390
            if emails is not None:
10123.4.3 by Brad Crittenden
Changes from review. Typos fixed. Tests made more clear.
391
                # We can have multiple email addresses selected, and in this
10123.4.1 by Brad Crittenden
Don't show hidden emails when merging accounts.
392
                # case emails will be a list. Otherwise it will be a string
393
                # and we need to make a list with that value to use in the for
394
                # loop.
395
                if not isinstance(emails, list):
396
                    emails = [emails]
397
398
                for emailaddress in emails:
399
                    email = emailaddrset.getByEmail(emailaddress)
11594.1.2 by Curtis Hovey
Ask the user to reselect the email addresses when requesting a merge if the dupe account changes.
400
                    if email is None or email not in self.dupeemails:
11594.1.1 by Curtis Hovey
Save hack from another branch that may fix an oops.
401
                        # The dupe person has changes his email addresses.
402
                        # See bug 239838.
11594.1.2 by Curtis Hovey
Ask the user to reselect the email addresses when requesting a merge if the dupe account changes.
403
                        self.request.response.addNotification(
404
                            "An address was removed from the duplicate "
11594.1.1 by Curtis Hovey
Save hack from another branch that may fix an oops.
405
                            "account while you were making this merge "
406
                            "request. Select again.")
407
                        return
10123.4.1 by Brad Crittenden
Don't show hidden emails when merging accounts.
408
                    email_addresses.append(emailaddress)
409
410
        for emailaddress in email_addresses:
411
            token = logintokenset.new(
10123.4.5 by Brad Crittenden
Removed breadcrumbs as they showed up on other /people pages. Simplified page template.
412
                self.user, login, emailaddress, LoginTokenType.ACCOUNTMERGE)
10123.4.1 by Brad Crittenden
Don't show hidden emails when merging accounts.
413
            token.sendMergeRequestEmail()
414
            self.notified_addresses.append(emailaddress)
11594.1.3 by Curtis Hovey
Move form_processes after the test that acutally made changes. Added a test for
415
        self.form_processed = True
10123.4.1 by Brad Crittenden
Don't show hidden emails when merging accounts.
416
6596.1.1 by Guilherme Salgado
Fix https://launchpad.net/bugs/241332
417
    @property
10123.4.5 by Brad Crittenden
Removed breadcrumbs as they showed up on other /people pages. Simplified page template.
418
    def cancel_url(self):
419
        """Cancel URL."""
420
        return canonical_url(self.user)
421
422
    @property
10123.4.1 by Brad Crittenden
Don't show hidden emails when merging accounts.
423
    def email_hidden(self):
424
        """Does the duplicate account hide email addresses?"""
425
        return self.dupe.hide_email_addresses
12167.1.6 by j.c.sackett
Lint fixes.
426
12167.1.3 by j.c.sackett
Added ppa validation step to RequestPeopleMergeView
427
12167.1.5 by j.c.sackett
Updated validator.
428
class RequestPeopleMergeView(ValidatingMergeView):
12167.1.2 by j.c.sackett
Grouped RequestPeopleMergeView with the other RequestPeopleMerge views.
429
    """The view for the page where the user asks a merge of two accounts.
430
431
    If the dupe account have only one email address we send a message to that
432
    address and then redirect the user to other page saying that everything
433
    went fine. Otherwise we redirect the user to another page where we list
434
    all email addresses owned by the dupe account and the user selects which
435
    of those (s)he wants to claim.
436
    """
437
438
    label = 'Merge Launchpad accounts'
439
    page_title = label
440
    schema = IRequestPeopleMerge
441
442
    @property
443
    def cancel_url(self):
444
        return canonical_url(getUtility(IPersonSet))
445
446
    @action('Continue', name='continue')
447
    def continue_action(self, action, data):
448
        dupeaccount = data['dupe_person']
449
        if dupeaccount == self.user:
450
            # Please, don't try to merge you into yourself.
451
            return
452
453
        emails = getUtility(IEmailAddressSet).getByPerson(dupeaccount)
454
        emails_count = emails.count()
455
        if emails_count > 1:
456
            # The dupe account have more than one email address. Must redirect
457
            # the user to another page to ask which of those emails (s)he
458
            # wants to claim.
459
            self.next_url = '+requestmerge-multiple?dupe=%d' % dupeaccount.id
460
            return
461
462
        assert emails_count == 1
463
        email = emails[0]
464
        login = getUtility(ILaunchBag).login
465
        logintokenset = getUtility(ILoginTokenSet)
466
        # Need to remove the security proxy because the dupe account may have
467
        # hidden email addresses.
468
        token = logintokenset.new(
469
            self.user, login, removeSecurityProxy(email).email,
470
            LoginTokenType.ACCOUNTMERGE)
471
        token.sendMergeRequestEmail()
472
        self.next_url = './+mergerequest-sent?dupe=%d' % dupeaccount.id