Merge lp:~adeuring/launchpad/bug-333531-bug-status-expired into lp:launchpad

Proposed by Abel Deuring
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~adeuring/launchpad/bug-333531-bug-status-expired
Merge into: lp:launchpad
Diff against target: 533 lines (+183/-44)
11 files modified
lib/lp/bugs/browser/bugtask.py (+7/-5)
lib/lp/bugs/browser/tests/test_bugtask.py (+95/-2)
lib/lp/bugs/doc/bugtask-expiration.txt (+13/-24)
lib/lp/bugs/doc/bugtask-status-workflow.txt (+8/-1)
lib/lp/bugs/interfaces/bugtask.py (+10/-2)
lib/lp/bugs/model/bugtask.py (+2/-1)
lib/lp/bugs/scripts/bugexpire.py (+3/-3)
lib/lp/bugs/tests/bugs-emailinterface.txt (+19/-1)
lib/lp/bugs/tests/bugtarget-bugcount.txt (+2/-0)
lib/lp/bugs/tests/test_bugtask_status.txt (+20/-4)
lib/lp/testing/factory.py (+4/-1)
To merge this branch: bzr merge lp:~adeuring/launchpad/bug-333531-bug-status-expired
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Abel Deuring (community) Needs Resubmitting
Review via email: mp+23396@code.launchpad.net

Description of the change

This branch adds a new bug status "expired" and changes the bug expiry script to set the status of bugs considered to be expired to this status.

Why the integer value 19 for the new status? The method BugTask.transitionToStatus() assumes that the status values have a "workflow related" ordering. For instance, if the old status is "lower" than CONFIRMED and the new status is "greater or equal" to CONFIRMED, date_confirmed is set to the current time. A similar logic exists for other statuses like TRIAGED, INPROGRESS etc.

The value 19 keeps the new status "below" those statuses for which a date is implicitly set.

Aside from the obvious change of the bug expiry script I changed the classes BugTaskEditView and BugTaskTableRowView so that the status value "expired" cannot be selected in the web UI: This status should only be set by the expiry script.

tests:

./bin/test -vvt test_bugtask
./bin/test -vvt bugtask-expiration.txt

lint: lots of noise but nothing related to my changes, I think.

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/bugs/browser/bugtask.py
  lib/lp/bugs/browser/tests/test_bugtask.py
  lib/lp/bugs/doc/bugtask-expiration.txt
  lib/lp/bugs/interfaces/bugtask.py
  lib/lp/bugs/scripts/bugexpire.py

== Pylint notices ==

lib/lp/bugs/browser/bugtask.py
    77: [F0401] Unable to import 'z3c.ptcompat' (No module named z3c.ptcompat)
    78: [F0401] Unable to import 'lazr.delegates' (No module named delegates)
    79: [F0401] Unable to import 'lazr.enum' (No module named enum)
    81: [F0401] Unable to import 'lazr.lifecycle.event' (No module named lifecycle)
    82: [F0401] Unable to import 'lazr.lifecycle.snapshot' (No module named lifecycle)
    83: [F0401] Unable to import 'lazr.restful.interface' (No module named restful)
    84: [F0401] Unable to import 'lazr.restful.interfaces' (No module named restful)
    88: [F0401] Unable to import 'canonical.config' (No module named canonical.config)
    90: [F0401] Unable to import 'canonical.launchpad' (No module named canonical.launchpad)
    91: [F0401] Unable to import 'canonical.cachedproperty' (No module named canonical.cachedproperty)
    92: [F0401] Unable to import 'canonical.launchpad.fields' (No module named canonical.launchpad.fields)
    93: [F0401] Unable to import 'canonical.launchpad.mailnotification' (No module named canonical.launchpad.mailnotification)
    94: [F0401] Unable to import 'canonical.launchpad.validators' (No module named canonical.launchpad.validators)
    95: [F0401] Unable to import 'canonical.launchpad.webapp' (No module named canonical.launchpad.webapp)
    99: [F0401] Unable to import 'canonical.lazr.utils' (No module named canonical.lazr.utils)
    101: [F0401] Unable to import 'lp.answers.interfaces.questiontarget' (No module named lp.answers.interfaces.questiontarget)
    102: [F0401] Unable to import 'lp.bugs.interfaces.bugattachment' (No module named lp.bugs.interfaces.bugattachment)
    104: [F0401] Unable to import 'lp.bugs.interfaces.bugactivity' (No module named lp.bugs.interfaces.bugactivity)
    105: [F0401] Unable to import 'lp.bugs.interfaces.bugnomination' (No module named lp.bugs.interfaces.bugnomination)
    107: [F0401] Unable to import 'lp.bugs.interfaces.bug' (No module named lp.bugs.interfaces.bug)
    108: [F0401] Unable to import 'lp.bugs.interfaces.bugtask' (No module named lp.bugs.interfaces.bugtask)
    117: [F0401] Unable to import 'lp.bugs.interfaces.bugtracker' (No module named lp.bugs.interfaces.bugtracker)
    118: [F0401] Unable to import 'lp.bugs.interfaces.cve' (No module named lp.bugs.interfaces.cve)
    119: [F0401] Unable to import 'lp.bugs.interfaces.malone' (No module named lp.bugs.interfaces.malone)
    120: [F0401] Unable to import 'lp.registry.interfaces.distribution' (No module named lp.registry.interfaces.distribution)
    121: [F0401] Unable to import 'lp.registry.interfaces.distributionsourcepackage' (No module named lp.registry.interfaces.distributionsourcepackage)
    123: [F0401] Unable to import 'lp.registry.interfaces.distroseries' (No module named lp.registry.interfaces.distroseries)
    124: [F0401] Unable to import 'canonical.launchpad.interfaces.launchpad' (No module named canonical.launchpad.interfaces.launchpad)
    126: [F0401] Unable to import 'lp.registry.interfaces.person' (No module named lp.registry.interfaces.person)
    127: [F0401] Unable to import 'lp.registry.interfaces.product' (No module named lp.registry.interfaces.product)
    128: [F0401] Unable to import 'lp.registry.interfaces.productseries' (No module named lp.registry.interfaces.productseries)
    129: [F0401] Unable to import 'lp.registry.interfaces.projectgroup' (No module named lp.registry.interfaces.projectgroup)
    130: [F0401] Unable to import 'lp.registry.interfaces.sourcepackage' (No module named lp.registry.interfaces.sourcepackage)
    131: [F0401] Unable to import 'canonical.launchpad.interfaces.validation' (No module named canonical.launchpad.interfaces.validation)
    133: [F0401] Unable to import 'canonical.launchpad.webapp.breadcrumb' (No module named canonical.launchpad.webapp.breadcrumb)
    134: [F0401] Unable to import 'canonical.launchpad.webapp.interfaces' (No module named canonical.launchpad.webapp.interfaces)
    137: [F0401] Unable to import 'canonical.launchpad.searchbuilder' (No module named canonical.launchpad.searchbuilder)
    139: [F0401] Unable to import 'canonical.launchpad' (No module named canonical.launchpad)
    141: [F0401] Unable to import 'lp.bugs.browser.bug' (No module named lp.bugs.browser.bug)
    142: [F0401] Unable to import 'lp.bugs.browser.bugcomment' (No module named lp.bugs.browser.bugcomment)
    143: [F0401] Unable to import 'canonical.launchpad.browser.feeds' (No module named canonical.launchpad.browser.feeds)
    145: [F0401] Unable to import 'lp.registry.browser.mentoringoffer' (No module named lp.registry.browser.mentoringoffer)
    146: [F0401] Unable to import 'canonical.launchpad.browser.launchpad' (No module named canonical.launchpad.browser.launchpad)
    148: [F0401] Unable to import 'canonical.launchpad.webapp.authorization' (No module named canonical.launchpad.webapp.authorization)
    149: [F0401] Unable to import 'canonical.launchpad.webapp.batching' (No module named canonical.launchpad.webapp.batching)
    150: [F0401] Unable to import 'canonical.launchpad.webapp.menu' (No module named canonical.launchpad.webapp.menu)
    151: [F0401] Unable to import 'canonical.launchpad.webapp.tales' (No module named canonical.launchpad.webapp.tales)
    154: [F0401] Unable to import 'canonical.lazr.interfaces' (No module named canonical.lazr.interfaces)
    155: [F0401] Unable to import 'lazr.restful.interfaces' (No module named restful)
    157: [F0401] Unable to import 'canonical.widgets.bug' (No module named canonical.widgets.bug)
    158: [F0401] Unable to import 'canonical.widgets.bugtask' (No module named canonical.widgets.bugtask)
    162: [F0401] Unable to import 'canonical.widgets.itemswidgets' (No module named canonical.widgets.itemswidgets)
    163: [F0401] Unable to import 'canonical.widgets.lazrjs' (No module named canonical.widgets.lazrjs)
    166: [F0401] Unable to import 'canonical.widgets.project' (No module named canonical.widgets.project)
    168: [F0401] Unable to import 'lp.registry.vocabularies' (No module named lp.registry.vocabularies)
    1196: [E1002, BugTaskEditView.initialize] Use super on an old style class
    1293: [E1002, BugTaskEditView.setUpFields] Use super on an old style class
    3817: [E1002, BugTaskBreadcrumb.__init__] Use super on an old style class

lib/lp/bugs/browser/tests/test_bugtask.py
    12: [F0401] Unable to import 'canonical.launchpad.ftests' (No module named canonical.launchpad.ftests)
    13: [F0401] Unable to import 'canonical.launchpad.testing.systemdocs' (No module named canonical.launchpad.testing.systemdocs)
    15: [F0401] Unable to import 'canonical.launchpad.webapp.servers' (No module named canonical.launchpad.webapp.servers)
    16: [F0401] Unable to import 'canonical.testing' (No module named canonical.testing)
    18: [F0401] Unable to import 'lp.bugs.browser' (No module named lp.bugs.browser)
    19: [F0401] Unable to import 'lp.bugs.browser.bugtask' (No module named lp.bugs.browser.bugtask)
    21: [F0401] Unable to import 'lp.bugs.interfaces.bugtask' (No module named lp.bugs.interfaces.bugtask)
    22: [F0401] Unable to import 'lp.testing' (No module named lp.testing)
    29: [E1002, TestBugTasksAndNominationsView.setUp] Use super on an old style class
    218: [E1002, TestBugTaskEditViewStatusField.setUp] Use super on an old style class

lib/lp/bugs/interfaces/bugtask.py
    54: [F0401] Unable to import 'lazr.enum' (No module named enum)
    57: [F0401] Unable to import 'canonical.launchpad' (No module named canonical.launchpad)
    58: [F0401] Unable to import 'canonical.launchpad.fields' (No module named canonical.launchpad.fields)
    61: [F0401] Unable to import 'lp.bugs.interfaces.bugwatch' (No module named lp.bugs.interfaces.bugwatch)
    63: [F0401] Unable to import 'lp.soyuz.interfaces.component' (No module named lp.soyuz.interfaces.component)
    64: [F0401] Unable to import 'canonical.launchpad.interfaces.launchpad' (No module named canonical.launchpad.interfaces.launchpad)
    65: [F0401] Unable to import 'lp.registry.interfaces.mentoringoffer' (No module named lp.registry.interfaces.mentoringoffer)
    66: [F0401] Unable to import 'canonical.launchpad.searchbuilder' (No module named canonical.launchpad.searchbuilder)
    67: [F0401] Unable to import 'canonical.launchpad.validators' (No module named canonical.launchpad.validators)
    68: [F0401] Unable to import 'canonical.launchpad.validators.name' (No module named canonical.launchpad.validators.name)
    69: [F0401] Unable to import 'canonical.launchpad.webapp.interfaces' (No module named canonical.launchpad.webapp.interfaces)
    70: [F0401] Unable to import 'lazr.restful.interface' (No module named restful)
    71: [F0401] Unable to import 'lazr.restful.declarations' (No module named restful)
    76: [F0401] Unable to import 'lazr.restful.fields' (No module named restful)
    1097: [F0401, BugTaskSearchParams.setSourcePackage] Unable to import 'lp.registry.interfaces.sourcepackage' (No module named lp.registry.interfaces.sourcepackage)
    1162: [F0401, BugTaskSearchParams.fromSearchForm] Unable to import 'lp.bugs.interfaces.bugattachment' (No module named lp.bugs.interfaces.bugattachment)
    1212: [C0322, BugTaskSearchParams.fromSearchForm] Operator not preceded by a space
    search_params.linked_branches=linked_branches
    ^

lib/lp/bugs/scripts/bugexpire.py
    17: [F0401] Unable to import 'lazr.lifecycle.snapshot' (No module named lifecycle)
    19: [F0401] Unable to import 'canonical.config' (No module named canonical.config)
    20: [F0401] Unable to import 'lazr.lifecycle.event' (No module named lifecycle)
    21: [F0401] Unable to import 'canonical.launchpad.interfaces.launchpad' (No module named canonical.launchpad.interfaces.launchpad)
    22: [F0401] Unable to import 'lp.bugs.interfaces.bugtask' (No module named lp.bugs.interfaces.bugtask)
    23: [F0401] Unable to import 'canonical.launchpad.webapp.interfaces' (No module named canonical.launchpad.webapp.interfaces)
    24: [F0401] Unable to import 'canonical.launchpad.webapp.interaction' (No module named canonical.launchpad.webapp.interaction)

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Looks good :)

The conversation we had on IRC:

<allenap> adeuring: Is there going to be another branch (or two) to add a date_expired field to IBugTask?
<adeuring> allenap: argh... i forgot completely about that...
<adeuring> yes, there _should_ be a branch...
<allenap> adeuring: I think the bug number in the branch name is wrong.
<adeuring> allenap: right...
<allenap> adeuring: Line 75, could you login(product_owner) to avoid the removeSecurityProxy() call?
<allenap> adeuring: Or even login_person(product_owner)
<adeuring> allenap: in theory, yes. Problem that things become then more complicated: the owner can't assign another person as the supervisor, so I need to create a team, let another person join that team and finally run the test with that other user
<adeuring> allenap: I was too lazy to do all that ;)
<allenap> adeuring: Argh. Leave it then :)
<adeuring> allenap: OK ;)
<allenap> adeuring: Perhaps the removeSecurityProxy() hack could be put in LaunchpadObjectFactory.makeProduct(..., bug_supervisor=None).
<adeuring> allenap: right, good idea!
<allenap> adeuring: Line 124, the bug supervisor or the product owner should be able to call transitionToStatus(), so consider using that rather than removing the proxy.
<adeuring> allenap: right
<allenap> adeuring: 136s/Epired/Expired/
<adeuring> allenap: fixed
<allenap> adeuring: One last thing. There's probably some doc test out there that does a similar thing to TestBugTaskEditViewStatusField. If you know where, consider removing it.
<adeuring> allenap: I did not find anything like that...
<adeuring> that's why I added it ;)

review: Approve
Revision history for this message
Robert Collins (lifeless) wrote :

Can API's set the new status of expired? This is something that custom user
tools would be great at using.

Revision history for this message
Gavin Panella (allenap) wrote :

> Can API's set the new status of expired?

I can't see a reason why not, subject to the same access control, but I also haven't checked it. Something for QA I reckon.

Revision history for this message
Abel Deuring (adeuring) wrote :

On 15.04.2010 10:32, Gavin Panella wrote:
>> Can API's set the new status of expired?
>
> I can't see a reason why not, subject to the same access control, but I also haven't checked it. Something for QA I reckon.
>

Just tried it locally: yes, it is possible to set a bug task to
"expired" via the API, provided that you have enough privileges. You can
also set this status via email. But this uncovered a problem: Everyvbody
can set this status via email. It should be limited to the same persons
 which can set it via the API (project owner and bug supervisor)...

Revision history for this message
Abel Deuring (adeuring) wrote :

I added the status EXPIRED to the set of statuses that only project owners and bug supervisors can change. This prevents the problems I mentioned.

diff of the changes since the last review (includes replacement of makePersonNoCommit() by makePerson() -- the former method disappeared...)

review: Needs Resubmitting
Revision history for this message
Abel Deuring (adeuring) wrote :
Download full text (5.2 KiB)

...here is the diff:

=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py 2010-04-14 15:51:13 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2010-04-15 10:38:07 +0000
@@ -217,10 +217,9 @@

     def setUp(self):
         super(TestBugTaskEditViewStatusField, self).setUp()
- product_owner = self.factory.makePersonNoCommit(name='product-owner')
- bug_supervisor = self.factory.makePersonNoCommit(
- name='bug-supervisor')
- product = self.factory.makeProductNoCommit(
+ product_owner = self.factory.makePerson(name='product-owner')
+ bug_supervisor = self.factory.makePerson(name='bug-supervisor')
+ product = self.factory.makeProduct(
             owner=product_owner, bug_supervisor=bug_supervisor)
         self.bug = self.factory.makeBug(product=product)

=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py 2010-04-14 13:23:02 +0000
+++ lib/lp/bugs/interfaces/bugtask.py 2010-04-15 13:08:31 +0000
@@ -317,6 +317,7 @@

 BUG_SUPERVISOR_BUGTASK_STATUSES = (
     BugTaskStatus.WONTFIX,
+ BugTaskStatus.EXPIRED,
     BugTaskStatus.TRIAGED)

 DEFAULT_SEARCH_BUGTASK_STATUSES = (

=== modified file 'lib/lp/bugs/tests/bugs-emailinterface.txt'
--- lib/lp/bugs/tests/bugs-emailinterface.txt 2009-08-13 15:12:16 +0000
+++ lib/lp/bugs/tests/bugs-emailinterface.txt 2010-04-15 14:00:55 +0000
@@ -1374,6 +1374,11 @@
     >>> print upstream_task.status.title
     Won't Fix

+ >>> submit_commands(bug_four, 'status expired')
+ >>> sync(upstream_task)
+ >>> print upstream_task.status.title
+ Expired
+
 Everyone else gets an explanatory error message:

     >>> from canonical.launchpad.interfaces import BugTaskStatus
@@ -1400,6 +1405,19 @@
     registrant or a bug supervisor for Mozilla Firefox.
     ...

+ >>> submit_commands(bug_four, 'affects firefox', 'status expired')
+ >>> print_latest_email()
+ Subject: Submit Request Failure
+ To: <email address hidden>
+ <BLANKLINE>
+ ...
+ Failing command:
+ status expired
+ ...
+ The status cannot be changed to expired because you are not the
+ registrant or a bug supervisor for Mozilla Firefox.
+ ...
+
 Let's take a look at all the other error messages that the sub
 commands can produce.

@@ -1417,7 +1435,7 @@
         status foo
     ...
     The 'status' command expects any of the following arguments:
- new, incomplete, invalid, wontfix, confirmed, triaged, inprogress, fixcommitted, fixreleased
+ new, incomplete, invalid, wontfix, expired, confirmed, triaged, inprogress, fixcommitted, fixreleased
     <BLANKLINE>
     For example:
     <BLANKLINE>

=== modified file 'lib/lp/bugs/tests/bugtarget-bugcount.txt'
--- lib/lp/bugs/tests/bugtarget-bugcount.txt 2009-06-12 16:36:02 +0000
+++ lib/lp/bugs/tests/bugtarget-bugcount.txt 2010-04-15 11:46:39 +0000
@@ -13,6 +13,7 @@
     INCOMPLETE
     INVALID
     WONTFIX
+ EXPIRED
     CONFIRMED
     TRIAGED
     INPROGRESS
@@ -55,6 +56,7 @@
     INCOMPLETE: 5 bug(s) more
     INVALID: 5 bug(s) more
     WONTFIX: 5 bug(s) more
+ EXPIRED: 5 bug(s) more
     CONFIRMED: 5 ...

Read more...

Revision history for this message
Gavin Panella (allenap) wrote :

Good catch on the email interface. The diff looks good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/bugtask.py'
2--- lib/lp/bugs/browser/bugtask.py 2010-04-05 21:10:31 +0000
3+++ lib/lp/bugs/browser/bugtask.py 2010-04-16 11:43:29 +0000
4@@ -1302,10 +1302,11 @@
5 # it uses based on the permissions of the user viewing form.
6 if 'status' in self.editable_field_names:
7 if self.user is None:
8- status_noshow = list(BugTaskStatus.items)
9+ status_noshow = set(BugTaskStatus.items)
10 else:
11- status_noshow = [BugTaskStatus.UNKNOWN]
12- status_noshow.extend(
13+ status_noshow = set((
14+ BugTaskStatus.UNKNOWN, BugTaskStatus.EXPIRED))
15+ status_noshow.update(
16 status for status in BugTaskStatus.items
17 if not self.context.canTransitionToStatus(
18 status, self.user))
19@@ -2564,7 +2565,7 @@
20 dict(
21 value=term.token, title=term.title or term.token,
22 checked=term.value in default_values))
23- return helpers.shortlist(widget_values, longest_expected=10)
24+ return helpers.shortlist(widget_values, longest_expected=11)
25
26 def getStatusWidgetValues(self):
27 """Return data used to render the status checkboxes."""
28@@ -3344,7 +3345,8 @@
29 # the title as the token for backwards compatibility.
30 status_items = [
31 (item.title, item) for item in BugTaskStatus.items
32- if item != BugTaskStatus.UNKNOWN]
33+ if item not in (BugTaskStatus.UNKNOWN,
34+ BugTaskStatus.EXPIRED)]
35
36 disabled_items = [status for status in BugTaskStatus.items
37 if not self.context.canTransitionToStatus(status, self.user)]
38
39=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
40--- lib/lp/bugs/browser/tests/test_bugtask.py 2009-12-10 15:48:31 +0000
41+++ lib/lp/bugs/browser/tests/test_bugtask.py 2010-04-16 11:43:29 +0000
42@@ -6,16 +6,19 @@
43
44 import unittest
45
46+from zope.security.proxy import removeSecurityProxy
47 from zope.testing.doctest import DocTestSuite
48
49-from canonical.launchpad.ftests import login
50+from canonical.launchpad.ftests import ANONYMOUS, login, login_person
51 from canonical.launchpad.testing.systemdocs import (
52 LayeredDocFileSuite, setUp, tearDown)
53 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
54 from canonical.testing import LaunchpadFunctionalLayer
55
56 from lp.bugs.browser import bugtask
57-from lp.bugs.browser.bugtask import BugTasksAndNominationsView
58+from lp.bugs.browser.bugtask import (
59+ BugTaskEditView, BugTasksAndNominationsView)
60+from lp.bugs.interfaces.bugtask import BugTaskStatus
61 from lp.testing import TestCaseWithFactory
62
63
64@@ -205,9 +208,99 @@
65 self.view.anon_affected_statement)
66
67
68+class TestBugTaskEditViewStatusField(TestCaseWithFactory):
69+ """We show only those options as possible value in the status
70+ field that the user can select.
71+ """
72+
73+ layer = LaunchpadFunctionalLayer
74+
75+ def setUp(self):
76+ super(TestBugTaskEditViewStatusField, self).setUp()
77+ product_owner = self.factory.makePerson(name='product-owner')
78+ bug_supervisor = self.factory.makePerson(name='bug-supervisor')
79+ product = self.factory.makeProduct(
80+ owner=product_owner, bug_supervisor=bug_supervisor)
81+ self.bug = self.factory.makeBug(product=product)
82+
83+ def getWidgetOptionTitles(self, widget):
84+ """Return the titles of options of the given choice widget."""
85+ return [
86+ item.value.title for item in widget.field.vocabulary]
87+
88+ def test_status_field_items_for_anonymous(self):
89+ # Anonymous users see only the current value.
90+ login(ANONYMOUS)
91+ view = BugTaskEditView(
92+ self.bug.default_bugtask, LaunchpadTestRequest())
93+ view.initialize()
94+ self.assertEqual(
95+ ['New'], self.getWidgetOptionTitles(view.form_fields['status']))
96+
97+ def test_status_field_items_for_ordinary_users(self):
98+ # Ordinary users can set the status to all values except Won't fix,
99+ # Expired, Triaged, Unknown.
100+ login('no-priv@canonical.com')
101+ view = BugTaskEditView(
102+ self.bug.default_bugtask, LaunchpadTestRequest())
103+ view.initialize()
104+ self.assertEqual(
105+ ['New', 'Incomplete', 'Invalid', 'Confirmed', 'In Progress',
106+ 'Fix Committed', 'Fix Released'],
107+ self.getWidgetOptionTitles(view.form_fields['status']))
108+
109+ def test_status_field_privileged_persons(self):
110+ # The bug target owner and the bug target supervisor can set
111+ # the status to any value except Unknown and Expired.
112+ for user in (
113+ self.bug.default_bugtask.pillar.owner,
114+ self.bug.default_bugtask.pillar.bug_supervisor):
115+ login_person(user)
116+ view = BugTaskEditView(
117+ self.bug.default_bugtask, LaunchpadTestRequest())
118+ view.initialize()
119+ self.assertEqual(
120+ ['New', 'Incomplete', 'Invalid', "Won't Fix", 'Confirmed',
121+ 'Triaged', 'In Progress', 'Fix Committed', 'Fix Released'],
122+ self.getWidgetOptionTitles(view.form_fields['status']),
123+ 'Unexpected set of settable status options for %s'
124+ % user.name)
125+
126+ def test_status_field_bug_task_in_status_unknown(self):
127+ # If a bugtask has the status Unknown, this status is included
128+ # in the options.
129+ owner = self.bug.default_bugtask.pillar.owner
130+ login_person(owner)
131+ self.bug.default_bugtask.transitionToStatus(
132+ BugTaskStatus.UNKNOWN, owner)
133+ login('no-priv@canonical.com')
134+ view = BugTaskEditView(
135+ self.bug.default_bugtask, LaunchpadTestRequest())
136+ view.initialize()
137+ self.assertEqual(
138+ ['New', 'Incomplete', 'Invalid', 'Confirmed', 'In Progress',
139+ 'Fix Committed', 'Fix Released', 'Unknown'],
140+ self.getWidgetOptionTitles(view.form_fields['status']))
141+
142+ def test_status_field_bug_task_in_status_expired(self):
143+ # If a bugtask has the status Expired, this status is included
144+ # in the options.
145+ removeSecurityProxy(self.bug.default_bugtask).status = (
146+ BugTaskStatus.EXPIRED)
147+ login('no-priv@canonical.com')
148+ view = BugTaskEditView(
149+ self.bug.default_bugtask, LaunchpadTestRequest())
150+ view.initialize()
151+ self.assertEqual(
152+ ['New', 'Incomplete', 'Invalid', 'Expired', 'Confirmed',
153+ 'In Progress', 'Fix Committed', 'Fix Released'],
154+ self.getWidgetOptionTitles(view.form_fields['status']))
155+
156+
157 def test_suite():
158 suite = unittest.TestSuite()
159 suite.addTest(unittest.makeSuite(TestBugTasksAndNominationsView))
160+ suite.addTest(unittest.makeSuite(TestBugTaskEditViewStatusField))
161 suite.addTest(DocTestSuite(bugtask))
162 suite.addTest(LayeredDocFileSuite(
163 'bugtask-target-link-titles.txt', setUp=setUp, tearDown=tearDown,
164
165=== modified file 'lib/lp/bugs/doc/bugtask-expiration.txt'
166--- lib/lp/bugs/doc/bugtask-expiration.txt 2009-06-12 16:36:02 +0000
167+++ lib/lp/bugs/doc/bugtask-expiration.txt 2010-04-16 11:43:29 +0000
168@@ -3,7 +3,7 @@
169 Old unattended Incomplete bugtasks clutter the search results of
170 Launchpad Bugs making the bug staff's job difficult. A script is run
171 daily to locate unattended Incomplete bugtasks that have not been
172-updated in 2 months, and sets their status to Invalid. Only bugtasks
173+updated in 2 months, and sets their status to Expired. Only bugtasks
174 for projects that use Launchpad to track bugs and have
175 enable_bug_expiration set to True will be expired; this rule does not
176 apply to Bugs imported from upstream bug trackers. The preconditions
177@@ -17,7 +17,7 @@
178 6. The bugtask is not assigned to anyone.
179 7. The bugtask does not have a milestone.
180
181-Bugtasks cannot transition to Invalid automatically unless they meet
182+Bugtasks cannot transition to Expired automatically unless they meet
183 all the rules stated above.
184
185
186@@ -102,13 +102,13 @@
187 True
188
189 A bugtask for a product with a bug watch. Note that this bugtask
190-has otherwise the same parameters as jokosher_bugtask. The
191+has otherwise the same parameters as jokosher_bugtask. The
192 bugwatch prevents expiration, hence this bugtask will not appear
193 in the listings of expirable bugtasks below.
194
195 >>> from canonical.launchpad.interfaces import IBugTrackerSet
196 >>> mozilla_bugtracker = getUtility(IBugTrackerSet)['mozilla.org']
197- >>> jokosher_bugtask_watched = create_old_bug('jokosher watched',
198+ >>> jokosher_bugtask_watched = create_old_bug('jokosher watched',
199 ... 61, jokosher, external_bugtracker=mozilla_bugtracker)
200 >>> jokosher_bugtask_watched.bug.can_expire
201 False
202@@ -213,7 +213,7 @@
203
204 >>> bugtasks = [ubuntu_bugtask, hoary_bugtask, jokosher_bugtask,
205 ... jokosher_bugtask_watched, new_bugtask, assigned_bugtask,
206- ... confirmed_bugtask, duplicate_bugtask, external_bugtask,
207+ ... confirmed_bugtask, duplicate_bugtask, external_bugtask,
208 ... milestone_bugtask, recent_bugtask, no_expiration_bugtask]
209
210 >>> from lp.bugs.tests.bug import summarize_bugtasks
211@@ -417,11 +417,11 @@
212
213 == Running the script ==
214
215-There is one Invalid Bugtasks in sampledata, from the tests above.
216+There are no Expired Bugtasks in sampledata, from the tests above.
217
218 >>> from canonical.launchpad.database import BugTask
219- >>> BugTask.selectBy(status=BugTaskStatus.INVALID).count()
220- 1
221+ >>> BugTask.selectBy(status=BugTaskStatus.EXPIRED).count()
222+ 0
223
224 >>> # We want to check the hoary bugtask messages later.
225 >>> starting_bug_messages_count = (hoary_bugtask.bug.messages.count())
226@@ -462,16 +462,16 @@
227
228 == After the script has run ==
229
230-There are three Invalid bugtasks. Jokosher, hoary and ubuntu were
231+There are three Expired bugtasks. Jokosher, hoary and ubuntu were
232 expired by the expiration process. Although ubuntu was never returned
233 by findExpirableBugTasks(), it was expired because its master (hoary)
234 was expired. The remaining bugtasks are unchanged.
235
236 >>> summarize_bugtasks(bugtasks)
237 ROLE EXPIRE AGE STATUS ASSIGNED DUP MILE REPLIES
238- ubuntu False 0 Invalid False False False False
239- hoary False 0 Invalid False False False False
240- jokosher False 0 Invalid False False False False
241+ ubuntu False 0 Expired False False False False
242+ hoary False 0 Expired False False False False
243+ jokosher False 0 Expired False False False False
244 jokosher watched False 61 Incomplete False False False False
245 thunderbird False 0 Won't Fix False False False False
246 assigned False 61 Incomplete True False False False
247@@ -482,17 +482,6 @@
248 recent True 31 Incomplete False False False False
249 no_expire False 61 Incomplete False False False False
250
251-Only the three test bugtasks were expired in the entire database. The
252-evolution task was already Invalid.
253-
254- >>> invalid_bugtasks = BugTask.selectBy(status=BugTaskStatus.INVALID)
255- >>> summarize_bugtasks(invalid_bugtasks)
256- ROLE EXPIRE AGE STATUS ASSIGNED DUP MILE REPLIES
257- ubuntu False 0 Invalid False False False False
258- hoary False 0 Invalid False False False False
259- jokosher False 0 Invalid False False False False
260- evolution False 61 Invalid False False False False
261-
262 The bugtasks statusexplanation was updated to explain the change in
263 status.
264
265@@ -522,7 +511,7 @@
266 >>> print "%s %s %s %s" % (
267 ... activity.person.displayname, activity.whatchanged,
268 ... activity.oldvalue, activity.newvalue)
269- Launchpad Janitor Ubuntu Hoary: status Incomplete Invalid
270+ Launchpad Janitor Ubuntu Hoary: status Incomplete Expired
271
272
273 == enable_bug_expiration ==
274
275=== modified file 'lib/lp/bugs/doc/bugtask-status-workflow.txt'
276--- lib/lp/bugs/doc/bugtask-status-workflow.txt 2009-06-12 16:36:02 +0000
277+++ lib/lp/bugs/doc/bugtask-status-workflow.txt 2010-04-16 11:43:29 +0000
278@@ -165,7 +165,7 @@
279 None
280
281 If the status is changed from any unresolved status to any resolved
282-status (Invalid or Fix Released), the date_closed property is
283+status (Invalid, Expired or Fix Released), the date_closed property is
284 set. The date_closed is always set to None when the task's status is
285 set to an open status. Note in the transition to FIXRELEASED the
286 date_inprogress is also set, when it had previously been None.
287@@ -181,6 +181,13 @@
288 >>> ubuntu_firefox_task.date_closed is None
289 True
290
291+ >>> ubuntu_firefox_task.transitionToStatus(
292+ ... BugTaskStatus.EXPIRED, getUtility(ILaunchBag).user)
293+ >>> ubuntu_firefox_task.date_closed
294+ datetime.datetime...
295+
296+ >>> ubuntu_firefox_task.transitionToStatus(
297+ ... BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
298 >>> ubuntu_firefox_task.date_inprogress is None
299 True
300 >>> ubuntu_firefox_task.transitionToStatus(
301
302=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
303--- lib/lp/bugs/interfaces/bugtask.py 2010-04-05 21:42:33 +0000
304+++ lib/lp/bugs/interfaces/bugtask.py 2010-04-16 11:43:29 +0000
305@@ -171,6 +171,12 @@
306 fixing, or it might not be fixed in this release.
307 """)
308
309+ EXPIRED = DBItem(19, """
310+ Expired
311+
312+ This bug is expired. There was no activity since a longer time.
313+ """)
314+
315 CONFIRMED = DBItem(20, """
316 Confirmed
317
318@@ -228,7 +234,7 @@
319
320 sort_order = (
321 'NEW', 'INCOMPLETE_WITH_RESPONSE', 'INCOMPLETE_WITHOUT_RESPONSE',
322- 'INCOMPLETE', 'INVALID', 'WONTFIX', 'CONFIRMED', 'TRIAGED',
323+ 'INCOMPLETE', 'INVALID', 'WONTFIX', 'EXPIRED', 'CONFIRMED', 'TRIAGED',
324 'INPROGRESS', 'FIXCOMMITTED', 'FIXRELEASED')
325
326 INCOMPLETE_WITH_RESPONSE = DBItem(35, """
327@@ -306,10 +312,12 @@
328 RESOLVED_BUGTASK_STATUSES = (
329 BugTaskStatus.FIXRELEASED,
330 BugTaskStatus.INVALID,
331- BugTaskStatus.WONTFIX)
332+ BugTaskStatus.WONTFIX,
333+ BugTaskStatus.EXPIRED)
334
335 BUG_SUPERVISOR_BUGTASK_STATUSES = (
336 BugTaskStatus.WONTFIX,
337+ BugTaskStatus.EXPIRED,
338 BugTaskStatus.TRIAGED)
339
340 DEFAULT_SEARCH_BUGTASK_STATUSES = (
341
342=== modified file 'lib/lp/bugs/model/bugtask.py'
343--- lib/lp/bugs/model/bugtask.py 2010-04-01 21:37:09 +0000
344+++ lib/lp/bugs/model/bugtask.py 2010-04-16 11:43:29 +0000
345@@ -810,7 +810,8 @@
346 if (user.inTeam(self.pillar.bug_supervisor) or
347 user.inTeam(self.pillar.owner) or
348 user.id == celebrities.bug_watch_updater.id or
349- user.id == celebrities.bug_importer.id):
350+ user.id == celebrities.bug_importer.id or
351+ user.id == celebrities.janitor.id):
352 return True
353 else:
354 return (self.status is not BugTaskStatus.WONTFIX and
355
356=== modified file 'lib/lp/bugs/scripts/bugexpire.py'
357--- lib/lp/bugs/scripts/bugexpire.py 2009-06-25 00:40:31 +0000
358+++ lib/lp/bugs/scripts/bugexpire.py 2010-04-16 11:43:29 +0000
359@@ -27,7 +27,7 @@
360
361 class BugJanitor:
362 """Expire Incomplete BugTasks that are older than a configurable period.
363-
364+
365 The BugTask must be unassigned, and the project it is associated with
366 must use Malone for bug tracking.
367 """
368@@ -80,7 +80,7 @@
369 bugtask_before_modification = Snapshot(
370 bugtask, providing=providedBy(bugtask))
371 bugtask.transitionToStatus(
372- BugTaskStatus.INVALID, self.janitor)
373+ BugTaskStatus.EXPIRED, self.janitor)
374 content = message_template % (
375 bugtask.bugtargetdisplayname, self.days_before_expiration)
376 bugtask.bug.newMessage(
377@@ -105,7 +105,7 @@
378
379 def _login(self):
380 """Setup an interaction as the bug janitor.
381-
382+
383 The role of bug janitor is usually played by bug_watch_updater.
384 """
385 auth_utility = getUtility(IPlacelessAuthUtility)
386
387=== modified file 'lib/lp/bugs/tests/bugs-emailinterface.txt'
388--- lib/lp/bugs/tests/bugs-emailinterface.txt 2009-08-13 15:12:16 +0000
389+++ lib/lp/bugs/tests/bugs-emailinterface.txt 2010-04-16 11:43:29 +0000
390@@ -1374,6 +1374,11 @@
391 >>> print upstream_task.status.title
392 Won't Fix
393
394+ >>> submit_commands(bug_four, 'status expired')
395+ >>> sync(upstream_task)
396+ >>> print upstream_task.status.title
397+ Expired
398+
399 Everyone else gets an explanatory error message:
400
401 >>> from canonical.launchpad.interfaces import BugTaskStatus
402@@ -1400,6 +1405,19 @@
403 registrant or a bug supervisor for Mozilla Firefox.
404 ...
405
406+ >>> submit_commands(bug_four, 'affects firefox', 'status expired')
407+ >>> print_latest_email()
408+ Subject: Submit Request Failure
409+ To: no-priv@canonical.com
410+ <BLANKLINE>
411+ ...
412+ Failing command:
413+ status expired
414+ ...
415+ The status cannot be changed to expired because you are not the
416+ registrant or a bug supervisor for Mozilla Firefox.
417+ ...
418+
419 Let's take a look at all the other error messages that the sub
420 commands can produce.
421
422@@ -1417,7 +1435,7 @@
423 status foo
424 ...
425 The 'status' command expects any of the following arguments:
426- new, incomplete, invalid, wontfix, confirmed, triaged, inprogress, fixcommitted, fixreleased
427+ new, incomplete, invalid, wontfix, expired, confirmed, triaged, inprogress, fixcommitted, fixreleased
428 <BLANKLINE>
429 For example:
430 <BLANKLINE>
431
432=== modified file 'lib/lp/bugs/tests/bugtarget-bugcount.txt'
433--- lib/lp/bugs/tests/bugtarget-bugcount.txt 2009-06-12 16:36:02 +0000
434+++ lib/lp/bugs/tests/bugtarget-bugcount.txt 2010-04-16 11:43:29 +0000
435@@ -13,6 +13,7 @@
436 INCOMPLETE
437 INVALID
438 WONTFIX
439+ EXPIRED
440 CONFIRMED
441 TRIAGED
442 INPROGRESS
443@@ -55,6 +56,7 @@
444 INCOMPLETE: 5 bug(s) more
445 INVALID: 5 bug(s) more
446 WONTFIX: 5 bug(s) more
447+ EXPIRED: 5 bug(s) more
448 CONFIRMED: 5 bug(s) more
449 TRIAGED: 5 bug(s) more
450 INPROGRESS: 5 bug(s) more
451
452=== modified file 'lib/lp/bugs/tests/test_bugtask_status.txt'
453--- lib/lp/bugs/tests/test_bugtask_status.txt 2010-04-01 17:13:37 +0000
454+++ lib/lp/bugs/tests/test_bugtask_status.txt 2010-04-16 11:43:29 +0000
455@@ -13,8 +13,8 @@
456 ... nopriv_user, 'Test bug', 'Something')
457 >>> firefox_bug = firefox.createBug(bug_params)
458
459-Bug Supervisors can transition bugs to the Won't Fix and Triaged
460-statuses.
461+Bug Supervisors can transition bugs to the Won't Fix, Expired and
462+Triaged statuses.
463
464 >>> [firefox_bugtask] = firefox_bug.bugtasks
465 >>> firefox_bugtask.transitionToStatus(
466@@ -23,12 +23,17 @@
467 Won't Fix
468
469 >>> firefox_bugtask.transitionToStatus(
470+ ... BugTaskStatus.EXPIRED, nopriv_user)
471+ >>> print firefox_bugtask.status.title
472+ Expired
473+
474+ >>> firefox_bugtask.transitionToStatus(
475 ... BugTaskStatus.TRIAGED, nopriv_user)
476 >>> print firefox_bugtask.status.title
477 Triaged
478
479-The product registrant can transition to the Won't Fix and Triaged
480-statuses too.
481+The product registrant can transition to the Won't Fix, Expired and
482+Triaged statuses too.
483
484 >>> firefox_bugtask.transitionToStatus(
485 ... BugTaskStatus.CONFIRMED, nopriv_user)
486@@ -44,6 +49,11 @@
487 Won't Fix
488
489 >>> firefox_bugtask.transitionToStatus(
490+ ... BugTaskStatus.EXPIRED, firefox.owner)
491+ >>> print firefox_bugtask.status.title
492+ Expired
493+
494+ >>> firefox_bugtask.transitionToStatus(
495 ... BugTaskStatus.TRIAGED, firefox.owner)
496 >>> print firefox_bugtask.status.title
497 Triaged
498@@ -65,6 +75,12 @@
499 UserCannotEditBugTaskStatus: Only Bug Supervisors may change status to Won't Fix.
500
501 >>> firefox_bugtask.transitionToStatus(
502+ ... BugTaskStatus.EXPIRED, nopriv_user)
503+ Traceback (most recent call last):
504+ ...
505+ UserCannotEditBugTaskStatus: Only Bug Supervisors may change status to Expired.
506+
507+ >>> firefox_bugtask.transitionToStatus(
508 ... BugTaskStatus.TRIAGED, nopriv_user)
509 Traceback (most recent call last):
510 ...
511
512=== modified file 'lib/lp/testing/factory.py'
513--- lib/lp/testing/factory.py 2010-04-15 14:38:43 +0000
514+++ lib/lp/testing/factory.py 2010-04-16 11:43:29 +0000
515@@ -645,7 +645,7 @@
516 self, name=None, project=None, displayname=None,
517 licenses=None, owner=None, registrant=None,
518 title=None, summary=None, official_malone=None,
519- official_rosetta=None):
520+ official_rosetta=None, bug_supervisor=None):
521 """Create and return a new, arbitrary Product."""
522 if owner is None:
523 owner = self.makePerson()
524@@ -676,6 +676,9 @@
525 product.official_malone = official_malone
526 if official_rosetta is not None:
527 removeSecurityProxy(product).official_rosetta = official_rosetta
528+ if bug_supervisor is not None:
529+ naked_product = removeSecurityProxy(product)
530+ naked_product.bug_supervisor = bug_supervisor
531 return product
532
533 def makeProductSeries(self, product=None, name=None, owner=None,