1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
|
Bug Activity
~~~~~~~~~~~~
Bugs are problems that occur in software. In Malone, various things can
be added to, edited and removed from a bug over the course of a bug's
lifetime. We call this stuff bug activity. This document is about bug
activity.
Each activity can happen more than once to a bug over the course of its
lifetime. For auditing reasons, it's useful to track when these happen, why
these things happen, and who made them happen.
This file contains old tests for bug activity. New tests are in
tests/test_bugchanges.py.
Before going any further--because editing a bug requires an
authenticated user--let's login:
>>> login("test@canonical.com")
Bug activity tracking is done using event subscribers. The handlers
are simple little functions.
>>> from zope.event import notify
>>> from lazr.lifecycle.event import ObjectModifiedEvent
>>> from lazr.lifecycle.snapshot import Snapshot
>>> from lp.bugs.interfaces.bugtask import (
... IBugTask,
... IBugTaskSet,
... )
>>> from lp.registry.interfaces.product import IProductSet
>>> user = getUtility(ILaunchBag).user
User files a bug
================
>>> from lp.bugs.interfaces.bug import CreateBugParams
>>> firefox = getUtility(IProductSet)['firefox']
>>> params = CreateBugParams(
... title="a test bug",
... comment="this is only a test bug\nplease ignore",
... owner=user)
>>> bug = firefox.createBug(params)
>>> latest_activity = bug.activity[-1]
>>> latest_activity.person == user
True
>>> latest_activity.whatchanged
u'bug'
>>> latest_activity.message
u'added bug'
Bug title edited
================
>>> from lp.bugs.interfaces.bug import IBug
>>> old_state = Snapshot(bug, providing=IBug)
>>> bug.title = "new bug title"
>>> bug_edited = ObjectModifiedEvent(
... bug, old_state, ["title", "description"])
>>> notify(bug_edited)
>>> latest_activity = bug.activity[-1]
>>> latest_activity.whatchanged
u'summary'
>>> latest_activity.oldvalue
u'a test bug'
>>> latest_activity.newvalue
u'new bug title'
Source package assignment edited
================================
>>> from lazr.lifecycle.snapshot import Snapshot
>>> from lp.bugs.interfaces.bugtask import BugTaskStatus
>>> from lp.registry.interfaces.distribution import IDistributionSet
>>> from lp.registry.interfaces.sourcepackagename import (
... ISourcePackageNameSet)
>>> mozilla_firefox = getUtility(ISourcePackageNameSet)['mozilla-firefox']
>>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
>>> source_package_assignment = getUtility(IBugTaskSet).createTask(
... bug, user, ubuntu.getSourcePackage(mozilla_firefox))
>>> edit_fields = [
... "distribution", "sourcepackagename", "milestone", "status",
... "importance", "assignee", "bugwatch"]
>>> old_source_package_assignment = Snapshot(
... source_package_assignment, providing=IBugTask)
>>> source_package_assignment.transitionToStatus(
... BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
>>> source_package_assignment_edited = ObjectModifiedEvent(
... source_package_assignment, old_source_package_assignment,
... edit_fields)
>>> notify(source_package_assignment_edited)
>>> latest_activity = bug.activity[-1]
>>> latest_activity.whatchanged
u'mozilla-firefox (Ubuntu): status'
>>> latest_activity.oldvalue == BugTaskStatus.NEW.title
True
>>> latest_activity.newvalue == BugTaskStatus.CONFIRMED.title
True
You will notice that the `whatchanged` attribute in this case specifies
a project and an attribute. This happens when the change is to a bugtask.
The activity object object provides a couple of simple attributes to separate
out these values: `target` and `attribute`.
>>> latest_activity.target
u'mozilla-firefox (Ubuntu)'
>>> latest_activity.attribute
u'status'
If the activity is not for a bug task, `target` is None, and `attribute` is
typically the same as `whatchanged`. However, in some cases (ideally,
whenever necessary), it is normalized to show the actual attribute name.
For instance, look at the attributes on the previous activity.
>>> print bug.activity[-2].target
None
>>> bug.activity[-2].whatchanged
u'summary'
>>> bug.activity[-2].attribute
'title'
(This is covered more comprehensively in tests/test_bugchanges.py).
Upstream product assignment edited
==================================
>>> product_assignment = getUtility(IBugTaskSet).createTask(
... bug, user, getUtility(IProductSet)['thunderbird'])
>>> edit_fields = [
... "product", "milestone", "status", "assignee", "bugwatch",
... "importance"]
>>> old_product_assignment = Snapshot(
... product_assignment, providing=IBugTask)
>>> product_assignment.transitionToStatus(
... BugTaskStatus.INVALID, getUtility(ILaunchBag).user)
>>> product_assignment_edited = ObjectModifiedEvent(
... product_assignment, old_product_assignment, edit_fields)
>>> notify(product_assignment_edited)
>>> latest_activity = bug.activity[-1]
>>> latest_activity.whatchanged
u'thunderbird: status'
>>> latest_activity.target
u'thunderbird'
>>> latest_activity.attribute
u'status'
>>> latest_activity.oldvalue == BugTaskStatus.NEW.title
True
>>> latest_activity.newvalue == BugTaskStatus.INVALID.title
True
Bug report is marked as a duplicate of another bug report
=========================================================
>>> edit_fields = [
... "id", "title", "description", "name",
... "private", "duplicateof", "security_related"]
>>> old_bug = Snapshot(bug, providing=IBug)
>>> latest_bug = factory.makeBug()
>>> bug.markAsDuplicate(latest_bug)
>>> bug_edited = ObjectModifiedEvent(bug, old_bug, edit_fields)
>>> notify(bug_edited)
>>> latest_activity = bug.activity[-1]
>>> latest_activity.whatchanged
u'marked as duplicate'
>>> latest_activity.oldvalue is None
True
>>> latest_activity.newvalue == unicode(latest_bug.id)
True
Bug report has its duplicate marker changed to another bug report
=================================================================
>>> edit_fields = [
... "id", "title", "description", "name", "private", "duplicateof",
... "security_related"]
>>> old_bug = Snapshot(bug, providing=IBug)
>>> another_bug = factory.makeBug()
>>> bug.markAsDuplicate(another_bug)
>>> bug_edited = ObjectModifiedEvent(bug, old_bug, edit_fields)
>>> notify(bug_edited)
>>> latest_activity = bug.activity[-1]
>>> latest_activity.whatchanged
u'changed duplicate marker'
>>> latest_activity.oldvalue == unicode(latest_bug.id)
True
>>> latest_activity.newvalue == unicode(another_bug.id)
True
The bug report is un-duplicated
===============================
>>> edit_fields = [
... "id", "title", "description", "name", "private", "duplicateof",
... "security_related"]
>>> old_bug = Snapshot(bug, providing=IBug)
>>> bug.markAsDuplicate(None)
>>> bug_edited = ObjectModifiedEvent(bug, old_bug, edit_fields)
>>> notify(bug_edited)
>>> latest_activity = bug.activity[-1]
>>> latest_activity.whatchanged
u'removed duplicate marker'
>>> latest_activity.oldvalue == unicode(another_bug.id)
True
>>> latest_activity.newvalue is None
True
A bug with multiple duplicates
==============================
When a bug has multiple duplicates and is itself marked a duplicate,
the duplicates are automatically duped to the same master bug. These changes
are then reflected in the activity log for each bug itself.
>>> edit_fields = [
... "id", "title", "description", "name", "private", "duplicateof",
... "security_related"]
>>> initial_bug = factory.makeBug()
>>> dupe_one = factory.makeBug()
>>> dupe_two = factory.makeBug()
>>> dupe_one.markAsDuplicate(initial_bug)
>>> dupe_two.markAsDuplicate(initial_bug)
After creating a few bugs to work with, we create a final bug and duplicate
the initial bug against it.
>>> final_bug = factory.makeBug()
>>> initial_bug.markAsDuplicate(final_bug)
Now, we confirm the activity log for the other bugs correctly list the
final_bug as their master bug.
>>> latest_activity = dupe_one.activity[-1]
>>> print latest_activity.whatchanged
changed duplicate marker
>>> latest_activity.oldvalue == unicode(initial_bug.id)
True
>>> latest_activity.newvalue == unicode(final_bug.id)
True
>>> latest_activity = dupe_two.activity[-1]
>>> print latest_activity.whatchanged
changed duplicate marker
>>> latest_activity.oldvalue == unicode(initial_bug.id)
True
>>> latest_activity.newvalue == unicode(final_bug.id)
True
BugActivityItem
===============
BugActivityItem implements the stuff that BugActivity doesn't need to
know about.
>>> import pytz
>>> from datetime import datetime
>>> from lp.bugs.browser.bugtask import BugActivityItem
>>> from lp.bugs.interfaces.bug import IBugSet
>>> from lp.bugs.interfaces.bugactivity import (
... IBugActivitySet)
>>> nowish = datetime(
... 2009, 3, 26, 16, 40, 31, tzinfo=pytz.timezone('UTC'))
>>> bug_one = getUtility(IBugSet).get(1)
>>> activity = getUtility(IBugActivitySet).new(
... bug=bug_one, whatchanged='summary', oldvalue='Old value',
... newvalue='New value', person=user, datechanged=nowish)
>>> activity_item = BugActivityItem(activity)
The BugActivityItem offers properties that can be used to render the
activity sensibly in an HTML interface. In most cases it just returns
activity.whatchanged.
>>> print activity_item.change_summary
summary
Summary changes are represented as unified diffs in the interface, in
the same way as they are in notifications. To ensure they display
properly in the UI, they're returned with newline characters replaces
with HTML line-breaks.
>>> print activity_item.change_details
- Old value<br />+ New value
BugActivityItem delegates to IBugActivity, so we can still access the
original BugActivity's properties if we want.
>>> print "%s: %s => %s" % (
... activity_item.whatchanged, activity_item.oldvalue,
... activity_item.newvalue)
summary: Old value => New value
For simpler changes, activity_item.change_details will simply return the
change in the form old_value -> new_value. The arrow will be represented
by the unicode character →.
>>> activity = getUtility(IBugActivitySet).new(
... bug=bug_one, whatchanged='security vulnerability',
... oldvalue='no', newvalue='yes', person=user,
... datechanged=nowish)
>>> activity_item = BugActivityItem(activity)
>>> print activity_item.change_details
no → yes
>>> activity = getUtility(IBugActivitySet).new(
... bug=bug_one, whatchanged='visibility', oldvalue='public',
... newvalue='private', person=user, datechanged=nowish)
>>> activity_item = BugActivityItem(activity)
>>> print activity_item.change_details
public → private
Tag changes use the _formatted_tags_change property of BugActivityItem
to create a nicely formatted change_details.
>>> activity = getUtility(IBugActivitySet).new(
... bug=bug_one, whatchanged='tags', oldvalue='tag1 tag2',
... newvalue='tag1 tag3', person=user, datechanged=nowish)
>>> activity_item = BugActivityItem(activity)
>>> print activity_item._formatted_tags_change
added: tag3
removed: tag2
The change_details value for this change will be that returned by
_formatted_tags_change but with newlines replaced by HTML line-breaks.
>>> print activity_item.change_details
added: tag3<br />removed: tag2
For changes to bug tasks, BugActivityItem returns the name of the attribute
that was changed (using the `attribute` property on the bug activity
discussed above).
>>> activity = getUtility(IBugActivitySet).new(
... bug=bug_one, whatchanged='malone: status', oldvalue='New',
... newvalue='Triaged', person=user, datechanged=nowish)
>>> activity_item = BugActivityItem(activity)
>>> print activity_item.change_summary
status
The change_details are expressed as a simple change.
>>> print activity_item.change_details
New → Triaged
For assignee changes, BugActivityItem will ensure that old or new values
of None will be converted to the string 'nobody'.
>>> activity = getUtility(IBugActivitySet).new(
... bug=bug_one, whatchanged='malone: assignee', oldvalue=None,
... newvalue='somebody', person=user, datechanged=nowish)
>>> activity_item = BugActivityItem(activity)
>>> print activity_item.change_details
nobody → somebody
>>> activity = getUtility(IBugActivitySet).new(
... bug=bug_one, whatchanged='malone: assignee',
... oldvalue='somebody', newvalue=None, person=user,
... datechanged=nowish)
>>> activity_item = BugActivityItem(activity)
>>> print activity_item.change_details
somebody → nobody
For changes to a bug's description, we simply return the word 'updated,'
since such changes may be too long to be useful as a diff.
>>> activity = getUtility(IBugActivitySet).new(
... bug=bug_one, whatchanged='description',
... oldvalue='Old description', newvalue='New description',
... person=user, datechanged=nowish)
>>> activity_item = BugActivityItem(activity)
>>> print "%s: %s" % (
... activity_item.change_summary, activity_item.change_details)
description: updated
|