~launchpad-pqm/launchpad/devel

13890.2.1 by Steve Kowalik
Dirty, dirty first shot attempt.
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
8687.15.15 by Karl Fogel
Add the copyright header block to files under lib/lp/bugs/.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
2396 by Canonical.com Patch Queue Manager
[r=spiv] launchpad support tracker
3
4
"""Views for BugSubscription."""
5
6
__metaclass__ = type
8224.2.9 by Bjorn Tillenius
Add a view class for the subscribers portlet.
7
__all__ = [
11869.3.2 by Graham Binns
Added documentation and did some more refactoring.
8
    'AdvancedSubscriptionMixin',
12622.1.1 by Graham Binns
The mute view does its thing. Hurrah.
9
    'BugMuteSelfView',
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
10
    'BugPortletSubscribersWithDetails',
8224.2.9 by Bjorn Tillenius
Add a view class for the subscribers portlet.
11
    'BugSubscriptionAddView',
12348.1.1 by Graham Binns
Added a vestigial page.
12
    'BugSubscriptionListView',
8224.2.9 by Bjorn Tillenius
Add a view class for the subscribers portlet.
13
    ]
2396 by Canonical.com Patch Queue Manager
[r=spiv] launchpad support tracker
14
11654.2.4 by Graham Binns
Hurrah for code that fixes a problem for vague reasons.
15
import cgi
16
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
17
from lazr.delegates import delegates
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
18
from lazr.restful.interfaces import (
19
    IJSONRequestCache,
20
    IWebServiceClientRequest,
14027.3.7 by Jeroen Vermeulen
Conflicts.
21
    )
9894.4.1 by Graham Binns
Added a bug-portlet-subscrcribers-ids view.
22
from simplejson import dumps
11654.3.15 by Graham Binns
Reverted the reversion.
23
from zope import formlib
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
24
from zope.app.form import CustomWidgetFactory
25
from zope.app.form.browser.itemswidgets import RadioWidget
26
from zope.schema import Choice
27
from zope.schema.vocabulary import (
28
    SimpleTerm,
29
    SimpleVocabulary,
30
    )
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
31
from zope.security.proxy import removeSecurityProxy
32
from zope.traversing.browser import absoluteURL
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
33
11654.3.15 by Graham Binns
Reverted the reversion.
34
from canonical.launchpad import _
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
35
from canonical.launchpad.webapp import (
36
    canonical_url,
37
    LaunchpadView,
38
    )
14235.4.4 by Ian Booth
Add launchpad.Exists permission for bug subscribers and assignees
39
from canonical.launchpad.webapp.authorization import (
40
    check_permission,
41
    precache_permission_for_objects,
42
    )
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
43
from canonical.launchpad.webapp.launchpadform import ReturnToReferrerMixin
11654.2.6 by Graham Binns
Test fixage.
44
from canonical.launchpad.webapp.menu import structured
11929.9.1 by Tim Penhey
Move launchpadform into lp.app.browser.
45
from lp.app.browser.launchpadform import (
46
    action,
47
    LaunchpadFormView,
48
    )
12393.10.2 by Gary Poster
fix some broken code, and eliminate some redendant code. JS is still broken.
49
from lp.bugs.browser.structuralsubscription import (
12393.30.17 by Brad Crittenden
Merge from yellow + use expose_structural_subscription_data_to_js instead of new JSMixin
50
    expose_structural_subscription_data_to_js,
12393.10.2 by Gary Poster
fix some broken code, and eliminate some redendant code. JS is still broken.
51
    )
7675.1139.1 by Danilo Segan
Get rid of all NOTHING usage.
52
from lp.bugs.enum import BugNotificationLevel
14188.2.6 by j.c.sackett
Made changes per lifeless's review.
53
from lp.bugs.errors import SubscriptionPrivacyViolation
13251.1.1 by Danilo Segan
Show the subscribers list for anonymous users.
54
from lp.bugs.interfaces.bug import IBug
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
55
from lp.bugs.interfaces.bugsubscription import IBugSubscription
12521.6.19 by Danilo Segan
Remove unused stuff, add sample description.
56
from lp.bugs.model.personsubscriptioninfo import PersonSubscriptions
12393.10.2 by Gary Poster
fix some broken code, and eliminate some redendant code. JS is still broken.
57
from lp.bugs.model.structuralsubscription import (
58
    get_structural_subscriptions_for_bug,
12393.8.5 by Benji York
- move a couple of functions to saner places
59
    )
11654.3.15 by Graham Binns
Reverted the reversion.
60
from lp.services.propertycache import cachedproperty
6368.2.1 by Bjorn Tillenius
make sure only one notification are sent to new subscribers.
61
62
63
class BugSubscriptionAddView(LaunchpadFormView):
3554.1.48 by Brad Bollenbach
Fix bug 49598 (Unable to unsubscribe from private bug)
64
    """Browser view class for subscribing someone else to a bug."""
2396 by Canonical.com Patch Queue Manager
[r=spiv] launchpad support tracker
65
6368.2.1 by Bjorn Tillenius
make sure only one notification are sent to new subscribers.
66
    schema = IBugSubscription
67
11318.7.7 by Graham Binns
Reverted a bunch of unneccessary stuff.
68
    field_names = ['person']
6368.2.1 by Bjorn Tillenius
make sure only one notification are sent to new subscribers.
69
70
    def setUpFields(self):
71
        """Set up 'person' as an input field."""
72
        super(BugSubscriptionAddView, self).setUpFields()
73
        self.form_fields['person'].for_input = True
74
9270.2.2 by Abel Deuring
removed subscribers portlet; changed button text from "Add" to "Subscribe user
75
    @action('Subscribe user', name='add')
6368.2.1 by Bjorn Tillenius
make sure only one notification are sent to new subscribers.
76
    def add_action(self, action, data):
77
        person = data['person']
14188.2.6 by j.c.sackett
Made changes per lifeless's review.
78
        try:
14291.1.2 by Jeroen Vermeulen
Lint.
79
            self.context.bug.subscribe(
80
                person, self.user, suppress_notify=False)
14188.2.6 by j.c.sackett
Made changes per lifeless's review.
81
        except SubscriptionPrivacyViolation as error:
82
            self.setFieldError('person', unicode(error))
4716.1.1 by Diogo Matsubara
Fixes bug 125598 ('i can haz cheezburger?' message when subscribing groups)
83
        else:
14449.6.1 by Curtis Hovey
Remove isTeam(). Replace calls with .is_team.
84
            if person.is_team:
14188.2.6 by j.c.sackett
Made changes per lifeless's review.
85
                message = '%s team has been subscribed to this bug.'
86
            else:
87
                message = '%s has been subscribed to this bug.'
88
            self.request.response.addInfoNotification(
89
                message % person.displayname)
6856.2.4 by Gavin Panella
Add a Cancel button to the +addsubscriber page.
90
91
    @property
92
    def next_url(self):
93
        return canonical_url(self.context)
6918.1.1 by Gavin Panella
Use the correct type of cancel button/link.
94
95
    cancel_url = next_url
8037.3.10 by Brad Crittenden
Change vocabularies to include private teams; support for bug subscription, bug activity, and branch ownership from private teams.
96
9270.2.1 by Abel Deuring
LP3 layout for bug-addsubscriber.pt
97
    @property
98
    def label(self):
99
        return 'Subscribe someone else to bug #%i' % self.context.bug.id
100
101
    page_title = label
102
8224.2.9 by Bjorn Tillenius
Add a view class for the subscribers portlet.
103
11869.3.1 by Graham Binns
Refactored stuff into a mixin.
104
class AdvancedSubscriptionMixin:
11869.3.2 by Graham Binns
Added documentation and did some more refactoring.
105
    """A mixin of advanced subscription code for views.
106
107
    In order to use this mixin in a view the view must:
108
     - Define a current_user_subscription property which returns the
109
       current BugSubscription or StructuralSubscription for request.user.
11869.3.3 by Graham Binns
Review changes for abel.
110
       If there's no subscription for the given user in the given
111
       context, current_user_subscription must return None.
11869.3.2 by Graham Binns
Added documentation and did some more refactoring.
112
     - Define a dict, _bug_notification_level_descriptions, which maps
113
       BugNotificationLevel values to string descriptions for the
114
       current context (see `BugSubscriptionSubscribeSelfView` for an
115
       example).
116
     - Update the view's setUpFields() to call
117
       _setUpBugNotificationLevelField().
118
    """
11869.3.1 by Graham Binns
Refactored stuff into a mixin.
119
120
    @cachedproperty
121
    def _bug_notification_level_field(self):
122
        """Return a custom form field for bug_notification_level."""
123
        # We rebuild the items that we show in the field so that the
124
        # labels shown are human readable and specific to the +subscribe
125
        # form. The BugNotificationLevel descriptions are too generic.
126
        bug_notification_level_terms = [
127
            SimpleTerm(
12189.2.8 by Graham Binns
The overlay now allows you to subscribe using a bug_notification_level. Hurrah.
128
                level, level.title,
11869.3.1 by Graham Binns
Refactored stuff into a mixin.
129
                self._bug_notification_level_descriptions[level])
7675.1139.1 by Danilo Segan
Get rid of all NOTHING usage.
130
            # We reorder the items so that COMMENTS comes first.
131
            for level in sorted(BugNotificationLevel.items, reverse=True)]
11869.3.1 by Graham Binns
Refactored stuff into a mixin.
132
        bug_notification_vocabulary = SimpleVocabulary(
133
            bug_notification_level_terms)
134
7675.1139.1 by Danilo Segan
Get rid of all NOTHING usage.
135
        if self.current_user_subscription is not None:
11869.3.1 by Graham Binns
Refactored stuff into a mixin.
136
            default_value = (
137
                self.current_user_subscription.bug_notification_level)
138
        else:
139
            default_value = BugNotificationLevel.COMMENTS
140
141
        bug_notification_level_field = Choice(
142
            __name__='bug_notification_level', title=_("Tell me when"),
143
            vocabulary=bug_notification_vocabulary, required=True,
144
            default=default_value)
145
        return bug_notification_level_field
146
11869.3.2 by Graham Binns
Added documentation and did some more refactoring.
147
    def _setUpBugNotificationLevelField(self):
148
        """Set up the bug_notification_level field."""
149
        self.form_fields = self.form_fields.omit('bug_notification_level')
150
        self.form_fields += formlib.form.Fields(
151
            self._bug_notification_level_field)
152
        self.form_fields['bug_notification_level'].custom_widget = (
153
            CustomWidgetFactory(RadioWidget))
154
11869.3.1 by Graham Binns
Refactored stuff into a mixin.
155
11654.3.18 by Graham Binns
Put ReturnToRefererMixin back into the mix to fix some bugs.
156
class BugSubscriptionSubscribeSelfView(LaunchpadFormView,
11869.3.1 by Graham Binns
Refactored stuff into a mixin.
157
                                       ReturnToReferrerMixin,
158
                                       AdvancedSubscriptionMixin):
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
159
    """A view to handle the +subscribe page for a bug."""
160
11654.3.15 by Graham Binns
Reverted the reversion.
161
    schema = IBugSubscription
14433.2.19 by Curtis Hovey
Moved page_title into view.
162
    page_title = 'Subscription options'
11654.3.15 by Graham Binns
Reverted the reversion.
163
11654.4.16 by Graham Binns
Updated the pagetest.
164
    # A mapping of BugNotificationLevel values to descriptions to be
165
    # shown on the +subscribe page.
166
    _bug_notification_level_descriptions = {
12828.5.1 by Brad Crittenden
Handle the lack of a mute_link without falling over. Fix lint, spelling, and poor html markup.
167
        BugNotificationLevel.COMMENTS: (
168
            "a change is made to this bug or a new comment is added, "),
169
        BugNotificationLevel.METADATA: (
170
            "any change is made to this bug, other than a new comment "
171
            "being added, or"),
11654.4.16 by Graham Binns
Updated the pagetest.
172
        BugNotificationLevel.LIFECYCLE: (
12898.1.1 by Brad Crittenden
Fixed tiny grammar issue
173
            "this bug is fixed or re-opened."),
11654.4.16 by Graham Binns
Updated the pagetest.
174
        }
175
11654.3.9 by Graham Binns
Added feature flags.
176
    @property
11654.4.1 by Graham Binns
Re-added the feature-flagged bnl work.
177
    def field_names(self):
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
178
        return ['bug_notification_level']
11654.4.1 by Graham Binns
Re-added the feature-flagged bnl work.
179
180
    @property
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
181
    def next_url(self):
182
        """Provided so returning to the page they came from works."""
11654.3.18 by Graham Binns
Put ReturnToRefererMixin back into the mix to fix some bugs.
183
        referer = self._return_url
11654.3.17 by Graham Binns
Fixed the test breakages.
184
        context_url = canonical_url(self.context)
11654.2.3 by Graham Binns
Merged Brian's changes and fixed conflicts.
185
186
        # XXX bdmurray 2010-09-30 bug=98437: work around zope's test
187
        # browser setting referer to localhost.
11654.3.17 by Graham Binns
Fixed the test breakages.
188
        # We also ignore the current request URL and the context URL as
189
        # far as referrers are concerned so that we can handle privacy
190
        # issues properly.
191
        ignored_referrer_urls = (
11654.4.12 by Graham Binns
Review changes for brad.
192
            'localhost', self.request.getURL(), context_url)
11654.3.17 by Graham Binns
Fixed the test breakages.
193
        if referer and referer not in ignored_referrer_urls:
11654.2.3 by Graham Binns
Merged Brian's changes and fixed conflicts.
194
            next_url = referer
11654.3.17 by Graham Binns
Fixed the test breakages.
195
        elif self._redirecting_to_bug_list:
196
            next_url = canonical_url(self.context.target, view_name="+bugs")
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
197
        else:
11654.3.17 by Graham Binns
Fixed the test breakages.
198
            next_url = context_url
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
199
        return next_url
200
11654.2.6 by Graham Binns
Test fixage.
201
    cancel_url = next_url
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
202
11654.3.15 by Graham Binns
Reverted the reversion.
203
    @cachedproperty
204
    def _subscribers_for_current_user(self):
205
        """Return a dict of the subscribers for the current user."""
206
        persons_for_user = {}
207
        person_count = 0
208
        bug = self.context.bug
209
        for person in bug.getSubscribersForPerson(self.user):
210
            if person.id not in persons_for_user:
211
                persons_for_user[person.id] = person
212
                person_count += 1
213
214
        self._subscriber_count_for_current_user = person_count
215
        return persons_for_user.values()
216
11654.4.9 by Graham Binns
Began adding tests.
217
    def initialize(self):
218
        """See `LaunchpadFormView`."""
219
        self._subscriber_count_for_current_user = 0
220
        self._redirecting_to_bug_list = False
11654.4.11 by Graham Binns
Finally added a unit test to cover the view's behaviour with bug notficiation levels.
221
        super(BugSubscriptionSubscribeSelfView, self).initialize()
11654.4.9 by Graham Binns
Began adding tests.
222
11654.4.18 by Graham Binns
Review change for abel.
223
    @cachedproperty
11843.1.5 by Graham Binns
Added tests for unsubscribing and for the default value when updating.
224
    def current_user_subscription(self):
225
        return self.context.bug.getSubscriptionForPerson(self.user)
226
227
    @cachedproperty
11843.1.3 by Graham Binns
It's now possible to update your subscription. Hurrah.
228
    def _update_subscription_term(self):
7675.1151.2 by Gary Poster
remove test that is no longer pertinent, and make some other small changes prior to MP
229
        label = "update my current subscription"
12599.1.6 by Graham Binns
Update now unmutes.
230
        return SimpleTerm(
231
            'update-subscription', 'update-subscription', label)
12599.1.3 by Graham Binns
Tweaked the way the plain unmute option is shown.
232
233
    @cachedproperty
234
    def _unsubscribe_current_user_term(self):
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
235
        if self.user_is_muted:
12828.5.2 by Brad Crittenden
Fixed capitalization and punctuation of list elements per review suggestion.
236
            label = "unmute bug mail from this bug"
12599.1.3 by Graham Binns
Tweaked the way the plain unmute option is shown.
237
        else:
12828.5.2 by Brad Crittenden
Fixed capitalization and punctuation of list elements per review suggestion.
238
            label = 'unsubscribe me from this bug'
12599.1.3 by Graham Binns
Tweaked the way the plain unmute option is shown.
239
        return SimpleTerm(self.user, self.user.name, label)
240
241
    @cachedproperty
7675.1151.1 by Danilo Segan
Server side changes for edit subscription.
242
    def _unmute_user_term(self):
243
        if self.user_is_subscribed_directly:
244
            return SimpleTerm(
245
                'update-subscription', 'update-subscription',
246
                "unmute bug mail from this bug and restore my subscription")
247
        else:
248
            return SimpleTerm(self.user, self.user.name,
249
                              "unmute bug mail from this bug")
250
251
    @cachedproperty
11843.1.1 by Graham Binns
Moved the subscription field into its own cachedproperty to make things a bit less knotty.
252
    def _subscription_field(self):
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
253
        subscription_terms = []
254
        self_subscribed = False
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
255
        is_really_muted = self.user_is_muted
7675.1151.1 by Danilo Segan
Server side changes for edit subscription.
256
        if is_really_muted:
257
            subscription_terms.insert(0, self._unmute_user_term)
11654.3.15 by Graham Binns
Reverted the reversion.
258
        for person in self._subscribers_for_current_user:
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
259
            if person.id == self.user.id:
7675.1151.1 by Danilo Segan
Server side changes for edit subscription.
260
                if is_really_muted:
261
                    # We've already added the unmute option.
262
                    continue
263
                else:
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
264
                    if self.user_is_subscribed_directly:
12599.1.4 by Graham Binns
Added an unmute_and_update_option.
265
                        subscription_terms.append(
266
                            self._update_subscription_term)
7675.1151.1 by Danilo Segan
Server side changes for edit subscription.
267
                    subscription_terms.insert(
268
                        0, self._unsubscribe_current_user_term)
269
                    self_subscribed = True
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
270
            else:
271
                subscription_terms.append(
272
                    SimpleTerm(
273
                        person, person.name,
12828.5.2 by Brad Crittenden
Fixed capitalization and punctuation of list elements per review suggestion.
274
                        'unsubscribe <a href="%s">%s</a> from this bug' % (
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
275
                            canonical_url(person),
276
                            cgi.escape(person.displayname))))
7675.1178.1 by Gary Poster
add in mute icons; fix all known bugs
277
        if not self_subscribed:
278
            if not is_really_muted:
279
                subscription_terms.insert(0,
280
                    SimpleTerm(
281
                        self.user, self.user.name,
282
                        'subscribe me to this bug'))
283
            elif not self.user_is_subscribed_directly:
284
                subscription_terms.insert(0,
285
                    SimpleTerm(
286
                        'update-subscription', 'update-subscription',
287
                        'unmute bug mail from this bug and subscribe me to '
288
                        'this bug'))
12828.5.2 by Brad Crittenden
Fixed capitalization and punctuation of list elements per review suggestion.
289
290
        # Add punctuation to the list of terms.
291
        if len(subscription_terms) > 1:
292
            for term in subscription_terms[:-1]:
293
                term.title += ','
294
            subscription_terms[-2].title += ' or'
295
            subscription_terms[-1].title += '.'
296
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
297
        subscription_vocabulary = SimpleVocabulary(subscription_terms)
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
298
        if self.user_is_subscribed_directly or self.user_is_muted:
11843.1.3 by Graham Binns
It's now possible to update your subscription. Hurrah.
299
            default_subscription_value = self._update_subscription_term.value
300
        else:
11843.1.4 by Graham Binns
Refactoring.
301
            default_subscription_value = (
302
                subscription_vocabulary.getTermByToken(self.user.name).value)
12828.5.2 by Brad Crittenden
Fixed capitalization and punctuation of list elements per review suggestion.
303
11654.3.15 by Graham Binns
Reverted the reversion.
304
        subscription_field = Choice(
305
            __name__='subscription', title=_("Subscription options"),
306
            vocabulary=subscription_vocabulary, required=True,
307
            default=default_subscription_value)
11843.1.1 by Graham Binns
Moved the subscription field into its own cachedproperty to make things a bit less knotty.
308
        return subscription_field
309
310
    def setUpFields(self):
311
        """See `LaunchpadFormView`."""
312
        super(BugSubscriptionSubscribeSelfView, self).setUpFields()
313
        if self.user is None:
314
            return
11654.4.1 by Graham Binns
Re-added the feature-flagged bnl work.
315
11939.1.2 by Graham Binns
Minor tweak for abel.
316
        self.form_fields += formlib.form.Fields(self._subscription_field)
317
        self._setUpBugNotificationLevelField()
11654.3.15 by Graham Binns
Reverted the reversion.
318
        self.form_fields['subscription'].custom_widget = CustomWidgetFactory(
319
            RadioWidget)
320
11654.4.1 by Graham Binns
Re-added the feature-flagged bnl work.
321
    def setUpWidgets(self):
322
        """See `LaunchpadFormView`."""
323
        super(BugSubscriptionSubscribeSelfView, self).setUpWidgets()
13033.1.1 by Deryck Hodge
Add a custom class to the subscription form to give
324
        self.widgets['subscription'].widget_class = 'bug-subscription-basic'
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
325
        self.widgets['bug_notification_level'].widget_class = (
326
            'bug-notification-level-field')
327
        if (len(self.form_fields['subscription'].field.vocabulary) == 1):
328
            # We hide the subscription widget if the user isn't
329
            # subscribed, since we know who the subscriber is and we
330
            # don't need to present them with a single radio button.
331
            self.widgets['subscription'].visible = False
332
        else:
333
            # We show the subscription widget when the user is
334
            # subscribed via a team, because they can either
335
            # subscribe theirself or unsubscribe their team.
336
            self.widgets['subscription'].visible = True
11654.4.1 by Graham Binns
Re-added the feature-flagged bnl work.
337
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
338
        if self.user_is_subscribed_to_dupes_only:
339
            # If the user is subscribed via a duplicate but is not
340
            # directly subscribed, we hide the
341
            # bug_notification_level field, since it's not used.
342
            self.widgets['bug_notification_level'].visible = False
11893.1.1 by Graham Binns
Added a test to cover the behaviour of the bug_notification_level field for indirect subscriptions.
343
11654.3.15 by Graham Binns
Reverted the reversion.
344
    @cachedproperty
12599.1.3 by Graham Binns
Tweaked the way the plain unmute option is shown.
345
    def user_is_muted(self):
346
        return self.context.bug.isMuted(self.user)
347
348
    @cachedproperty
11893.1.4 by Graham Binns
Review changes for Gavin.
349
    def user_is_subscribed_directly(self):
350
        """Is the user subscribed directly to this bug?"""
7675.1151.1 by Danilo Segan
Server side changes for edit subscription.
351
        return self.context.bug.isSubscribed(self.user)
11893.1.4 by Graham Binns
Review changes for Gavin.
352
353
    @cachedproperty
354
    def user_is_subscribed_to_dupes(self):
355
        """Is the user subscribed to dupes of this bug?"""
7675.1151.1 by Danilo Segan
Server side changes for edit subscription.
356
        return self.context.bug.isSubscribedToDupes(self.user)
11893.1.4 by Graham Binns
Review changes for Gavin.
357
358
    @property
11654.3.15 by Graham Binns
Reverted the reversion.
359
    def user_is_subscribed(self):
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
360
        """Is the user subscribed to this bug?"""
361
        return (
11893.1.4 by Graham Binns
Review changes for Gavin.
362
            self.user_is_subscribed_directly or
363
            self.user_is_subscribed_to_dupes)
364
365
    @property
366
    def user_is_subscribed_to_dupes_only(self):
367
        """Is the user subscribed to this bug only via a dupe?"""
368
        return (
369
            self.user_is_subscribed_to_dupes and
370
            not self.user_is_subscribed_directly)
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
371
372
    def shouldShowUnsubscribeFromDupesWarning(self):
373
        """Should we warn the user about unsubscribing and duplicates?
374
375
        The warning should tell the user that, when unsubscribing, they
376
        will also be unsubscribed from dupes of this bug.
377
        """
11654.3.15 by Graham Binns
Reverted the reversion.
378
        if self.user_is_subscribed:
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
379
            return True
380
381
        bug = self.context.bug
382
        for team in self.user.teams_participated_in:
383
            if bug.isSubscribed(team) or bug.isSubscribedToDupes(team):
384
                return True
385
386
        return False
387
11654.3.15 by Graham Binns
Reverted the reversion.
388
    @action('Continue', name='continue')
389
    def subscribe_action(self, action, data):
390
        """Handle subscription requests."""
391
        subscription_person = self.widgets['subscription'].getInputValue()
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
392
        bug_notification_level = data.get('bug_notification_level', None)
11843.1.5 by Graham Binns
Added tests for unsubscribing and for the default value when updating.
393
11843.1.3 by Graham Binns
It's now possible to update your subscription. Hurrah.
394
        if (subscription_person == self._update_subscription_term.value and
12599.1.7 by Graham Binns
More tests and tweaks.
395
            (self.user_is_subscribed or self.user_is_muted)):
7675.1138.14 by Danilo Segan
Fix test failures.
396
            if self.user_is_muted:
397
                self._handleUnmute()
398
            if self.user_is_subscribed:
399
                self._handleUpdateSubscription(level=bug_notification_level)
400
            else:
401
                self._handleSubscribe(level=bug_notification_level)
12599.1.5 by Graham Binns
Unmute unmutes.
402
        elif self.user_is_muted and subscription_person == self.user:
7675.1138.14 by Danilo Segan
Fix test failures.
403
            self._handleUnmute()
11843.1.3 by Graham Binns
It's now possible to update your subscription. Hurrah.
404
        elif (not self.user_is_subscribed and
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
405
            (subscription_person == self.user)):
11654.4.11 by Graham Binns
Finally added a unit test to cover the view's behaviour with bug notficiation levels.
406
            self._handleSubscribe(bug_notification_level)
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
407
        else:
408
            self._handleUnsubscribe(subscription_person)
11654.3.15 by Graham Binns
Reverted the reversion.
409
        self.request.response.redirect(self.next_url)
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
410
11654.4.11 by Graham Binns
Finally added a unit test to cover the view's behaviour with bug notficiation levels.
411
    def _handleSubscribe(self, level=None):
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
412
        """Handle a subscribe request."""
11654.4.11 by Graham Binns
Finally added a unit test to cover the view's behaviour with bug notficiation levels.
413
        self.context.bug.subscribe(self.user, self.user, level=level)
11654.2.3 by Graham Binns
Merged Brian's changes and fixed conflicts.
414
        self.request.response.addNotification(
13045.16.1 by Chris Johnston
Makes text clearer when subscribing to a bug report.
415
            "You have subscribed to this bug report.")
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
416
417
    def _handleUnsubscribe(self, user):
418
        """Handle an unsubscribe request."""
419
        if user == self.user:
420
            self._handleUnsubscribeCurrentUser()
421
        else:
422
            self._handleUnsubscribeOtherUser(user)
423
7675.1138.14 by Danilo Segan
Fix test failures.
424
    def _handleUnmute(self):
425
        """Handle an unmute request."""
426
        self.context.bug.unmute(self.user, self.user)
427
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
428
    def _handleUnsubscribeCurrentUser(self):
13890.2.1 by Steve Kowalik
Dirty, dirty first shot attempt.
429
        """Handle the special cases for unsubscribing the current user."""
430
        # We call unsubscribeFromDupes() before unsubscribe(), because
431
        # if the bug is private, the current user will be prevented from
432
        # calling methods on the main bug after they unsubscribe from it.
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
433
        unsubed_dupes = self.context.bug.unsubscribeFromDupes(
434
            self.user, self.user)
435
        self.context.bug.unsubscribe(self.user, self.user)
436
437
        self.request.response.addNotification(
438
            structured(
439
                self._getUnsubscribeNotification(self.user, unsubed_dupes)))
440
441
        # Because the unsubscribe above may change what the security policy
442
        # says about the bug, we need to clear its cache.
443
        self.request.clearSecurityPolicyCache()
444
445
        if not check_permission("launchpad.View", self.context.bug):
446
            # Redirect the user to the bug listing, because they can no
447
            # longer see a private bug from which they've unsubscribed.
448
            self._redirecting_to_bug_list = True
449
450
    def _handleUnsubscribeOtherUser(self, user):
451
        """Handle unsubscribing someone other than the current user."""
452
        assert user != self.user, (
453
            "Expected a user other than the currently logged-in user.")
454
455
        # We'll also unsubscribe the other user from dupes of this bug,
456
        # otherwise they'll keep getting this bug's mail.
457
        self.context.bug.unsubscribe(user, self.user)
458
        unsubed_dupes = self.context.bug.unsubscribeFromDupes(user, user)
459
        self.request.response.addNotification(
460
            structured(
461
                self._getUnsubscribeNotification(user, unsubed_dupes)))
462
11843.1.3 by Graham Binns
It's now possible to update your subscription. Hurrah.
463
    def _handleUpdateSubscription(self, level):
464
        """Handle updating a user's subscription."""
11843.1.5 by Graham Binns
Added tests for unsubscribing and for the default value when updating.
465
        subscription = self.current_user_subscription
12225.9.13 by Graham Binns
Tests pass, so I'm committing.
466
        subscription.bug_notification_level = level
11843.1.5 by Graham Binns
Added tests for unsubscribing and for the default value when updating.
467
        self.request.response.addNotification(
13045.17.2 by Chris Johnston
Reads as reviewer states.
468
            "Your bug report subscription has been updated.")
11843.1.3 by Graham Binns
It's now possible to update your subscription. Hurrah.
469
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
470
    def _getUnsubscribeNotification(self, user, unsubed_dupes):
471
        """Construct and return the unsubscribe-from-bug feedback message.
472
473
        :user: The IPerson or ITeam that was unsubscribed from the bug.
474
        :unsubed_dupes: The list of IBugs that are dupes from which the
475
                        user was unsubscribed.
476
        """
477
        current_bug = self.context.bug
478
        current_user = self.user
479
        unsubed_dupes_msg_fragment = self._getUnsubscribedDupesMsgFragment(
480
            unsubed_dupes)
481
482
        if user == current_user:
483
            # Consider that the current user may have been "locked out"
484
            # of a bug if they unsubscribed themselves from a private
485
            # bug!
486
            if check_permission("launchpad.View", current_bug):
487
                # The user still has permission to see this bug, so no
488
                # special-casing needed.
489
                return (
490
                    "You have been unsubscribed from bug %d%s." % (
491
                    current_bug.id, unsubed_dupes_msg_fragment))
492
            else:
493
                return (
494
                    "You have been unsubscribed from bug %d%s. You no "
495
                    "longer have access to this private bug.") % (
496
                        current_bug.id, unsubed_dupes_msg_fragment)
497
        else:
11654.2.9 by Graham Binns
Merged a change from Brian's branch.
498
            return "%s has been unsubscribed from bug %d%s." % (
499
                cgi.escape(user.displayname), current_bug.id,
500
                unsubed_dupes_msg_fragment)
11654.2.2 by Graham Binns
Big refactoring for BugSubscription view stuff.
501
502
    def _getUnsubscribedDupesMsgFragment(self, unsubed_dupes):
503
        """Return the duplicates fragment of the unsubscription notification.
504
505
        This piece lists the duplicates from which the user was
506
        unsubscribed.
507
        """
508
        if not unsubed_dupes:
509
            return ""
510
511
        dupe_links = []
512
        for unsubed_dupe in unsubed_dupes:
513
            dupe_links.append(
514
                '<a href="%s" title="%s">#%d</a>' % (
515
                canonical_url(unsubed_dupe), unsubed_dupe.title,
516
                unsubed_dupe.id))
517
        dupe_links_string = ", ".join(dupe_links)
518
519
        num_dupes = len(unsubed_dupes)
520
        if num_dupes > 1:
521
            plural_suffix = "s"
522
        else:
523
            plural_suffix = ""
524
525
        return (
526
            " and %(num_dupes)d duplicate%(plural_suffix)s "
527
            "(%(dupe_links_string)s)") % ({
528
                'num_dupes': num_dupes,
529
                'plural_suffix': plural_suffix,
530
                'dupe_links_string': dupe_links_string})
531
532
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
533
class BugPortletSubscribersWithDetails(LaunchpadView):
534
    """A view that returns a JSON dump of the subscriber details for a bug."""
535
13469.2.5 by Brad Crittenden
Precache the 'subscribed_by' person for performance. Add tests showing expected query count of 1.
536
    @cachedproperty
537
    def api_request(self):
538
        return IWebServiceClientRequest(self.request)
539
540
    def direct_subscriber_data(self, bug):
541
        """Get the direct subscriber data.
542
543
        This method is isolated from the subscriber_data_js so that query
544
        count testing can be done accurately and robustly.
545
        """
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
546
        data = []
13251.1.1 by Danilo Segan
Show the subscribers list for anonymous users.
547
        details = list(bug.getDirectSubscribersWithDetails())
13469.2.5 by Brad Crittenden
Precache the 'subscribed_by' person for performance. Add tests showing expected query count of 1.
548
        for person, subscribed_by, subscription in details:
13316.8.1 by Ian Booth
People who subscribe someone to a bug can also unsubscribe them
549
            can_edit = subscription.canBeUnsubscribedByUser(self.user)
14235.4.4 by Ian Booth
Add launchpad.Exists permission for bug subscribers and assignees
550
            if person == self.user:
551
                # Skip the current user viewing the page.
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
552
                continue
14235.4.7 by Ian Booth
Rename permission to launchpad.See and write tests
553
            if self.user is None and person.private:
554
                # Do not include private teams if there's no logged in user.
555
                continue
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
556
14550.3.3 by Ian Booth
Improve sql used in security adaptor, migrate doc test to unit test, add tests
557
            # If we have made it to here then the logged in user can see the
558
            # bug, hence they can see any subscribers.
559
            # The security adaptor will do the job also but we don't want or
560
            # need the expense of running several complex SQL queries.
561
            precache_permission_for_objects(
562
                        self.request, 'launchpad.LimitedView', [person])
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
563
            subscriber = {
564
                'name': person.name,
565
                'display_name': person.displayname,
566
                'web_link': canonical_url(person, rootsite='mainsite'),
13469.2.5 by Brad Crittenden
Precache the 'subscribed_by' person for performance. Add tests showing expected query count of 1.
567
                'self_link': absoluteURL(person, self.api_request),
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
568
                'is_team': person.is_team,
569
                'can_edit': can_edit,
13469.2.6 by Brad Crittenden
Incorporate brilliant suggestions from review.
570
                'display_subscribed_by': subscription.display_subscribed_by,
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
571
                }
572
            record = {
573
                'subscriber': subscriber,
574
                'subscription_level': str(
575
                    removeSecurityProxy(subscription.bug_notification_level)),
576
                }
577
            data.append(record)
13469.2.5 by Brad Crittenden
Precache the 'subscribed_by' person for performance. Add tests showing expected query count of 1.
578
        return data
579
580
    @property
14027.2.1 by Ian Booth
Fix bug secrecy view ajax
581
    def subscriber_data(self):
13469.2.5 by Brad Crittenden
Precache the 'subscribed_by' person for performance. Add tests showing expected query count of 1.
582
        """Return subscriber_ids in a form suitable for JavaScript use."""
13469.2.6 by Brad Crittenden
Incorporate brilliant suggestions from review.
583
        bug = IBug(self.context)
13469.2.5 by Brad Crittenden
Precache the 'subscribed_by' person for performance. Add tests showing expected query count of 1.
584
        data = self.direct_subscriber_data(bug)
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
585
13251.1.1 by Danilo Segan
Show the subscribers list for anonymous users.
586
        others = list(bug.getIndirectSubscribers())
14235.4.4 by Ian Booth
Add launchpad.Exists permission for bug subscribers and assignees
587
        # If we have made it to here then the logged in user can see the
14550.3.2 by Ian Booth
Add private team limited view checks for direct subscribers and assignees
588
        # bug, hence they can see any indirect subscribers.
14235.4.7 by Ian Booth
Rename permission to launchpad.See and write tests
589
        include_private = self.user is not None
590
        if include_private:
14235.4.4 by Ian Booth
Add launchpad.Exists permission for bug subscribers and assignees
591
            precache_permission_for_objects(
14235.4.10 by Ian Booth
Rename launchpad.See to launchpad.LimitedView
592
                self.request, 'launchpad.LimitedView', others)
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
593
        for person in others:
14235.4.4 by Ian Booth
Add launchpad.Exists permission for bug subscribers and assignees
594
            if person == self.user:
14235.4.2 by Ian Booth
Add logic to exclude unauthorised indirect subscribers also
595
                # Skip the current user viewing the page,
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
596
                continue
14235.4.7 by Ian Booth
Rename permission to launchpad.See and write tests
597
            if not include_private and person.private:
598
                # Do not include private teams if there's no logged in user.
599
                continue
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
600
            subscriber = {
601
                'name': person.name,
602
                'display_name': person.displayname,
603
                'web_link': canonical_url(person, rootsite='mainsite'),
13469.2.5 by Brad Crittenden
Precache the 'subscribed_by' person for performance. Add tests showing expected query count of 1.
604
                'self_link': absoluteURL(person, self.api_request),
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
605
                'is_team': person.is_team,
606
                'can_edit': False,
607
                }
608
            record = {
609
                'subscriber': subscriber,
610
                'subscription_level': 'Maybe',
611
                }
612
            data.append(record)
14027.2.1 by Ian Booth
Fix bug secrecy view ajax
613
        return data
614
615
    @property
616
    def subscriber_data_js(self):
617
        return dumps(self.subscriber_data)
7675.1193.16 by Danilo Segan
Better performing version of subscribers loading.
618
619
    def render(self):
620
        """Override the default render() to return only JSON."""
621
        self.request.response.setHeader('content-type', 'application/json')
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
622
        return self.subscriber_data_js
7675.1193.16 by Danilo Segan
Better performing version of subscribers loading.
623
624
8857.4.5 by Deryck Hodge
A basically working pass at using a subscriber_ids mapping
625
class SubscriptionAttrDecorator:
626
    """A BugSubscription with added attributes for HTML/JS."""
627
    delegates(IBugSubscription, 'subscription')
628
629
    def __init__(self, subscription):
630
        self.subscription = subscription
631
632
    @property
633
    def css_name(self):
634
        return 'subscriber-%s' % self.subscription.person.id
12348.1.1 by Graham Binns
Added a vestigial page.
635
12556.5.5 by Gary Poster
save some work on strings
636
12393.30.17 by Brad Crittenden
Merge from yellow + use expose_structural_subscription_data_to_js instead of new JSMixin
637
class BugSubscriptionListView(LaunchpadView):
12348.1.1 by Graham Binns
Added a vestigial page.
638
    """A view to show all a person's subscriptions to a bug."""
639
12393.30.17 by Brad Crittenden
Merge from yellow + use expose_structural_subscription_data_to_js instead of new JSMixin
640
    def initialize(self):
641
        super(BugSubscriptionListView, self).initialize()
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
642
        subscriptions = list(get_structural_subscriptions_for_bug(
643
            self.context.bug, self.user))
12393.30.17 by Brad Crittenden
Merge from yellow + use expose_structural_subscription_data_to_js instead of new JSMixin
644
        expose_structural_subscription_data_to_js(
12556.5.6 by Gary Poster
merge devel
645
            self.context, self.request, self.user, subscriptions)
12556.5.9 by Gary Poster
progress towards getting data in page
646
        subscriptions_info = PersonSubscriptions(
647
                self.user, self.context.bug)
648
        subdata, references = subscriptions_info.getDataForClient()
12556.5.13 by Gary Poster
add more tests, get page rendering to work
649
        cache = IJSONRequestCache(self.request).objects
12556.5.9 by Gary Poster
progress towards getting data in page
650
        cache.update(references)
651
        cache['bug_subscription_info'] = subdata
13890.2.5 by Steve Kowalik
Correct XXX, and remove two fallacies.
652
        cache['bug_is_private'] = self.context.bug.private
12393.8.1 by Benji York
checkpoint
653
12348.1.1 by Graham Binns
Added a vestigial page.
654
    @property
655
    def label(self):
12556.5.4 by Gary Poster
big refactoring of personsubscriptioninfo to have more information we need.
656
        return "Your subscriptions to bug %d" % self.context.bug.id
12348.1.1 by Graham Binns
Added a vestigial page.
657
658
    page_title = label
12412.2.2 by Gary Poster
make the +subscriptions page have access to the subscriptions for the current user
659
12622.1.1 by Graham Binns
The mute view does its thing. Hurrah.
660
661
class BugMuteSelfView(LaunchpadFormView):
662
    """A view to mute a user's bug mail for a given bug."""
663
664
    schema = IBugSubscription
665
    field_names = []
666
667
    @property
668
    def label(self):
13023.7.2 by Danilo Segan
Split Gary's server-side changes.
669
        if self.context.bug.isMuted(self.user):
670
            return "Unmute bug mail for bug %s" % self.context.bug.id
671
        else:
672
            return "Mute bug mail for bug %s" % self.context.bug.id
12622.1.1 by Graham Binns
The mute view does its thing. Hurrah.
673
674
    page_title = label
675
676
    @property
677
    def next_url(self):
678
        return canonical_url(self.context)
679
680
    cancel_url = next_url
681
12521.6.19 by Danilo Segan
Remove unused stuff, add sample description.
682
    def initialize(self):
13023.7.2 by Danilo Segan
Split Gary's server-side changes.
683
        self.is_muted = self.context.bug.isMuted(self.user)
12622.1.1 by Graham Binns
The mute view does its thing. Hurrah.
684
        super(BugMuteSelfView, self).initialize()
685
13023.7.2 by Danilo Segan
Split Gary's server-side changes.
686
    @action('Mute bug mail',
687
            name='mute',
688
            condition=lambda form, action: not form.is_muted)
12622.1.1 by Graham Binns
The mute view does its thing. Hurrah.
689
    def mute_action(self, action, data):
690
        self.context.bug.mute(self.user, self.user)
691
        self.request.response.addInfoNotification(
12622.1.3 by Graham Binns
Review changes.
692
            "Mail for bug #%s has been muted." % self.context.bug.id)
13023.7.2 by Danilo Segan
Split Gary's server-side changes.
693
694
    @action('Unmute bug mail',
695
            name='unmute',
696
            condition=lambda form, action: form.is_muted)
697
    def unmute_action(self, action, data):
698
        self.context.bug.unmute(self.user, self.user)
699
        self.request.response.addInfoNotification(
700
            "Mail for bug #%s has been unmuted." % self.context.bug.id)