Merge lp:~brian-murray/launchpad/595124 into lp:launchpad
- 595124
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 11108 | ||||
Proposed branch: | lp:~brian-murray/launchpad/595124 | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
443 lines (+164/-41) 12 files modified
lib/lp/bugs/browser/bugtask.py (+7/-4) lib/lp/bugs/configure.zcml (+2/-1) lib/lp/bugs/doc/bug.txt (+6/-6) lib/lp/bugs/doc/bugtask-expiration.txt (+30/-7) lib/lp/bugs/interfaces/bug.py (+18/-2) lib/lp/bugs/model/bug.py (+36/-9) lib/lp/bugs/model/bugtask.py (+1/-1) lib/lp/bugs/stories/bugs/xx-incomplete-bugs.txt (+16/-6) lib/lp/bugs/stories/webservice/xx-bug.txt (+42/-0) lib/lp/bugs/templates/bug-listing-expirable.pt (+3/-3) lib/lp/bugs/templates/bugtask-index.pt (+1/-1) lib/lp/bugs/tests/bug.py (+2/-1) |
||||
To merge this branch: | bzr merge lp:~brian-murray/launchpad/595124 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Leonard Richardson (community) | Approve | ||
Review via email: mp+28543@code.launchpad.net |
Commit message
unexport IBug.can_expire which is confusing instead create and export IBug.isExpirable() which can use a custom number of days.
Description of the change
A bug report's can_expire attribute has lead to some confusion as it shows if a bug may expire not whether or not it should expire. To remove any confusion I've modified can_expire to call findExpirableTasks with days_old equal to config.
This also changes the results displayed at +expirable-bugs so that only bugs that are expirable will show up not ones that match the criteria and have a days_old of less than 60. Deryck and I discussed this and are in agreement that this makes the most sense. However, individual bug pages will still display "will be marked for expiration in XYZ days" as isExpirable is called with days_old=0.
I've also created IBug.isExpirable() which accepts a quantity of days as an argument we can call findExpirableTasks with a different quantity of days_old - for example 0. This is also exported in the API as it may be useful for a project that wanted a different expiration period than the default or if a package maintainer wanted to be particularly aggressive in expiring bugs.
Tests include:
bin/test -cvvt xx-bug.txt -t bugtask-
Francis J. Lacoste (flacoste) wrote : | # |
I don't mind the backward incompatibility either. I think Brian is one of the heavy user of that API and if that's fine by him, I wouldn't second guess him here.
I have another concern which isn't introduce by this branch, which is that this means that sending a bugs representation makes an additional query to get the value of the can_expire attribute. I think this is very bad from a performance point of view. Especially when navigating through bug searches.
A better API would be to turn this into a collection exposed on the context and return the expirable bug tasks.
And just remove can_expire from the model. Maybe that's what we should do now? Mark it as removed, export only isExpirable() which can be used to retrieve the information if really needed. Exporting the set of expirable bugs might be done at another time (not sure if the IBugTarget has findExpirableBugs in it already).
Brian Murray (brian-murray) wrote : | # |
This made sense to me so I've removed can_expire from being exported in the development version, and kept it in previous versions, of the API and will report a bug about exporting findExirableBugs.
Leonard Richardson (leonardr) wrote : | # |
This now looks good. I have one advisory comment: only make this change if it makes sense.
isExpirable() uses the janitor to do a search because it doesn't have access to the user making the request. If you make that method take a user argument, you can have the web service fill in the current user automatically:
from lazr.restful.
@call_with(
def isExpirable(who, days_old=None):
...
Preview Diff
1 | === modified file 'lib/lp/bugs/browser/bugtask.py' | |||
2 | --- lib/lp/bugs/browser/bugtask.py 2010-06-05 10:41:52 +0000 | |||
3 | +++ lib/lp/bugs/browser/bugtask.py 2010-07-08 13:37:09 +0000 | |||
4 | @@ -1004,7 +1004,7 @@ | |||
5 | 1004 | @property | 1004 | @property |
6 | 1005 | def days_to_expiration(self): | 1005 | def days_to_expiration(self): |
7 | 1006 | """Return the number of days before the bug is expired, or None.""" | 1006 | """Return the number of days before the bug is expired, or None.""" |
9 | 1007 | if not self.context.bug.can_expire: | 1007 | if not self.context.bug.isExpirable(days_old=0): |
10 | 1008 | return None | 1008 | return None |
11 | 1009 | 1009 | ||
12 | 1010 | expire_after = timedelta(days=config.malone.days_before_expiration) | 1010 | expire_after = timedelta(days=config.malone.days_before_expiration) |
13 | @@ -1023,7 +1023,7 @@ | |||
14 | 1023 | 1023 | ||
15 | 1024 | If the bug is not due to be expired None will be returned. | 1024 | If the bug is not due to be expired None will be returned. |
16 | 1025 | """ | 1025 | """ |
18 | 1026 | if not self.context.bug.can_expire: | 1026 | if not self.context.bug.isExpirable(days_old=0): |
19 | 1027 | return None | 1027 | return None |
20 | 1028 | 1028 | ||
21 | 1029 | days_to_expiration = self.days_to_expiration | 1029 | days_to_expiration = self.days_to_expiration |
22 | @@ -1943,9 +1943,11 @@ | |||
23 | 1943 | The bugtarget may be an `IDistribution`, `IDistroSeries`, `IProduct`, | 1943 | The bugtarget may be an `IDistribution`, `IDistroSeries`, `IProduct`, |
24 | 1944 | or `IProductSeries`. | 1944 | or `IProductSeries`. |
25 | 1945 | """ | 1945 | """ |
26 | 1946 | days_old = config.malone.days_before_expiration | ||
27 | 1947 | |||
28 | 1946 | if target_has_expirable_bugs_listing(self.context): | 1948 | if target_has_expirable_bugs_listing(self.context): |
29 | 1947 | return getUtility(IBugTaskSet).findExpirableBugTasks( | 1949 | return getUtility(IBugTaskSet).findExpirableBugTasks( |
31 | 1948 | 0, user=self.user, target=self.context).count() | 1950 | days_old, user=self.user, target=self.context).count() |
32 | 1949 | else: | 1951 | else: |
33 | 1950 | return None | 1952 | return None |
34 | 1951 | 1953 | ||
35 | @@ -3761,9 +3763,10 @@ | |||
36 | 3761 | @property | 3763 | @property |
37 | 3762 | def search(self): | 3764 | def search(self): |
38 | 3763 | """Return an `ITableBatchNavigator` for the expirable bugtasks.""" | 3765 | """Return an `ITableBatchNavigator` for the expirable bugtasks.""" |
39 | 3766 | days_old = config.malone.days_before_expiration | ||
40 | 3764 | bugtaskset = getUtility(IBugTaskSet) | 3767 | bugtaskset = getUtility(IBugTaskSet) |
41 | 3765 | bugtasks = bugtaskset.findExpirableBugTasks( | 3768 | bugtasks = bugtaskset.findExpirableBugTasks( |
43 | 3766 | 0, user=self.user, target=self.context) | 3769 | days_old, user=self.user, target=self.context) |
44 | 3767 | return BugListingBatchNavigator( | 3770 | return BugListingBatchNavigator( |
45 | 3768 | bugtasks, self.request, columns_to_show=self.columns_to_show, | 3771 | bugtasks, self.request, columns_to_show=self.columns_to_show, |
46 | 3769 | size=config.malone.buglist_batch_size) | 3772 | size=config.malone.buglist_batch_size) |
47 | 3770 | 3773 | ||
48 | === modified file 'lib/lp/bugs/configure.zcml' | |||
49 | --- lib/lp/bugs/configure.zcml 2010-06-21 15:46:36 +0000 | |||
50 | +++ lib/lp/bugs/configure.zcml 2010-07-08 13:37:09 +0000 | |||
51 | @@ -661,7 +661,8 @@ | |||
52 | 661 | users_affected_with_dupes | 661 | users_affected_with_dupes |
53 | 662 | bug_messages | 662 | bug_messages |
54 | 663 | isUserAffected | 663 | isUserAffected |
56 | 664 | getHWSubmissions"/> | 664 | getHWSubmissions |
57 | 665 | isExpirable"/> | ||
58 | 665 | <require | 666 | <require |
59 | 666 | permission="launchpad.Edit" | 667 | permission="launchpad.Edit" |
60 | 667 | attributes=" | 668 | attributes=" |
61 | 668 | 669 | ||
62 | === modified file 'lib/lp/bugs/doc/bug.txt' | |||
63 | --- lib/lp/bugs/doc/bug.txt 2010-04-14 12:55:44 +0000 | |||
64 | +++ lib/lp/bugs/doc/bug.txt 2010-07-08 13:37:09 +0000 | |||
65 | @@ -1067,7 +1067,7 @@ | |||
66 | 1067 | -------------- | 1067 | -------------- |
67 | 1068 | 1068 | ||
68 | 1069 | Incomplete bug reports may expire when they become inactive. Expiration | 1069 | Incomplete bug reports may expire when they become inactive. Expiration |
70 | 1070 | is only available to projects that use Launchpad to track bugs. There | 1070 | is only available to projects that use Launchpad to track bugs. There are |
71 | 1071 | two properties related to expiration. IBug.permits_expiration tests | 1071 | two properties related to expiration. IBug.permits_expiration tests |
72 | 1072 | that the state of the bug permits expiration, and returns True or False. | 1072 | that the state of the bug permits expiration, and returns True or False. |
73 | 1073 | IBug.can_expire property returns True or False as to whether the bug | 1073 | IBug.can_expire property returns True or False as to whether the bug |
74 | @@ -1102,10 +1102,10 @@ | |||
75 | 1102 | False | 1102 | False |
76 | 1103 | 1103 | ||
77 | 1104 | Ubuntu has enabled bug expiration. Incomplete, unattended bugs can | 1104 | Ubuntu has enabled bug expiration. Incomplete, unattended bugs can |
79 | 1105 | expired. | 1105 | expire. |
80 | 1106 | 1106 | ||
81 | 1107 | >>> expirable_bugtask = create_old_bug( | 1107 | >>> expirable_bugtask = create_old_bug( |
83 | 1108 | ... 'bug c', 1, ubuntu, with_message=False) | 1108 | ... 'bug c', 61, ubuntu, with_message=False) |
84 | 1109 | >>> sync_bugtasks(expirable_bugtask) | 1109 | >>> sync_bugtasks(expirable_bugtask) |
85 | 1110 | 1110 | ||
86 | 1111 | >>> expirable_bugtask.status.name | 1111 | >>> expirable_bugtask.status.name |
87 | @@ -1117,9 +1117,9 @@ | |||
88 | 1117 | >>> expirable_bugtask.bug.can_expire | 1117 | >>> expirable_bugtask.bug.can_expire |
89 | 1118 | True | 1118 | True |
90 | 1119 | 1119 | ||
94 | 1120 | When the expirable_bugtask assigned, the bugtask is no longer in | 1120 | When the expirable_bugtask is assigned, the bugtask is no longer in an |
95 | 1121 | an expirable state, thus the bug cannot expire even though | 1121 | expirable state, thus the bug cannot expire even though bug permits |
96 | 1122 | bug permits expiration. | 1122 | expiration. |
97 | 1123 | 1123 | ||
98 | 1124 | >>> expirable_bugtask.transitionToAssignee(sample_person) | 1124 | >>> expirable_bugtask.transitionToAssignee(sample_person) |
99 | 1125 | >>> sync_bugtasks(expirable_bugtask) | 1125 | >>> sync_bugtasks(expirable_bugtask) |
100 | 1126 | 1126 | ||
101 | === modified file 'lib/lp/bugs/doc/bugtask-expiration.txt' | |||
102 | --- lib/lp/bugs/doc/bugtask-expiration.txt 2010-04-17 16:13:48 +0000 | |||
103 | +++ lib/lp/bugs/doc/bugtask-expiration.txt 2010-07-08 13:37:09 +0000 | |||
104 | @@ -188,14 +188,12 @@ | |||
105 | 188 | >>> milestone_bugtask.bug.can_expire | 188 | >>> milestone_bugtask.bug.can_expire |
106 | 189 | False | 189 | False |
107 | 190 | 190 | ||
111 | 191 | # A bugtask can be subject for expiration, even though it is less | 191 | # Create a bugtask that is not old enough to expire |
109 | 192 | # than the min_days_old. can_expire indicates whether the bugtask can be | ||
110 | 193 | # expired in the future, if it doesn't get any further activity. | ||
112 | 194 | >>> recent_bugtask = create_old_bug('recent', 31, ubuntu) | 192 | >>> recent_bugtask = create_old_bug('recent', 31, ubuntu) |
113 | 195 | >>> recent_bugtask.bug.permits_expiration | 193 | >>> recent_bugtask.bug.permits_expiration |
114 | 196 | True | 194 | True |
115 | 197 | >>> recent_bugtask.bug.can_expire | 195 | >>> recent_bugtask.bug.can_expire |
117 | 198 | True | 196 | False |
118 | 199 | 197 | ||
119 | 200 | # A bugtask that is not expirable; while the product uses Launchpad to | 198 | # A bugtask that is not expirable; while the product uses Launchpad to |
120 | 201 | # track bugs, enable_bug_expiration is set to False | 199 | # track bugs, enable_bug_expiration is set to False |
121 | @@ -229,9 +227,34 @@ | |||
122 | 229 | duplicate False 61 Incomplete False True False False | 227 | duplicate False 61 Incomplete False True False False |
123 | 230 | external False 61 Incomplete False False False False | 228 | external False 61 Incomplete False False False False |
124 | 231 | milestone False 61 Incomplete False False True False | 229 | milestone False 61 Incomplete False False True False |
126 | 232 | recent True 31 Incomplete False False False False | 230 | recent False 31 Incomplete False False False False |
127 | 233 | no_expire False 61 Incomplete False False False False | 231 | no_expire False 61 Incomplete False False False False |
128 | 234 | 232 | ||
129 | 233 | == isExpirable() == | ||
130 | 234 | |||
131 | 235 | In addition to can_expire bugs have an isExpirable method to which a custom | ||
132 | 236 | number of days, days_old, can be passed. days_old is then used with | ||
133 | 237 | findExpirableBugTasks. This allows projects to create their own janitor using | ||
134 | 238 | a different period for bug expiration. | ||
135 | 239 | |||
136 | 240 | # Check to ensure that isExpirable() works without days_old, then set the | ||
137 | 241 | # bug to Invalid so it doesn't affect the rest of the doctest | ||
138 | 242 | >>> from lp.bugs.tests.bug import create_old_bug | ||
139 | 243 | >>> very_old_bugtask = create_old_bug('expirable_distro', 351, ubuntu) | ||
140 | 244 | >>> very_old_bugtask.bug.isExpirable() | ||
141 | 245 | True | ||
142 | 246 | >>> very_old_bugtask.transitionToStatus( | ||
143 | 247 | ... BugTaskStatus.INVALID, sample_person) | ||
144 | 248 | |||
145 | 249 | # Pass isExpirable() a days_old parameter, then set the bug to Invalid so it | ||
146 | 250 | # doesn't affect the rest of the doctest | ||
147 | 251 | >>> from lp.bugs.tests.bug import create_old_bug | ||
148 | 252 | >>> not_so_old_bugtask = create_old_bug('expirable_distro', 31, ubuntu) | ||
149 | 253 | >>> not_so_old_bugtask.bug.isExpirable(days_old=14) | ||
150 | 254 | True | ||
151 | 255 | >>> not_so_old_bugtask.transitionToStatus( | ||
152 | 256 | ... BugTaskStatus.INVALID, sample_person) | ||
153 | 257 | |||
154 | 235 | 258 | ||
155 | 236 | == findExpirableBugTasks() Part 2 == | 259 | == findExpirableBugTasks() Part 2 == |
156 | 237 | 260 | ||
157 | @@ -322,7 +345,7 @@ | |||
158 | 322 | ROLE EXPIRE AGE STATUS ASSIGNED DUP MILE REPLIES | 345 | ROLE EXPIRE AGE STATUS ASSIGNED DUP MILE REPLIES |
159 | 323 | ubuntu True 351 Incomplete False False False False | 346 | ubuntu True 351 Incomplete False False False False |
160 | 324 | hoary True 351 Incomplete False False False False | 347 | hoary True 351 Incomplete False False False False |
162 | 325 | recent True 31 Incomplete False False False False | 348 | recent False 31 Incomplete False False False False |
163 | 326 | 349 | ||
164 | 327 | Thunderbird has not enabled bug expiration. Even when the min_days_old | 350 | Thunderbird has not enabled bug expiration. Even when the min_days_old |
165 | 328 | is set to 0, no bugtasks are replaced. | 351 | is set to 0, no bugtasks are replaced. |
166 | @@ -479,7 +502,7 @@ | |||
167 | 479 | duplicate False 61 Incomplete False True False False | 502 | duplicate False 61 Incomplete False True False False |
168 | 480 | external False 61 Incomplete False False False False | 503 | external False 61 Incomplete False False False False |
169 | 481 | milestone False 61 Incomplete False False True False | 504 | milestone False 61 Incomplete False False True False |
171 | 482 | recent True 31 Incomplete False False False False | 505 | recent False 31 Incomplete False False False False |
172 | 483 | no_expire False 61 Incomplete False False False False | 506 | no_expire False 61 Incomplete False False False False |
173 | 484 | 507 | ||
174 | 485 | The bugtasks statusexplanation was updated to explain the change in | 508 | The bugtasks statusexplanation was updated to explain the change in |
175 | 486 | 509 | ||
176 | === modified file 'lib/lp/bugs/interfaces/bug.py' | |||
177 | --- lib/lp/bugs/interfaces/bug.py 2010-06-10 18:55:22 +0000 | |||
178 | +++ lib/lp/bugs/interfaces/bug.py 2010-07-08 13:37:09 +0000 | |||
179 | @@ -277,10 +277,11 @@ | |||
180 | 277 | readonly=True) | 277 | readonly=True) |
181 | 278 | can_expire = exported( | 278 | can_expire = exported( |
182 | 279 | Bool( | 279 | Bool( |
184 | 280 | title=_("Can the Incomplete bug expire if it becomes inactive? " | 280 | title=_("Can the Incomplete bug expire? " |
185 | 281 | "Expiration may happen when the bug permits expiration, " | 281 | "Expiration may happen when the bug permits expiration, " |
186 | 282 | "and a bugtask cannot be confirmed."), | 282 | "and a bugtask cannot be confirmed."), |
188 | 283 | readonly=True)) | 283 | readonly=True), |
189 | 284 | ('devel', dict(exported=False)), exported=True) | ||
190 | 284 | date_last_message = exported( | 285 | date_last_message = exported( |
191 | 285 | Datetime(title=_("Date of last bug message"), | 286 | Datetime(title=_("Date of last bug message"), |
192 | 286 | required=False, readonly=True)) | 287 | required=False, readonly=True)) |
193 | @@ -809,6 +810,21 @@ | |||
194 | 809 | def updateHeat(): | 810 | def updateHeat(): |
195 | 810 | """Update the heat for the bug.""" | 811 | """Update the heat for the bug.""" |
196 | 811 | 812 | ||
197 | 813 | @operation_parameters( | ||
198 | 814 | days_old=Int( | ||
199 | 815 | title=_('Number of days of inactivity for which to check.'), | ||
200 | 816 | required=False)) | ||
201 | 817 | @export_read_operation() | ||
202 | 818 | def isExpirable(days_old=None): | ||
203 | 819 | """Is this bug eligible for expiration and was it last updated | ||
204 | 820 | more than X days ago? | ||
205 | 821 | |||
206 | 822 | If days_old is None the default number of days without activity | ||
207 | 823 | is used. | ||
208 | 824 | |||
209 | 825 | Returns True or False. | ||
210 | 826 | """ | ||
211 | 827 | |||
212 | 812 | class InvalidDuplicateValue(Exception): | 828 | class InvalidDuplicateValue(Exception): |
213 | 813 | """A bug cannot be set as the duplicate of another.""" | 829 | """A bug cannot be set as the duplicate of another.""" |
214 | 814 | webservice_error(417) | 830 | webservice_error(417) |
215 | 815 | 831 | ||
216 | === modified file 'lib/lp/bugs/model/bug.py' | |||
217 | --- lib/lp/bugs/model/bug.py 2010-06-22 16:08:05 +0000 | |||
218 | +++ lib/lp/bugs/model/bug.py 2010-07-08 13:37:09 +0000 | |||
219 | @@ -42,6 +42,7 @@ | |||
220 | 42 | ObjectCreatedEvent, ObjectDeletedEvent, ObjectModifiedEvent) | 42 | ObjectCreatedEvent, ObjectDeletedEvent, ObjectModifiedEvent) |
221 | 43 | from lazr.lifecycle.snapshot import Snapshot | 43 | from lazr.lifecycle.snapshot import Snapshot |
222 | 44 | 44 | ||
223 | 45 | from canonical.config import config | ||
224 | 45 | from canonical.database.constants import UTC_NOW | 46 | from canonical.database.constants import UTC_NOW |
225 | 46 | from canonical.database.datetimecol import UtcDateTimeCol | 47 | from canonical.database.datetimecol import UtcDateTimeCol |
226 | 47 | from canonical.database.sqlbase import cursor, SQLBase, sqlvalues | 48 | from canonical.database.sqlbase import cursor, SQLBase, sqlvalues |
227 | @@ -436,7 +437,7 @@ | |||
228 | 436 | enabled_bug_expiration set to True can be expired. To qualify for | 437 | enabled_bug_expiration set to True can be expired. To qualify for |
229 | 437 | expiration, the bug and its bugtasks meet the follow conditions: | 438 | expiration, the bug and its bugtasks meet the follow conditions: |
230 | 438 | 439 | ||
232 | 439 | 1. The bug is inactive; the last update of the is older than | 440 | 1. The bug is inactive; the last update of the bug is older than |
233 | 440 | Launchpad expiration age. | 441 | Launchpad expiration age. |
234 | 441 | 2. The bug is not a duplicate. | 442 | 2. The bug is not a duplicate. |
235 | 442 | 3. The bug has at least one message (a request for more information). | 443 | 3. The bug has at least one message (a request for more information). |
236 | @@ -454,14 +455,40 @@ | |||
237 | 454 | if not self.permits_expiration: | 455 | if not self.permits_expiration: |
238 | 455 | return False | 456 | return False |
239 | 456 | 457 | ||
248 | 457 | # Do the search as the Janitor, to ensure that this bug can be | 458 | days_old = config.malone.days_before_expiration |
249 | 458 | # found, even if it's private. We don't have access to the user | 459 | # Do the search as the Janitor, to ensure that this bug can be |
250 | 459 | # calling this property. If the user has access to view this | 460 | # found, even if it's private. We don't have access to the user |
251 | 460 | # property, he has permission to see the bug, so we're not | 461 | # calling this property. If the user has access to view this |
252 | 461 | # exposing something we shouldn't. The Janitor has access to | 462 | # property, he has permission to see the bug, so we're not |
253 | 462 | # view all bugs. | 463 | # exposing something we shouldn't. The Janitor has access to |
254 | 463 | bugtasks = getUtility(IBugTaskSet).findExpirableBugTasks( | 464 | # view all bugs. |
255 | 464 | 0, getUtility(ILaunchpadCelebrities).janitor, bug=self) | 465 | bugtasks = getUtility(IBugTaskSet).findExpirableBugTasks( |
256 | 466 | days_old, getUtility(ILaunchpadCelebrities).janitor, bug=self) | ||
257 | 467 | return bugtasks.count() > 0 | ||
258 | 468 | |||
259 | 469 | def isExpirable(self, days_old=None): | ||
260 | 470 | """See `IBug`.""" | ||
261 | 471 | |||
262 | 472 | # If days_old is None read it from the Launchpad configuration | ||
263 | 473 | # and use that value | ||
264 | 474 | if days_old is None: | ||
265 | 475 | days_old = config.malone.days_before_expiration | ||
266 | 476 | |||
267 | 477 | # IBugTaskSet.findExpirableBugTasks() is the authoritative determiner | ||
268 | 478 | # if a bug can expire, but it is expensive. We do a general check | ||
269 | 479 | # to verify the bug permits expiration before using IBugTaskSet to | ||
270 | 480 | # determine if a bugtask can cause expiration. | ||
271 | 481 | if not self.permits_expiration: | ||
272 | 482 | return False | ||
273 | 483 | |||
274 | 484 | # Do the search as the Janitor, to ensure that this bug can be | ||
275 | 485 | # found, even if it's private. We don't have access to the user | ||
276 | 486 | # calling this property. If the user has access to view this | ||
277 | 487 | # property, he has permission to see the bug, so we're not | ||
278 | 488 | # exposing something we shouldn't. The Janitor has access to | ||
279 | 489 | # view all bugs. | ||
280 | 490 | bugtasks = getUtility(IBugTaskSet).findExpirableBugTasks( | ||
281 | 491 | days_old, getUtility(ILaunchpadCelebrities).janitor, bug=self) | ||
282 | 465 | return bugtasks.count() > 0 | 492 | return bugtasks.count() > 0 |
283 | 466 | 493 | ||
284 | 467 | @property | 494 | @property |
285 | 468 | 495 | ||
286 | === modified file 'lib/lp/bugs/model/bugtask.py' | |||
287 | --- lib/lp/bugs/model/bugtask.py 2010-06-25 18:48:13 +0000 | |||
288 | +++ lib/lp/bugs/model/bugtask.py 2010-07-08 13:37:09 +0000 | |||
289 | @@ -2265,7 +2265,7 @@ | |||
290 | 2265 | transitionToStatus() method. See 'Conjoined Bug Tasks' in | 2265 | transitionToStatus() method. See 'Conjoined Bug Tasks' in |
291 | 2266 | c.l.doc/bugtasks.txt. | 2266 | c.l.doc/bugtasks.txt. |
292 | 2267 | 2267 | ||
294 | 2268 | Only bugtask the specified user has permission to view are | 2268 | Only bugtasks the specified user has permission to view are |
295 | 2269 | returned. The Janitor celebrity has permission to view all bugs. | 2269 | returned. The Janitor celebrity has permission to view all bugs. |
296 | 2270 | """ | 2270 | """ |
297 | 2271 | if bug is None: | 2271 | if bug is None: |
298 | 2272 | 2272 | ||
299 | === modified file 'lib/lp/bugs/stories/bugs/xx-incomplete-bugs.txt' | |||
300 | --- lib/lp/bugs/stories/bugs/xx-incomplete-bugs.txt 2010-01-23 14:17:49 +0000 | |||
301 | +++ lib/lp/bugs/stories/bugs/xx-incomplete-bugs.txt 2010-07-08 13:37:09 +0000 | |||
302 | @@ -130,11 +130,18 @@ | |||
303 | 130 | 130 | ||
304 | 131 | Users can view a list of expirable bugs via a link on the project's | 131 | Users can view a list of expirable bugs via a link on the project's |
305 | 132 | bug page. To see the behaviour of the bug listing, we need another | 132 | bug page. To see the behaviour of the bug listing, we need another |
307 | 133 | expirable bug. No Privileges Person marks another bug as incomplete. | 133 | expirable bug. No Privileges Person marks another bug as Incomplete |
308 | 134 | and does so some time ago so it appears as expirable. | ||
309 | 134 | 135 | ||
310 | 135 | >>> user_browser.open('http://bugs.launchpad.dev/jokosher/+bug/12') | 136 | >>> user_browser.open('http://bugs.launchpad.dev/jokosher/+bug/12') |
311 | 136 | >>> user_browser.getControl('Status').value = ['Incomplete'] | 137 | >>> user_browser.getControl('Status').value = ['Incomplete'] |
312 | 137 | >>> user_browser.getControl('Save Changes', index=0).click() | 138 | >>> user_browser.getControl('Save Changes', index=0).click() |
313 | 139 | >>> login('test@canonical.com') | ||
314 | 140 | >>> bug_12 = getUtility(IBugSet).get(12) | ||
315 | 141 | >>> time_delta = timedelta(days=60) | ||
316 | 142 | >>> bug_12.date_last_updated = bug_12.date_last_updated - time_delta | ||
317 | 143 | >>> flush_database_updates() | ||
318 | 144 | >>> logout() | ||
319 | 138 | 145 | ||
320 | 139 | The project's bug page reports the number of bugs that will expire if | 146 | The project's bug page reports the number of bugs that will expire if |
321 | 140 | they are not confirmed. No Privileges Person sees that Jokosher has 2 | 147 | they are not confirmed. No Privileges Person sees that Jokosher has 2 |
322 | @@ -180,12 +187,15 @@ | |||
323 | 180 | 187 | ||
324 | 181 | The listing is sorted in order of most inactive to least inactive. The | 188 | The listing is sorted in order of most inactive to least inactive. The |
325 | 182 | bugs at the top of the list will expire before the ones at the bottom. | 189 | bugs at the top of the list will expire before the ones at the bottom. |
328 | 183 | When No Privileges Person adds a comment to the oldest bug, it is | 190 | When bug 12's date_last_updated is modified to be older than bug 11's it |
329 | 184 | pushed to the bottom of the list. | 191 | appears as more inactive than bug 11 and is pushed to the top of the list. |
330 | 185 | 192 | ||
334 | 186 | >>> user_browser.getLink('Make Jokosher use autoaudiosink').click() | 193 | >>> login('test@canonical.com') |
335 | 187 | >>> user_browser.getControl(name='field.comment').value = "bump" | 194 | >>> bug_12 = getUtility(IBugSet).get(12) |
336 | 188 | >>> user_browser.getControl('Post Comment').click() | 195 | >>> time_delta = timedelta(days=2) |
337 | 196 | >>> bug_12.date_last_updated = bug_12.date_last_updated - time_delta | ||
338 | 197 | >>> flush_database_updates() | ||
339 | 198 | >>> logout() | ||
340 | 189 | >>> user_browser.getLink('Bugs').click() | 199 | >>> user_browser.getLink('Bugs').click() |
341 | 190 | >>> user_browser.getLink('Incomplete bugs').click() | 200 | >>> user_browser.getLink('Incomplete bugs').click() |
342 | 191 | >>> contents = find_main_content(user_browser.contents) | 201 | >>> contents = find_main_content(user_browser.contents) |
343 | 192 | 202 | ||
344 | === modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt' | |||
345 | --- lib/lp/bugs/stories/webservice/xx-bug.txt 2010-06-24 17:09:14 +0000 | |||
346 | +++ lib/lp/bugs/stories/webservice/xx-bug.txt 2010-07-08 13:37:09 +0000 | |||
347 | @@ -2002,3 +2002,45 @@ | |||
348 | 2002 | ... branch_entry['bug_link']).jsonBody() | 2002 | ... branch_entry['bug_link']).jsonBody() |
349 | 2003 | >>> print bug_link['self_link'] | 2003 | >>> print bug_link['self_link'] |
350 | 2004 | http://.../bugs/4 | 2004 | http://.../bugs/4 |
351 | 2005 | |||
352 | 2006 | Bug expiration | ||
353 | 2007 | -------------- | ||
354 | 2008 | |||
355 | 2009 | In addition to can_expire bugs have an isExpirable method to which a custom time | ||
356 | 2010 | period, days_old, can be passed. This is then used with | ||
357 | 2011 | findExpirableBugTasks. This allows projects to create their own janitor using | ||
358 | 2012 | a different period for bug expiration. | ||
359 | 2013 | |||
360 | 2014 | Check to ensure that isExpirable() works without days_old. | ||
361 | 2015 | |||
362 | 2016 | >>> bug_four = webservice.get("/bugs/4").jsonBody() | ||
363 | 2017 | >>> print webservice.named_get(bug_four['self_link'], | ||
364 | 2018 | ... 'isExpirable').jsonBody() | ||
365 | 2019 | False | ||
366 | 2020 | |||
367 | 2021 | Pass isExpirable() an integer for days_old. | ||
368 | 2022 | |||
369 | 2023 | >>> bug_four = webservice.get("/bugs/4").jsonBody() | ||
370 | 2024 | >>> print webservice.named_get(bug_four['self_link'], 'isExpirable', | ||
371 | 2025 | ... days_old='14').jsonBody() | ||
372 | 2026 | False | ||
373 | 2027 | |||
374 | 2028 | Pass isExpirable() a string for days_old. | ||
375 | 2029 | |||
376 | 2030 | >>> bug_four = webservice.get("/bugs/4").jsonBody() | ||
377 | 2031 | >>> print webservice.named_get(bug_four['self_link'], 'isExpirable', | ||
378 | 2032 | ... days_old='sixty') | ||
379 | 2033 | HTTP/1.1 400 Bad Request | ||
380 | 2034 | ... | ||
381 | 2035 | days_old: got 'unicode', expected int: u'sixty' | ||
382 | 2036 | |||
383 | 2037 | Can expire | ||
384 | 2038 | ---------- | ||
385 | 2039 | |||
386 | 2040 | can_expire is not exported in the development version of the API. | ||
387 | 2041 | |||
388 | 2042 | >>> bug_four = webservice.get("/bugs/4", api_version='devel').jsonBody() | ||
389 | 2043 | >>> bug_four[can_expire] | ||
390 | 2044 | Traceback (most recent call last): | ||
391 | 2045 | ... | ||
392 | 2046 | NameError: name 'can_expire' is not defined | ||
393 | 2005 | 2047 | ||
394 | === modified file 'lib/lp/bugs/templates/bug-listing-expirable.pt' | |||
395 | --- lib/lp/bugs/templates/bug-listing-expirable.pt 2009-09-01 11:06:23 +0000 | |||
396 | +++ lib/lp/bugs/templates/bug-listing-expirable.pt 2010-07-08 13:37:09 +0000 | |||
397 | @@ -25,9 +25,9 @@ | |||
398 | 25 | 25 | ||
399 | 26 | <tal:expirable-bugs condition="view/can_show_expirable_bugs"> | 26 | <tal:expirable-bugs condition="view/can_show_expirable_bugs"> |
400 | 27 | <p> | 27 | <p> |
404 | 28 | Incomplete bug reports can expire if they are unattended and they | 28 | Incomplete bug reports can expire if they are unattended and are |
405 | 29 | become inactive. These bugs are not confirmed, or in a status | 29 | inactive. These bugs are not confirmed, or in a status that |
406 | 30 | that indicates they were confirmed. An Incomplete bug can remain | 30 | indicates they were confirmed. An Incomplete bug can remain |
407 | 31 | in this list indefinitely, so long as the bug is regularly updated. | 31 | in this list indefinitely, so long as the bug is regularly updated. |
408 | 32 | See <a href="https://help.launchpad.net/Bugs/Expiry">Bugs/Expiry. | 32 | See <a href="https://help.launchpad.net/Bugs/Expiry">Bugs/Expiry. |
409 | 33 | </a> | 33 | </a> |
410 | 34 | 34 | ||
411 | === modified file 'lib/lp/bugs/templates/bugtask-index.pt' | |||
412 | --- lib/lp/bugs/templates/bugtask-index.pt 2010-06-24 13:13:01 +0000 | |||
413 | +++ lib/lp/bugs/templates/bugtask-index.pt 2010-07-08 13:37:09 +0000 | |||
414 | @@ -72,7 +72,7 @@ | |||
415 | 72 | 72 | ||
416 | 73 | <p | 73 | <p |
417 | 74 | id="can-expire" | 74 | id="can-expire" |
419 | 75 | tal:condition="context/bug/can_expire" | 75 | tal:condition="python: context.bug.isExpirable(days_old=0)" |
420 | 76 | > | 76 | > |
421 | 77 | <tal:expiration_message replace="view/expiration_message" /> | 77 | <tal:expiration_message replace="view/expiration_message" /> |
422 | 78 | (<a href="https://help.launchpad.net/BugExpiry">find out why</a>) | 78 | (<a href="https://help.launchpad.net/BugExpiry">find out why</a>) |
423 | 79 | 79 | ||
424 | === modified file 'lib/lp/bugs/tests/bug.py' | |||
425 | --- lib/lp/bugs/tests/bug.py 2010-06-04 17:47:34 +0000 | |||
426 | +++ lib/lp/bugs/tests/bug.py 2010-07-08 13:37:09 +0000 | |||
427 | @@ -15,6 +15,7 @@ | |||
428 | 15 | from zope.component import getUtility | 15 | from zope.component import getUtility |
429 | 16 | from zope.security.proxy import removeSecurityProxy | 16 | from zope.security.proxy import removeSecurityProxy |
430 | 17 | 17 | ||
431 | 18 | from canonical.config import config | ||
432 | 18 | from canonical.launchpad.ftests import sync | 19 | from canonical.launchpad.ftests import sync |
433 | 19 | from canonical.launchpad.testing.pages import ( | 20 | from canonical.launchpad.testing.pages import ( |
434 | 20 | extract_text, find_tag_by_id, find_main_content, find_tags_by_class, | 21 | extract_text, find_tag_by_id, find_main_content, find_tags_by_class, |
435 | @@ -220,7 +221,7 @@ | |||
436 | 220 | """Summarize a sequence of bugtasks.""" | 221 | """Summarize a sequence of bugtasks.""" |
437 | 221 | bugtaskset = getUtility(IBugTaskSet) | 222 | bugtaskset = getUtility(IBugTaskSet) |
438 | 222 | expirable_bugtasks = list(bugtaskset.findExpirableBugTasks( | 223 | expirable_bugtasks = list(bugtaskset.findExpirableBugTasks( |
440 | 223 | 0, getUtility(ILaunchpadCelebrities).janitor)) | 224 | config.malone.days_before_expiration, getUtility(ILaunchpadCelebrities).janitor)) |
441 | 224 | print 'ROLE EXPIRE AGE STATUS ASSIGNED DUP MILE REPLIES' | 225 | print 'ROLE EXPIRE AGE STATUS ASSIGNED DUP MILE REPLIES' |
442 | 225 | for bugtask in sorted(set(bugtasks), key=attrgetter('id')): | 226 | for bugtask in sorted(set(bugtasks), key=attrgetter('id')): |
443 | 226 | if len(bugtask.bug.bugtasks) == 1: | 227 | if len(bugtask.bug.bugtasks) == 1: |
This all looks good except for one minor point: you are changing the behavior of the web service's can_expire attribute in a backwards- incompatible way. Bugs that used to have can_expire=False will now have can_expire=True without the bugs themselves changing.
I personally don't think the change is big enough to warrant backwards- compatibility, but here's how to do it if Francis disagrees.
Declare two attributes in IBug, can_expire (which uses your current implementation) and can_expire_beta:
can_expire = exported(Bool(...), ('devel', dict(exported= True),
'beta' , dict(exported= False)) )
can_expire_beta = exported(Bool(...), exported_ as='can_ expire' ,
('devel' , dict(exported= False))
This will make can_expire_beta show up as 'can_expire' in 'beta' and '1.0', but will replace it with can_expire in 'devel'.
I may not have the syntax right; refer to lazr.restful src/lazr/ restful/ examples/ multiversion/ resource. py and src/lazr/ restful/ docs/multiversi on.txt for help.