~launchpad-pqm/launchpad/devel

14027.3.4 by Jeroen Vermeulen
Automated import fixes and copyright updates.
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).
14235.4.7 by Ian Booth
Rename permission to launchpad.See and write tests
3
from BeautifulSoup import BeautifulSoup
4
from lp.registry.interfaces.person import PersonVisibility
2770.1.40 by Guilherme Salgado
A few fixes Bjorn suggested and removing two unused templates.
5
8971.26.2 by Gavin Panella
New property current_user_affected_js_status, and tests.
6
__metaclass__ = type
7
14128.3.21 by Aaron Bentley
Test the mustache rendering and placeholder.
8
from contextlib import contextmanager
14128.9.3 by Aaron Bentley
Provide bug age field, hidden by default.
9
from datetime import (
10
    datetime,
11
    timedelta,
12
    )
14128.3.21 by Aaron Bentley
Test the mustache rendering and placeholder.
13
import re
14186.3.12 by Ian Booth
When deleting the current bugtask, redirect to the default bug task afterwards, plus add tests
14
import simplejson
14128.3.47 by Aaron Bentley
Update tests.
15
import urllib
2770.1.40 by Guilherme Salgado
A few fixes Bjorn suggested and removing two unused templates.
16
12792.8.1 by William Grant
Add failing test for BugActivityItem assignee escaping.
17
from lazr.lifecycle.event import ObjectModifiedEvent
14128.3.20 by Aaron Bentley
Ensure the json cache is populated appropriately.
18
from lazr.restful.interfaces import IJSONRequestCache
12792.8.1 by William Grant
Add failing test for BugActivityItem assignee escaping.
19
from lazr.lifecycle.snapshot import Snapshot
12075.3.3 by Gavin Panella
Factor out the selection of interesting activity.
20
from pytz import UTC
14128.3.21 by Aaron Bentley
Test the mustache rendering and placeholder.
21
import soupmatchers
11474.2.3 by Robert Collins
Add getSubscribersForPerson to IBug, permitting single query setup of bug index pages, which according to OOPS is about 1.2 seconds (but if we're underestimating could be more) and will make analysing performance on the page easier.
22
from storm.store import Store
14128.3.21 by Aaron Bentley
Test the mustache rendering and placeholder.
23
from testtools.matchers import (
24
    LessThan,
25
    Not,
26
    )
14027.3.4 by Jeroen Vermeulen
Automated import fixes and copyright updates.
27
import transaction
12799.1.2 by Ian Booth
Rework implementation and do unit tests instead of doc tests
28
from zope.component import (
29
    getMultiAdapter,
30
    getUtility,
31
    )
12792.8.1 by William Grant
Add failing test for BugActivityItem assignee escaping.
32
from zope.event import notify
33
from zope.interface import providedBy
10680.2.1 by Abel Deuring
Add a new bug task status 'Expired'; the bug task expiration script sets expired bug tasks to this status; related adjustments to the bug task views
34
from zope.security.proxy import removeSecurityProxy
2770.1.40 by Guilherme Salgado
A few fixes Bjorn suggested and removing two unused templates.
35
13752.3.9 by Graham Binns
The default batch size is now a config option.
36
from canonical.config import config
13955.1.7 by Graham Binns
Review changes for Jeroen.
37
from canonical.database.constants import UTC_NOW
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
38
from canonical.launchpad.ftests import (
39
    ANONYMOUS,
40
    login,
41
    login_person,
42
    )
11655.1.1 by Brad Crittenden
Do not show bug information on a project group page with no projects that use Launchpad.
43
from canonical.launchpad.testing.pages import find_tag_by_id
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
44
from canonical.launchpad.webapp import canonical_url
14186.3.3 by Ian Booth
Implement code review changes
45
from canonical.launchpad.webapp.authorization import clear_cache
14186.4.1 by Ian Booth
Use ReturnToReferrerMixin for bugtask deletion view
46
from canonical.launchpad.webapp.interfaces import (
47
    ILaunchBag,
48
    ILaunchpadRoot,
49
    )
9521.3.1 by Tom Berger
patch from allenap
50
from canonical.launchpad.webapp.servers import LaunchpadTestRequest
12736.8.2 by William Grant
Test that BugTask:+index doesn't scale with attachments.
51
from canonical.testing.layers import (
52
    DatabaseFunctionalLayer,
53
    LaunchpadFunctionalLayer,
54
    )
13130.1.12 by Curtis Hovey
Sorted imports.
55
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
13752.3.4 by Graham Binns
Added some basic tests.
56
from lp.bugs.adapters.bugchange import BugTaskStatusChange
10680.2.1 by Abel Deuring
Add a new bug task status 'Expired'; the bug task expiration script sets expired bug tasks to this status; related adjustments to the bug task views
57
from lp.bugs.browser.bugtask import (
12792.8.1 by William Grant
Add failing test for BugActivityItem assignee escaping.
58
    BugActivityItem,
14128.7.1 by Aaron Bentley
Control whether the bug_id is displayed using the mustache template.
59
    BugListingBatchNavigator,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
60
    BugTaskEditView,
14128.3.19 by Aaron Bentley
Test BugTaskListingItem.model
61
    BugTaskListingItem,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
62
    BugTasksAndNominationsView,
63
    )
12075.3.3 by Gavin Panella
Factor out the selection of interesting activity.
64
from lp.bugs.interfaces.bugactivity import IBugActivitySet
12799.1.2 by Ian Booth
Rework implementation and do unit tests instead of doc tests
65
from lp.bugs.interfaces.bugnomination import IBugNomination
66
from lp.bugs.interfaces.bugtask import (
67
    BugTaskStatus,
68
    IBugTask,
69
    IBugTaskSet,
70
    )
14174.2.10 by Ian Booth
Remove some unnecessary checks and refactor
71
from lp.services.features.testing import FeatureFixture
12075.3.3 by Gavin Panella
Factor out the selection of interesting activity.
72
from lp.services.propertycache import get_property_cache
12561.3.20 by Curtis Hovey
Reconstructed the BugTasksAndNominationsView.getTargetLinkTitle test.
73
from lp.soyuz.interfaces.component import IComponentSet
11655.1.1 by Brad Crittenden
Do not show bug information on a project group page with no projects that use Launchpad.
74
from lp.testing import (
14128.3.21 by Aaron Bentley
Test the mustache rendering and placeholder.
75
    BrowserTestCase,
12736.8.2 by William Grant
Test that BugTask:+index doesn't scale with attachments.
76
    celebrity_logged_in,
14128.3.20 by Aaron Bentley
Ensure the json cache is populated appropriately.
77
    feature_flags,
11655.1.1 by Brad Crittenden
Do not show bug information on a project group page with no projects that use Launchpad.
78
    person_logged_in,
14128.3.20 by Aaron Bentley
Ensure the json cache is populated appropriately.
79
    set_feature_flag,
11655.1.1 by Brad Crittenden
Do not show bug information on a project group page with no projects that use Launchpad.
80
    TestCaseWithFactory,
81
    )
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
82
from lp.testing._webservice import QueryCollector
12736.8.2 by William Grant
Test that BugTask:+index doesn't scale with attachments.
83
from lp.testing.matchers import (
84
    BrowsesWithQueryLimit,
85
    HasQueryCount,
86
    )
11474.2.1 by Robert Collins
Use sampledata constants.
87
from lp.testing.sampledata import (
88
    ADMIN_EMAIL,
89
    NO_PRIVILEGE_EMAIL,
90
    USER_EMAIL,
91
    )
11655.1.3 by Brad Crittenden
De-lint
92
from lp.testing.views import create_initialized_view
8971.26.2 by Gavin Panella
New property current_user_affected_js_status, and tests.
93
94
14186.3.2 by Ian Booth
Add tests for delete view
95
DELETE_BUGTASK_ENABLED = {u"disclosure.delete_bugtask.enabled": u"on"}
96
97
11474.2.3 by Robert Collins
Add getSubscribersForPerson to IBug, permitting single query setup of bug index pages, which according to OOPS is about 1.2 seconds (but if we're underestimating could be more) and will make analysing performance on the page easier.
98
class TestBugTaskView(TestCaseWithFactory):
99
12736.8.2 by William Grant
Test that BugTask:+index doesn't scale with attachments.
100
    layer = LaunchpadFunctionalLayer
11474.2.3 by Robert Collins
Add getSubscribersForPerson to IBug, permitting single query setup of bug index pages, which according to OOPS is about 1.2 seconds (but if we're underestimating could be more) and will make analysing performance on the page easier.
101
102
    def invalidate_caches(self, obj):
103
        store = Store.of(obj)
104
        # Make sure everything is in the database.
105
        store.flush()
106
        # And invalidate the cache (not a reset, because that stops us using
107
        # the domain objects)
108
        store.invalidate()
109
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
110
    def test_rendered_query_counts_constant_with_team_memberships(self):
11474.2.3 by Robert Collins
Add getSubscribersForPerson to IBug, permitting single query setup of bug index pages, which according to OOPS is about 1.2 seconds (but if we're underestimating could be more) and will make analysing performance on the page easier.
111
        login(ADMIN_EMAIL)
11618.1.1 by Deryck Hodge
Drive the query count down by two for bugtask+index, and fix up
112
        task = self.factory.makeBugTask()
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
113
        person_no_teams = self.factory.makePerson(password='test')
114
        person_with_teams = self.factory.makePerson(password='test')
11474.2.4 by Robert Collins
Testing tweaks per review.
115
        for _ in range(10):
116
            self.factory.makeTeam(members=[person_with_teams])
11474.2.3 by Robert Collins
Add getSubscribersForPerson to IBug, permitting single query setup of bug index pages, which according to OOPS is about 1.2 seconds (but if we're underestimating could be more) and will make analysing performance on the page easier.
117
        # count with no teams
11618.1.1 by Deryck Hodge
Drive the query count down by two for bugtask+index, and fix up
118
        url = canonical_url(task)
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
119
        recorder = QueryCollector()
120
        recorder.register()
121
        self.addCleanup(recorder.unregister)
11618.1.1 by Deryck Hodge
Drive the query count down by two for bugtask+index, and fix up
122
        self.invalidate_caches(task)
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
123
        self.getUserBrowser(url, person_no_teams)
11655.1.3 by Brad Crittenden
De-lint
124
        # This may seem large: it is; there is easily another 30% fat in
125
        # there.
14189.6.9 by mbp at canonical
Add other_users_affected_count_with_dupes to get the right answers when the current user is affected by a dupe
126
        self.assertThat(recorder, HasQueryCount(LessThan(85)))
11474.2.3 by Robert Collins
Add getSubscribersForPerson to IBug, permitting single query setup of bug index pages, which according to OOPS is about 1.2 seconds (but if we're underestimating could be more) and will make analysing performance on the page easier.
127
        count_with_no_teams = recorder.count
11474.2.4 by Robert Collins
Testing tweaks per review.
128
        # count with many teams
11618.1.1 by Deryck Hodge
Drive the query count down by two for bugtask+index, and fix up
129
        self.invalidate_caches(task)
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
130
        self.getUserBrowser(url, person_with_teams)
11474.2.3 by Robert Collins
Add getSubscribersForPerson to IBug, permitting single query setup of bug index pages, which according to OOPS is about 1.2 seconds (but if we're underestimating could be more) and will make analysing performance on the page easier.
131
        # Allow an increase of one because storm bug 619017 causes additional
11474.2.4 by Robert Collins
Testing tweaks per review.
132
        # queries, revalidating things unnecessarily. An increase which is
133
        # less than the number of new teams shows it is definitely not
134
        # growing per-team.
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
135
        self.assertThat(recorder, HasQueryCount(
11474.2.4 by Robert Collins
Testing tweaks per review.
136
            LessThan(count_with_no_teams + 3),
11474.2.3 by Robert Collins
Add getSubscribersForPerson to IBug, permitting single query setup of bug index pages, which according to OOPS is about 1.2 seconds (but if we're underestimating could be more) and will make analysing performance on the page easier.
137
            ))
138
12736.8.2 by William Grant
Test that BugTask:+index doesn't scale with attachments.
139
    def test_rendered_query_counts_constant_with_attachments(self):
140
        with celebrity_logged_in('admin'):
141
            browses_under_limit = BrowsesWithQueryLimit(
14189.6.9 by mbp at canonical
Add other_users_affected_count_with_dupes to get the right answers when the current user is affected by a dupe
142
                88, self.factory.makePerson())
12736.8.2 by William Grant
Test that BugTask:+index doesn't scale with attachments.
143
144
            # First test with a single attachment.
145
            task = self.factory.makeBugTask()
146
            self.factory.makeBugAttachment(bug=task.bug)
147
        self.assertThat(task, browses_under_limit)
148
149
        with celebrity_logged_in('admin'):
150
            # And now with 10.
151
            task = self.factory.makeBugTask()
152
            self.factory.makeBugTask(bug=task.bug)
153
            for i in range(10):
154
                self.factory.makeBugAttachment(bug=task.bug)
155
        self.assertThat(task, browses_under_limit)
156
13827.1.2 by Gary Poster
respond to review
157
    def makeLinkedBranchMergeProposal(self, sourcepackage, bug, owner):
158
        with person_logged_in(owner):
159
            f = self.factory
160
            target_branch = f.makePackageBranch(
161
                sourcepackage=sourcepackage, owner=owner)
162
            source_branch = f.makeBranchTargetBranch(
163
                target_branch.target, owner=owner)
164
            bug.linkBranch(source_branch, owner)
165
            return f.makeBranchMergeProposal(
166
                target_branch=target_branch,
167
                registrant=owner,
168
                source_branch=source_branch)
169
13827.1.1 by Gary Poster
add optimization for bug page with many branches: this time for sure!
170
    def test_rendered_query_counts_reduced_with_branches(self):
171
        f = self.factory
172
        owner = f.makePerson()
173
        ds = f.makeDistroSeries()
13827.1.2 by Gary Poster
respond to review
174
        bug = f.makeBug()
13827.1.1 by Gary Poster
add optimization for bug page with many branches: this time for sure!
175
        sourcepackages = [
13827.1.2 by Gary Poster
respond to review
176
            f.makeSourcePackage(distroseries=ds, publish=True)
177
            for i in range(5)]
13827.1.1 by Gary Poster
add optimization for bug page with many branches: this time for sure!
178
        for sp in sourcepackages:
14027.3.1 by Jeroen Vermeulen
Fix lots of lint in recently-changed files.
179
            f.makeBugTask(bug=bug, owner=owner, target=sp)
13827.1.2 by Gary Poster
respond to review
180
        url = canonical_url(bug.default_bugtask)
13827.1.1 by Gary Poster
add optimization for bug page with many branches: this time for sure!
181
        recorder = QueryCollector()
182
        recorder.register()
183
        self.addCleanup(recorder.unregister)
13827.1.2 by Gary Poster
respond to review
184
        self.invalidate_caches(bug.default_bugtask)
13827.1.1 by Gary Poster
add optimization for bug page with many branches: this time for sure!
185
        self.getUserBrowser(url, owner)
186
        # At least 20 of these should be removed.
14280.2.6 by Steve Kowalik
Fix one last test failure.
187
        self.assertThat(recorder, HasQueryCount(LessThan(101)))
13827.1.1 by Gary Poster
add optimization for bug page with many branches: this time for sure!
188
        count_with_no_branches = recorder.count
13827.1.2 by Gary Poster
respond to review
189
        for sp in sourcepackages:
190
            self.makeLinkedBranchMergeProposal(sp, bug, owner)
191
        self.invalidate_caches(bug.default_bugtask)
192
        self.getUserBrowser(url, owner)  # This triggers the query recorder.
13827.1.1 by Gary Poster
add optimization for bug page with many branches: this time for sure!
193
        # Ideally this should be much fewer, but this tries to keep a win of
194
        # removing more than half of these.
195
        self.assertThat(recorder, HasQueryCount(
14189.6.3 by mbp at canonical
Counting total affected users across dupes can be disabled by a feature flag.
196
            LessThan(count_with_no_branches + 46),
13827.1.1 by Gary Poster
add optimization for bug page with many branches: this time for sure!
197
            ))
198
12075.3.3 by Gavin Panella
Factor out the selection of interesting activity.
199
    def test_interesting_activity(self):
200
        # The interesting_activity property returns a tuple of interesting
201
        # `BugActivityItem`s.
202
        bug = self.factory.makeBug()
203
        view = create_initialized_view(
204
            bug.default_bugtask, name=u'+index', rootsite='bugs')
205
206
        def add_activity(what, old=None, new=None, message=None):
207
            getUtility(IBugActivitySet).new(
208
                bug, datetime.now(UTC), bug.owner, whatchanged=what,
209
                oldvalue=old, newvalue=new, message=message)
210
            del get_property_cache(view).interesting_activity
211
212
        # A fresh bug has no interesting activity.
213
        self.assertEqual((), view.interesting_activity)
214
215
        # Some activity is not considered interesting.
216
        add_activity("boring")
217
        self.assertEqual((), view.interesting_activity)
218
219
        # A description change is interesting.
220
        add_activity("description")
221
        self.assertEqual(1, len(view.interesting_activity))
222
        [activity] = view.interesting_activity
223
        self.assertEqual("description", activity.whatchanged)
224
13402.4.1 by Graham Binns
I can't quite believe this, but the tests pass.
225
    def test_error_for_changing_target_with_invalid_status(self):
226
        # If a user moves a bug task with a restricted status (say,
227
        # Triaged) to a target where they do not have permission to set
228
        # that status, they will be unable to complete the retargeting
229
        # and will instead receive an error in the UI.
230
        person = self.factory.makePerson()
231
        product = self.factory.makeProduct(
232
            name='product1', owner=person, official_malone=True)
233
        with person_logged_in(person):
234
            product.setBugSupervisor(person, person)
235
        product_2 = self.factory.makeProduct(
236
            name='product2', official_malone=True)
237
        with person_logged_in(product_2.owner):
238
            product_2.setBugSupervisor(product_2.owner, product_2.owner)
239
        bug = self.factory.makeBug(
240
            product=product, owner=person)
241
        # We need to commit here, otherwise all the sample data we
242
        # created gets destroyed when the transaction is rolled back.
243
        transaction.commit()
244
        with person_logged_in(person):
245
            form_data = {
13494.2.12 by William Grant
Fix test_bugtask.
246
                '%s.target' % product.name: 'product',
247
                '%s.target.product' % product.name: product_2.name,
13402.4.1 by Graham Binns
I can't quite believe this, but the tests pass.
248
                '%s.status' % product.name: BugTaskStatus.TRIAGED.title,
249
                '%s.actions.save' % product.name: 'Save Changes',
250
                }
251
            view = create_initialized_view(
252
                bug.default_bugtask, name=u'+editstatus',
253
                form=form_data)
254
            # The bugtask's target won't have changed, since an error
255
            # happend. The error will be listed in the view.
13402.4.5 by Graham Binns
Tweaked according to Rob's requirements.
256
            self.assertEqual(1, len(view.errors))
13402.4.1 by Graham Binns
I can't quite believe this, but the tests pass.
257
            self.assertEqual(product, bug.default_bugtask.target)
258
11474.2.3 by Robert Collins
Add getSubscribersForPerson to IBug, permitting single query setup of bug index pages, which according to OOPS is about 1.2 seconds (but if we're underestimating could be more) and will make analysing performance on the page easier.
259
8971.26.2 by Gavin Panella
New property current_user_affected_js_status, and tests.
260
class TestBugTasksAndNominationsView(TestCaseWithFactory):
261
12561.3.12 by Curtis Hovey
Fixed broken view while rewriting a broken test.
262
    layer = DatabaseFunctionalLayer
8971.26.2 by Gavin Panella
New property current_user_affected_js_status, and tests.
263
264
    def setUp(self):
265
        super(TestBugTasksAndNominationsView, self).setUp()
11474.2.1 by Robert Collins
Use sampledata constants.
266
        login(ADMIN_EMAIL)
8971.26.2 by Gavin Panella
New property current_user_affected_js_status, and tests.
267
        self.bug = self.factory.makeBug()
9521.3.1 by Tom Berger
patch from allenap
268
        self.view = BugTasksAndNominationsView(
269
            self.bug, LaunchpadTestRequest())
8971.26.2 by Gavin Panella
New property current_user_affected_js_status, and tests.
270
11582.2.5 by Robert Collins
Fix up test fallout.
271
    def refresh(self):
11582.2.6 by Robert Collins
More tests not-quite-right.
272
        # The view caches, to see different scenarios, a refresh is needed.
11582.2.5 by Robert Collins
Fix up test fallout.
273
        self.view = BugTasksAndNominationsView(
274
            self.bug, LaunchpadTestRequest())
275
8971.26.2 by Gavin Panella
New property current_user_affected_js_status, and tests.
276
    def test_current_user_affected_status(self):
277
        self.failUnlessEqual(
278
            None, self.view.current_user_affected_status)
11582.2.5 by Robert Collins
Fix up test fallout.
279
        self.bug.markUserAffected(self.view.user, True)
280
        self.refresh()
8971.26.2 by Gavin Panella
New property current_user_affected_js_status, and tests.
281
        self.failUnlessEqual(
282
            True, self.view.current_user_affected_status)
11582.2.5 by Robert Collins
Fix up test fallout.
283
        self.bug.markUserAffected(self.view.user, False)
284
        self.refresh()
8971.26.2 by Gavin Panella
New property current_user_affected_js_status, and tests.
285
        self.failUnlessEqual(
286
            False, self.view.current_user_affected_status)
287
288
    def test_current_user_affected_js_status(self):
289
        self.failUnlessEqual(
290
            'null', self.view.current_user_affected_js_status)
11582.2.5 by Robert Collins
Fix up test fallout.
291
        self.bug.markUserAffected(self.view.user, True)
292
        self.refresh()
8971.26.2 by Gavin Panella
New property current_user_affected_js_status, and tests.
293
        self.failUnlessEqual(
294
            'true', self.view.current_user_affected_js_status)
11582.2.5 by Robert Collins
Fix up test fallout.
295
        self.bug.markUserAffected(self.view.user, False)
296
        self.refresh()
8971.26.2 by Gavin Panella
New property current_user_affected_js_status, and tests.
297
        self.failUnlessEqual(
298
            'false', self.view.current_user_affected_js_status)
299
9521.3.1 by Tom Berger
patch from allenap
300
    def test_not_many_bugtasks(self):
301
        for count in range(10 - len(self.bug.bugtasks) - 1):
302
            self.factory.makeBugTask(bug=self.bug)
303
        self.view.initialize()
304
        self.failIf(self.view.many_bugtasks)
305
        row_view = self.view._getTableRowView(
306
            self.bug.default_bugtask, False, False)
307
        self.failIf(row_view.many_bugtasks)
308
309
    def test_many_bugtasks(self):
310
        for count in range(10 - len(self.bug.bugtasks)):
311
            self.factory.makeBugTask(bug=self.bug)
312
        self.view.initialize()
313
        self.failUnless(self.view.many_bugtasks)
314
        row_view = self.view._getTableRowView(
315
            self.bug.default_bugtask, False, False)
316
        self.failUnless(row_view.many_bugtasks)
317
10015.1.2 by Gavin Panella
New view property other_users_affected_count.
318
    def test_other_users_affected_count(self):
319
        # The number of other users affected does not change when the
320
        # logged-in user marked him or herself as affected or not.
321
        self.failUnlessEqual(
322
            1, self.view.other_users_affected_count)
11582.2.5 by Robert Collins
Fix up test fallout.
323
        self.bug.markUserAffected(self.view.user, True)
324
        self.refresh()
10015.1.2 by Gavin Panella
New view property other_users_affected_count.
325
        self.failUnlessEqual(
326
            1, self.view.other_users_affected_count)
11582.2.5 by Robert Collins
Fix up test fallout.
327
        self.bug.markUserAffected(self.view.user, False)
328
        self.refresh()
10015.1.2 by Gavin Panella
New view property other_users_affected_count.
329
        self.failUnlessEqual(
330
            1, self.view.other_users_affected_count)
331
332
    def test_other_users_affected_count_other_users(self):
333
        # The number of other users affected only changes when other
334
        # users mark themselves as affected.
335
        self.failUnlessEqual(
336
            1, self.view.other_users_affected_count)
337
        other_user_1 = self.factory.makePerson()
11582.2.5 by Robert Collins
Fix up test fallout.
338
        self.bug.markUserAffected(other_user_1, True)
14189.6.9 by mbp at canonical
Add other_users_affected_count_with_dupes to get the right answers when the current user is affected by a dupe
339
        self.refresh()
10015.1.2 by Gavin Panella
New view property other_users_affected_count.
340
        self.failUnlessEqual(
341
            2, self.view.other_users_affected_count)
342
        other_user_2 = self.factory.makePerson()
11582.2.5 by Robert Collins
Fix up test fallout.
343
        self.bug.markUserAffected(other_user_2, True)
14189.6.9 by mbp at canonical
Add other_users_affected_count_with_dupes to get the right answers when the current user is affected by a dupe
344
        self.refresh()
10015.1.2 by Gavin Panella
New view property other_users_affected_count.
345
        self.failUnlessEqual(
346
            3, self.view.other_users_affected_count)
11582.2.5 by Robert Collins
Fix up test fallout.
347
        self.bug.markUserAffected(other_user_1, False)
14189.6.9 by mbp at canonical
Add other_users_affected_count_with_dupes to get the right answers when the current user is affected by a dupe
348
        self.refresh()
10015.1.2 by Gavin Panella
New view property other_users_affected_count.
349
        self.failUnlessEqual(
350
            2, self.view.other_users_affected_count)
11582.2.5 by Robert Collins
Fix up test fallout.
351
        self.bug.markUserAffected(self.view.user, True)
352
        self.refresh()
10015.1.2 by Gavin Panella
New view property other_users_affected_count.
353
        self.failUnlessEqual(
354
            2, self.view.other_users_affected_count)
355
14189.6.9 by mbp at canonical
Add other_users_affected_count_with_dupes to get the right answers when the current user is affected by a dupe
356
    def makeDuplicate(self):
357
        user2 = self.factory.makePerson()
358
        self.bug2 = self.factory.makeBug()
359
        self.bug2.markUserAffected(user2, True)
360
        self.assertEqual(
361
            2, self.bug2.users_affected_count)
362
        self.bug2.markAsDuplicate(self.bug)
363
        # After this there are three users already affected: the creators of
364
        # the two bugs, plus user2.  The current user is not yet affected by
365
        # any of them.
366
367
    def test_counts_user_unaffected(self):
14189.6.3 by mbp at canonical
Counting total affected users across dupes can be disabled by a feature flag.
368
        self.useFixture(FeatureFixture(
369
            {'bugs.affected_count_includes_dupes.disabled': ''}))
14189.6.9 by mbp at canonical
Add other_users_affected_count_with_dupes to get the right answers when the current user is affected by a dupe
370
        self.makeDuplicate()
14189.6.3 by mbp at canonical
Counting total affected users across dupes can be disabled by a feature flag.
371
        self.assertEqual(
372
            3, self.view.total_users_affected_count)
14189.6.9 by mbp at canonical
Add other_users_affected_count_with_dupes to get the right answers when the current user is affected by a dupe
373
        self.assertEqual(
374
            "This bug affects 3 people. Does this bug affect you?",
375
            self.view.affected_statement)
376
        self.assertEqual(
377
            "This bug affects 3 people",
378
            self.view.anon_affected_statement)
379
        self.assertEqual(
380
            self.view.other_users_affected_count,
381
            3)
382
383
    def test_counts_affected_by_duplicate(self):
384
        self.useFixture(FeatureFixture(
385
            {'bugs.affected_count_includes_dupes.disabled': ''}))
386
        self.makeDuplicate()
387
        # Now with you affected by the duplicate, but not the master.
388
        self.bug2.markUserAffected(self.view.user, True)
389
        self.refresh()
390
        self.assertEqual(
391
            "This bug affects 3 people. Does this bug affect you?",
392
            self.view.affected_statement)
393
        self.assertEqual(
394
            "This bug affects 4 people",
395
            self.view.anon_affected_statement)
396
        self.assertEqual(
397
            self.view.other_users_affected_count,
398
            3)
399
400
    def test_counts_affected_by_master(self):
401
        self.useFixture(FeatureFixture(
402
            {'bugs.affected_count_includes_dupes.disabled': ''}))
403
        self.makeDuplicate()
404
        # And now with you also affected by the master.
405
        self.bug.markUserAffected(self.view.user, True)
406
        self.refresh()
407
        self.assertEqual(
408
            "This bug affects you and 3 other people",
409
            self.view.affected_statement)
410
        self.assertEqual(
411
            "This bug affects 4 people",
412
            self.view.anon_affected_statement)
413
        self.assertEqual(
414
            self.view.other_users_affected_count,
415
            3)
416
417
    def test_counts_affected_by_duplicate_not_by_master(self):
418
        self.useFixture(FeatureFixture(
419
            {'bugs.affected_count_includes_dupes.disabled': ''}))
420
        self.makeDuplicate()
421
        self.bug2.markUserAffected(self.view.user, True)
422
        self.bug.markUserAffected(self.view.user, False)
423
        # You're not included in this count, even though you are affected by
424
        # the dupe.
425
        self.assertEqual(
426
            "This bug affects 3 people, but not you",
427
            self.view.affected_statement)
428
        # It would be reasonable for Anon to see this bug cluster affecting
429
        # either 3 or 4 people.  However at the moment the "No" answer on the
430
        # master is more authoritative than the "Yes" on the dupe.
431
        self.assertEqual(
432
            "This bug affects 3 people",
433
            self.view.anon_affected_statement)
434
        self.assertEqual(
435
            self.view.other_users_affected_count,
436
            3)
14189.6.3 by mbp at canonical
Counting total affected users across dupes can be disabled by a feature flag.
437
438
    def test_total_users_affected_count_without_dupes(self):
439
        self.useFixture(FeatureFixture(
440
            {'bugs.affected_count_includes_dupes.disabled': 'on'}))
14189.6.9 by mbp at canonical
Add other_users_affected_count_with_dupes to get the right answers when the current user is affected by a dupe
441
        self.makeDuplicate()
14189.6.3 by mbp at canonical
Counting total affected users across dupes can be disabled by a feature flag.
442
        self.refresh()
443
        # Does not count the two users of bug2, so just 1.
444
        self.assertEqual(
445
            1, self.view.total_users_affected_count)
14189.6.4 by mbp at canonical
Also count across dupes in the anonymous affects statement; check both affectsmetoo statements
446
        self.assertEqual(
447
            "This bug affects 1 person. Does this bug affect you?",
448
            self.view.affected_statement)
449
        self.assertEqual(
450
            "This bug affects 1 person",
451
            self.view.anon_affected_statement)
14189.6.6 by mbp at canonical
other_users_affected_count also counts across dupes to work with js
452
        self.assertEqual(
453
            1,
454
            self.view.other_users_affected_count)
14189.6.3 by mbp at canonical
Counting total affected users across dupes can be disabled by a feature flag.
455
10015.1.3 by Gavin Panella
New view property affected_statement.
456
    def test_affected_statement_no_one_affected(self):
457
        self.bug.markUserAffected(self.bug.owner, False)
458
        self.failUnlessEqual(
459
            0, self.view.other_users_affected_count)
460
        self.failUnlessEqual(
461
            "Does this bug affect you?",
462
            self.view.affected_statement)
463
464
    def test_affected_statement_only_you(self):
465
        self.view.context.markUserAffected(self.view.user, True)
466
        self.failUnless(self.bug.isUserAffected(self.view.user))
467
        self.view.context.markUserAffected(self.bug.owner, False)
468
        self.failUnlessEqual(
469
            0, self.view.other_users_affected_count)
470
        self.failUnlessEqual(
10015.1.7 by Gavin Panella
Remove trailing full-stops from the affected statement.
471
            "This bug affects you",
10015.1.3 by Gavin Panella
New view property affected_statement.
472
            self.view.affected_statement)
473
10015.1.8 by Gavin Panella
Make affected_statement different when a user has explicitly said they are not affected, and when they have not.
474
    def test_affected_statement_only_not_you(self):
475
        self.view.context.markUserAffected(self.view.user, False)
476
        self.failIf(self.bug.isUserAffected(self.view.user))
477
        self.view.context.markUserAffected(self.bug.owner, False)
478
        self.failUnlessEqual(
479
            0, self.view.other_users_affected_count)
480
        self.failUnlessEqual(
10015.1.10 by Gavin Panella
Say "doesn't" instead of "does not".
481
            "This bug doesn't affect you",
10015.1.8 by Gavin Panella
Make affected_statement different when a user has explicitly said they are not affected, and when they have not.
482
            self.view.affected_statement)
483
10015.1.3 by Gavin Panella
New view property affected_statement.
484
    def test_affected_statement_1_person_not_you(self):
485
        self.assertIs(None, self.bug.isUserAffected(self.view.user))
486
        self.failUnlessEqual(
487
            1, self.view.other_users_affected_count)
488
        self.failUnlessEqual(
489
            "This bug affects 1 person. Does this bug affect you?",
490
            self.view.affected_statement)
491
492
    def test_affected_statement_1_person_and_you(self):
493
        self.view.context.markUserAffected(self.view.user, True)
494
        self.failUnless(self.bug.isUserAffected(self.view.user))
495
        self.failUnlessEqual(
496
            1, self.view.other_users_affected_count)
497
        self.failUnlessEqual(
10015.1.7 by Gavin Panella
Remove trailing full-stops from the affected statement.
498
            "This bug affects you and 1 other person",
10015.1.3 by Gavin Panella
New view property affected_statement.
499
            self.view.affected_statement)
500
10015.1.8 by Gavin Panella
Make affected_statement different when a user has explicitly said they are not affected, and when they have not.
501
    def test_affected_statement_1_person_and_not_you(self):
502
        self.view.context.markUserAffected(self.view.user, False)
503
        self.failIf(self.bug.isUserAffected(self.view.user))
504
        self.failUnlessEqual(
505
            1, self.view.other_users_affected_count)
506
        self.failUnlessEqual(
507
            "This bug affects 1 person, but not you",
508
            self.view.affected_statement)
509
10015.1.3 by Gavin Panella
New view property affected_statement.
510
    def test_affected_statement_more_than_1_person_not_you(self):
511
        self.assertIs(None, self.bug.isUserAffected(self.view.user))
512
        other_user = self.factory.makePerson()
513
        self.view.context.markUserAffected(other_user, True)
514
        self.failUnlessEqual(
515
            2, self.view.other_users_affected_count)
516
        self.failUnlessEqual(
517
            "This bug affects 2 people. Does this bug affect you?",
518
            self.view.affected_statement)
519
520
    def test_affected_statement_more_than_1_person_and_you(self):
521
        self.view.context.markUserAffected(self.view.user, True)
522
        self.failUnless(self.bug.isUserAffected(self.view.user))
523
        other_user = self.factory.makePerson()
524
        self.view.context.markUserAffected(other_user, True)
525
        self.failUnlessEqual(
526
            2, self.view.other_users_affected_count)
527
        self.failUnlessEqual(
10015.1.7 by Gavin Panella
Remove trailing full-stops from the affected statement.
528
            "This bug affects you and 2 other people",
10015.1.3 by Gavin Panella
New view property affected_statement.
529
            self.view.affected_statement)
530
10015.1.8 by Gavin Panella
Make affected_statement different when a user has explicitly said they are not affected, and when they have not.
531
    def test_affected_statement_more_than_1_person_and_not_you(self):
532
        self.view.context.markUserAffected(self.view.user, False)
533
        self.failIf(self.bug.isUserAffected(self.view.user))
534
        other_user = self.factory.makePerson()
535
        self.view.context.markUserAffected(other_user, True)
536
        self.failUnlessEqual(
537
            2, self.view.other_users_affected_count)
538
        self.failUnlessEqual(
539
            "This bug affects 2 people, but not you",
540
            self.view.affected_statement)
541
10015.1.18 by Gavin Panella
New view property anon_affected_statement.
542
    def test_anon_affected_statement_no_one_affected(self):
543
        self.bug.markUserAffected(self.bug.owner, False)
544
        self.failUnlessEqual(0, self.bug.users_affected_count)
545
        self.assertIs(None, self.view.anon_affected_statement)
546
547
    def test_anon_affected_statement_1_user_affected(self):
548
        self.failUnlessEqual(1, self.bug.users_affected_count)
549
        self.failUnlessEqual(
550
            "This bug affects 1 person",
551
            self.view.anon_affected_statement)
552
553
    def test_anon_affected_statement_2_users_affected(self):
554
        self.view.context.markUserAffected(self.view.user, True)
555
        self.failUnlessEqual(2, self.bug.users_affected_count)
556
        self.failUnlessEqual(
557
            "This bug affects 2 people",
558
            self.view.anon_affected_statement)
559
12561.3.19 by Curtis Hovey
Moved product and productseries getTargetLinkTitle tests to a unit test.
560
    def test_getTargetLinkTitle_product(self):
561
        # The target link title is always none for products.
562
        target = self.factory.makeProduct()
563
        bug_task = self.factory.makeBugTask(bug=self.bug, target=target)
12561.3.20 by Curtis Hovey
Reconstructed the BugTasksAndNominationsView.getTargetLinkTitle test.
564
        self.view.initialize()
565
        self.assertEqual(None, self.view.getTargetLinkTitle(bug_task.target))
12561.3.19 by Curtis Hovey
Moved product and productseries getTargetLinkTitle tests to a unit test.
566
567
    def test_getTargetLinkTitle_productseries(self):
12561.3.20 by Curtis Hovey
Reconstructed the BugTasksAndNominationsView.getTargetLinkTitle test.
568
        # The target link title is always none for productseries.
12561.3.19 by Curtis Hovey
Moved product and productseries getTargetLinkTitle tests to a unit test.
569
        target = self.factory.makeProductSeries()
570
        bug_task = self.factory.makeBugTask(bug=self.bug, target=target)
12561.3.20 by Curtis Hovey
Reconstructed the BugTasksAndNominationsView.getTargetLinkTitle test.
571
        self.view.initialize()
572
        self.assertEqual(None, self.view.getTargetLinkTitle(bug_task.target))
573
574
    def test_getTargetLinkTitle_distribution(self):
575
        # The target link title is always none for distributions.
576
        target = self.factory.makeDistribution()
577
        bug_task = self.factory.makeBugTask(bug=self.bug, target=target)
578
        self.view.initialize()
579
        self.assertEqual(None, self.view.getTargetLinkTitle(bug_task.target))
580
581
    def test_getTargetLinkTitle_distroseries(self):
582
        # The target link title is always none for distroseries.
583
        target = self.factory.makeDistroSeries()
584
        bug_task = self.factory.makeBugTask(bug=self.bug, target=target)
585
        self.view.initialize()
586
        self.assertEqual(None, self.view.getTargetLinkTitle(bug_task.target))
587
588
    def test_getTargetLinkTitle_unpublished_distributionsourcepackage(self):
589
        # The target link title states that the package is not published
590
        # in the current release.
591
        distribution = self.factory.makeDistribution(name='boy')
592
        spn = self.factory.makeSourcePackageName('badger')
593
        component = getUtility(IComponentSet)['universe']
594
        maintainer = self.factory.makePerson(name="jim")
595
        creator = self.factory.makePerson(name="tim")
596
        self.factory.makeSourcePackagePublishingHistory(
597
            distroseries=distribution.currentseries, version='2.0',
598
            component=component, sourcepackagename=spn,
599
            date_uploaded=datetime(2008, 7, 18, 10, 20, 30, tzinfo=UTC),
600
            maintainer=maintainer, creator=creator)
601
        target = distribution.getSourcePackage('badger')
602
        bug_task = self.factory.makeBugTask(
603
            bug=self.bug, target=target, publish=False)
604
        self.view.initialize()
12561.3.21 by Curtis Hovey
Removed duplicate test.
605
        self.assertEqual({}, self.view.target_releases)
12561.3.20 by Curtis Hovey
Reconstructed the BugTasksAndNominationsView.getTargetLinkTitle test.
606
        self.assertEqual(
607
            'No current release for this source package in Boy',
608
            self.view.getTargetLinkTitle(bug_task.target))
609
610
    def test_getTargetLinkTitle_published_distributionsourcepackage(self):
611
        # The target link title states the information about the current
612
        # package in the distro.
613
        distribution = self.factory.makeDistribution(name='koi')
614
        distroseries = self.factory.makeDistroSeries(
615
            distribution=distribution)
616
        spn = self.factory.makeSourcePackageName('finch')
617
        component = getUtility(IComponentSet)['universe']
618
        maintainer = self.factory.makePerson(name="jim")
619
        creator = self.factory.makePerson(name="tim")
620
        self.factory.makeSourcePackagePublishingHistory(
621
            distroseries=distroseries, version='2.0',
622
            component=component, sourcepackagename=spn,
623
            date_uploaded=datetime(2008, 7, 18, 10, 20, 30, tzinfo=UTC),
624
            maintainer=maintainer, creator=creator)
625
        target = distribution.getSourcePackage('finch')
626
        bug_task = self.factory.makeBugTask(
627
            bug=self.bug, target=target, publish=False)
628
        self.view.initialize()
12561.3.21 by Curtis Hovey
Removed duplicate test.
629
        self.assertTrue(
630
            target in self.view.target_releases.keys())
12561.3.20 by Curtis Hovey
Reconstructed the BugTasksAndNominationsView.getTargetLinkTitle test.
631
        self.assertEqual(
632
            'Latest release: 2.0, uploaded to universe on '
633
            '2008-07-18 10:20:30+00:00 by Tim (tim), maintained by Jim (jim)',
634
            self.view.getTargetLinkTitle(bug_task.target))
635
636
    def test_getTargetLinkTitle_published_sourcepackage(self):
637
        # The target link title states the information about the current
638
        # package in the distro.
639
        distroseries = self.factory.makeDistroSeries()
640
        spn = self.factory.makeSourcePackageName('bunny')
641
        component = getUtility(IComponentSet)['universe']
642
        maintainer = self.factory.makePerson(name="jim")
643
        creator = self.factory.makePerson(name="tim")
644
        self.factory.makeSourcePackagePublishingHistory(
645
            distroseries=distroseries, version='2.0',
646
            component=component, sourcepackagename=spn,
647
            date_uploaded=datetime(2008, 7, 18, 10, 20, 30, tzinfo=UTC),
648
            maintainer=maintainer, creator=creator)
649
        target = distroseries.getSourcePackage('bunny')
650
        bug_task = self.factory.makeBugTask(
651
            bug=self.bug, target=target, publish=False)
652
        self.view.initialize()
12561.3.21 by Curtis Hovey
Removed duplicate test.
653
        self.assertTrue(
654
            target in self.view.target_releases.keys())
12561.3.20 by Curtis Hovey
Reconstructed the BugTasksAndNominationsView.getTargetLinkTitle test.
655
        self.assertEqual(
656
            'Latest release: 2.0, uploaded to universe on '
657
            '2008-07-18 10:20:30+00:00 by Tim (tim), maintained by Jim (jim)',
658
            self.view.getTargetLinkTitle(bug_task.target))
12561.3.19 by Curtis Hovey
Moved product and productseries getTargetLinkTitle tests to a unit test.
659
12799.1.2 by Ian Booth
Rework implementation and do unit tests instead of doc tests
660
    def _get_object_type(self, task_or_nomination):
661
        if IBugTask.providedBy(task_or_nomination):
662
            return "bugtask"
663
        elif IBugNomination.providedBy(task_or_nomination):
664
            return "nomination"
665
        else:
666
            return "unknown"
667
668
    def test_bugtask_listing_for_inactive_projects(self):
669
        # Bugtasks should only be listed for active projects.
670
671
        product_foo = self.factory.makeProduct(name="foo")
672
        product_bar = self.factory.makeProduct(name="bar")
673
        foo_bug = self.factory.makeBug(product=product_foo)
674
        bugtask_set = getUtility(IBugTaskSet)
13571.2.2 by William Grant
BugTaskSet.createTask now takes an IBugTarget, not a key. Blergh.
675
        bugtask_set.createTask(foo_bug, foo_bug.owner, product_bar)
12799.1.2 by Ian Booth
Rework implementation and do unit tests instead of doc tests
676
        removeSecurityProxy(product_bar).active = False
677
678
        request = LaunchpadTestRequest()
679
        foo_bugtasks_and_nominations_view = getMultiAdapter(
14186.3.8 by Ian Booth
Add ajax bug task delete functionality
680
            (foo_bug, request), name="+bugtasks-and-nominations-portal")
12799.1.2 by Ian Booth
Rework implementation and do unit tests instead of doc tests
681
        foo_bugtasks_and_nominations_view.initialize()
682
683
        task_and_nomination_views = (
684
            foo_bugtasks_and_nominations_view.getBugTaskAndNominationViews())
685
        actual_results = []
686
        for task_or_nomination_view in task_and_nomination_views:
687
            task_or_nomination = task_or_nomination_view.context
688
            actual_results.append((
689
                self._get_object_type(task_or_nomination),
690
                task_or_nomination.status.title,
691
                task_or_nomination.target.bugtargetdisplayname))
692
        # Only the one active project's task should be listed.
693
        self.assertEqual([("bugtask", "New", "Foo")], actual_results)
694
695
    def test_listing_with_no_bugtasks(self):
696
        # Test the situation when there are no bugtasks to show.
697
698
        product_foo = self.factory.makeProduct(name="foo")
699
        foo_bug = self.factory.makeBug(product=product_foo)
700
        removeSecurityProxy(product_foo).active = False
701
702
        request = LaunchpadTestRequest()
703
        foo_bugtasks_and_nominations_view = getMultiAdapter(
14186.3.8 by Ian Booth
Add ajax bug task delete functionality
704
            (foo_bug, request), name="+bugtasks-and-nominations-portal")
12799.1.2 by Ian Booth
Rework implementation and do unit tests instead of doc tests
705
        foo_bugtasks_and_nominations_view.initialize()
706
707
        task_and_nomination_views = (
708
            foo_bugtasks_and_nominations_view.getBugTaskAndNominationViews())
709
        self.assertEqual([], task_and_nomination_views)
710
13543.10.1 by William Grant
Display a faded statusless row for the parents of orphaned bugtasks.
711
    def test_bugtarget_parent_shown_for_orphaned_series_tasks(self):
712
        # Test that a row is shown for the parent of a series task, even
713
        # if the parent doesn't actually have a task.
714
        series = self.factory.makeProductSeries()
715
        bug = self.factory.makeBug(series=series)
716
        self.assertEqual(2, len(bug.bugtasks))
717
        new_prod = self.factory.makeProduct()
718
        bug.getBugTask(series.product).transitionToTarget(new_prod)
719
720
        view = create_initialized_view(bug, "+bugtasks-and-nominations-table")
721
        subviews = view.getBugTaskAndNominationViews()
722
        self.assertEqual([
723
            (series.product, '+bugtasks-and-nominations-table-row'),
724
            (bug.getBugTask(series), '+bugtasks-and-nominations-table-row'),
725
            (bug.getBugTask(new_prod), '+bugtasks-and-nominations-table-row'),
726
            ], [(v.context, v.__name__) for v in subviews])
727
728
        content = subviews[0]()
729
        self.assertIn(
730
            'href="%s"' % canonical_url(
731
                series.product, path_only_if_possible=True),
732
            content)
733
        self.assertIn(series.product.displayname, content)
734
14235.4.7 by Ian Booth
Rename permission to launchpad.See and write tests
735
    def test_bugtask_listing_for_private_assignees(self):
736
        # Private assignees are rendered in the bug portal view.
737
738
        # Create a bugtask with a private assignee.
739
        product_foo = self.factory.makeProduct(name="foo")
740
        foo_bug = self.factory.makeBug(product=product_foo)
741
        assignee = self.factory.makeTeam(
742
            name="assignee",
743
            visibility=PersonVisibility.PRIVATE)
744
        foo_bug.default_bugtask.transitionToAssignee(assignee)
745
746
        # Render the view.
747
        request = LaunchpadTestRequest()
748
        any_person = self.factory.makePerson()
749
        login_person(any_person, request)
750
        foo_bugtasks_and_nominations_view = getMultiAdapter(
751
            (foo_bug, request), name="+bugtasks-and-nominations-portal")
752
        foo_bugtasks_and_nominations_view.initialize()
753
        task_and_nomination_views = (
754
            foo_bugtasks_and_nominations_view.getBugTaskAndNominationViews())
755
        getUtility(ILaunchBag).add(foo_bug.default_bugtask)
756
        self.assertEqual(1, len(task_and_nomination_views))
757
        content = task_and_nomination_views[0]()
758
759
        # Check the result.
760
        soup = BeautifulSoup(content)
761
        tag = soup.find('label', attrs={'for': "foo.assignee.assigned_to"})
762
        tag_text = tag.renderContents().strip()
763
        self.assertEqual(assignee.unique_displayname, tag_text)
764
2770.1.40 by Guilherme Salgado
A few fixes Bjorn suggested and removing two unused templates.
765
14186.3.2 by Ian Booth
Add tests for delete view
766
class TestBugTaskDeleteLinks(TestCaseWithFactory):
14186.3.3 by Ian Booth
Implement code review changes
767
    """ Test that the delete icons/links are correctly rendered.
14186.3.2 by Ian Booth
Add tests for delete view
768
769
        Bug task deletion is protected by a feature flag.
770
        """
771
772
    layer = DatabaseFunctionalLayer
773
774
    def test_cannot_delete_only_bugtask(self):
775
        # The last bugtask cannot be deleted.
776
        bug = self.factory.makeBug()
777
        login_person(bug.owner)
778
        view = create_initialized_view(
779
            bug, name='+bugtasks-and-nominations-table')
780
        row_view = view._getTableRowView(bug.default_bugtask, False, False)
781
        self.assertFalse(row_view.user_can_delete_bugtask)
782
        del get_property_cache(row_view).user_can_delete_bugtask
783
        with FeatureFixture(DELETE_BUGTASK_ENABLED):
784
            self.assertFalse(row_view.user_can_delete_bugtask)
785
786
    def test_can_delete_bugtask_if_authorised(self):
787
        # The bugtask can be deleted if the user if authorised.
788
        bug = self.factory.makeBug()
789
        bugtask = self.factory.makeBugTask(bug=bug)
790
        login_person(bugtask.owner)
791
        view = create_initialized_view(
14186.3.3 by Ian Booth
Implement code review changes
792
            bug, name='+bugtasks-and-nominations-table',
793
            principal=bugtask.owner)
14186.3.2 by Ian Booth
Add tests for delete view
794
        row_view = view._getTableRowView(bugtask, False, False)
795
        self.assertFalse(row_view.user_can_delete_bugtask)
796
        del get_property_cache(row_view).user_can_delete_bugtask
14186.3.3 by Ian Booth
Implement code review changes
797
        clear_cache()
14186.3.2 by Ian Booth
Add tests for delete view
798
        with FeatureFixture(DELETE_BUGTASK_ENABLED):
799
            self.assertTrue(row_view.user_can_delete_bugtask)
800
801
    def test_bugtask_delete_icon(self):
802
        # The bugtask delete icon is rendered correctly for those tasks the
803
        # user is allowed to delete.
804
        bug = self.factory.makeBug()
14186.3.3 by Ian Booth
Implement code review changes
805
        bugtask_owner = self.factory.makePerson()
806
        bugtask = self.factory.makeBugTask(bug=bug, owner=bugtask_owner)
14186.3.2 by Ian Booth
Add tests for delete view
807
        with FeatureFixture(DELETE_BUGTASK_ENABLED):
14186.3.5 by Ian Booth
Figured out how to replace TestBrowser with create_view
808
            login_person(bugtask.owner)
809
            getUtility(ILaunchBag).add(bug.default_bugtask)
810
            view = create_initialized_view(
811
                bug, name='+bugtasks-and-nominations-table',
812
                principal=bugtask.owner)
813
            # We render the bug task table rows - there are 2 bug tasks.
814
            subviews = view.getBugTaskAndNominationViews()
815
            self.assertEqual(2, len(subviews))
816
            default_bugtask_contents = subviews[0]()
817
            bugtask_contents = subviews[1]()
14186.3.2 by Ian Booth
Add tests for delete view
818
            # bugtask can be deleted because the user owns it.
819
            delete_icon = find_tag_by_id(
14186.3.5 by Ian Booth
Figured out how to replace TestBrowser with create_view
820
                bugtask_contents, 'bugtask-delete-task%d' % bugtask.id)
14186.3.3 by Ian Booth
Implement code review changes
821
            delete_url = canonical_url(
822
                bugtask, rootsite='bugs', view_name='+delete')
823
            self.assertEqual(delete_url, delete_icon['href'])
14186.3.2 by Ian Booth
Add tests for delete view
824
            # default_bugtask cannot be deleted.
825
            delete_icon = find_tag_by_id(
14186.3.5 by Ian Booth
Figured out how to replace TestBrowser with create_view
826
                default_bugtask_contents,
14186.3.2 by Ian Booth
Add tests for delete view
827
                'bugtask-delete-task%d' % bug.default_bugtask.id)
828
            self.assertIsNone(delete_icon)
829
14186.3.16 by Ian Booth
Add unit test
830
    def test_client_cache_contents(self):
831
        """ Test that the client cache contains the expected data.
832
14186.3.18 by Ian Booth
Lint and code review fixes
833
        The cache data is used by the Javascript to enable the delete
834
        links to work as expected.
835
        """
14186.3.16 by Ian Booth
Add unit test
836
        bug = self.factory.makeBug()
837
        bugtask_owner = self.factory.makePerson()
838
        bugtask = self.factory.makeBugTask(bug=bug, owner=bugtask_owner)
839
        with FeatureFixture(DELETE_BUGTASK_ENABLED):
840
            login_person(bugtask.owner)
841
            getUtility(ILaunchBag).add(bug.default_bugtask)
842
            view = create_initialized_view(
843
                bug, name='+bugtasks-and-nominations-table',
844
                principal=bugtask.owner)
845
            view.render()
846
            cache = IJSONRequestCache(view.request)
847
            all_bugtask_data = cache.objects['bugtask_data']
848
849
            def check_bugtask_data(bugtask, can_delete):
850
                self.assertIn(bugtask.id, all_bugtask_data)
851
                bugtask_data = all_bugtask_data[bugtask.id]
852
                self.assertEqual(
853
                    'task%d' % bugtask.id, bugtask_data['form_row_id'])
854
                self.assertEqual(
855
                    'tasksummary%d' % bugtask.id, bugtask_data['row_id'])
856
                self.assertEqual(can_delete, bugtask_data['user_can_delete'])
857
858
            check_bugtask_data(bug.default_bugtask, False)
859
            check_bugtask_data(bugtask, True)
860
14186.3.2 by Ian Booth
Add tests for delete view
861
862
class TestBugTaskDeleteView(TestCaseWithFactory):
863
    """Test the bug task delete form."""
864
865
    layer = DatabaseFunctionalLayer
866
867
    def test_delete_view_rendering(self):
868
        # Test the view rendering, including confirmation message, cancel url.
869
        bug = self.factory.makeBug()
870
        bugtask = self.factory.makeBugTask(bug=bug)
14186.4.1 by Ian Booth
Use ReturnToReferrerMixin for bugtask deletion view
871
        bug_url = canonical_url(bugtask.bug, rootsite='bugs')
872
        # Set up request so that the ReturnToReferrerMixin can correctly
873
        # extra the referer url.
874
        server_url = canonical_url(
875
            getUtility(ILaunchpadRoot), rootsite='bugs')
876
        extra = {'HTTP_REFERER': bug_url}
14186.3.2 by Ian Booth
Add tests for delete view
877
        with FeatureFixture(DELETE_BUGTASK_ENABLED):
878
            login_person(bugtask.owner)
879
            view = create_initialized_view(
14186.4.1 by Ian Booth
Use ReturnToReferrerMixin for bugtask deletion view
880
                bugtask, name='+delete', principal=bugtask.owner,
881
                server_url=server_url, **extra)
14186.3.3 by Ian Booth
Implement code review changes
882
            contents = view.render()
883
            confirmation_message = find_tag_by_id(
884
                contents, 'confirmation-message')
885
            self.assertIsNotNone(confirmation_message)
14186.4.1 by Ian Booth
Use ReturnToReferrerMixin for bugtask deletion view
886
            self.assertEqual(bug_url, view.cancel_url)
14186.3.2 by Ian Booth
Add tests for delete view
887
888
    def test_delete_action(self):
889
        # Test that the delete action works as expected.
890
        bug = self.factory.makeBug()
891
        bugtask = self.factory.makeBugTask(bug=bug)
14186.3.7 by Ian Booth
Tweak cancel url and add delete icon to series targets
892
        target_name = bugtask.bugtargetdisplayname
14186.3.2 by Ian Booth
Add tests for delete view
893
        with FeatureFixture(DELETE_BUGTASK_ENABLED):
894
            login_person(bugtask.owner)
895
            form = {
896
                'field.actions.delete_bugtask': 'Delete',
897
                }
898
            view = create_initialized_view(
899
                bugtask, name='+delete', form=form, principal=bugtask.owner)
900
            self.assertEqual([bug.default_bugtask], bug.bugtasks)
901
            notifications = view.request.response.notifications
902
            self.assertEqual(1, len(notifications))
14186.3.7 by Ian Booth
Tweak cancel url and add delete icon to series targets
903
            expected = 'This bug no longer affects %s.' % target_name
14186.3.2 by Ian Booth
Add tests for delete view
904
            self.assertEqual(expected, notifications[0].message)
905
14307.1.1 by Ian Booth
Add extra test
906
    def test_delete_only_bugtask(self):
907
        # Test that the deleting the only bugtask results in an error message.
908
        bug = self.factory.makeBug()
909
        with FeatureFixture(DELETE_BUGTASK_ENABLED):
910
            login_person(bug.owner)
911
            form = {
912
                'field.actions.delete_bugtask': 'Delete',
913
                }
914
            view = create_initialized_view(
915
                bug.default_bugtask, name='+delete', form=form,
916
                principal=bug.owner)
917
            self.assertEqual([bug.default_bugtask], bug.bugtasks)
918
            notifications = view.request.response.notifications
919
            self.assertEqual(1, len(notifications))
920
            expected = ('Cannot delete only bugtask affecting: %s.'
921
                % bug.default_bugtask.target.bugtargetdisplayname)
922
            self.assertEqual(expected, notifications[0].message)
923
14186.3.18 by Ian Booth
Lint and code review fixes
924
    def _create_bugtask_to_delete(self):
925
        bug = self.factory.makeBug()
926
        bugtask = self.factory.makeBugTask(bug=bug)
927
        target_name = bugtask.bugtargetdisplayname
928
        bugtask_url = canonical_url(bugtask, rootsite='bugs')
929
        return bug, bugtask, target_name, bugtask_url
930
14186.3.12 by Ian Booth
When deleting the current bugtask, redirect to the default bug task afterwards, plus add tests
931
    def test_ajax_delete_current_bugtask(self):
932
        # Test that deleting the current bugtask returns a JSON dict
933
        # containing the URL of the bug's default task to redirect to.
14186.3.18 by Ian Booth
Lint and code review fixes
934
        bug, bugtask, target_name, bugtask_url = (
935
            self._create_bugtask_to_delete())
14186.3.12 by Ian Booth
When deleting the current bugtask, redirect to the default bug task afterwards, plus add tests
936
        with FeatureFixture(DELETE_BUGTASK_ENABLED):
937
            login_person(bugtask.owner)
938
            # Set up the request so that we correctly simulate an XHR call
939
            # from the URL of the bugtask we are deleting.
940
            server_url = canonical_url(
941
                getUtility(ILaunchpadRoot), rootsite='bugs')
942
            extra = {
943
                'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest',
944
                'HTTP_REFERER': bugtask_url,
945
                }
946
            form = {
947
                'field.actions.delete_bugtask': 'Delete'
948
                }
949
            view = create_initialized_view(
950
                bugtask, name='+delete', server_url=server_url, form=form,
951
                principal=bugtask.owner, **extra)
952
            result_data = simplejson.loads(view.render())
953
            self.assertEqual([bug.default_bugtask], bug.bugtasks)
954
            notifications = simplejson.loads(
955
                view.request.response.getHeader('X-Lazr-Notifications'))
956
            self.assertEqual(1, len(notifications))
957
            expected = 'This bug no longer affects %s.' % target_name
958
            self.assertEqual(expected, notifications[0][1])
959
            self.assertEqual(
14186.3.18 by Ian Booth
Lint and code review fixes
960
                'application/json',
961
                view.request.response.getHeader('content-type'))
14186.3.12 by Ian Booth
When deleting the current bugtask, redirect to the default bug task afterwards, plus add tests
962
            expected_url = canonical_url(bug.default_bugtask, rootsite='bugs')
963
            self.assertEqual(dict(bugtask_url=expected_url), result_data)
964
14307.1.1 by Ian Booth
Add extra test
965
    def test_ajax_delete_only_bugtask(self):
966
        # Test that deleting the only bugtask returns an empty JSON response
967
        # with an error notification.
968
        bug = self.factory.makeBug()
969
        with FeatureFixture(DELETE_BUGTASK_ENABLED):
970
            login_person(bug.owner)
971
            # Set up the request so that we correctly simulate an XHR call
972
            # from the URL of the bugtask we are deleting.
973
            server_url = canonical_url(
974
                getUtility(ILaunchpadRoot), rootsite='bugs')
975
            extra = {
976
                'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest',
977
                }
978
            form = {
979
                'field.actions.delete_bugtask': 'Delete'
980
                }
981
            view = create_initialized_view(
982
                bug.default_bugtask, name='+delete', server_url=server_url,
983
                form=form, principal=bug.owner, **extra)
984
            result_data = simplejson.loads(view.render())
985
            self.assertEqual([bug.default_bugtask], bug.bugtasks)
986
            notifications = simplejson.loads(
987
                view.request.response.getHeader('X-Lazr-Notifications'))
988
            self.assertEqual(1, len(notifications))
989
            expected = ('Cannot delete only bugtask affecting: %s.'
990
                % bug.default_bugtask.target.bugtargetdisplayname)
991
            self.assertEqual(expected, notifications[0][1])
992
            self.assertEqual(
993
                'application/json',
994
                view.request.response.getHeader('content-type'))
995
            self.assertEqual(None, result_data)
996
14186.3.12 by Ian Booth
When deleting the current bugtask, redirect to the default bug task afterwards, plus add tests
997
    def test_ajax_delete_non_current_bugtask(self):
998
        # Test that deleting the non-current bugtask returns the new bugtasks
999
        # table as HTML.
14186.3.18 by Ian Booth
Lint and code review fixes
1000
        bug, bugtask, target_name, bugtask_url = (
1001
            self._create_bugtask_to_delete())
14186.3.12 by Ian Booth
When deleting the current bugtask, redirect to the default bug task afterwards, plus add tests
1002
        default_bugtask_url = canonical_url(
1003
            bug.default_bugtask, rootsite='bugs')
1004
        with FeatureFixture(DELETE_BUGTASK_ENABLED):
1005
            login_person(bugtask.owner)
1006
            # Set up the request so that we correctly simulate an XHR call
1007
            # from the URL of the default bugtask, not the one we are
1008
            # deleting.
1009
            server_url = canonical_url(
1010
                getUtility(ILaunchpadRoot), rootsite='bugs')
1011
            extra = {
1012
                'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest',
1013
                'HTTP_REFERER': default_bugtask_url,
1014
                }
1015
            form = {
1016
                'field.actions.delete_bugtask': 'Delete'
1017
                }
1018
            view = create_initialized_view(
1019
                bugtask, name='+delete', server_url=server_url, form=form,
1020
                principal=bugtask.owner, **extra)
1021
            result_html = view.render()
1022
            self.assertEqual([bug.default_bugtask], bug.bugtasks)
1023
            notifications = view.request.response.notifications
1024
            self.assertEqual(1, len(notifications))
1025
            expected = 'This bug no longer affects %s.' % target_name
1026
            self.assertEqual(expected, notifications[0].message)
1027
            self.assertEqual(
1028
                view.request.response.getHeader('content-type'), 'text/html')
1029
            table = find_tag_by_id(result_html, 'affected-software')
1030
            self.assertIsNotNone(table)
1031
            [row] = table.tbody.findAll('tr', {'class': 'highlight'})
1032
            target_link = row.find('a', {'class': 'sprite product'})
1033
            self.assertIn(
1034
                bug.default_bugtask.bugtargetdisplayname, target_link)
1035
14186.3.2 by Ian Booth
Add tests for delete view
1036
14174.2.10 by Ian Booth
Remove some unnecessary checks and refactor
1037
class TestBugTasksAndNominationsViewAlsoAffects(TestCaseWithFactory):
1038
    """ Tests the boolean methods on the view used to indicate whether the
14174.2.11 by Ian Booth
Fix implementation error for canAffect... methods
1039
        Also Affects... links should be allowed or not. Currently these
1040
        restrictions are only used for private bugs. ie where body.private
1041
        is true.
14174.2.10 by Ian Booth
Remove some unnecessary checks and refactor
1042
1043
        A feature flag is used to turn off the new restrictions. Each test
1044
        is performed with and without the feature flag.
1045
    """
1046
1047
    layer = DatabaseFunctionalLayer
1048
1049
    feature_flag = {'disclosure.allow_multipillar_private_bugs.enabled': 'on'}
1050
1051
    def _createView(self, bug):
1052
        request = LaunchpadTestRequest()
1053
        bugtasks_and_nominations_view = getMultiAdapter(
14186.3.8 by Ian Booth
Add ajax bug task delete functionality
1054
            (bug, request), name="+bugtasks-and-nominations-portal")
14174.2.10 by Ian Booth
Remove some unnecessary checks and refactor
1055
        return bugtasks_and_nominations_view
1056
14174.2.11 by Ian Booth
Fix implementation error for canAffect... methods
1057
    def test_project_bug_cannot_affect_something_else(self):
1058
        # A bug affecting a project cannot also affect another project or
1059
        # package.
14174.2.10 by Ian Booth
Remove some unnecessary checks and refactor
1060
        bug = self.factory.makeBug()
1061
        view = self._createView(bug)
14174.2.11 by Ian Booth
Fix implementation error for canAffect... methods
1062
        self.assertFalse(view.canAddProjectTask())
1063
        self.assertFalse(view.canAddPackageTask())
14174.2.10 by Ian Booth
Remove some unnecessary checks and refactor
1064
        with FeatureFixture(self.feature_flag):
1065
            self.assertTrue(view.canAddProjectTask())
1066
            self.assertTrue(view.canAddPackageTask())
1067
14174.2.11 by Ian Booth
Fix implementation error for canAffect... methods
1068
    def test_distro_bug_cannot_affect_project(self):
1069
        # A bug affecting a distro cannot also affect another project but it
1070
        # could affect another package.
14174.2.10 by Ian Booth
Remove some unnecessary checks and refactor
1071
        distro = self.factory.makeDistribution()
1072
        bug = self.factory.makeBug(distribution=distro)
1073
        view = self._createView(bug)
14174.2.11 by Ian Booth
Fix implementation error for canAffect... methods
1074
        self.assertFalse(view.canAddProjectTask())
1075
        self.assertTrue(view.canAddPackageTask())
1076
        with FeatureFixture(self.feature_flag):
1077
            self.assertTrue(view.canAddProjectTask())
1078
            self.assertTrue(view.canAddPackageTask())
1079
1080
    def test_sourcepkg_bug_cannot_affect_project(self):
1081
        # A bug affecting a source pkg cannot also affect another project but
1082
        # it could affect another package.
14174.2.10 by Ian Booth
Remove some unnecessary checks and refactor
1083
        distro = self.factory.makeDistribution()
1084
        distroseries = self.factory.makeDistroSeries(distribution=distro)
1085
        sp_name = self.factory.getOrMakeSourcePackageName()
1086
        self.factory.makeSourcePackage(
1087
            sourcepackagename=sp_name, distroseries=distroseries)
1088
        bug = self.factory.makeBug(
14174.2.11 by Ian Booth
Fix implementation error for canAffect... methods
1089
            distribution=distro, sourcepackagename=sp_name)
14174.2.10 by Ian Booth
Remove some unnecessary checks and refactor
1090
        view = self._createView(bug)
1091
        self.assertFalse(view.canAddProjectTask())
1092
        self.assertTrue(view.canAddPackageTask())
1093
        with FeatureFixture(self.feature_flag):
1094
            self.assertTrue(view.canAddProjectTask())
1095
            self.assertTrue(view.canAddPackageTask())
1096
1097
10680.2.1 by Abel Deuring
Add a new bug task status 'Expired'; the bug task expiration script sets expired bug tasks to this status; related adjustments to the bug task views
1098
class TestBugTaskEditViewStatusField(TestCaseWithFactory):
1099
    """We show only those options as possible value in the status
1100
    field that the user can select.
1101
    """
1102
12561.3.12 by Curtis Hovey
Fixed broken view while rewriting a broken test.
1103
    layer = DatabaseFunctionalLayer
10680.2.1 by Abel Deuring
Add a new bug task status 'Expired'; the bug task expiration script sets expired bug tasks to this status; related adjustments to the bug task views
1104
1105
    def setUp(self):
1106
        super(TestBugTaskEditViewStatusField, self).setUp()
10680.2.4 by Abel Deuring
replaced the no longer existing method factoy.makPersonNoCommit(9 by makePerson(); ensure that only project owners and supervisors can set a bugtask status to 'expired'
1107
        product_owner = self.factory.makePerson(name='product-owner')
1108
        bug_supervisor = self.factory.makePerson(name='bug-supervisor')
1109
        product = self.factory.makeProduct(
10680.2.2 by Abel Deuring
implemented reviewer's comments
1110
            owner=product_owner, bug_supervisor=bug_supervisor)
10680.2.1 by Abel Deuring
Add a new bug task status 'Expired'; the bug task expiration script sets expired bug tasks to this status; related adjustments to the bug task views
1111
        self.bug = self.factory.makeBug(product=product)
1112
1113
    def getWidgetOptionTitles(self, widget):
1114
        """Return the titles of options of the given choice widget."""
1115
        return [
1116
            item.value.title for item in widget.field.vocabulary]
1117
1118
    def test_status_field_items_for_anonymous(self):
1119
        # Anonymous users see only the current value.
1120
        login(ANONYMOUS)
1121
        view = BugTaskEditView(
1122
            self.bug.default_bugtask, LaunchpadTestRequest())
1123
        view.initialize()
1124
        self.assertEqual(
1125
            ['New'], self.getWidgetOptionTitles(view.form_fields['status']))
1126
1127
    def test_status_field_items_for_ordinary_users(self):
1128
        # Ordinary users can set the status to all values except Won't fix,
1129
        # Expired, Triaged, Unknown.
11474.2.1 by Robert Collins
Use sampledata constants.
1130
        login(NO_PRIVILEGE_EMAIL)
10680.2.1 by Abel Deuring
Add a new bug task status 'Expired'; the bug task expiration script sets expired bug tasks to this status; related adjustments to the bug task views
1131
        view = BugTaskEditView(
1132
            self.bug.default_bugtask, LaunchpadTestRequest())
1133
        view.initialize()
1134
        self.assertEqual(
7675.718.1 by Abel Deuring
add a bug task status OPINION
1135
            ['New', 'Incomplete', 'Opinion', 'Invalid', 'Confirmed',
1136
             'In Progress', 'Fix Committed', 'Fix Released'],
10680.2.1 by Abel Deuring
Add a new bug task status 'Expired'; the bug task expiration script sets expired bug tasks to this status; related adjustments to the bug task views
1137
            self.getWidgetOptionTitles(view.form_fields['status']))
1138
1139
    def test_status_field_privileged_persons(self):
1140
        # The bug target owner and the bug target supervisor can set
1141
        # the status to any value except Unknown and Expired.
1142
        for user in (
1143
            self.bug.default_bugtask.pillar.owner,
1144
            self.bug.default_bugtask.pillar.bug_supervisor):
1145
            login_person(user)
1146
            view = BugTaskEditView(
1147
                self.bug.default_bugtask, LaunchpadTestRequest())
1148
            view.initialize()
1149
            self.assertEqual(
7675.718.1 by Abel Deuring
add a bug task status OPINION
1150
                ['New', 'Incomplete', 'Opinion', 'Invalid', "Won't Fix",
1151
                 'Confirmed', 'Triaged', 'In Progress', 'Fix Committed',
1152
                 'Fix Released'],
10680.2.1 by Abel Deuring
Add a new bug task status 'Expired'; the bug task expiration script sets expired bug tasks to this status; related adjustments to the bug task views
1153
                self.getWidgetOptionTitles(view.form_fields['status']),
1154
                'Unexpected set of settable status options for %s'
1155
                % user.name)
1156
1157
    def test_status_field_bug_task_in_status_unknown(self):
1158
        # If a bugtask has the status Unknown, this status is included
1159
        # in the options.
10680.2.2 by Abel Deuring
implemented reviewer's comments
1160
        owner = self.bug.default_bugtask.pillar.owner
1161
        login_person(owner)
1162
        self.bug.default_bugtask.transitionToStatus(
1163
            BugTaskStatus.UNKNOWN, owner)
11474.2.1 by Robert Collins
Use sampledata constants.
1164
        login(NO_PRIVILEGE_EMAIL)
10680.2.1 by Abel Deuring
Add a new bug task status 'Expired'; the bug task expiration script sets expired bug tasks to this status; related adjustments to the bug task views
1165
        view = BugTaskEditView(
1166
            self.bug.default_bugtask, LaunchpadTestRequest())
1167
        view.initialize()
1168
        self.assertEqual(
7675.718.1 by Abel Deuring
add a bug task status OPINION
1169
            ['New', 'Incomplete', 'Opinion', 'Invalid', 'Confirmed',
1170
             'In Progress', 'Fix Committed', 'Fix Released', 'Unknown'],
10680.2.1 by Abel Deuring
Add a new bug task status 'Expired'; the bug task expiration script sets expired bug tasks to this status; related adjustments to the bug task views
1171
            self.getWidgetOptionTitles(view.form_fields['status']))
1172
1173
    def test_status_field_bug_task_in_status_expired(self):
10680.2.2 by Abel Deuring
implemented reviewer's comments
1174
        # If a bugtask has the status Expired, this status is included
10680.2.1 by Abel Deuring
Add a new bug task status 'Expired'; the bug task expiration script sets expired bug tasks to this status; related adjustments to the bug task views
1175
        # in the options.
13973.2.5 by Brad Crittenden
Version with lots of debugging junk
1176
        removeSecurityProxy(self.bug.default_bugtask)._status = (
10680.2.1 by Abel Deuring
Add a new bug task status 'Expired'; the bug task expiration script sets expired bug tasks to this status; related adjustments to the bug task views
1177
            BugTaskStatus.EXPIRED)
11474.2.1 by Robert Collins
Use sampledata constants.
1178
        login(NO_PRIVILEGE_EMAIL)
10680.2.1 by Abel Deuring
Add a new bug task status 'Expired'; the bug task expiration script sets expired bug tasks to this status; related adjustments to the bug task views
1179
        view = BugTaskEditView(
1180
            self.bug.default_bugtask, LaunchpadTestRequest())
1181
        view.initialize()
1182
        self.assertEqual(
7675.718.1 by Abel Deuring
add a bug task status OPINION
1183
            ['New', 'Incomplete', 'Opinion', 'Invalid', 'Expired',
1184
             'Confirmed', 'In Progress', 'Fix Committed', 'Fix Released'],
10680.2.1 by Abel Deuring
Add a new bug task status 'Expired'; the bug task expiration script sets expired bug tasks to this status; related adjustments to the bug task views
1185
            self.getWidgetOptionTitles(view.form_fields['status']))
1186
1187
10788.5.2 by Abel Deuring
ordinary users can (un)assign a bug task only to hemselves and their teams
1188
class TestBugTaskEditViewAssigneeField(TestCaseWithFactory):
1189
12561.3.12 by Curtis Hovey
Fixed broken view while rewriting a broken test.
1190
    layer = DatabaseFunctionalLayer
10788.5.2 by Abel Deuring
ordinary users can (un)assign a bug task only to hemselves and their teams
1191
1192
    def setUp(self):
1193
        super(TestBugTaskEditViewAssigneeField, self).setUp()
11435.6.11 by Deryck Hodge
Fix browser test.
1194
        self.owner = self.factory.makePerson()
1195
        self.product = self.factory.makeProduct(owner=self.owner)
1196
        self.bugtask = self.factory.makeBug(
1197
            product=self.product).default_bugtask
10788.5.2 by Abel Deuring
ordinary users can (un)assign a bug task only to hemselves and their teams
1198
11435.6.11 by Deryck Hodge
Fix browser test.
1199
    def test_assignee_vocabulary_regular_user_with_bug_supervisor(self):
10788.5.2 by Abel Deuring
ordinary users can (un)assign a bug task only to hemselves and their teams
1200
        # For regular users, the assignee vocabulary is
11435.6.11 by Deryck Hodge
Fix browser test.
1201
        # AllUserTeamsParticipation if there is a bug supervisor defined.
1202
        login_person(self.owner)
1203
        self.product.setBugSupervisor(self.owner, self.owner)
11474.2.1 by Robert Collins
Use sampledata constants.
1204
        login(USER_EMAIL)
10788.5.2 by Abel Deuring
ordinary users can (un)assign a bug task only to hemselves and their teams
1205
        view = BugTaskEditView(self.bugtask, LaunchpadTestRequest())
1206
        view.initialize()
1207
        self.assertEqual(
1208
            'AllUserTeamsParticipation',
1209
            view.form_fields['assignee'].field.vocabularyName)
1210
11435.6.11 by Deryck Hodge
Fix browser test.
1211
    def test_assignee_vocabulary_regular_user_without_bug_supervisor(self):
1212
        # For regular users, the assignee vocabulary is
1213
        # ValidAssignee is there is not a bug supervisor defined.
1214
        login_person(self.owner)
1215
        self.product.setBugSupervisor(None, self.owner)
11474.2.7 by Robert Collins
Resolve conflicts with trunk.
1216
        login(USER_EMAIL)
11435.6.11 by Deryck Hodge
Fix browser test.
1217
        view = BugTaskEditView(self.bugtask, LaunchpadTestRequest())
1218
        view.initialize()
1219
        self.assertEqual(
1220
            'ValidAssignee',
1221
            view.form_fields['assignee'].field.vocabularyName)
1222
10788.5.2 by Abel Deuring
ordinary users can (un)assign a bug task only to hemselves and their teams
1223
    def test_assignee_field_vocabulary_privileged_user(self):
1224
        # Privileged users, like the bug task target owner, can
1225
        # assign anybody.
1226
        login_person(self.bugtask.target.owner)
1227
        view = BugTaskEditView(self.bugtask, LaunchpadTestRequest())
1228
        view.initialize()
1229
        self.assertEqual(
1230
            'ValidAssignee',
1231
            view.form_fields['assignee'].field.vocabularyName)
1232
1233
12561.3.12 by Curtis Hovey
Fixed broken view while rewriting a broken test.
1234
class TestBugTaskEditView(TestCaseWithFactory):
12622.5.1 by Curtis Hovey
Always remove the bugtask milestone when retargeting the product.
1235
    """Test the bug task edit form."""
12561.3.12 by Curtis Hovey
Fixed broken view while rewriting a broken test.
1236
1237
    layer = DatabaseFunctionalLayer
1238
12599.4.2 by Leonard Richardson
Merge from trunk.
1239
    def test_retarget_already_exists_error(self):
12561.3.12 by Curtis Hovey
Fixed broken view while rewriting a broken test.
1240
        user = self.factory.makePerson()
1241
        login_person(user)
1242
        ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
1243
        dsp_1 = self.factory.makeDistributionSourcePackage(
1244
            distribution=ubuntu, sourcepackagename='mouse')
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
1245
        self.factory.makeSourcePackagePublishingHistory(
12561.3.12 by Curtis Hovey
Fixed broken view while rewriting a broken test.
1246
            distroseries=ubuntu.currentseries,
1247
            sourcepackagename=dsp_1.sourcepackagename)
1248
        bug_task_1 = self.factory.makeBugTask(target=dsp_1)
1249
        dsp_2 = self.factory.makeDistributionSourcePackage(
1250
            distribution=ubuntu, sourcepackagename='rabbit')
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
1251
        self.factory.makeSourcePackagePublishingHistory(
12561.3.12 by Curtis Hovey
Fixed broken view while rewriting a broken test.
1252
            distroseries=ubuntu.currentseries,
1253
            sourcepackagename=dsp_2.sourcepackagename)
1254
        bug_task_2 = self.factory.makeBugTask(
1255
            bug=bug_task_1.bug, target=dsp_2)
1256
        form = {
1257
            'ubuntu_rabbit.actions.save': 'Save Changes',
1258
            'ubuntu_rabbit.status': 'In Progress',
1259
            'ubuntu_rabbit.importance': 'High',
1260
            'ubuntu_rabbit.assignee.option':
1261
                'ubuntu_rabbit.assignee.assign_to_nobody',
13494.2.13 by William Grant
Fix more test_bugtask.
1262
            'ubuntu_rabbit.target': 'package',
1263
            'ubuntu_rabbit.target.distribution': 'ubuntu',
1264
            'ubuntu_rabbit.target.package': 'mouse',
12561.3.12 by Curtis Hovey
Fixed broken view while rewriting a broken test.
1265
            }
1266
        view = create_initialized_view(
12599.4.2 by Leonard Richardson
Merge from trunk.
1267
            bug_task_2, name='+editstatus', form=form, principal=user)
12561.3.12 by Curtis Hovey
Fixed broken view while rewriting a broken test.
1268
        self.assertEqual(1, len(view.errors))
1269
        self.assertEqual(
13506.4.17 by William Grant
Fix test.
1270
            'A fix for this bug has already been requested for mouse in '
1271
            'Ubuntu',
12561.3.12 by Curtis Hovey
Fixed broken view while rewriting a broken test.
1272
            view.errors[0])
1273
12622.5.2 by Curtis Hovey
Added a test to verify that assiging a milestone during a retargeting is ignored.
1274
    def setUpRetargetMilestone(self):
1275
        """Setup a bugtask with a milestone and a product to retarget to."""
12622.5.1 by Curtis Hovey
Always remove the bugtask milestone when retargeting the product.
1276
        first_product = self.factory.makeProduct(name='bunny')
1277
        with person_logged_in(first_product.owner):
1278
            first_product.official_malone = True
1279
            bug = self.factory.makeBug(product=first_product)
1280
            bug_task = bug.bugtasks[0]
1281
            milestone = self.factory.makeMilestone(
1282
                productseries=first_product.development_focus, name='1.0')
1283
            bug_task.transitionToMilestone(milestone, first_product.owner)
1284
        second_product = self.factory.makeProduct(name='duck')
1285
        with person_logged_in(second_product.owner):
1286
            second_product.official_malone = True
12622.5.2 by Curtis Hovey
Added a test to verify that assiging a milestone during a retargeting is ignored.
1287
        return bug_task, second_product
1288
1289
    def test_retarget_product_with_milestone(self):
1290
        # Milestones are always cleared when retargeting a product bug task.
1291
        bug_task, second_product = self.setUpRetargetMilestone()
12622.5.1 by Curtis Hovey
Always remove the bugtask milestone when retargeting the product.
1292
        user = self.factory.makePerson()
1293
        login_person(user)
1294
        form = {
1295
            'bunny.status': 'In Progress',
1296
            'bunny.assignee.option': 'bunny.assignee.assign_to_nobody',
13494.2.13 by William Grant
Fix more test_bugtask.
1297
            'bunny.target': 'product',
1298
            'bunny.target.product': 'duck',
12622.5.1 by Curtis Hovey
Always remove the bugtask milestone when retargeting the product.
1299
            'bunny.actions.save': 'Save Changes',
1300
            }
1301
        view = create_initialized_view(
1302
            bug_task, name='+editstatus', form=form)
1303
        self.assertEqual([], view.errors)
1304
        self.assertEqual(second_product, bug_task.target)
1305
        self.assertEqual(None, bug_task.milestone)
12622.5.2 by Curtis Hovey
Added a test to verify that assiging a milestone during a retargeting is ignored.
1306
        notifications = view.request.response.notifications
1307
        self.assertEqual(1, len(notifications))
1308
        expected = ('The Bunny 1.0 milestone setting has been removed')
1309
        self.assertTrue(notifications.pop().message.startswith(expected))
1310
1311
    def test_retarget_product_and_assign_milestone(self):
1312
        # Milestones are always cleared when retargeting a product bug task.
1313
        bug_task, second_product = self.setUpRetargetMilestone()
1314
        login_person(bug_task.target.owner)
1315
        milestone_id = bug_task.milestone.id
1316
        bug_task.transitionToMilestone(None, bug_task.target.owner)
1317
        form = {
1318
            'bunny.status': 'In Progress',
1319
            'bunny.assignee.option': 'bunny.assignee.assign_to_nobody',
13494.2.13 by William Grant
Fix more test_bugtask.
1320
            'bunny.target': 'product',
1321
            'bunny.target.product': 'duck',
12622.5.2 by Curtis Hovey
Added a test to verify that assiging a milestone during a retargeting is ignored.
1322
            'bunny.milestone': milestone_id,
1323
            'bunny.actions.save': 'Save Changes',
1324
            }
1325
        view = create_initialized_view(
1326
            bug_task, name='+editstatus', form=form)
1327
        self.assertEqual([], view.errors)
1328
        self.assertEqual(second_product, bug_task.target)
1329
        self.assertEqual(None, bug_task.milestone)
1330
        notifications = view.request.response.notifications
1331
        self.assertEqual(1, len(notifications))
1332
        expected = ('The milestone setting was ignored')
1333
        self.assertTrue(notifications.pop().message.startswith(expected))
12622.5.1 by Curtis Hovey
Always remove the bugtask milestone when retargeting the product.
1334
13494.2.29 by William Grant
Fix helper.
1335
    def createNameChangingViewForSourcePackageTask(self, bug_task, new_name):
13494.2.28 by William Grant
Test it a bit.
1336
        login_person(bug_task.owner)
1337
        form_prefix = '%s_%s_%s' % (
1338
            bug_task.target.distroseries.distribution.name,
1339
            bug_task.target.distroseries.name,
1340
            bug_task.target.sourcepackagename.name)
1341
        form = {
1342
            form_prefix + '.sourcepackagename': new_name,
1343
            form_prefix + '.actions.save': 'Save Changes',
1344
            }
1345
        view = create_initialized_view(
1346
            bug_task, name='+editstatus', form=form)
1347
        return view
1348
1349
    def test_retarget_sourcepackage(self):
1350
        # The sourcepackagename of a SourcePackage task can be changed.
1351
        ds = self.factory.makeDistroSeries()
1352
        sp1 = self.factory.makeSourcePackage(distroseries=ds, publish=True)
1353
        sp2 = self.factory.makeSourcePackage(distroseries=ds, publish=True)
1354
        bug_task = self.factory.makeBugTask(target=sp1)
1355
1356
        view = self.createNameChangingViewForSourcePackageTask(
1357
            bug_task, sp2.sourcepackagename.name)
1358
        self.assertEqual([], view.errors)
1359
        self.assertEqual(sp2, bug_task.target)
1360
        notifications = view.request.response.notifications
1361
        self.assertEqual(0, len(notifications))
1362
1363
    def test_retarget_sourcepackage_to_binary_name(self):
1364
        # The sourcepackagename of a SourcePackage task can be changed
1365
        # to a binarypackagename, which gets mapped back to the source.
1366
        ds = self.factory.makeDistroSeries()
1367
        das = self.factory.makeDistroArchSeries(distroseries=ds)
1368
        sp1 = self.factory.makeSourcePackage(distroseries=ds, publish=True)
1369
        # Now create a binary and its corresponding SourcePackage.
1370
        bp = self.factory.makeBinaryPackagePublishingHistory(
1371
            distroarchseries=das)
1372
        bpr = bp.binarypackagerelease
1373
        spn = bpr.build.source_package_release.sourcepackagename
1374
        sp2 = self.factory.makeSourcePackage(
1375
            distroseries=ds, sourcepackagename=spn, publish=True)
1376
        bug_task = self.factory.makeBugTask(target=sp1)
1377
1378
        view = self.createNameChangingViewForSourcePackageTask(
1379
            bug_task, bpr.binarypackagename.name)
1380
        self.assertEqual([], view.errors)
1381
        self.assertEqual(sp2, bug_task.target)
1382
        notifications = view.request.response.notifications
1383
        self.assertEqual(1, len(notifications))
1384
        expected = (
1385
            "'%s' is a binary package. This bug has been assigned to its "
1386
            "source package '%s' instead."
1387
            % (bpr.binarypackagename.name, spn.name))
1388
        self.assertTrue(notifications.pop().message.startswith(expected))
1389
1390
    def test_retarget_sourcepackage_to_distroseries(self):
1391
        # A SourcePackage task can be changed to a DistroSeries one.
1392
        ds = self.factory.makeDistroSeries()
1393
        sp = self.factory.makeSourcePackage(distroseries=ds, publish=True)
1394
        bug_task = self.factory.makeBugTask(target=sp)
1395
1396
        view = self.createNameChangingViewForSourcePackageTask(
1397
            bug_task, '')
1398
        self.assertEqual([], view.errors)
1399
        self.assertEqual(ds, bug_task.target)
1400
        notifications = view.request.response.notifications
1401
        self.assertEqual(0, len(notifications))
1402
14086.2.1 by Ian Booth
Handle case where private bugtask retargetted to new pillar becomes invisible
1403
    def test_retarget_private_bug(self):
1404
        # If a private bug is re-targetted such that the bug is no longer
1405
        # visible to the user, they are redirected to the pillar's bug index
1406
        # page with a suitable message. This corner case can occur when the
1407
        # disclosure.private_bug_visibility_rules.enabled feature flag is on
1408
        # and a bugtask is re-targetted to a pillar for which the user is not
1409
        # authorised to see any private bugs.
1410
        first_product = self.factory.makeProduct(name='bunny')
1411
        with person_logged_in(first_product.owner):
1412
            bug = self.factory.makeBug(product=first_product, private=True)
1413
            bug_task = bug.bugtasks[0]
1414
        second_product = self.factory.makeProduct(name='duck')
1415
1416
        # The first product owner can see the private bug. We will re-target
1417
        # it to second_product where it will not be visible to that user.
1418
        with person_logged_in(first_product.owner):
1419
            form = {
1420
                'bunny.target': 'product',
1421
                'bunny.target.product': 'duck',
1422
                'bunny.actions.save': 'Save Changes',
1423
                }
14205.1.5 by William Grant
Fix more tests to not rely on pillar owners.
1424
            with FeatureFixture({
1425
                'disclosure.private_bug_visibility_rules.enabled': 'on'}):
1426
                view = create_initialized_view(
1427
                    bug_task, name='+editstatus', form=form)
14086.2.1 by Ian Booth
Handle case where private bugtask retargetted to new pillar becomes invisible
1428
            self.assertEqual(
1429
                canonical_url(bug_task.pillar, rootsite='bugs'),
1430
                view.next_url)
1431
        self.assertEqual([], view.errors)
1432
        self.assertEqual(second_product, bug_task.target)
1433
        notifications = view.request.response.notifications
1434
        self.assertEqual(1, len(notifications))
1435
        expected = ('The bug you have just updated is now a private bug for')
1436
        self.assertTrue(notifications.pop().message.startswith(expected))
1437
1438
14423.2.7 by Curtis Hovey
Added missing test for persons.
1439
class TestPersonBugs(TestCaseWithFactory):
1440
    """Test the bugs overview page for distributions."""
1441
1442
    layer = DatabaseFunctionalLayer
1443
1444
    def setUp(self):
14423.2.8 by Curtis Hovey
Show the widget when there are subordinate structures.
1445
        super(TestPersonBugs, self).setUp()
14423.2.7 by Curtis Hovey
Added missing test for persons.
1446
        self.target = self.factory.makePerson()
1447
1448
    def test_shouldShowStructuralSubscriberWidget(self):
1449
        view = create_initialized_view(
1450
            self.target, name=u'+bugs', rootsite='bugs')
14423.2.8 by Curtis Hovey
Show the widget when there are subordinate structures.
1451
        self.assertTrue(view.shouldShowStructuralSubscriberWidget())
14423.2.7 by Curtis Hovey
Added missing test for persons.
1452
14423.2.9 by Curtis Hovey
Provide informative labels for the bugtarget.
1453
    def test_structural_subscriber_label(self):
1454
        view = create_initialized_view(
1455
            self.target, name=u'+bugs', rootsite='bugs')
1456
        self.assertEqual(
1457
            'Project, distribution, package, or series subscriber',
1458
            view.structural_subscriber_label)
1459
14423.2.7 by Curtis Hovey
Added missing test for persons.
1460
14423.2.6 by Curtis Hovey
Added tests for the current behaviour.
1461
class TestDistributionBugs(TestCaseWithFactory):
1462
    """Test the bugs overview page for distributions."""
1463
1464
    layer = DatabaseFunctionalLayer
1465
1466
    def setUp(self):
1467
        super(TestDistributionBugs, self).setUp()
1468
        self.target = self.factory.makeDistribution()
1469
1470
    def test_shouldShowStructuralSubscriberWidget(self):
1471
        view = create_initialized_view(
1472
            self.target, name=u'+bugs', rootsite='bugs')
14423.2.8 by Curtis Hovey
Show the widget when there are subordinate structures.
1473
        self.assertTrue(view.shouldShowStructuralSubscriberWidget())
14423.2.6 by Curtis Hovey
Added tests for the current behaviour.
1474
14423.2.9 by Curtis Hovey
Provide informative labels for the bugtarget.
1475
    def test_structural_subscriber_label(self):
1476
        view = create_initialized_view(
1477
            self.target, name=u'+bugs', rootsite='bugs')
1478
        self.assertEqual(
1479
            'Package, or series subscriber', view.structural_subscriber_label)
1480
14423.2.6 by Curtis Hovey
Added tests for the current behaviour.
1481
1482
class TestDistroSeriesBugs(TestCaseWithFactory):
1483
    """Test the bugs overview page for distro series."""
1484
1485
    layer = DatabaseFunctionalLayer
1486
1487
    def setUp(self):
1488
        super(TestDistroSeriesBugs, self).setUp()
1489
        self.target = self.factory.makeDistroSeries()
1490
1491
    def test_shouldShowStructuralSubscriberWidget(self):
1492
        view = create_initialized_view(
1493
            self.target, name=u'+bugs', rootsite='bugs')
14423.2.8 by Curtis Hovey
Show the widget when there are subordinate structures.
1494
        self.assertTrue(view.shouldShowStructuralSubscriberWidget())
14423.2.6 by Curtis Hovey
Added tests for the current behaviour.
1495
14423.2.9 by Curtis Hovey
Provide informative labels for the bugtarget.
1496
    def test_structural_subscriber_label(self):
1497
        view = create_initialized_view(
1498
            self.target, name=u'+bugs', rootsite='bugs')
1499
        self.assertEqual(
1500
            'Package subscriber', view.structural_subscriber_label)
1501
14423.2.6 by Curtis Hovey
Added tests for the current behaviour.
1502
1503
class TestDistributionSourcePackageBugs(TestCaseWithFactory):
1504
    """Test the bugs overview page for distribution source packages."""
1505
1506
    layer = DatabaseFunctionalLayer
1507
1508
    def setUp(self):
1509
        super(TestDistributionSourcePackageBugs, self).setUp()
1510
        self.target = self.factory.makeDistributionSourcePackage()
1511
1512
    def test_shouldShowStructuralSubscriberWidget(self):
1513
        view = create_initialized_view(
1514
            self.target, name=u'+bugs', rootsite='bugs')
14423.2.8 by Curtis Hovey
Show the widget when there are subordinate structures.
1515
        self.assertFalse(view.shouldShowStructuralSubscriberWidget())
14423.2.6 by Curtis Hovey
Added tests for the current behaviour.
1516
1517
1518
class TestDistroSeriesSourcePackageBugs(TestCaseWithFactory):
1519
    """Test the bugs overview page for distro series source packages."""
1520
1521
    layer = DatabaseFunctionalLayer
1522
1523
    def setUp(self):
1524
        super(TestDistroSeriesSourcePackageBugs, self).setUp()
1525
        self.target = self.factory.makeSourcePackage()
1526
1527
    def test_shouldShowStructuralSubscriberWidget(self):
1528
        view = create_initialized_view(
1529
            self.target, name=u'+bugs', rootsite='bugs')
14423.2.8 by Curtis Hovey
Show the widget when there are subordinate structures.
1530
        self.assertFalse(view.shouldShowStructuralSubscriberWidget())
14423.2.6 by Curtis Hovey
Added tests for the current behaviour.
1531
1532
1533
class TestProductBugs(TestCaseWithFactory):
1534
    """Test the bugs overview page for projects."""
1535
1536
    layer = DatabaseFunctionalLayer
1537
1538
    def setUp(self):
1539
        super(TestProductBugs, self).setUp()
1540
        self.target = self.factory.makeProduct()
1541
1542
    def test_shouldShowStructuralSubscriberWidget(self):
1543
        view = create_initialized_view(
1544
            self.target, name=u'+bugs', rootsite='bugs')
14423.2.8 by Curtis Hovey
Show the widget when there are subordinate structures.
1545
        self.assertTrue(view.shouldShowStructuralSubscriberWidget())
14423.2.6 by Curtis Hovey
Added tests for the current behaviour.
1546
14423.2.9 by Curtis Hovey
Provide informative labels for the bugtarget.
1547
    def test_structural_subscriber_label(self):
1548
        view = create_initialized_view(
1549
            self.target, name=u'+bugs', rootsite='bugs')
1550
        self.assertEqual(
1551
            'Series subscriber', view.structural_subscriber_label)
1552
14423.2.6 by Curtis Hovey
Added tests for the current behaviour.
1553
1554
class TestProductSeriesBugs(TestCaseWithFactory):
1555
    """Test the bugs overview page for project series."""
1556
1557
    layer = DatabaseFunctionalLayer
1558
1559
    def setUp(self):
1560
        super(TestProductSeriesBugs, self).setUp()
1561
        self.target = self.factory.makeProductSeries()
1562
1563
    def test_shouldShowStructuralSubscriberWidget(self):
1564
        view = create_initialized_view(
1565
            self.target, name=u'+bugs', rootsite='bugs')
14423.2.8 by Curtis Hovey
Show the widget when there are subordinate structures.
1566
        self.assertFalse(view.shouldShowStructuralSubscriberWidget())
14423.2.6 by Curtis Hovey
Added tests for the current behaviour.
1567
1568
11655.1.1 by Brad Crittenden
Do not show bug information on a project group page with no projects that use Launchpad.
1569
class TestProjectGroupBugs(TestCaseWithFactory):
14423.2.6 by Curtis Hovey
Added tests for the current behaviour.
1570
    """Test the bugs overview page for project groups."""
11655.1.1 by Brad Crittenden
Do not show bug information on a project group page with no projects that use Launchpad.
1571
12561.3.12 by Curtis Hovey
Fixed broken view while rewriting a broken test.
1572
    layer = DatabaseFunctionalLayer
11655.1.1 by Brad Crittenden
Do not show bug information on a project group page with no projects that use Launchpad.
1573
1574
    def setUp(self):
1575
        super(TestProjectGroupBugs, self).setUp()
1576
        self.owner = self.factory.makePerson(name='bob')
1577
        self.projectgroup = self.factory.makeProject(name='container',
1578
                                                     owner=self.owner)
1579
1580
    def makeSubordinateProduct(self, tracks_bugs_in_lp):
1581
        """Create a new product and add it to the project group."""
1582
        product = self.factory.makeProduct(official_malone=tracks_bugs_in_lp)
1583
        with person_logged_in(product.owner):
1584
            product.project = self.projectgroup
1585
1586
    def test_empty_project_group(self):
1587
        # An empty project group does not use Launchpad for bugs.
1588
        view = create_initialized_view(
1589
            self.projectgroup, name=u'+bugs', rootsite='bugs')
1590
        self.assertFalse(self.projectgroup.hasProducts())
1591
        self.assertFalse(view.should_show_bug_information)
1592
1593
    def test_project_group_with_subordinate_not_using_launchpad(self):
1594
        # A project group with all subordinates not using Launchpad
1595
        # will itself be marked as not using Launchpad for bugs.
1596
        self.makeSubordinateProduct(False)
1597
        self.assertTrue(self.projectgroup.hasProducts())
1598
        view = create_initialized_view(
1599
            self.projectgroup, name=u'+bugs', rootsite='bugs')
1600
        self.assertFalse(view.should_show_bug_information)
1601
1602
    def test_project_group_with_subordinate_using_launchpad(self):
1603
        # A project group with one subordinate using Launchpad
1604
        # will itself be marked as using Launchpad for bugs.
1605
        self.makeSubordinateProduct(True)
1606
        self.assertTrue(self.projectgroup.hasProducts())
1607
        view = create_initialized_view(
1608
            self.projectgroup, name=u'+bugs', rootsite='bugs')
1609
        self.assertTrue(view.should_show_bug_information)
1610
1611
    def test_project_group_with_mixed_subordinates(self):
1612
        # A project group with one or more subordinates using Launchpad
1613
        # will itself be marked as using Launchpad for bugs.
1614
        self.makeSubordinateProduct(False)
1615
        self.makeSubordinateProduct(True)
1616
        self.assertTrue(self.projectgroup.hasProducts())
1617
        view = create_initialized_view(
1618
            self.projectgroup, name=u'+bugs', rootsite='bugs')
1619
        self.assertTrue(view.should_show_bug_information)
1620
1621
    def test_project_group_has_no_portlets_if_not_using_LP(self):
11655.1.5 by Brad Crittenden
Post review fixes
1622
        # A project group that has no projects using Launchpad will not have
1623
        # bug portlets.
11655.1.1 by Brad Crittenden
Do not show bug information on a project group page with no projects that use Launchpad.
1624
        self.makeSubordinateProduct(False)
1625
        view = create_initialized_view(
1626
            self.projectgroup, name=u'+bugs', rootsite='bugs',
1627
            current_request=True)
1628
        self.assertFalse(view.should_show_bug_information)
1629
        contents = view.render()
1630
        report_a_bug = find_tag_by_id(contents, 'bug-portlets')
1631
        self.assertIs(None, report_a_bug)
1632
1633
    def test_project_group_has_portlets_link_if_using_LP(self):
11655.1.5 by Brad Crittenden
Post review fixes
1634
        # A project group that has projects using Launchpad will have a
1635
        # portlets.
11655.1.1 by Brad Crittenden
Do not show bug information on a project group page with no projects that use Launchpad.
1636
        self.makeSubordinateProduct(True)
1637
        view = create_initialized_view(
1638
            self.projectgroup, name=u'+bugs', rootsite='bugs',
1639
            current_request=True)
1640
        self.assertTrue(view.should_show_bug_information)
1641
        contents = view.render()
1642
        report_a_bug = find_tag_by_id(contents, 'bug-portlets')
1643
        self.assertIsNot(None, report_a_bug)
1644
11655.1.9 by Brad Crittenden
Add help link for configuring bugs
1645
    def test_project_group_has_help_link_if_not_using_LP(self):
1646
        # A project group that has no projects using Launchpad will have
1647
        # a 'Getting started' help link.
1648
        self.makeSubordinateProduct(False)
1649
        view = create_initialized_view(
1650
            self.projectgroup, name=u'+bugs', rootsite='bugs',
1651
            current_request=True)
1652
        contents = view.render()
1653
        help_link = find_tag_by_id(contents, 'getting-started-help')
1654
        self.assertIsNot(None, help_link)
1655
1656
    def test_project_group_has_no_help_link_if_using_LP(self):
1657
        # A project group that has no projects using Launchpad will not have
1658
        # a 'Getting started' help link.
1659
        self.makeSubordinateProduct(True)
1660
        view = create_initialized_view(
1661
            self.projectgroup, name=u'+bugs', rootsite='bugs',
1662
            current_request=True)
1663
        contents = view.render()
1664
        help_link = find_tag_by_id(contents, 'getting-started-help')
1665
        self.assertIs(None, help_link)
12792.8.1 by William Grant
Add failing test for BugActivityItem assignee escaping.
1666
14423.2.6 by Curtis Hovey
Added tests for the current behaviour.
1667
    def test_shouldShowStructuralSubscriberWidget(self):
1668
        view = create_initialized_view(
1669
            self.projectgroup, name=u'+bugs', rootsite='bugs')
14423.2.8 by Curtis Hovey
Show the widget when there are subordinate structures.
1670
        self.assertTrue(view.shouldShowStructuralSubscriberWidget())
14423.2.6 by Curtis Hovey
Added tests for the current behaviour.
1671
14423.2.9 by Curtis Hovey
Provide informative labels for the bugtarget.
1672
    def test_structural_subscriber_label(self):
1673
        view = create_initialized_view(
1674
            self.projectgroup, name=u'+bugs', rootsite='bugs')
1675
        self.assertEqual(
1676
            'Project or series subscriber', view.structural_subscriber_label)
1677
12792.8.1 by William Grant
Add failing test for BugActivityItem assignee escaping.
1678
1679
class TestBugActivityItem(TestCaseWithFactory):
1680
1681
    layer = DatabaseFunctionalLayer
1682
1683
    def setAttribute(self, obj, attribute, value):
1684
        obj_before_modification = Snapshot(obj, providing=providedBy(obj))
1685
        setattr(removeSecurityProxy(obj), attribute, value)
1686
        notify(ObjectModifiedEvent(
1687
            obj, obj_before_modification, [attribute],
1688
            self.factory.makePerson()))
1689
1690
    def test_escapes_assignee(self):
1691
        with celebrity_logged_in('admin'):
1692
            task = self.factory.makeBugTask()
1693
            self.setAttribute(
1694
                task, 'assignee',
1695
                self.factory.makePerson(displayname="Foo &<>", name='foo'))
1696
        self.assertEquals(
1697
            "nobody &#8594; Foo &amp;&lt;&gt; (foo)",
1698
            BugActivityItem(task.bug.activity[-1]).change_details)
1699
1700
    def test_escapes_title(self):
1701
        with celebrity_logged_in('admin'):
1702
            bug = self.factory.makeBug(title="foo")
1703
            self.setAttribute(bug, 'title', "bar &<>")
1704
        self.assertEquals(
1705
            "- foo<br />+ bar &amp;&lt;&gt;",
1706
            BugActivityItem(bug.activity[-1]).change_details)
13752.3.4 by Graham Binns
Added some basic tests.
1707
1708
1709
class TestBugTaskBatchedCommentsAndActivityView(TestCaseWithFactory):
1710
    """Tests for the BugTaskBatchedCommentsAndActivityView class."""
1711
1712
    layer = LaunchpadFunctionalLayer
1713
13955.1.3 by Graham Binns
Yay, fixed some test failures.
1714
    def _makeNoisyBug(self, comments_only=False, number_of_comments=10,
1715
                      number_of_changes=10):
13752.3.4 by Graham Binns
Added some basic tests.
1716
        """Create and return a bug with a lot of comments and activity."""
13955.1.7 by Graham Binns
Review changes for Jeroen.
1717
        bug = self.factory.makeBug()
13752.3.5 by Graham Binns
Somehow, I made that test pass. Wow.
1718
        with person_logged_in(bug.owner):
1719
            if not comments_only:
13955.1.3 by Graham Binns
Yay, fixed some test failures.
1720
                for i in range(number_of_changes):
13752.3.5 by Graham Binns
Somehow, I made that test pass. Wow.
1721
                    change = BugTaskStatusChange(
13955.1.7 by Graham Binns
Review changes for Jeroen.
1722
                        bug.default_bugtask, UTC_NOW,
13955.1.3 by Graham Binns
Yay, fixed some test failures.
1723
                        bug.default_bugtask.product.owner, 'status',
13752.3.5 by Graham Binns
Somehow, I made that test pass. Wow.
1724
                        BugTaskStatus.NEW, BugTaskStatus.TRIAGED)
1725
                    bug.addChange(change)
14027.3.1 by Jeroen Vermeulen
Fix lots of lint in recently-changed files.
1726
            for i in range(number_of_comments):
13752.3.5 by Graham Binns
Somehow, I made that test pass. Wow.
1727
                msg = self.factory.makeMessage(
13955.1.7 by Graham Binns
Review changes for Jeroen.
1728
                    owner=bug.owner, content="Message %i." % i)
13752.3.5 by Graham Binns
Somehow, I made that test pass. Wow.
1729
                bug.linkMessage(msg, user=bug.owner)
13752.3.4 by Graham Binns
Added some basic tests.
1730
        return bug
1731
13955.1.7 by Graham Binns
Review changes for Jeroen.
1732
    def _assertThatUnbatchedAndBatchedActivityMatch(self, unbatched_activity,
1733
                                                    batched_activity):
1734
        zipped_activity = zip(
1735
            unbatched_activity, batched_activity)
1736
        for index, items in enumerate(zipped_activity):
1737
            unbatched_item, batched_item = items
1738
            self.assertEqual(
1739
                unbatched_item['comment'].index,
1740
                batched_item['comment'].index,
1741
                "The comments at index %i don't match. Expected to see "
1742
                "comment %i, got comment %i instead." %
1743
                (index, unbatched_item['comment'].index,
1744
                batched_item['comment'].index))
1745
13752.3.4 by Graham Binns
Added some basic tests.
1746
    def test_offset(self):
1747
        # BugTaskBatchedCommentsAndActivityView.offset returns the
1748
        # current offset being used to select a batch of bug comments
13955.1.2 by Graham Binns
Fixed the remaining test failures. Stick a knife in it, it's done (except for the icing of the review).
1749
        # and activity. If one is not specified, the offset will be the
1750
        # view's visible_initial_comments count + 1 (so that comments
1751
        # already shown on the page won't appear twice).
13752.3.4 by Graham Binns
Added some basic tests.
1752
        bug_task = self.factory.makeBugTask()
1753
        view = create_initialized_view(bug_task, '+batched-comments')
14027.3.1 by Jeroen Vermeulen
Fix lots of lint in recently-changed files.
1754
        self.assertEqual(view.visible_initial_comments + 1, view.offset)
13752.3.4 by Graham Binns
Added some basic tests.
1755
        view = create_initialized_view(
1756
            bug_task, '+batched-comments', form={'offset': 100})
1757
        self.assertEqual(100, view.offset)
1758
1759
    def test_batch_size(self):
1760
        # BugTaskBatchedCommentsAndActivityView.batch_size returns the
1761
        # current batch_size being used to select a batch of bug comments
13752.3.9 by Graham Binns
The default batch size is now a config option.
1762
        # and activity or the default configured batch size if one has
1763
        # not been specified.
13752.3.4 by Graham Binns
Added some basic tests.
1764
        bug_task = self.factory.makeBugTask()
1765
        view = create_initialized_view(bug_task, '+batched-comments')
13752.3.9 by Graham Binns
The default batch size is now a config option.
1766
        self.assertEqual(
1767
            config.malone.comments_list_default_batch_size,
1768
            view.batch_size)
13752.3.4 by Graham Binns
Added some basic tests.
1769
        view = create_initialized_view(
13752.3.5 by Graham Binns
Somehow, I made that test pass. Wow.
1770
            bug_task, '+batched-comments', form={'batch_size': 20})
1771
        self.assertEqual(20, view.batch_size)
1772
1773
    def test_event_groups_only_returns_batch_size_results(self):
1774
        # BugTaskBatchedCommentsAndActivityView._event_groups will
1775
        # return only batch_size results.
13955.1.3 by Graham Binns
Yay, fixed some test failures.
1776
        bug = self._makeNoisyBug(number_of_comments=20)
1777
        view = create_initialized_view(
1778
            bug.default_bugtask, '+batched-comments',
1779
            form={'batch_size': 10, 'offset': 1})
1780
        self.assertEqual(10, len([group for group in view._event_groups]))
1781
1782
    def test_event_groups_excludes_visible_recent_comments(self):
1783
        # BugTaskBatchedCommentsAndActivityView._event_groups will
1784
        # not return the last view comments - those covered by the
1785
        # visible_recent_comments property.
13955.1.4 by Graham Binns
The tests... pass. I don't know what to do now.
1786
        bug = self._makeNoisyBug(number_of_comments=20, comments_only=True)
13955.1.7 by Graham Binns
Review changes for Jeroen.
1787
        batched_view = create_initialized_view(
13752.3.5 by Graham Binns
Somehow, I made that test pass. Wow.
1788
            bug.default_bugtask, '+batched-comments',
13955.1.4 by Graham Binns
The tests... pass. I don't know what to do now.
1789
            form={'batch_size': 10, 'offset': 10})
13955.1.7 by Graham Binns
Review changes for Jeroen.
1790
        expected_length = 10 - batched_view.visible_recent_comments
1791
        actual_length = len([group for group in batched_view._event_groups])
13955.1.3 by Graham Binns
Yay, fixed some test failures.
1792
        self.assertEqual(
13955.1.4 by Graham Binns
The tests... pass. I don't know what to do now.
1793
            expected_length, actual_length,
1794
            "Expected %i comments, got %i." %
1795
            (expected_length, actual_length))
13955.1.7 by Graham Binns
Review changes for Jeroen.
1796
        unbatched_view = create_initialized_view(
1797
            bug.default_bugtask, '+index', form={'comments': 'all'})
1798
        self._assertThatUnbatchedAndBatchedActivityMatch(
1799
            unbatched_view.activity_and_comments[9:],
1800
            batched_view.activity_and_comments)
13752.3.5 by Graham Binns
Somehow, I made that test pass. Wow.
1801
1802
    def test_activity_and_comments_matches_unbatched_version(self):
1803
        # BugTaskBatchedCommentsAndActivityView extends BugTaskView in
1804
        # order to add the batching logic and reduce rendering
1805
        # overheads. The results of activity_and_comments is the same
1806
        # for both.
1807
        # We create a bug with comments only so that we can test the
1808
        # contents of activity_and_comments properly. Trying to test it
1809
        # with multiply different datatypes is fragile at best.
13955.1.3 by Graham Binns
Yay, fixed some test failures.
1810
        bug = self._makeNoisyBug(comments_only=True, number_of_comments=20)
13752.3.8 by Graham Binns
Fixed up test failures.
1811
        # We create a batched view with an offset of 0 so that all the
1812
        # comments are returned.
13752.3.5 by Graham Binns
Somehow, I made that test pass. Wow.
1813
        batched_view = create_initialized_view(
13752.3.8 by Graham Binns
Fixed up test failures.
1814
            bug.default_bugtask, '+batched-comments',
13955.1.3 by Graham Binns
Yay, fixed some test failures.
1815
            {'offset': 5, 'batch_size': 10})
13752.3.5 by Graham Binns
Somehow, I made that test pass. Wow.
1816
        unbatched_view = create_initialized_view(
13955.1.3 by Graham Binns
Yay, fixed some test failures.
1817
            bug.default_bugtask, '+index', form={'comments': 'all'})
1818
        # It may look slightly confusing, but it's because the unbatched
1819
        # view's activity_and_comments list is indexed from comment 1,
1820
        # whereas the batched view indexes from zero for ease-of-coding.
1821
        # Comment 0 is the original bug description and so is rarely
1822
        # returned.
13955.1.7 by Graham Binns
Review changes for Jeroen.
1823
        self._assertThatUnbatchedAndBatchedActivityMatch(
1824
            unbatched_view.activity_and_comments[4:],
1825
            batched_view.activity_and_comments)
14128.3.19 by Aaron Bentley
Test BugTaskListingItem.model
1826
1827
14128.3.20 by Aaron Bentley
Ensure the json cache is populated appropriately.
1828
def make_bug_task_listing_item(factory):
1829
    owner = factory.makePerson()
1830
    bug = factory.makeBug(
1831
        owner=owner, private=True, security_related=True)
14174.2.15 by Ian Booth
Fix tests for private bugs and prepare makeBugTask for single pillar bugs
1832
    bugtask = bug.default_bugtask
14128.3.20 by Aaron Bentley
Ensure the json cache is populated appropriately.
1833
    bug_task_set = getUtility(IBugTaskSet)
1834
    bug_badge_properties = bug_task_set.getBugTaskBadgeProperties(
1835
        [bugtask])
1836
    badge_property = bug_badge_properties[bugtask]
1837
    return owner, BugTaskListingItem(
1838
        bugtask,
1839
        badge_property['has_branch'],
1840
        badge_property['has_specification'],
1841
        badge_property['has_patch'],
1842
        target_context=bugtask.target)
1843
1844
14128.3.21 by Aaron Bentley
Test the mustache rendering and placeholder.
1845
class TestBugTaskSearchListingView(BrowserTestCase):
14128.3.20 by Aaron Bentley
Ensure the json cache is populated appropriately.
1846
1847
    layer = DatabaseFunctionalLayer
1848
14128.3.21 by Aaron Bentley
Test the mustache rendering and placeholder.
1849
    client_listing = soupmatchers.Tag(
1850
        'client-listing', True, attrs={'id': 'client-listing'})
1851
14128.3.56 by Aaron Bentley
Get batch caching working.
1852
    def makeView(self, bugtask=None, size=None, memo=None, orderby=None,
14317.1.16 by Deryck Hodge
Add test that should pass once field_visibility is set from a cookie.
1853
                 forwards=True, cookie=None):
14128.3.61 by Aaron Bentley
Update docs.
1854
        """Make a BugTaskSearchListingView.
1855
1856
        :param bugtask: The task to use for searching.
1857
        :param size: The size of the batches.  Required if forwards is False.
1858
        :param memo: Batch identifier.
1859
        :param orderby: The way to order the batch.
1860
        :param forwards: If true, walk forwards from the memo.  Else walk
1861
            backwards.
1862
1863
        """
14128.3.47 by Aaron Bentley
Update tests.
1864
        query_vars = {}
1865
        if size is not None:
14128.3.51 by Aaron Bentley
Fix lint.
1866
            query_vars['batch'] = size
14128.3.47 by Aaron Bentley
Update tests.
1867
        if memo is not None:
1868
            query_vars['memo'] = memo
14128.3.56 by Aaron Bentley
Get batch caching working.
1869
            if forwards:
1870
                query_vars['start'] = memo
1871
            else:
1872
                query_vars['start'] = int(memo) - size
1873
        if not forwards:
1874
            query_vars['direction'] = 'backwards'
14128.3.47 by Aaron Bentley
Update tests.
1875
        query_string = urllib.urlencode(query_vars)
14128.3.49 by Aaron Bentley
Get cache keys from model.
1876
        request = LaunchpadTestRequest(
14317.1.16 by Deryck Hodge
Add test that should pass once field_visibility is set from a cookie.
1877
            QUERY_STRING=query_string, orderby=orderby, HTTP_COOKIE=cookie)
14128.3.20 by Aaron Bentley
Ensure the json cache is populated appropriately.
1878
        if bugtask is None:
1879
            bugtask = self.factory.makeBugTask()
14382.1.12 by Aaron Bentley
Fix and simplify tests.
1880
        view = getMultiAdapter((bugtask.target, request), name='+bugs')
14128.3.20 by Aaron Bentley
Ensure the json cache is populated appropriately.
1881
        view.initialize()
1882
        return view
1883
14128.3.21 by Aaron Bentley
Test the mustache rendering and placeholder.
1884
    @contextmanager
1885
    def dynamic_listings(self):
14128.3.61 by Aaron Bentley
Update docs.
1886
        """Context manager to enable new bug listings."""
14128.3.21 by Aaron Bentley
Test the mustache rendering and placeholder.
1887
        with feature_flags():
1888
            set_feature_flag(u'bugs.dynamic_bug_listings.enabled', u'on')
1889
            yield
1890
14128.3.20 by Aaron Bentley
Ensure the json cache is populated appropriately.
1891
    def test_mustache_model_missing_if_no_flag(self):
1892
        """The IJSONRequestCache should contain mustache_model."""
1893
        view = self.makeView()
1894
        cache = IJSONRequestCache(view.request)
1895
        self.assertIs(None, cache.objects.get('mustache_model'))
1896
1897
    def test_mustache_model_in_json(self):
1898
        """The IJSONRequestCache should contain mustache_model.
1899
1900
        mustache_model should contain bugtasks, the BugTaskListingItem.model
1901
        for each BugTask.
1902
        """
1903
        owner, item = make_bug_task_listing_item(self.factory)
1904
        self.useContext(person_logged_in(owner))
14128.3.21 by Aaron Bentley
Test the mustache rendering and placeholder.
1905
        with self.dynamic_listings():
14128.3.20 by Aaron Bentley
Ensure the json cache is populated appropriately.
1906
            view = self.makeView(item.bugtask)
1907
        cache = IJSONRequestCache(view.request)
1908
        bugtasks = cache.objects['mustache_model']['bugtasks']
1909
        self.assertEqual(1, len(bugtasks))
14128.8.5 by Aaron Bentley
Fix failing test.
1910
        combined = dict(item.model)
1911
        combined.update(view.search().field_visibility)
1912
        self.assertEqual(combined, bugtasks[0])
14128.3.20 by Aaron Bentley
Ensure the json cache is populated appropriately.
1913
14128.3.47 by Aaron Bentley
Update tests.
1914
    def test_no_next_prev_for_single_batch(self):
1915
        """The IJSONRequestCache should contain data about ajacent batches.
1916
1917
        mustache_model should contain bugtasks, the BugTaskListingItem.model
1918
        for each BugTask.
1919
        """
1920
        owner, item = make_bug_task_listing_item(self.factory)
1921
        self.useContext(person_logged_in(owner))
1922
        with self.dynamic_listings():
1923
            view = self.makeView(item.bugtask)
1924
        cache = IJSONRequestCache(view.request)
1925
        self.assertIs(None, cache.objects.get('next'))
1926
        self.assertIs(None, cache.objects.get('prev'))
1927
1928
    def test_next_for_multiple_batch(self):
1929
        """The IJSONRequestCache should contain data about the next batch.
1930
1931
        mustache_model should contain bugtasks, the BugTaskListingItem.model
1932
        for each BugTask.
1933
        """
1934
        task = self.factory.makeBugTask()
14128.3.51 by Aaron Bentley
Fix lint.
1935
        self.factory.makeBugTask(target=task.target)
14128.3.47 by Aaron Bentley
Update tests.
1936
        with self.dynamic_listings():
1937
            view = self.makeView(task, size=1)
1938
        cache = IJSONRequestCache(view.request)
1939
        self.assertEqual({'memo': '1', 'start': 1}, cache.objects.get('next'))
1940
1941
    def test_prev_for_multiple_batch(self):
1942
        """The IJSONRequestCache should contain data about the next batch.
1943
1944
        mustache_model should contain bugtasks, the BugTaskListingItem.model
1945
        for each BugTask.
1946
        """
1947
        task = self.factory.makeBugTask()
1948
        task2 = self.factory.makeBugTask(target=task.target)
1949
        with self.dynamic_listings():
1950
            view = self.makeView(task2, size=1, memo=1)
1951
        cache = IJSONRequestCache(view.request)
1952
        self.assertEqual({'memo': '1', 'start': 0}, cache.objects.get('prev'))
1953
14382.1.5 by Aaron Bentley
view_name is included in BugTaskSearchListingView RequestCache.
1954
    def test_provides_view_name(self):
1955
        """The IJSONRequestCache should provide the view's name."""
1956
        self.useContext(self.dynamic_listings())
14382.1.12 by Aaron Bentley
Fix and simplify tests.
1957
        view = self.makeView()
14382.1.5 by Aaron Bentley
view_name is included in BugTaskSearchListingView RequestCache.
1958
        cache = IJSONRequestCache(view.request)
1959
        self.assertEqual('+bugs', cache.objects['view_name'])
14382.1.12 by Aaron Bentley
Fix and simplify tests.
1960
        person = self.factory.makePerson()
14382.1.7 by Aaron Bentley
Simplify iter_view_registrations, fix tests.
1961
        commentview = getMultiAdapter(
14382.1.12 by Aaron Bentley
Fix and simplify tests.
1962
            (person, LaunchpadTestRequest()), name='+commentedbugs')
14382.1.5 by Aaron Bentley
view_name is included in BugTaskSearchListingView RequestCache.
1963
        commentview.initialize()
1964
        cache = IJSONRequestCache(commentview.request)
1965
        self.assertEqual('+commentedbugs', cache.objects['view_name'])
1966
14128.3.49 by Aaron Bentley
Get cache keys from model.
1967
    def test_default_order_by(self):
14128.3.61 by Aaron Bentley
Update docs.
1968
        """order_by defaults to '-importance in JSONRequestCache"""
14128.3.49 by Aaron Bentley
Get cache keys from model.
1969
        task = self.factory.makeBugTask()
1970
        with self.dynamic_listings():
1971
            view = self.makeView(task)
1972
        cache = IJSONRequestCache(view.request)
1973
        self.assertEqual('-importance', cache.objects['order_by'])
1974
1975
    def test_order_by_importance(self):
14128.3.61 by Aaron Bentley
Update docs.
1976
        """order_by follows query params in JSONRequestCache"""
14128.3.49 by Aaron Bentley
Get cache keys from model.
1977
        task = self.factory.makeBugTask()
1978
        with self.dynamic_listings():
1979
            view = self.makeView(task, orderby='importance')
1980
        cache = IJSONRequestCache(view.request)
1981
        self.assertEqual('importance', cache.objects['order_by'])
1982
14128.3.56 by Aaron Bentley
Get batch caching working.
1983
    def test_cache_has_all_batch_vars_defaults(self):
14128.3.61 by Aaron Bentley
Update docs.
1984
        """Cache has all the needed variables.
1985
1986
        order_by, memo, start, forwards.  These default to sane values.
1987
        """
14128.3.56 by Aaron Bentley
Get batch caching working.
1988
        task = self.factory.makeBugTask()
1989
        with self.dynamic_listings():
1990
            view = self.makeView(task)
1991
        cache = IJSONRequestCache(view.request)
1992
        self.assertEqual('-importance', cache.objects['order_by'])
1993
        self.assertIs(None, cache.objects['memo'])
1994
        self.assertEqual(0, cache.objects['start'])
1995
        self.assertTrue(cache.objects['forwards'])
14128.3.73 by Aaron Bentley
Fake merge of r14223
1996
        self.assertEqual(1, cache.objects['total'])
14128.3.56 by Aaron Bentley
Get batch caching working.
1997
1998
    def test_cache_has_all_batch_vars_specified(self):
14128.3.61 by Aaron Bentley
Update docs.
1999
        """Cache has all the needed variables.
2000
2001
        order_by, memo, start, forwards.  These are calculated appropriately.
2002
        """
14128.3.56 by Aaron Bentley
Get batch caching working.
2003
        task = self.factory.makeBugTask()
2004
        with self.dynamic_listings():
2005
            view = self.makeView(task, memo=1, forwards=False, size=1)
2006
        cache = IJSONRequestCache(view.request)
2007
        self.assertEqual('1', cache.objects['memo'])
2008
        self.assertEqual(0, cache.objects['start'])
2009
        self.assertFalse(cache.objects['forwards'])
14128.3.59 by Aaron Bentley
Fix last link to use correct start param.
2010
        self.assertEqual(0, cache.objects['last_start'])
14128.3.56 by Aaron Bentley
Get batch caching working.
2011
14128.7.18 by Aaron Bentley
Put field_visibility in model, rename set_field_visibility to change_fields.
2012
    def test_cache_field_visibility(self):
14128.7.19 by Aaron Bentley
Update docs
2013
        """Cache contains sane-looking field_visibility values."""
14128.7.18 by Aaron Bentley
Put field_visibility in model, rename set_field_visibility to change_fields.
2014
        task = self.factory.makeBugTask()
2015
        with self.dynamic_listings():
2016
            view = self.makeView(task, memo=1, forwards=False, size=1)
2017
        cache = IJSONRequestCache(view.request)
2018
        field_visibility = cache.objects['field_visibility']
14385.1.1 by Aaron Bentley
Make bug title a mandatory field.
2019
        self.assertTrue(field_visibility['show_id'])
14128.7.18 by Aaron Bentley
Put field_visibility in model, rename set_field_visibility to change_fields.
2020
14317.1.25 by Deryck Hodge
Add test that cookie name is in cache.
2021
    def test_cache_cookie_name(self):
2022
        """The cookie name should be in cache for js code access."""
2023
        task = self.factory.makeBugTask()
2024
        with self.dynamic_listings():
2025
            view = self.makeView(task, memo=1, forwards=False, size=1)
2026
        cache = IJSONRequestCache(view.request)
2027
        cookie_name = cache.objects['cbl_cookie_name']
2028
        self.assertEqual('anon-buglist-fields', cookie_name)
2029
14317.1.16 by Deryck Hodge
Add test that should pass once field_visibility is set from a cookie.
2030
    def test_cache_field_visibility_matches_cookie(self):
2031
        """Cache contains cookie-matching values for field_visibiliy."""
2032
        task = self.factory.makeBugTask()
2033
        cookie = (
14317.1.17 by Deryck Hodge
Update field_visibility if cookie is present.
2034
            'anon-buglist-fields=show_age=true&show_reporter=true'
14385.1.1 by Aaron Bentley
Make bug title a mandatory field.
2035
            '&show_id=true&show_bugtarget=true'
14317.1.16 by Deryck Hodge
Add test that should pass once field_visibility is set from a cookie.
2036
            '&show_milestone_name=true&show_last_updated=true'
2037
            '&show_assignee=true&show_bug_heat=true&show_tags=true'
2038
            '&show_importance=true&show_status=true')
2039
        with self.dynamic_listings():
2040
            view = self.makeView(
2041
                task, memo=1, forwards=False, size=1, cookie=cookie)
2042
        cache = IJSONRequestCache(view.request)
2043
        field_visibility = cache.objects['field_visibility']
2044
        self.assertTrue(field_visibility['show_tags'])
2045
14385.1.1 by Aaron Bentley
Make bug title a mandatory field.
2046
    def test_exclude_unsupported_cookie_values(self):
2047
        """Cookie values not present in defaults are ignored."""
2048
        task = self.factory.makeBugTask()
2049
        cookie = (
2050
            'anon-buglist-fields=show_age=true&show_reporter=true'
2051
            '&show_id=true&show_bugtarget=true'
2052
            '&show_milestone_name=true&show_last_updated=true'
2053
            '&show_assignee=true&show_bug_heat=true&show_tags=true'
2054
            '&show_importance=true&show_status=true&show_title=true')
2055
        with self.dynamic_listings():
2056
            view = self.makeView(
2057
                task, memo=1, forwards=False, size=1, cookie=cookie)
2058
        cache = IJSONRequestCache(view.request)
2059
        field_visibility = cache.objects['field_visibility']
2060
        self.assertNotIn('show_title', field_visibility)
2061
2062
    def test_add_defaults_to_cookie_values(self):
2063
        """Where cookie values are missing, defaults are used"""
2064
        task = self.factory.makeBugTask()
2065
        cookie = (
2066
            'anon-buglist-fields=show_age=true&show_reporter=true'
2067
            '&show_id=true&show_bugtarget=true'
2068
            '&show_milestone_name=true&show_last_updated=true'
2069
            '&show_assignee=true&show_bug_heat=true&show_tags=true'
2070
            '&show_importance=true&show_title=true')
2071
        with self.dynamic_listings():
2072
            view = self.makeView(
2073
                task, memo=1, forwards=False, size=1, cookie=cookie)
2074
        cache = IJSONRequestCache(view.request)
2075
        field_visibility = cache.objects['field_visibility']
2076
        self.assertIn('show_status', field_visibility)
2077
14241.3.83 by Deryck Hodge
Add test for adding field_visibility_defaults.
2078
    def test_cache_field_visibility_defaults(self):
2079
        """Cache contains sane-looking field_visibility_defaults values."""
2080
        task = self.factory.makeBugTask()
2081
        with self.dynamic_listings():
2082
            view = self.makeView(task, memo=1, forwards=False, size=1)
2083
        cache = IJSONRequestCache(view.request)
2084
        field_visibility_defaults = cache.objects['field_visibility_defaults']
14385.1.1 by Aaron Bentley
Make bug title a mandatory field.
2085
        self.assertTrue(field_visibility_defaults['show_id'])
14241.3.83 by Deryck Hodge
Add test for adding field_visibility_defaults.
2086
14265.2.3 by Aaron Bentley
Obfuscate values for unauthenicated users.
2087
    def getBugtaskBrowser(self, title=None, no_login=False):
14128.3.61 by Aaron Bentley
Update docs.
2088
        """Return a browser for a new bugtask."""
14128.3.21 by Aaron Bentley
Test the mustache rendering and placeholder.
2089
        bugtask = self.factory.makeBugTask()
2090
        with person_logged_in(bugtask.target.owner):
2091
            bugtask.target.official_malone = True
14265.2.3 by Aaron Bentley
Obfuscate values for unauthenicated users.
2092
            if title is not None:
2093
                bugtask.bug.title = title
14128.3.23 by Aaron Bentley
Fix lint.
2094
        browser = self.getViewBrowser(
14265.2.3 by Aaron Bentley
Obfuscate values for unauthenicated users.
2095
            bugtask.target, '+bugs', rootsite='bugs', no_login=no_login)
14128.3.21 by Aaron Bentley
Test the mustache rendering and placeholder.
2096
        return bugtask, browser
2097
2098
    def assertHTML(self, browser, *tags, **kwargs):
14128.3.61 by Aaron Bentley
Update docs.
2099
        """Assert something about a browser's HTML."""
14128.3.21 by Aaron Bentley
Test the mustache rendering and placeholder.
2100
        matcher = soupmatchers.HTMLContains(*tags)
2101
        if kwargs.get('invert', False):
2102
            matcher = Not(matcher)
2103
        self.assertThat(browser.contents, matcher)
2104
2105
    @staticmethod
2106
    def getBugNumberTag(bug_task):
2107
        """Bug numbers with a leading hash are unique to new rendering."""
14128.3.23 by Aaron Bentley
Fix lint.
2108
        bug_number_re = re.compile(r'\#%d' % bug_task.bug.id)
14241.3.70 by Deryck Hodge
Little markup fix in template and test to get broken test passing.
2109
        return soupmatchers.Tag('bugnumber', 'span', text=bug_number_re)
14128.3.21 by Aaron Bentley
Test the mustache rendering and placeholder.
2110
2111
    def test_mustache_rendering_missing_if_no_flag(self):
2112
        """If the flag is missing, then no mustache features appear."""
2113
        bug_task, browser = self.getBugtaskBrowser()
2114
        number_tag = self.getBugNumberTag(bug_task)
2115
        self.assertHTML(browser, number_tag, invert=True)
2116
        self.assertHTML(browser, self.client_listing, invert=True)
2117
2118
    def test_mustache_rendering(self):
2119
        """If the flag is present, then all mustache features appear."""
2120
        with self.dynamic_listings():
2121
            bug_task, browser = self.getBugtaskBrowser()
2122
        bug_number = self.getBugNumberTag(bug_task)
14128.3.47 by Aaron Bentley
Update tests.
2123
        self.assertHTML(browser, self.client_listing, bug_number)
14128.3.21 by Aaron Bentley
Test the mustache rendering and placeholder.
2124
14265.2.3 by Aaron Bentley
Obfuscate values for unauthenicated users.
2125
    def test_mustache_rendering_obfuscation(self):
14265.2.6 by Aaron Bentley
Update docs
2126
        """For anonymous users, email addresses are obfuscated."""
14265.2.3 by Aaron Bentley
Obfuscate values for unauthenicated users.
2127
        with self.dynamic_listings():
2128
            bug_task, browser = self.getBugtaskBrowser(title='a@example.com',
2129
                no_login=True)
2130
        self.assertNotIn('a@example.com', browser.contents)
2131
14128.7.6 by Aaron Bentley
Allow bug target to be shown and hidden.
2132
    def getNavigator(self):
14128.7.1 by Aaron Bentley
Control whether the bug_id is displayed using the mustache template.
2133
        request = LaunchpadTestRequest()
14128.7.10 by Aaron Bentley
Support milestone_name as a field.
2134
        navigator = BugListingBatchNavigator([], request, [], 1)
14128.7.1 by Aaron Bentley
Control whether the bug_id is displayed using the mustache template.
2135
        cache = IJSONRequestCache(request)
14128.7.10 by Aaron Bentley
Support milestone_name as a field.
2136
        bugtask = {
14128.9.3 by Aaron Bentley
Provide bug age field, hidden by default.
2137
            'age': 'age1',
14128.9.1 by Aaron Bentley
Add assignee to bug fields.
2138
            'assignee': 'assignee1',
14128.7.10 by Aaron Bentley
Support milestone_name as a field.
2139
            'bugtarget': 'bugtarget1',
2140
            'bugtarget_css': 'bugtarget_css1',
2141
            'bug_heat_html': 'bug_heat_html1',
2142
            'bug_url': 'bug_url1',
14128.9.4 by Aaron Bentley
Add tags to listing.
2143
            'id': '3.14159',
2144
            'importance': 'importance1',
2145
            'importance_class': 'importance_class1',
14128.9.6 by Aaron Bentley
Include last_updated in optional fields.
2146
            'last_updated': 'updated1',
14128.9.4 by Aaron Bentley
Add tags to listing.
2147
            'milestone_name': 'milestone_name1',
2148
            'status': 'status1',
14128.9.5 by Aaron Bentley
Add bug reporter, distinguish assignee from reporter with prefixes.
2149
            'reporter': 'reporter1',
14128.9.4 by Aaron Bentley
Add tags to listing.
2150
            'tags': 'tags1',
2151
            'title': 'title1',
14128.7.10 by Aaron Bentley
Support milestone_name as a field.
2152
        }
2153
        bugtask.update(navigator.field_visibility)
14128.7.1 by Aaron Bentley
Control whether the bug_id is displayed using the mustache template.
2154
        cache.objects['mustache_model'] = {
14128.7.10 by Aaron Bentley
Support milestone_name as a field.
2155
            'bugtasks': [bugtask],
14128.7.1 by Aaron Bentley
Control whether the bug_id is displayed using the mustache template.
2156
        }
2157
        mustache_model = cache.objects['mustache_model']
14128.7.6 by Aaron Bentley
Allow bug target to be shown and hidden.
2158
        return navigator, mustache_model
2159
2160
    def test_hiding_bug_number(self):
14128.8.4 by Aaron Bentley
Update docs.
2161
        """Hiding a bug number makes it disappear from the page."""
14128.7.6 by Aaron Bentley
Allow bug target to be shown and hidden.
2162
        navigator, mustache_model = self.getNavigator()
14128.7.1 by Aaron Bentley
Control whether the bug_id is displayed using the mustache template.
2163
        self.assertIn('3.14159', navigator.mustache)
2164
        mustache_model['bugtasks'][0]['show_id'] = False
2165
        self.assertNotIn('3.14159', navigator.mustache)
14128.7.6 by Aaron Bentley
Allow bug target to be shown and hidden.
2166
2167
    def test_hiding_status(self):
14128.8.4 by Aaron Bentley
Update docs.
2168
        """Hiding status makes it disappear from the page."""
14128.7.6 by Aaron Bentley
Allow bug target to be shown and hidden.
2169
        navigator, mustache_model = self.getNavigator()
14128.7.4 by Aaron Bentley
Make status and importance togglable.
2170
        self.assertIn('status1', navigator.mustache)
2171
        mustache_model['bugtasks'][0]['show_status'] = False
2172
        self.assertNotIn('status1', navigator.mustache)
14128.7.6 by Aaron Bentley
Allow bug target to be shown and hidden.
2173
2174
    def test_hiding_importance(self):
14128.8.4 by Aaron Bentley
Update docs.
2175
        """Hiding importance removes the text and CSS."""
14128.7.6 by Aaron Bentley
Allow bug target to be shown and hidden.
2176
        navigator, mustache_model = self.getNavigator()
14128.7.4 by Aaron Bentley
Make status and importance togglable.
2177
        self.assertIn('importance1', navigator.mustache)
2178
        self.assertIn('importance_class1', navigator.mustache)
2179
        mustache_model['bugtasks'][0]['show_importance'] = False
2180
        self.assertNotIn('importance1', navigator.mustache)
2181
        self.assertNotIn('importance_class1', navigator.mustache)
14128.7.1 by Aaron Bentley
Control whether the bug_id is displayed using the mustache template.
2182
14128.7.6 by Aaron Bentley
Allow bug target to be shown and hidden.
2183
    def test_hiding_bugtarget(self):
14128.8.4 by Aaron Bentley
Update docs.
2184
        """Hiding bugtarget removes the text and CSS."""
14128.7.6 by Aaron Bentley
Allow bug target to be shown and hidden.
2185
        navigator, mustache_model = self.getNavigator()
2186
        self.assertIn('bugtarget1', navigator.mustache)
2187
        self.assertIn('bugtarget_css1', navigator.mustache)
2188
        mustache_model['bugtasks'][0]['show_bugtarget'] = False
2189
        self.assertNotIn('bugtarget1', navigator.mustache)
2190
        self.assertNotIn('bugtarget_css1', navigator.mustache)
2191
14128.7.8 by Aaron Bentley
Support hiding bug heat.
2192
    def test_hiding_bug_heat(self):
14128.8.4 by Aaron Bentley
Update docs.
2193
        """Hiding bug heat removes the html and CSS."""
14128.7.8 by Aaron Bentley
Support hiding bug heat.
2194
        navigator, mustache_model = self.getNavigator()
2195
        self.assertIn('bug_heat_html1', navigator.mustache)
2196
        self.assertIn('bug-heat-icons', navigator.mustache)
2197
        mustache_model['bugtasks'][0]['show_bug_heat'] = False
2198
        self.assertNotIn('bug_heat_html1', navigator.mustache)
2199
        self.assertNotIn('bug-heat-icons', navigator.mustache)
14128.3.20 by Aaron Bentley
Ensure the json cache is populated appropriately.
2200
14128.7.10 by Aaron Bentley
Support milestone_name as a field.
2201
    def test_hiding_milstone_name(self):
14128.8.4 by Aaron Bentley
Update docs.
2202
        """Showing milestone name shows the text."""
14128.7.10 by Aaron Bentley
Support milestone_name as a field.
2203
        navigator, mustache_model = self.getNavigator()
2204
        self.assertNotIn('milestone_name1', navigator.mustache)
2205
        mustache_model['bugtasks'][0]['show_milestone_name'] = True
2206
        self.assertIn('milestone_name1', navigator.mustache)
2207
14128.9.1 by Aaron Bentley
Add assignee to bug fields.
2208
    def test_hiding_assignee(self):
2209
        """Showing milestone name shows the text."""
2210
        navigator, mustache_model = self.getNavigator()
2211
        self.assertIn('show_assignee', navigator.field_visibility)
14128.9.5 by Aaron Bentley
Add bug reporter, distinguish assignee from reporter with prefixes.
2212
        self.assertNotIn('Assignee: assignee1', navigator.mustache)
14128.9.1 by Aaron Bentley
Add assignee to bug fields.
2213
        mustache_model['bugtasks'][0]['show_assignee'] = True
14128.9.5 by Aaron Bentley
Add bug reporter, distinguish assignee from reporter with prefixes.
2214
        self.assertIn('Assignee: assignee1', navigator.mustache)
14128.9.1 by Aaron Bentley
Add assignee to bug fields.
2215
14128.9.3 by Aaron Bentley
Provide bug age field, hidden by default.
2216
    def test_hiding_age(self):
2217
        """Showing age shows the text."""
2218
        navigator, mustache_model = self.getNavigator()
2219
        self.assertIn('show_age', navigator.field_visibility)
2220
        self.assertNotIn('age1', navigator.mustache)
2221
        mustache_model['bugtasks'][0]['show_age'] = True
2222
        self.assertIn('age1', navigator.mustache)
2223
14128.9.4 by Aaron Bentley
Add tags to listing.
2224
    def test_hiding_tags(self):
2225
        """Showing tags shows the text."""
2226
        navigator, mustache_model = self.getNavigator()
2227
        self.assertIn('show_tags', navigator.field_visibility)
2228
        self.assertNotIn('tags1', navigator.mustache)
2229
        mustache_model['bugtasks'][0]['show_tags'] = True
2230
        self.assertIn('tags1', navigator.mustache)
2231
14128.9.5 by Aaron Bentley
Add bug reporter, distinguish assignee from reporter with prefixes.
2232
    def test_hiding_reporter(self):
2233
        """Showing reporter shows the text."""
2234
        navigator, mustache_model = self.getNavigator()
2235
        self.assertIn('show_reporter', navigator.field_visibility)
2236
        self.assertNotIn('Reporter: reporter1', navigator.mustache)
2237
        mustache_model['bugtasks'][0]['show_reporter'] = True
2238
        self.assertIn('Reporter: reporter1', navigator.mustache)
2239
14128.9.6 by Aaron Bentley
Include last_updated in optional fields.
2240
    def test_hiding_last_updated(self):
2241
        """Showing last_updated shows the text."""
2242
        navigator, mustache_model = self.getNavigator()
2243
        self.assertIn('show_last_updated', navigator.field_visibility)
14424.3.6 by Aaron Bentley
Change capitalization of 'Last updated'
2244
        self.assertNotIn('Last updated updated1', navigator.mustache)
14128.9.6 by Aaron Bentley
Include last_updated in optional fields.
2245
        mustache_model['bugtasks'][0]['show_last_updated'] = True
14424.3.6 by Aaron Bentley
Change capitalization of 'Last updated'
2246
        self.assertIn('Last updated updated1', navigator.mustache)
14128.9.6 by Aaron Bentley
Include last_updated in optional fields.
2247
14128.7.9 by Aaron Bentley
Allow hiding bug title, linkify bug number.
2248
14195.1.1 by Aaron Bentley
Fix template-escaping bug.
2249
class TestBugListingBatchNavigator(TestCaseWithFactory):
2250
2251
    layer = DatabaseFunctionalLayer
2252
2253
    def test_mustache_listings_escaped(self):
2254
        """Mustache template is encoded such that it has no unescaped tags."""
2255
        navigator = BugListingBatchNavigator(
2256
            [], LaunchpadTestRequest(), [], 0)
2257
        self.assertNotIn('<', navigator.mustache_listings)
2258
        self.assertNotIn('>', navigator.mustache_listings)
2259
2260
14128.3.19 by Aaron Bentley
Test BugTaskListingItem.model
2261
class TestBugTaskListingItem(TestCaseWithFactory):
2262
2263
    layer = DatabaseFunctionalLayer
2264
2265
    def test_model(self):
2266
        """Model contains expected fields with expected values."""
14128.3.20 by Aaron Bentley
Ensure the json cache is populated appropriately.
2267
        owner, item = make_bug_task_listing_item(self.factory)
14128.3.19 by Aaron Bentley
Test BugTaskListingItem.model
2268
        with person_logged_in(owner):
2269
            model = item.model
2270
            self.assertEqual('Undecided', model['importance'])
2271
            self.assertEqual('importanceUNDECIDED', model['importance_class'])
2272
            self.assertEqual('New', model['status'])
2273
            self.assertEqual('statusNEW', model['status_class'])
2274
            self.assertEqual(item.bug.title, model['title'])
2275
            self.assertEqual(item.bug.id, model['id'])
2276
            self.assertEqual(canonical_url(item.bugtask), model['bug_url'])
2277
            self.assertEqual(item.bugtargetdisplayname, model['bugtarget'])
2278
            self.assertEqual('sprite product', model['bugtarget_css'])
2279
            self.assertEqual(item.bug_heat_html, model['bug_heat_html'])
2280
            self.assertEqual(
2281
                '<span alt="private" title="Private" class="sprite private">'
2282
                '&nbsp;</span>', model['badges'])
14128.7.10 by Aaron Bentley
Support milestone_name as a field.
2283
            self.assertEqual(None, model['milestone_name'])
2284
            item.bugtask.milestone = self.factory.makeMilestone(
2285
                product=item.bugtask.target)
2286
            milestone_name = item.milestone.displayname
2287
            self.assertEqual(milestone_name, item.model['milestone_name'])
14128.9.1 by Aaron Bentley
Add assignee to bug fields.
2288
2289
    def test_model_assignee(self):
2290
        """Model contains expected fields with expected values."""
2291
        owner, item = make_bug_task_listing_item(self.factory)
2292
        with person_logged_in(owner):
2293
            self.assertIs(None, item.model['assignee'])
2294
            assignee = self.factory.makePerson(displayname='Example Person')
2295
            item.bugtask.transitionToAssignee(assignee)
2296
            self.assertEqual('Example Person', item.model['assignee'])
2297
14128.9.4 by Aaron Bentley
Add tags to listing.
2298
    def test_model_age(self):
14128.9.3 by Aaron Bentley
Provide bug age field, hidden by default.
2299
        """Model contains bug age."""
2300
        owner, item = make_bug_task_listing_item(self.factory)
2301
        with person_logged_in(owner):
2302
            item.bug.datecreated = datetime.now(UTC) - timedelta(3, 0, 0)
2303
            self.assertEqual('3 days old', item.model['age'])
14128.9.4 by Aaron Bentley
Add tags to listing.
2304
2305
    def test_model_tags(self):
2306
        """Model contains bug tags."""
2307
        owner, item = make_bug_task_listing_item(self.factory)
2308
        with person_logged_in(owner):
2309
            item.bug.tags = ['tag1', 'tag2']
2310
            self.assertEqual('tag1 tag2', item.model['tags'])
14128.9.5 by Aaron Bentley
Add bug reporter, distinguish assignee from reporter with prefixes.
2311
2312
    def test_model_reporter(self):
2313
        """Model contains bug reporter."""
2314
        owner, item = make_bug_task_listing_item(self.factory)
2315
        with person_logged_in(owner):
2316
            self.assertEqual(owner.displayname, item.model['reporter'])
14128.9.6 by Aaron Bentley
Include last_updated in optional fields.
2317
2318
    def test_model_last_updated_date_last_updated(self):
2319
        """last_updated uses date_last_updated if newer."""
2320
        owner, item = make_bug_task_listing_item(self.factory)
2321
        with person_logged_in(owner):
2322
            item.bug.date_last_updated = datetime(2001, 1, 1, tzinfo=UTC)
2323
            removeSecurityProxy(item.bug).date_last_message = datetime(
2324
                2000, 1, 1, tzinfo=UTC)
2325
            self.assertEqual(
14128.9.10 by Aaron Bentley
Fix failing test.
2326
                'on 2001-01-01', item.model['last_updated'])
14128.9.6 by Aaron Bentley
Include last_updated in optional fields.
2327
2328
    def test_model_last_updated_date_last_message(self):
2329
        """last_updated uses date_last_message if newer."""
2330
        owner, item = make_bug_task_listing_item(self.factory)
2331
        with person_logged_in(owner):
2332
            item.bug.date_last_updated = datetime(2000, 1, 1, tzinfo=UTC)
2333
            removeSecurityProxy(item.bug).date_last_message = datetime(
2334
                2001, 1, 1, tzinfo=UTC)
2335
            self.assertEqual(
14128.9.10 by Aaron Bentley
Fix failing test.
2336
                'on 2001-01-01', item.model['last_updated'])