Merge lp:~brian-murray/launchpad/595124 into lp:launchpad

Proposed by Brian Murray
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
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.malone.days_before_expiration, (60 by the way), so it will reflect whether or not a bug should expire.

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-expiration.txt -t xx-incomplete-bugs.txt

To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

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/multiversion.txt for help.

review: Needs Fixing
Revision history for this message
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).

Revision history for this message
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.

Revision history for this message
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.declarations import call_with, REQUEST_USER
@call_with(who=REQUEST_USER)
def isExpirable(who, days_old=None):
    ...

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2010-06-05 10:41:52 +0000
+++ lib/lp/bugs/browser/bugtask.py 2010-07-08 13:37:09 +0000
@@ -1004,7 +1004,7 @@
1004 @property1004 @property
1005 def days_to_expiration(self):1005 def days_to_expiration(self):
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."""
1007 if not self.context.bug.can_expire:1007 if not self.context.bug.isExpirable(days_old=0):
1008 return None1008 return None
10091009
1010 expire_after = timedelta(days=config.malone.days_before_expiration)1010 expire_after = timedelta(days=config.malone.days_before_expiration)
@@ -1023,7 +1023,7 @@
10231023
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.
1025 """1025 """
1026 if not self.context.bug.can_expire:1026 if not self.context.bug.isExpirable(days_old=0):
1027 return None1027 return None
10281028
1029 days_to_expiration = self.days_to_expiration1029 days_to_expiration = self.days_to_expiration
@@ -1943,9 +1943,11 @@
1943 The bugtarget may be an `IDistribution`, `IDistroSeries`, `IProduct`,1943 The bugtarget may be an `IDistribution`, `IDistroSeries`, `IProduct`,
1944 or `IProductSeries`.1944 or `IProductSeries`.
1945 """1945 """
1946 days_old = config.malone.days_before_expiration
1947
1946 if target_has_expirable_bugs_listing(self.context):1948 if target_has_expirable_bugs_listing(self.context):
1947 return getUtility(IBugTaskSet).findExpirableBugTasks(1949 return getUtility(IBugTaskSet).findExpirableBugTasks(
1948 0, user=self.user, target=self.context).count()1950 days_old, user=self.user, target=self.context).count()
1949 else:1951 else:
1950 return None1952 return None
19511953
@@ -3761,9 +3763,10 @@
3761 @property3763 @property
3762 def search(self):3764 def search(self):
3763 """Return an `ITableBatchNavigator` for the expirable bugtasks."""3765 """Return an `ITableBatchNavigator` for the expirable bugtasks."""
3766 days_old = config.malone.days_before_expiration
3764 bugtaskset = getUtility(IBugTaskSet)3767 bugtaskset = getUtility(IBugTaskSet)
3765 bugtasks = bugtaskset.findExpirableBugTasks(3768 bugtasks = bugtaskset.findExpirableBugTasks(
3766 0, user=self.user, target=self.context)3769 days_old, user=self.user, target=self.context)
3767 return BugListingBatchNavigator(3770 return BugListingBatchNavigator(
3768 bugtasks, self.request, columns_to_show=self.columns_to_show,3771 bugtasks, self.request, columns_to_show=self.columns_to_show,
3769 size=config.malone.buglist_batch_size)3772 size=config.malone.buglist_batch_size)
37703773
=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml 2010-06-21 15:46:36 +0000
+++ lib/lp/bugs/configure.zcml 2010-07-08 13:37:09 +0000
@@ -661,7 +661,8 @@
661 users_affected_with_dupes661 users_affected_with_dupes
662 bug_messages662 bug_messages
663 isUserAffected663 isUserAffected
664 getHWSubmissions"/>664 getHWSubmissions
665 isExpirable"/>
665 <require666 <require
666 permission="launchpad.Edit"667 permission="launchpad.Edit"
667 attributes="668 attributes="
668669
=== modified file 'lib/lp/bugs/doc/bug.txt'
--- lib/lp/bugs/doc/bug.txt 2010-04-14 12:55:44 +0000
+++ lib/lp/bugs/doc/bug.txt 2010-07-08 13:37:09 +0000
@@ -1067,7 +1067,7 @@
1067--------------1067--------------
10681068
1069Incomplete bug reports may expire when they become inactive. Expiration1069Incomplete bug reports may expire when they become inactive. Expiration
1070is only available to projects that use Launchpad to track bugs. There1070is only available to projects that use Launchpad to track bugs. There are
1071two properties related to expiration. IBug.permits_expiration tests1071two properties related to expiration. IBug.permits_expiration tests
1072that the state of the bug permits expiration, and returns True or False.1072that the state of the bug permits expiration, and returns True or False.
1073IBug.can_expire property returns True or False as to whether the bug1073IBug.can_expire property returns True or False as to whether the bug
@@ -1102,10 +1102,10 @@
1102 False1102 False
11031103
1104Ubuntu has enabled bug expiration. Incomplete, unattended bugs can1104Ubuntu has enabled bug expiration. Incomplete, unattended bugs can
1105expired.1105expire.
11061106
1107 >>> expirable_bugtask = create_old_bug(1107 >>> expirable_bugtask = create_old_bug(
1108 ... 'bug c', 1, ubuntu, with_message=False)1108 ... 'bug c', 61, ubuntu, with_message=False)
1109 >>> sync_bugtasks(expirable_bugtask)1109 >>> sync_bugtasks(expirable_bugtask)
11101110
1111 >>> expirable_bugtask.status.name1111 >>> expirable_bugtask.status.name
@@ -1117,9 +1117,9 @@
1117 >>> expirable_bugtask.bug.can_expire1117 >>> expirable_bugtask.bug.can_expire
1118 True1118 True
11191119
1120When the expirable_bugtask assigned, the bugtask is no longer in1120When the expirable_bugtask is assigned, the bugtask is no longer in an
1121an expirable state, thus the bug cannot expire even though1121expirable state, thus the bug cannot expire even though bug permits
1122bug permits expiration.1122expiration.
11231123
1124 >>> expirable_bugtask.transitionToAssignee(sample_person)1124 >>> expirable_bugtask.transitionToAssignee(sample_person)
1125 >>> sync_bugtasks(expirable_bugtask)1125 >>> sync_bugtasks(expirable_bugtask)
11261126
=== modified file 'lib/lp/bugs/doc/bugtask-expiration.txt'
--- lib/lp/bugs/doc/bugtask-expiration.txt 2010-04-17 16:13:48 +0000
+++ lib/lp/bugs/doc/bugtask-expiration.txt 2010-07-08 13:37:09 +0000
@@ -188,14 +188,12 @@
188 >>> milestone_bugtask.bug.can_expire188 >>> milestone_bugtask.bug.can_expire
189 False189 False
190190
191 # A bugtask can be subject for expiration, even though it is less191 # Create a bugtask that is not old enough to expire
192 # than the min_days_old. can_expire indicates whether the bugtask can be
193 # expired in the future, if it doesn't get any further activity.
194 >>> recent_bugtask = create_old_bug('recent', 31, ubuntu)192 >>> recent_bugtask = create_old_bug('recent', 31, ubuntu)
195 >>> recent_bugtask.bug.permits_expiration193 >>> recent_bugtask.bug.permits_expiration
196 True194 True
197 >>> recent_bugtask.bug.can_expire195 >>> recent_bugtask.bug.can_expire
198 True196 False
199197
200 # A bugtask that is not expirable; while the product uses Launchpad to198 # A bugtask that is not expirable; while the product uses Launchpad to
201 # track bugs, enable_bug_expiration is set to False199 # track bugs, enable_bug_expiration is set to False
@@ -229,9 +227,34 @@
229 duplicate False 61 Incomplete False True False False227 duplicate False 61 Incomplete False True False False
230 external False 61 Incomplete False False False False228 external False 61 Incomplete False False False False
231 milestone False 61 Incomplete False False True False229 milestone False 61 Incomplete False False True False
232 recent True 31 Incomplete False False False False230 recent False 31 Incomplete False False False False
233 no_expire False 61 Incomplete False False False False231 no_expire False 61 Incomplete False False False False
234232
233== isExpirable() ==
234
235In addition to can_expire bugs have an isExpirable method to which a custom
236number of days, days_old, can be passed. days_old is then used with
237findExpirableBugTasks. This allows projects to create their own janitor using
238a different period for bug expiration.
239
240 # Check to ensure that isExpirable() works without days_old, then set the
241 # bug to Invalid so it doesn't affect the rest of the doctest
242 >>> from lp.bugs.tests.bug import create_old_bug
243 >>> very_old_bugtask = create_old_bug('expirable_distro', 351, ubuntu)
244 >>> very_old_bugtask.bug.isExpirable()
245 True
246 >>> very_old_bugtask.transitionToStatus(
247 ... BugTaskStatus.INVALID, sample_person)
248
249 # Pass isExpirable() a days_old parameter, then set the bug to Invalid so it
250 # doesn't affect the rest of the doctest
251 >>> from lp.bugs.tests.bug import create_old_bug
252 >>> not_so_old_bugtask = create_old_bug('expirable_distro', 31, ubuntu)
253 >>> not_so_old_bugtask.bug.isExpirable(days_old=14)
254 True
255 >>> not_so_old_bugtask.transitionToStatus(
256 ... BugTaskStatus.INVALID, sample_person)
257
235258
236== findExpirableBugTasks() Part 2 ==259== findExpirableBugTasks() Part 2 ==
237260
@@ -322,7 +345,7 @@
322 ROLE EXPIRE AGE STATUS ASSIGNED DUP MILE REPLIES345 ROLE EXPIRE AGE STATUS ASSIGNED DUP MILE REPLIES
323 ubuntu True 351 Incomplete False False False False346 ubuntu True 351 Incomplete False False False False
324 hoary True 351 Incomplete False False False False347 hoary True 351 Incomplete False False False False
325 recent True 31 Incomplete False False False False348 recent False 31 Incomplete False False False False
326349
327Thunderbird has not enabled bug expiration. Even when the min_days_old350Thunderbird has not enabled bug expiration. Even when the min_days_old
328is set to 0, no bugtasks are replaced.351is set to 0, no bugtasks are replaced.
@@ -479,7 +502,7 @@
479 duplicate False 61 Incomplete False True False False502 duplicate False 61 Incomplete False True False False
480 external False 61 Incomplete False False False False503 external False 61 Incomplete False False False False
481 milestone False 61 Incomplete False False True False504 milestone False 61 Incomplete False False True False
482 recent True 31 Incomplete False False False False505 recent False 31 Incomplete False False False False
483 no_expire False 61 Incomplete False False False False506 no_expire False 61 Incomplete False False False False
484507
485The bugtasks statusexplanation was updated to explain the change in508The bugtasks statusexplanation was updated to explain the change in
486509
=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py 2010-06-10 18:55:22 +0000
+++ lib/lp/bugs/interfaces/bug.py 2010-07-08 13:37:09 +0000
@@ -277,10 +277,11 @@
277 readonly=True)277 readonly=True)
278 can_expire = exported(278 can_expire = exported(
279 Bool(279 Bool(
280 title=_("Can the Incomplete bug expire if it becomes inactive? "280 title=_("Can the Incomplete bug expire? "
281 "Expiration may happen when the bug permits expiration, "281 "Expiration may happen when the bug permits expiration, "
282 "and a bugtask cannot be confirmed."),282 "and a bugtask cannot be confirmed."),
283 readonly=True))283 readonly=True),
284 ('devel', dict(exported=False)), exported=True)
284 date_last_message = exported(285 date_last_message = exported(
285 Datetime(title=_("Date of last bug message"),286 Datetime(title=_("Date of last bug message"),
286 required=False, readonly=True))287 required=False, readonly=True))
@@ -809,6 +810,21 @@
809 def updateHeat():810 def updateHeat():
810 """Update the heat for the bug."""811 """Update the heat for the bug."""
811812
813 @operation_parameters(
814 days_old=Int(
815 title=_('Number of days of inactivity for which to check.'),
816 required=False))
817 @export_read_operation()
818 def isExpirable(days_old=None):
819 """Is this bug eligible for expiration and was it last updated
820 more than X days ago?
821
822 If days_old is None the default number of days without activity
823 is used.
824
825 Returns True or False.
826 """
827
812class InvalidDuplicateValue(Exception):828class InvalidDuplicateValue(Exception):
813 """A bug cannot be set as the duplicate of another."""829 """A bug cannot be set as the duplicate of another."""
814 webservice_error(417)830 webservice_error(417)
815831
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2010-06-22 16:08:05 +0000
+++ lib/lp/bugs/model/bug.py 2010-07-08 13:37:09 +0000
@@ -42,6 +42,7 @@
42 ObjectCreatedEvent, ObjectDeletedEvent, ObjectModifiedEvent)42 ObjectCreatedEvent, ObjectDeletedEvent, ObjectModifiedEvent)
43from lazr.lifecycle.snapshot import Snapshot43from lazr.lifecycle.snapshot import Snapshot
4444
45from canonical.config import config
45from canonical.database.constants import UTC_NOW46from canonical.database.constants import UTC_NOW
46from canonical.database.datetimecol import UtcDateTimeCol47from canonical.database.datetimecol import UtcDateTimeCol
47from canonical.database.sqlbase import cursor, SQLBase, sqlvalues48from canonical.database.sqlbase import cursor, SQLBase, sqlvalues
@@ -436,7 +437,7 @@
436 enabled_bug_expiration set to True can be expired. To qualify for437 enabled_bug_expiration set to True can be expired. To qualify for
437 expiration, the bug and its bugtasks meet the follow conditions:438 expiration, the bug and its bugtasks meet the follow conditions:
438439
439 1. The bug is inactive; the last update of the is older than440 1. The bug is inactive; the last update of the bug is older than
440 Launchpad expiration age.441 Launchpad expiration age.
441 2. The bug is not a duplicate.442 2. The bug is not a duplicate.
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).
@@ -454,14 +455,40 @@
454 if not self.permits_expiration:455 if not self.permits_expiration:
455 return False456 return False
456457
457 # Do the search as the Janitor, to ensure that this bug can be458 days_old = config.malone.days_before_expiration
458 # found, even if it's private. We don't have access to the user459 # Do the search as the Janitor, to ensure that this bug can be
459 # calling this property. If the user has access to view this460 # found, even if it's private. We don't have access to the user
460 # property, he has permission to see the bug, so we're not461 # calling this property. If the user has access to view this
461 # exposing something we shouldn't. The Janitor has access to462 # property, he has permission to see the bug, so we're not
462 # view all bugs.463 # exposing something we shouldn't. The Janitor has access to
463 bugtasks = getUtility(IBugTaskSet).findExpirableBugTasks(464 # view all bugs.
464 0, getUtility(ILaunchpadCelebrities).janitor, bug=self)465 bugtasks = getUtility(IBugTaskSet).findExpirableBugTasks(
466 days_old, getUtility(ILaunchpadCelebrities).janitor, bug=self)
467 return bugtasks.count() > 0
468
469 def isExpirable(self, days_old=None):
470 """See `IBug`."""
471
472 # If days_old is None read it from the Launchpad configuration
473 # and use that value
474 if days_old is None:
475 days_old = config.malone.days_before_expiration
476
477 # IBugTaskSet.findExpirableBugTasks() is the authoritative determiner
478 # if a bug can expire, but it is expensive. We do a general check
479 # to verify the bug permits expiration before using IBugTaskSet to
480 # determine if a bugtask can cause expiration.
481 if not self.permits_expiration:
482 return False
483
484 # Do the search as the Janitor, to ensure that this bug can be
485 # found, even if it's private. We don't have access to the user
486 # calling this property. If the user has access to view this
487 # property, he has permission to see the bug, so we're not
488 # exposing something we shouldn't. The Janitor has access to
489 # view all bugs.
490 bugtasks = getUtility(IBugTaskSet).findExpirableBugTasks(
491 days_old, getUtility(ILaunchpadCelebrities).janitor, bug=self)
465 return bugtasks.count() > 0492 return bugtasks.count() > 0
466493
467 @property494 @property
468495
=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py 2010-06-25 18:48:13 +0000
+++ lib/lp/bugs/model/bugtask.py 2010-07-08 13:37:09 +0000
@@ -2265,7 +2265,7 @@
2265 transitionToStatus() method. See 'Conjoined Bug Tasks' in2265 transitionToStatus() method. See 'Conjoined Bug Tasks' in
2266 c.l.doc/bugtasks.txt.2266 c.l.doc/bugtasks.txt.
22672267
2268 Only bugtask the specified user has permission to view are2268 Only bugtasks the specified user has permission to view are
2269 returned. The Janitor celebrity has permission to view all bugs.2269 returned. The Janitor celebrity has permission to view all bugs.
2270 """2270 """
2271 if bug is None:2271 if bug is None:
22722272
=== modified file 'lib/lp/bugs/stories/bugs/xx-incomplete-bugs.txt'
--- lib/lp/bugs/stories/bugs/xx-incomplete-bugs.txt 2010-01-23 14:17:49 +0000
+++ lib/lp/bugs/stories/bugs/xx-incomplete-bugs.txt 2010-07-08 13:37:09 +0000
@@ -130,11 +130,18 @@
130130
131Users can view a list of expirable bugs via a link on the project's131Users can view a list of expirable bugs via a link on the project's
132bug page. To see the behaviour of the bug listing, we need another132bug page. To see the behaviour of the bug listing, we need another
133expirable bug. No Privileges Person marks another bug as incomplete.133expirable bug. No Privileges Person marks another bug as Incomplete
134and does so some time ago so it appears as expirable.
134135
135 >>> user_browser.open('http://bugs.launchpad.dev/jokosher/+bug/12')136 >>> user_browser.open('http://bugs.launchpad.dev/jokosher/+bug/12')
136 >>> user_browser.getControl('Status').value = ['Incomplete']137 >>> user_browser.getControl('Status').value = ['Incomplete']
137 >>> user_browser.getControl('Save Changes', index=0).click()138 >>> user_browser.getControl('Save Changes', index=0).click()
139 >>> login('test@canonical.com')
140 >>> bug_12 = getUtility(IBugSet).get(12)
141 >>> time_delta = timedelta(days=60)
142 >>> bug_12.date_last_updated = bug_12.date_last_updated - time_delta
143 >>> flush_database_updates()
144 >>> logout()
138145
139The project's bug page reports the number of bugs that will expire if146The project's bug page reports the number of bugs that will expire if
140they are not confirmed. No Privileges Person sees that Jokosher has 2147they are not confirmed. No Privileges Person sees that Jokosher has 2
@@ -180,12 +187,15 @@
180187
181The listing is sorted in order of most inactive to least inactive. The188The listing is sorted in order of most inactive to least inactive. The
182bugs at the top of the list will expire before the ones at the bottom.189bugs at the top of the list will expire before the ones at the bottom.
183When No Privileges Person adds a comment to the oldest bug, it is190When bug 12's date_last_updated is modified to be older than bug 11's it
184pushed to the bottom of the list.191appears as more inactive than bug 11 and is pushed to the top of the list.
185192
186 >>> user_browser.getLink('Make Jokosher use autoaudiosink').click()193 >>> login('test@canonical.com')
187 >>> user_browser.getControl(name='field.comment').value = "bump"194 >>> bug_12 = getUtility(IBugSet).get(12)
188 >>> user_browser.getControl('Post Comment').click()195 >>> time_delta = timedelta(days=2)
196 >>> bug_12.date_last_updated = bug_12.date_last_updated - time_delta
197 >>> flush_database_updates()
198 >>> logout()
189 >>> user_browser.getLink('Bugs').click()199 >>> user_browser.getLink('Bugs').click()
190 >>> user_browser.getLink('Incomplete bugs').click()200 >>> user_browser.getLink('Incomplete bugs').click()
191 >>> contents = find_main_content(user_browser.contents)201 >>> contents = find_main_content(user_browser.contents)
192202
=== modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt'
--- lib/lp/bugs/stories/webservice/xx-bug.txt 2010-06-24 17:09:14 +0000
+++ lib/lp/bugs/stories/webservice/xx-bug.txt 2010-07-08 13:37:09 +0000
@@ -2002,3 +2002,45 @@
2002 ... branch_entry['bug_link']).jsonBody()2002 ... branch_entry['bug_link']).jsonBody()
2003 >>> print bug_link['self_link']2003 >>> print bug_link['self_link']
2004 http://.../bugs/42004 http://.../bugs/4
2005
2006Bug expiration
2007--------------
2008
2009In addition to can_expire bugs have an isExpirable method to which a custom time
2010period, days_old, can be passed. This is then used with
2011findExpirableBugTasks. This allows projects to create their own janitor using
2012a different period for bug expiration.
2013
2014Check to ensure that isExpirable() works without days_old.
2015
2016 >>> bug_four = webservice.get("/bugs/4").jsonBody()
2017 >>> print webservice.named_get(bug_four['self_link'],
2018 ... 'isExpirable').jsonBody()
2019 False
2020
2021Pass isExpirable() an integer for days_old.
2022
2023 >>> bug_four = webservice.get("/bugs/4").jsonBody()
2024 >>> print webservice.named_get(bug_four['self_link'], 'isExpirable',
2025 ... days_old='14').jsonBody()
2026 False
2027
2028Pass isExpirable() a string for days_old.
2029
2030 >>> bug_four = webservice.get("/bugs/4").jsonBody()
2031 >>> print webservice.named_get(bug_four['self_link'], 'isExpirable',
2032 ... days_old='sixty')
2033 HTTP/1.1 400 Bad Request
2034 ...
2035 days_old: got 'unicode', expected int: u'sixty'
2036
2037Can expire
2038----------
2039
2040can_expire is not exported in the development version of the API.
2041
2042 >>> bug_four = webservice.get("/bugs/4", api_version='devel').jsonBody()
2043 >>> bug_four[can_expire]
2044 Traceback (most recent call last):
2045 ...
2046 NameError: name 'can_expire' is not defined
20052047
=== modified file 'lib/lp/bugs/templates/bug-listing-expirable.pt'
--- lib/lp/bugs/templates/bug-listing-expirable.pt 2009-09-01 11:06:23 +0000
+++ lib/lp/bugs/templates/bug-listing-expirable.pt 2010-07-08 13:37:09 +0000
@@ -25,9 +25,9 @@
2525
26 <tal:expirable-bugs condition="view/can_show_expirable_bugs">26 <tal:expirable-bugs condition="view/can_show_expirable_bugs">
27 <p>27 <p>
28 Incomplete bug reports can expire if they are unattended and they28 Incomplete bug reports can expire if they are unattended and are
29 become inactive. These bugs are not confirmed, or in a status29 inactive. These bugs are not confirmed, or in a status that
30 that indicates they were confirmed. An Incomplete bug can remain30 indicates they were confirmed. An Incomplete bug can remain
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.
32 See <a href="https://help.launchpad.net/Bugs/Expiry">Bugs/Expiry.32 See <a href="https://help.launchpad.net/Bugs/Expiry">Bugs/Expiry.
33 </a>33 </a>
3434
=== modified file 'lib/lp/bugs/templates/bugtask-index.pt'
--- lib/lp/bugs/templates/bugtask-index.pt 2010-06-24 13:13:01 +0000
+++ lib/lp/bugs/templates/bugtask-index.pt 2010-07-08 13:37:09 +0000
@@ -72,7 +72,7 @@
7272
73 <p73 <p
74 id="can-expire"74 id="can-expire"
75 tal:condition="context/bug/can_expire"75 tal:condition="python: context.bug.isExpirable(days_old=0)"
76 >76 >
77 <tal:expiration_message replace="view/expiration_message" />77 <tal:expiration_message replace="view/expiration_message" />
78 (<a href="https://help.launchpad.net/BugExpiry">find out why</a>)78 (<a href="https://help.launchpad.net/BugExpiry">find out why</a>)
7979
=== modified file 'lib/lp/bugs/tests/bug.py'
--- lib/lp/bugs/tests/bug.py 2010-06-04 17:47:34 +0000
+++ lib/lp/bugs/tests/bug.py 2010-07-08 13:37:09 +0000
@@ -15,6 +15,7 @@
15from zope.component import getUtility15from zope.component import getUtility
16from zope.security.proxy import removeSecurityProxy16from zope.security.proxy import removeSecurityProxy
1717
18from canonical.config import config
18from canonical.launchpad.ftests import sync19from canonical.launchpad.ftests import sync
19from canonical.launchpad.testing.pages import (20from canonical.launchpad.testing.pages import (
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,
@@ -220,7 +221,7 @@
220 """Summarize a sequence of bugtasks."""221 """Summarize a sequence of bugtasks."""
221 bugtaskset = getUtility(IBugTaskSet)222 bugtaskset = getUtility(IBugTaskSet)
222 expirable_bugtasks = list(bugtaskset.findExpirableBugTasks(223 expirable_bugtasks = list(bugtaskset.findExpirableBugTasks(
223 0, getUtility(ILaunchpadCelebrities).janitor))224 config.malone.days_before_expiration, getUtility(ILaunchpadCelebrities).janitor))
224 print 'ROLE EXPIRE AGE STATUS ASSIGNED DUP MILE REPLIES'225 print 'ROLE EXPIRE AGE STATUS ASSIGNED DUP MILE REPLIES'
225 for bugtask in sorted(set(bugtasks), key=attrgetter('id')):226 for bugtask in sorted(set(bugtasks), key=attrgetter('id')):
226 if len(bugtask.bug.bugtasks) == 1:227 if len(bugtask.bug.bugtasks) == 1: