Merge lp:~kfogel/launchpad/506018-patch-report into lp:launchpad

Proposed by Karl Fogel
Status: Merged
Merged at revision: not available
Proposed branch: lp:~kfogel/launchpad/506018-patch-report
Merge into: lp:launchpad
Diff against target: 743 lines (+567/-9)
9 files modified
lib/canonical/launchpad/icing/style.css (+5/-1)
lib/lp/bugs/browser/bugtarget.py (+74/-2)
lib/lp/bugs/browser/configure.zcml (+8/-1)
lib/lp/bugs/interfaces/bugtarget.py (+1/-1)
lib/lp/bugs/interfaces/bugtask.py (+4/-0)
lib/lp/bugs/model/bugtarget.py (+1/-1)
lib/lp/bugs/stories/patches-view/patches-view.txt (+323/-0)
lib/lp/bugs/templates/bugtarget-patches.pt (+125/-0)
lib/lp/testing/factory.py (+26/-3)
To merge this branch: bzr merge lp:~kfogel/launchpad/506018-patch-report
Reviewer Review Type Date Requested Status
Karl Fogel (community) Approve
Martin Albisetti (community) ui Approve
Abel Deuring (community) code Approve
Eleanor Berger (community) code Approve
Review via email: mp+18181@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Karl Fogel (kfogel) wrote :

With Abel, add a "+patches" view based on IHasBugs.

The patch report lists bugtasks -- for bugs that have patch attachments -- on products, series, and packages. The listing can be sorted by importance, status, and bug age. (Sorting on patch age is not yet implemented, and will be done a separate branch. Likewise, the patch report for persons/teams is not yet implemented, and will be a separate branch.)

To review, load new sampledata from http://people.canonical.com/~kfogel/506018/newsampledata-dev.sql.gz . Then visit pages like these:

  https://bugs.launchpad.dev/patches-view-test/+patches
  https://bugs.launchpad.dev/firefox/+patches
  https://launchpad.dev/ubuntu/+patches
  https://bugs.launchpad.dev/ubuntu/+source/cdrkit/+patches

Revision history for this message
Martin Albisetti (beuno) wrote :

Overall, this branch is fantastic. I know a lot of people will appreciate a lot your work on this.
A few small tweaks discussed on IRC:

- Padding in the tooltips would be super nice
- I'd add an "Order by: " label to the ordering drop down, and drop the "by" from each option to improve readability
- Bug icons don't respect importance

review: Needs Fixing (ui)
Revision history for this message
Eleanor Berger (intellectronica) wrote :
Download full text (30.7 KiB)

> === modified file 'lib/canonical/launchpad/icing/style.css'
> --- lib/canonical/launchpad/icing/style.css     2010-01-20 21:57:44 +0000
> +++ lib/canonical/launchpad/icing/style.css     2010-01-28 17:24:20 +0000
> @@ -1458,7 +1458,7 @@
>
>  div.popupTitle {
>   background: #ffffdc;
> -  padding: 0 1em;
> +  padding: 0.5em 1em;
>   border: 1px black solid;
>   position: absolute;
>   display: none;
>
> === modified file 'lib/lp/bugs/browser/bugtarget.py'
> --- lib/lp/bugs/browser/bugtarget.py    2010-01-07 05:41:58 +0000
> +++ lib/lp/bugs/browser/bugtarget.py    2010-01-28 17:24:20 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009 Canonical Ltd.  This software is licensed under the
> +# Copyright 2010 Canonical Ltd.  This software is licensed under the
>  # GNU Affero General Public License version 3 (see the file LICENSE).
>
>  """IBugTarget-related browser views."""
> @@ -15,12 +15,15 @@
>     "FileBugViewBase",
>     "OfficialBugTagsManageView",
>     "ProjectFileBugGuidedView",
> +    "BugsPatchesView",
>     ]

Let's sort this alphabetically, so that it's easier to locate an export later.

>
>  import cgi
>  from cStringIO import StringIO
> +from datetime import datetime
>  from email import message_from_string
>  from operator import itemgetter
> +from pytz import timezone
>  from simplejson import dumps
>  import tempfile
>  import urllib
> @@ -40,6 +43,7 @@
>  from canonical.config import config
>  from lp.bugs.browser.bugtask import BugTaskSearchListingView
>  from lp.bugs.interfaces.bug import IBug
> +from lp.bugs.interfaces.bugattachment import BugAttachmentType
>  from canonical.launchpad.browser.feeds import (
>     BugFeedLink, BugTargetLatestBugsFeedLink, FeedsMixin,
>     PersonLatestBugsFeedLink)
> @@ -73,6 +77,7 @@
>     LaunchpadEditFormView, LaunchpadFormView, LaunchpadView, action,
>     canonical_url, custom_widget, safe_action)
>  from canonical.launchpad.webapp.authorization import check_permission
> +from canonical.launchpad.webapp.batching import BatchNavigator
>  from canonical.launchpad.webapp.tales import BugTrackerFormatterAPI
>  from canonical.launchpad.validators.name import valid_name_pattern
>  from canonical.launchpad.webapp.menu import structured
> @@ -1372,3 +1377,51 @@
>  class BugsVHostBreadcrumb(Breadcrumb):
>     rootsite = 'bugs'
>     text = 'Bugs'
> +
> +
> +class BugsPatchesView(LaunchpadView):
> +    """View list of patch attachments associated with bugs."""
> +
> +    @property
> +    def label(self):
> +        """The display label for the view."""
> +        return 'Patch attachments in %s' % self.context.displayname
> +
> +    def batchedPatchTasks(self):
> +        """Return a BatchNavigator for bug tasks with patch attachments."""
> +        any_unresolved_plus_fixreleased = \
> +            UNRESOLVED_BUGTASK_STATUSES + (BugTaskStatus.FIXRELEASED,)

We avoid using line continuation in Launchpad code, because we don't like how
it messes with the interpretation of the text file (the parser consideres
everything after the \ to actually be on the same line). Instead we use
parentheses, like this:

       any_unresolved_plus_fixreleased = (
           UNRESOLVED_BUGTASK_STATUSES + (BugTaskStatus.FIXR...

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

On 28.01.2010 19:23, Tom Berger wrote:
>> === added directory 'lib/lp/bugs/stories/patches-view'
>> === added file 'lib/lp/bugs/stories/patches-view/patches-view.txt'
>> --- lib/lp/bugs/stories/patches-view/patches-view.txt 1970-01-01 00:00:00 +0000
>> +++ lib/lp/bugs/stories/patches-view/patches-view.txt 2010-01-28 17:24:20 +0000
>> @@ -0,0 +1,301 @@
>> +Patches View
>> +============
>> +
>> +Patches View by Product
>> +-----------------------
>> +
>> +We have a view listing patches attached to bugs that target a given
>> +product. At first, the product is new and has no bugs.
>> +
>> + >>> def make_thing(factory_method, **thing_args):
>> + ... login('<email address hidden>')
>> + ... result = factory_method(**thing_args)
>> + ... transaction.commit()
>> + ... logout()
>> + ... return result
>
> That's a nice helper function. Let's extract it out of this test and make it
> generally available to all tests. It probably deserves a more descriptive name,
> though.

A generally available function should have a way to specify the user
that should login: Some objects can only be created by script users.

And this is actually a good candidate for using the "with" statement --
provided that Jeroen does not object to using it for "transaction
wrapping". (see the notes for last reviewers meeting. Has Barry posted
them already?)

Something like

    with factory.database_login('<email address hidden>'):
 factory.makePerson(k1=v1, k2=v2)

would look much nicer than

    make_thing(factory.makePerson, k1=v1, k2=v2...)

Abel

Revision history for this message
Eleanor Berger (intellectronica) wrote :

On 28 January 2010 19:51, Abel Deuring <email address hidden> wrote:
> On 28.01.2010 19:23, Tom Berger wrote:
>>> === added directory 'lib/lp/bugs/stories/patches-view'
>>> === added file 'lib/lp/bugs/stories/patches-view/patches-view.txt'
>>> --- lib/lp/bugs/stories/patches-view/patches-view.txt   1970-01-01 00:00:00 +0000
>>> +++ lib/lp/bugs/stories/patches-view/patches-view.txt   2010-01-28 17:24:20 +0000
>>> @@ -0,0 +1,301 @@
>>> +Patches View
>>> +============
>>> +
>>> +Patches View by Product
>>> +-----------------------
>>> +
>>> +We have a view listing patches attached to bugs that target a given
>>> +product.  At first, the product is new and has no bugs.
>>> +
>>> +    >>> def make_thing(factory_method, **thing_args):
>>> +    ...     login('<email address hidden>')
>>> +    ...     result = factory_method(**thing_args)
>>> +    ...     transaction.commit()
>>> +    ...     logout()
>>> +    ...     return result
>>
>> That's a nice helper function. Let's extract it out of this test and make it
>> generally available to all tests. It probably deserves a more descriptive name,
>> though.
>
> A generally available function should have a way to specify the user
> that should login: Some objects can only be created by script users.
>
> And this is actually a good candidate for using the "with" statement --
> provided that Jeroen does not object to using it for "transaction
> wrapping". (see the notes for last reviewers meeting. Has Barry posted
> them already?)
>
> Something like
>
>    with factory.database_login('<email address hidden>'):
>        factory.makePerson(k1=v1, k2=v2)
>
> would look much nicer than
>
>    make_thing(factory.makePerson, k1=v1, k2=v2...)

+1000000

Revision history for this message
Karl Fogel (kfogel) wrote :

Well, doing the Python 'with' thing in story test code proved problematic. But otherwise, all comments have been incorporated now, and pushed up. Thanks, guys.

Revision history for this message
Karl Fogel (kfogel) wrote :

Note that Michael Hudson effectively reviewed my re-submission when he reviewed

 https://code.edge.launchpad.net/~intellectronica/launchpad/no-patches-message/+merge/18428

because he commented on my changes from here (which had been merged into that branch) as well as Tom's. I'm not sure he found any problems with Tom's, actually :-).

So, before reviewing this, please see Michael's comments; he may have already said what you were going to say.

review: Needs Fixing
Revision history for this message
Eleanor Berger (intellectronica) wrote :

You've addressed all of my concerns and the code looks great now.

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

Hi Karl,

I looked at the changes between revision 10189 and 10194 -- everything is fine.

review: Approve (code)
Revision history for this message
Abel Deuring (adeuring) wrote :
Revision history for this message
Martin Albisetti (beuno) wrote :

A wonderful piece of work. The sooner this lands, the better.

review: Approve (ui)
Revision history for this message
Karl Fogel (kfogel) wrote :

LGTM too now, not that I'm a reviewer, and it's mostly my own change.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/icing/style.css'
--- lib/canonical/launchpad/icing/style.css 2010-01-21 22:07:42 +0000
+++ lib/canonical/launchpad/icing/style.css 2010-02-02 18:46:23 +0000
@@ -1237,6 +1237,10 @@
1237 margin: 0;1237 margin: 0;
1238}1238}
12391239
1240.patches-view-cell {
1241 padding-right: 1em;
1242}
1243
1240/* --- Blueprints --- */1244/* --- Blueprints --- */
12411245
1242body.tab-specifications #actions, body.tab-specifications .results {1246body.tab-specifications #actions, body.tab-specifications .results {
@@ -1463,7 +1467,7 @@
14631467
1464div.popupTitle {1468div.popupTitle {
1465 background: #ffffdc;1469 background: #ffffdc;
1466 padding: 0 1em;1470 padding: 0.5em 1em;
1467 border: 1px black solid;1471 border: 1px black solid;
1468 position: absolute;1472 position: absolute;
1469 display: none;1473 display: none;
14701474
=== modified file 'lib/lp/bugs/browser/bugtarget.py'
--- lib/lp/bugs/browser/bugtarget.py 2010-01-29 19:00:47 +0000
+++ lib/lp/bugs/browser/bugtarget.py 2010-02-02 18:46:23 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""IBugTarget-related browser views."""4"""IBugTarget-related browser views."""
@@ -7,6 +7,7 @@
77
8__all__ = [8__all__ = [
9 "BugsVHostBreadcrumb",9 "BugsVHostBreadcrumb",
10 "BugsPatchesView",
10 "BugTargetBugListingView",11 "BugTargetBugListingView",
11 "BugTargetBugTagsView",12 "BugTargetBugTagsView",
12 "BugTargetBugsView",13 "BugTargetBugsView",
@@ -19,8 +20,10 @@
1920
20import cgi21import cgi
21from cStringIO import StringIO22from cStringIO import StringIO
23from datetime import datetime
22from email import message_from_string24from email import message_from_string
23from operator import itemgetter25from operator import itemgetter
26from pytz import timezone
24from simplejson import dumps27from simplejson import dumps
25import tempfile28import tempfile
26import urllib29import urllib
@@ -40,6 +43,7 @@
40from canonical.config import config43from canonical.config import config
41from lp.bugs.browser.bugtask import BugTaskSearchListingView44from lp.bugs.browser.bugtask import BugTaskSearchListingView
42from lp.bugs.interfaces.bug import IBug45from lp.bugs.interfaces.bug import IBug
46from lp.bugs.interfaces.bugattachment import BugAttachmentType
43from canonical.launchpad.browser.feeds import (47from canonical.launchpad.browser.feeds import (
44 BugFeedLink, BugTargetLatestBugsFeedLink, FeedsMixin,48 BugFeedLink, BugTargetLatestBugsFeedLink, FeedsMixin,
45 PersonLatestBugsFeedLink)49 PersonLatestBugsFeedLink)
@@ -48,7 +52,8 @@
48 IBugTarget, IOfficialBugTagTargetPublic, IOfficialBugTagTargetRestricted)52 IBugTarget, IOfficialBugTagTargetPublic, IOfficialBugTagTargetRestricted)
49from lp.bugs.interfaces.bug import IBugSet53from lp.bugs.interfaces.bug import IBugSet
50from lp.bugs.interfaces.bugtask import (54from lp.bugs.interfaces.bugtask import (
51 BugTaskStatus, IBugTaskSet, UNRESOLVED_BUGTASK_STATUSES)55 BugTaskStatus, IBugTaskSet, UNRESOLVED_BUGTASK_STATUSES,
56 UNRESOLVED_PLUS_FIXRELEASED_BUGTASK_STATUSES)
52from canonical.launchpad.interfaces.launchpad import (57from canonical.launchpad.interfaces.launchpad import (
53 IHasExternalBugTracker, ILaunchpadUsage)58 IHasExternalBugTracker, ILaunchpadUsage)
54from canonical.launchpad.interfaces.hwdb import IHWSubmissionSet59from canonical.launchpad.interfaces.hwdb import IHWSubmissionSet
@@ -73,6 +78,7 @@
73 LaunchpadEditFormView, LaunchpadFormView, LaunchpadView, action,78 LaunchpadEditFormView, LaunchpadFormView, LaunchpadView, action,
74 canonical_url, custom_widget, safe_action)79 canonical_url, custom_widget, safe_action)
75from canonical.launchpad.webapp.authorization import check_permission80from canonical.launchpad.webapp.authorization import check_permission
81from canonical.launchpad.webapp.batching import BatchNavigator
76from canonical.launchpad.webapp.tales import BugTrackerFormatterAPI82from canonical.launchpad.webapp.tales import BugTrackerFormatterAPI
77from canonical.launchpad.validators.name import valid_name_pattern83from canonical.launchpad.validators.name import valid_name_pattern
78from canonical.launchpad.webapp.menu import structured84from canonical.launchpad.webapp.menu import structured
@@ -1384,3 +1390,69 @@
1384class BugsVHostBreadcrumb(Breadcrumb):1390class BugsVHostBreadcrumb(Breadcrumb):
1385 rootsite = 'bugs'1391 rootsite = 'bugs'
1386 text = 'Bugs'1392 text = 'Bugs'
1393
1394
1395class BugsPatchesView(LaunchpadView):
1396 """View list of patch attachments associated with bugs."""
1397
1398 @property
1399 def label(self):
1400 """The display label for the view."""
1401 return 'Patch attachments in %s' % self.context.displayname
1402
1403 def batchedPatchTasks(self):
1404 """Return a BatchNavigator for bug tasks with patch attachments."""
1405 # XXX: Karl Fogel 2010-02-01 bug=515584: we should be using a
1406 # Zope form instead of validating the values by hand in the
1407 # code. Doing it the Zope form way would specify rendering
1408 # and validation from the same enum, and thus observe DRY.
1409 orderby = self.request.get("orderby")
1410 if (orderby is not None and
1411 orderby not in ["-importance", "status", "targetname",
1412 "datecreated", "-datecreated"]):
1413 raise UnexpectedFormData(
1414 "Unexpected value for field 'orderby': '%s'" % orderby)
1415 return BatchNavigator(
1416 self.context.searchTasks(
1417 None, user=self.user, order_by=orderby,
1418 status=UNRESOLVED_PLUS_FIXRELEASED_BUGTASK_STATUSES,
1419 omit_duplicates=True, has_patch=True),
1420 self.request)
1421
1422 def targetName(self):
1423 """Return the name of the current context's target type, or None.
1424
1425 The name is something like "Package" or "Project" (meaning
1426 Product); it is intended to be appropriate to use as a column
1427 name in a web page, for example. If no target type is
1428 appropriate for the current context, then return None.
1429 """
1430 if (IDistribution.providedBy(self.context) or
1431 IDistroSeries.providedBy(self.context)):
1432 return "Package"
1433 elif IProject.providedBy(self.context):
1434 return "Project" # meaning Product
1435 else:
1436 return None
1437
1438 def youngestPatch(self, bug):
1439 """Return the youngest patch attached to a bug, else error."""
1440 youngest = None
1441 # Loop over bugtasks, gathering youngest patch for each's bug.
1442 for attachment in bug.attachments:
1443 if attachment.is_patch:
1444 if youngest is None:
1445 youngest = attachment
1446 elif (attachment.message.datecreated >
1447 youngest.message.datecreated):
1448 youngest = attachment
1449 if youngest is None:
1450 # This is the patches view, so every bug under
1451 # consideration should have at least one patch attachment.
1452 raise AssertionError("bug %i has no patch attachments" % bug.id)
1453 return youngest
1454
1455 def patchAge(self, patch):
1456 """Return a timedelta object for the age of a patch attachment."""
1457 now = datetime.now(timezone('UTC'))
1458 return now - patch.message.datecreated
13871459
=== modified file 'lib/lp/bugs/browser/configure.zcml'
--- lib/lp/bugs/browser/configure.zcml 2010-01-18 21:44:59 +0000
+++ lib/lp/bugs/browser/configure.zcml 2010-02-02 18:46:23 +0000
@@ -1,4 +1,4 @@
1<!-- Copyright 2009 Canonical Ltd. This software is licensed under the1<!-- Copyright 2010 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
@@ -108,6 +108,13 @@
108 facet="bugs"108 facet="bugs"
109 permission="launchpad.Edit"109 permission="launchpad.Edit"
110 template="../templates/official-bug-target-manage-tags.pt"/>110 template="../templates/official-bug-target-manage-tags.pt"/>
111 <browser:page
112 name="+patches"
113 for="lp.bugs.interfaces.bugtarget.IHasBugs"
114 class="lp.bugs.browser.bugtarget.BugsPatchesView"
115 facet="bugs"
116 permission="zope.Public"
117 template="../templates/bugtarget-patches.pt"/>
111 </facet>118 </facet>
112 <browser:page119 <browser:page
113 name="+bugtarget-macros-search"120 name="+bugtarget-macros-search"
114121
=== modified file 'lib/lp/bugs/interfaces/bugtarget.py'
--- lib/lp/bugs/interfaces/bugtarget.py 2009-08-18 11:12:06 +0000
+++ lib/lp/bugs/interfaces/bugtarget.py 2010-02-02 18:46:23 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4# pylint: disable-msg=E0211,E02134# pylint: disable-msg=E0211,E0213
55
=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py 2010-02-01 22:47:12 +0000
+++ lib/lp/bugs/interfaces/bugtask.py 2010-02-02 18:46:23 +0000
@@ -36,6 +36,7 @@
36 'IUpstreamProductBugTaskSearch',36 'IUpstreamProductBugTaskSearch',
37 'RESOLVED_BUGTASK_STATUSES',37 'RESOLVED_BUGTASK_STATUSES',
38 'UNRESOLVED_BUGTASK_STATUSES',38 'UNRESOLVED_BUGTASK_STATUSES',
39 'UNRESOLVED_PLUS_FIXRELEASED_BUGTASK_STATUSES',
39 'UserCannotEditBugTaskImportance',40 'UserCannotEditBugTaskImportance',
40 'UserCannotEditBugTaskMilestone',41 'UserCannotEditBugTaskMilestone',
41 'UserCannotEditBugTaskStatus',42 'UserCannotEditBugTaskStatus',
@@ -287,6 +288,9 @@
287 BugTaskStatus.INPROGRESS,288 BugTaskStatus.INPROGRESS,
288 BugTaskStatus.FIXCOMMITTED)289 BugTaskStatus.FIXCOMMITTED)
289290
291UNRESOLVED_PLUS_FIXRELEASED_BUGTASK_STATUSES = (
292 UNRESOLVED_BUGTASK_STATUSES + (BugTaskStatus.FIXRELEASED,))
293
290RESOLVED_BUGTASK_STATUSES = (294RESOLVED_BUGTASK_STATUSES = (
291 BugTaskStatus.FIXRELEASED,295 BugTaskStatus.FIXRELEASED,
292 BugTaskStatus.INVALID,296 BugTaskStatus.INVALID,
293297
=== modified file 'lib/lp/bugs/model/bugtarget.py'
--- lib/lp/bugs/model/bugtarget.py 2010-01-21 16:47:24 +0000
+++ lib/lp/bugs/model/bugtarget.py 2010-02-02 18:46:23 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4# pylint: disable-msg=E0611,W02124# pylint: disable-msg=E0611,W0212
55
=== added directory 'lib/lp/bugs/stories/patches-view'
=== added file 'lib/lp/bugs/stories/patches-view/patches-view.txt'
--- lib/lp/bugs/stories/patches-view/patches-view.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/stories/patches-view/patches-view.txt 2010-02-02 18:46:23 +0000
@@ -0,0 +1,323 @@
1Patches View
2============
3
4Patches View by Product
5-----------------------
6
7We have a view listing patches attached to bugs that target a given
8product. At first, the product is new and has no bugs.
9
10 >>> patchy_product = factory.doAsUser(
11 ... 'foo.bar@canonical.com', factory.makeProduct,
12 ... name='patchy-product-1')
13
14We don't see any patches when we open the patches view.
15
16 >>> def show_patches_view(contents):
17 ... for tag in find_tags_by_class(contents, 'listing'):
18 ... print extract_text(tag)
19
20 >>> anon_browser.open(
21 ... 'http://bugs.launchpad.dev/patchy-product-1/+patches')
22 >>> show_patches_view(anon_browser.contents)
23 Bug Importance Status Patch Age
24
25After the product has a bug, it still doesn't show up in the patches
26view, because that bug has no patch attachments.
27
28 >>> from lp.bugs.interfaces.bugtask import (
29 ... BugTaskImportance, BugTaskStatus)
30 >>> def make_bug(
31 ... title, product, importance=BugTaskImportance.UNDECIDED,
32 ... status=BugTaskStatus.NEW):
33 ... bug = factory.makeBug(title=title, product=product)
34 ... bug.default_bugtask.transitionToImportance(
35 ... importance, product.owner)
36 ... bug.default_bugtask.transitionToStatus(
37 ... status, product.owner)
38 ... return bug
39
40 >>> bug_a = factory.doAsUser(
41 ... 'foo.bar@canonical.com', make_bug,
42 ... title="bug_a title", product=patchy_product)
43 >>> anon_browser.open(
44 ... 'http://bugs.launchpad.dev/patchy-product-1/+patches')
45 >>> show_patches_view(anon_browser.contents)
46 Bug Importance Status Patch Age
47
48After we add a non-patch attachment to that bug, the patches view
49still shows no patches.
50
51 >>> factory.doAsUser('foo.bar@canonical.com', factory.makeBugAttachment,
52 ... bug=bug_a, is_patch=False)
53 <BugAttachment at...
54 >>> anon_browser.open('http://bugs.launchpad.dev/patchy-product-1/+patches')
55 >>> show_patches_view(anon_browser.contents)
56 Bug Importance Status Patch Age
57
58After we add a patch attachment that's one day old, we see it in the
59patches view.
60
61 >>> patch_submitter = factory.doAsUser(
62 ... 'foo.bar@canonical.com', factory.makePerson,
63 ... name="patchy-person", displayname="Patchy Person")
64 >>> factory.doAsUser(
65 ... 'foo.bar@canonical.com', factory.makeBugAttachment,
66 ... comment="comment about patch a",
67 ... filename="patch_a.diff", owner=patch_submitter,
68 ... description="description of patch a", bug=bug_a, is_patch=True)
69 <BugAttachment at...
70 >>> anon_browser.open(
71 ... 'http://bugs.launchpad.dev/patchy-product-1/+patches')
72 >>> show_patches_view(anon_browser.contents)
73 Bug Importance Status Patch Age
74 Bug #16: bug_a title Undecided New ...second...
75 From: Patchy Person
76 Link: patch_a.diff description of patch a
77
78After creating some more bugs, with some non-patch and some patch
79attachments...
80
81 >>> bug_b = factory.doAsUser(
82 ... 'foo.bar@canonical.com', make_bug,
83 ... title="bug_b title", product=patchy_product,
84 ... importance=BugTaskImportance.CRITICAL,
85 ... status=BugTaskStatus.CONFIRMED)
86 >>> bug_c = factory.doAsUser(
87 ... 'foo.bar@canonical.com', make_bug,
88 ... title="bug_c title", product=patchy_product,
89 ... importance=BugTaskImportance.WISHLIST,
90 ... status=BugTaskStatus.FIXCOMMITTED)
91 >>> factory.doAsUser(
92 ... 'foo.bar@canonical.com', factory.makeBugAttachment,
93 ... comment="comment about patch b",
94 ... filename="patch_b.diff", owner=patch_submitter,
95 ... description="description of patch b", bug=bug_b, is_patch=True)
96 <BugAttachment at...
97 >>> factory.doAsUser(
98 ... 'foo.bar@canonical.com', factory.makeBugAttachment,
99 ... comment="comment about patch c",
100 ... filename="patch_c.diff", owner=patch_submitter,
101 ... description="description of patch c", bug=bug_b, is_patch=True)
102 <BugAttachment at...
103 >>> factory.doAsUser(
104 ... 'foo.bar@canonical.com', factory.makeBugAttachment,
105 ... bug=bug_c, is_patch=False)
106 <BugAttachment at...
107 >>> factory.doAsUser(
108 ... 'foo.bar@canonical.com', factory.makeBugAttachment,
109 ... comment="comment about patch d",
110 ... filename="patch_d.diff", owner=patch_submitter,
111 ... description="description of patch d", bug=bug_c, is_patch=True)
112 <BugAttachment at...
113 >>> factory.doAsUser(
114 ... 'foo.bar@canonical.com', factory.makeBugAttachment,
115 ... comment="comment about patch e",
116 ... filename="patch_e.diff", owner=patch_submitter,
117 ... description="description of patch e", bug=bug_c, is_patch=True)
118 <BugAttachment at...
119 >>> factory.doAsUser(
120 ... 'foo.bar@canonical.com', factory.makeBugAttachment,
121 ... comment="comment about patch f",
122 ... filename="patch_f.diff", owner=patch_submitter,
123 ... description="description of patch f", bug=bug_c, is_patch=True)
124 <BugAttachment at...
125
126...the youngest patch on each bug is visible is the patch report.
127
128 >>> anon_browser.open('http://bugs.launchpad.dev/patchy-product-1/+patches')
129 >>> show_patches_view(anon_browser.contents)
130 Bug Importance Status Patch Age
131 Bug #17: bug_b title Critical Confirmed ...second...
132 From: Patchy Person
133 Link: patch_c.diff description of patch c
134 Bug #18: bug_c title Wishlist Fix Committed ...second...
135 From: Patchy Person
136 Link: patch_f.diff description of patch f
137 Bug #16: bug_a title Undecided New ...second...
138 From: Patchy Person
139 Link: patch_a.diff description of patch a
140
141We can sort patches by importance and status.
142
143 >>> anon_browser.getControl(name="orderby").value = ['-importance']
144 >>> anon_browser.getControl("sort").click()
145 >>> anon_browser.url
146 'http://bugs.launchpad.dev/patchy-product-1/+patches?orderby=-importance'
147 >>> show_patches_view(anon_browser.contents)
148 Bug Importance Status Patch Age
149 Bug #17: bug_b title Critical Confirmed ...second...
150 From: Patchy Person
151 Link: patch_c.diff description of patch c
152 Bug #18: bug_c title Wishlist Fix Committed ...second...
153 From: Patchy Person
154 Link: patch_f.diff description of patch f
155 Bug #16: bug_a title Undecided New ...second...
156 From: Patchy Person
157 Link: patch_a.diff description of patch a
158
159 >>> anon_browser.getControl(name="orderby").value = ['status']
160 >>> anon_browser.getControl("sort").click()
161 >>> anon_browser.url
162 'http://bugs.launchpad.dev/patchy-product-1/+patches?orderby=status'
163 >>> show_patches_view(anon_browser.contents)
164 Bug Importance Status Patch Age
165 Bug #16: bug_a title Undecided New ...second...
166 From: Patchy Person
167 Link: patch_a.diff description of patch a
168 Bug #17: bug_b title Critical Confirmed ...second...
169 From: Patchy Person
170 Link: patch_c.diff description of patch c
171 Bug #18: bug_c title Wishlist Fix Committed ...second...
172 From: Patchy Person
173 Link: patch_f.diff description of patch f
174
175Bugs in a product series show up in the patches view for that series.
176
177 >>> from zope.component import getUtility
178 >>> from lp.registry.interfaces.distribution import IDistributionSet
179 >>> def make_bugtask(
180 ... # Meta-factory for making bugtasks.
181 ... #
182 ... # In all instances where a distro is needed, defaults to
183 ... # 'ubuntu' distro.
184 ... #
185 ... # :param bug: The bug with which the task is associated.
186 ... # :param target: The target to which to attach this bug.
187 ... # If the target is a string, then it names the target
188 ... # object, and exactly one of following two boolean
189 ... # parameters must be set to indicate the object type.
190 ... # :param target_is_spkg_name: If true, target is a string
191 ... # indicating the name of the source package for the task.
192 ... # :param target_is_distroseries_name: If true, target is a string
193 ... # indicating the name of the distroseries for the task.
194 ... # :param importance: The initial importance of the bugtask;
195 ... # if None, just use the default importance.
196 ... # :param status: The initial status of the bugtask;
197 ... # if None, just use the default status.
198 ... bug, target,
199 ... target_is_spkg_name=False,
200 ... target_is_distroseries_name=False,
201 ... importance=None, status=None):
202 ... ubuntu_distro = getUtility(IDistributionSet).getByName('ubuntu')
203 ... if target_is_spkg_name:
204 ... target = ubuntu_distro.getSourcePackage(target)
205 ... if target_is_distroseries_name:
206 ... target = ubuntu_distro.getSeries(target)
207 ... bugtask = factory.makeBugTask(bug=bug, target=target)
208 ... if importance is not None:
209 ... bugtask.transitionToImportance(importance, ubuntu_distro.owner)
210 ... if status is not None:
211 ... bugtask.transitionToStatus(status, ubuntu_distro.owner)
212 >>> patchy_product_series = patchy_product.getSeries('trunk')
213 >>> factory.doAsUser(
214 ... 'foo.bar@canonical.com', make_bugtask,
215 ... bug=bug_a, target=patchy_product_series)
216 >>> factory.doAsUser(
217 ... 'foo.bar@canonical.com', make_bugtask,
218 ... bug=bug_c, target=patchy_product_series)
219 >>> anon_browser.open(
220 ... 'https://launchpad.dev/patchy-product-1/trunk/+patches')
221 >>> show_patches_view(anon_browser.contents)
222 Bug Importance Status Patch Age
223 Bug #18: bug_c title Wishlist Fix Committed ...second...
224 From: Patchy Person
225 Link: patch_f.diff
226 description of patch f
227 Bug #16: bug_a title Undecided New ...second...
228 From: Patchy Person
229 Link: patch_a.diff
230 description of patch a
231
232Patches View by Distro
233----------------------
234
235The patches view also works for distributions, and it shows the target
236package when viewed via a distribution.
237
238 >>> factory.doAsUser(
239 ... 'foo.bar@canonical.com', make_bugtask, bug=bug_a,
240 ... target='evolution', target_is_spkg_name=True,
241 ... importance=BugTaskImportance.MEDIUM,
242 ... status=BugTaskStatus.FIXRELEASED)
243 >>> factory.doAsUser(
244 ... 'foo.bar@canonical.com', make_bugtask, bug=bug_c,
245 ... target='a52dec', target_is_spkg_name=True,
246 ... importance=BugTaskImportance.HIGH,
247 ... status=BugTaskStatus.TRIAGED)
248
249 >>> anon_browser.open('http://bugs.launchpad.dev/ubuntu/+patches')
250 >>> show_patches_view(anon_browser.contents)
251 Bug Importance Status Package Patch Age
252 Bug #18: bug_c title High Triaged a52dec ...second...
253 From: Patchy Person
254 Link: patch_f.diff description of patch f
255 Bug #16: bug_a title Medium Fix Released evolution ...second...
256 From: Patchy Person
257 Link: patch_a.diff description of patch a
258
259Patches View by Distro Series
260-----------------------------
261
262The patches view works for distro series.
263
264 >>> factory.doAsUser(
265 ... 'foo.bar@canonical.com', make_bugtask, bug=bug_a,
266 ... target='hoary', target_is_distroseries_name=True)
267 >>> factory.doAsUser(
268 ... 'foo.bar@canonical.com', make_bugtask, bug=bug_a,
269 ... target='warty', target_is_distroseries_name=True)
270 >>> factory.doAsUser(
271 ... 'foo.bar@canonical.com', make_bugtask, bug=bug_b,
272 ... target='warty', target_is_distroseries_name=True)
273 >>> factory.doAsUser(
274 ... 'foo.bar@canonical.com', make_bugtask, bug=bug_c,
275 ... target='warty', target_is_distroseries_name=True,
276 ... importance=BugTaskImportance.HIGH,
277 ... status=BugTaskStatus.TRIAGED)
278 >>> anon_browser.open('https://launchpad.dev/ubuntu/hoary/+patches')
279 >>> show_patches_view(anon_browser.contents)
280 Bug Importance Status Package Patch Age
281 Bug #16: bug_a title Undecided New hoary ...second...
282 From: Patchy Person
283 Link: patch_a.diff
284 description of patch a
285 >>> anon_browser.open('https://launchpad.dev/ubuntu/warty/+patches')
286 >>> show_patches_view(anon_browser.contents)
287 Bug Importance Status Package Patch Age
288 Bug #18: bug_c title High Triaged warty ...second...
289 From: Patchy Person
290 Link: patch_f.diff description of patch f
291 Bug #16: bug_a title Undecided New warty ...second...
292 From: Patchy Person
293 Link: patch_a.diff
294 description of patch a
295 Bug #17: bug_b title Undecided New warty ...second...
296 From: Patchy Person
297 Link: patch_c.diff
298 description of patch c
299
300Patches View by Source Package
301------------------------------
302
303The patches view works for source packages too. The view doesn't show
304target package column in that case, because the package is implied.
305
306 >>> anon_browser.open(
307 ... 'http://bugs.launchpad.dev/ubuntu/+source/a52dec/+patches')
308 >>> show_patches_view(anon_browser.contents)
309 Bug Importance Status Patch Age
310 Bug #18: bug_c title High Triaged ...second...
311 From: Patchy Person
312 Link: patch_f.diff description of patch f
313 >>> anon_browser.open(
314 ... 'http://bugs.launchpad.dev/ubuntu/+source/evolution/+patches')
315 >>> show_patches_view(anon_browser.contents)
316 Bug Importance Status Patch Age
317 Bug #16: bug_a title Medium Fix Released ...second...
318 From: Patchy Person
319 Link: patch_a.diff description of patch a
320
321# XXX Karl Fogel 2010-02-02 bug=516186: We should also test the
322# patches view on a project group, to make sure the "Package" column
323# becomes "Project" in that context.
0324
=== added file 'lib/lp/bugs/templates/bugtarget-patches.pt'
--- lib/lp/bugs/templates/bugtarget-patches.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/templates/bugtarget-patches.pt 2010-02-02 18:46:23 +0000
@@ -0,0 +1,125 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 xml:lang="en"
7 lang="en"
8 dir="ltr"
9 metal:use-macro="view/macro:page/main_only"
10 i18n:domain="malone"
11>
12
13 <body>
14 <div metal:fill-slot="main" class="tab-bugs"
15 tal:define="batchnav view/batchedPatchTasks;
16 batch batchnav/currentBatch">
17
18 <form class="lesser" id="sort" method="get"
19 tal:attributes="action string:${context/fmt:url}/+patches">
20
21 <script type="text/javascript">
22 YUI().use('base', 'node', 'event', function(Y) {
23 Y.on('domready', function(e) {
24 Y.get('#sort-button').setStyle('display', 'none');
25 Y.get('#orderby').on('change', function(e) {
26 Y.get('#sort').submit();
27 });
28 });
29 });
30 </script>
31
32 Order&nbsp;by:&nbsp;<select
33 name="orderby" id="orderby" size="1"
34 tal:define="orderby request/orderby|string:-importance">
35 <option
36 value="-importance"
37 tal:attributes="selected python:orderby == '-importance'"
38 >importance</option>
39 <option
40 value="status"
41 tal:attributes="selected python:orderby == 'status'"
42 >status</option>
43 <option
44 tal:condition="view/targetName"
45 tal:content="view/targetName"
46 tal:attributes="selected python:orderby == 'targetname'"
47 value="targetname"
48 ></option>
49 <option
50 value="datecreated"
51 tal:attributes="selected python:orderby == 'datecreated'"
52 >oldest first</option>
53 <option
54 value="-datecreated"
55 tal:attributes="selected python:orderby == '-datecreated'"
56 >newest first</option>
57 </select>
58 <input type="submit" value="sort" id="sort-button"/>
59 </form>
60
61 <table class="listing"><thead>
62 <tr>
63 <th class="patches-view-cell"
64 >Bug</th>
65 <th class="patches-view-cell"
66 >Importance</th>
67 <th class="patches-view-cell"
68 >Status</th>
69 <th class="patches-view-cell"
70 tal:condition="view/targetName"
71 tal:content="view/targetName"
72 ></th>
73 <th class="patches-view-cell"
74 >Patch Age</th>
75 </tr>
76 </thead>
77 <tr tal:repeat="patch_task batch">
78 <td class="patches-view-cell">
79 <a tal:replace="structure patch_task/fmt:link" /></td>
80 <td class="patches-view-cell"
81 tal:content="patch_task/importance/title"
82 tal:attributes="class string:importance${patch_task/importance/name}"></td>
83 <td class="patches-view-cell"
84 tal:content="patch_task/status/title"
85 tal:attributes="class string:status${patch_task/status/name}"></td>
86 <td class="patches-view-cell"
87 tal:condition="view/targetName">
88 <a tal:attributes="href patch_task/target/fmt:url"
89 tal:content="patch_task/target/name"></a></td>
90 <td class="patches-view-cell"
91 tal:define="p python:view.youngestPatch(patch_task.bug);
92 age python:view.patchAge(p)"
93 tal:attributes="id string:patch-cell-${repeat/patch_task/index}">
94 <a tal:attributes="href p/libraryfile/http_url"
95 tal:content="age/fmt:approximateduration/use-digits"></a>
96 <div class="popupTitle"
97 tal:attributes="id string:patch-popup-${repeat/patch_task/index};">
98 <p tal:define="submitter p/message/owner">
99 <strong>From:</strong>
100 <a tal:replace="structure submitter/fmt:link"></a><br/>
101 <strong>Link:</strong>
102 <a tal:attributes="href p/libraryfile/http_url"
103 tal:content="p/libraryfile/filename"></a></p>
104 <p tal:content="string:${p/title}"></p>
105 </div>
106 <script type="text/javascript" tal:content="string:
107 YUI().use('base', 'node', 'event', function(Y) {
108 Y.on('domready', function(e) {
109 var cell_id = '#patch-cell-${repeat/patch_task/index}';
110 var target_id = '#patch-popup-${repeat/patch_task/index}';
111 var elt = Y.get(cell_id);
112 elt.on('mouseover', function(e) {
113 Y.get(target_id).setStyle('display', 'block');
114 });
115 elt.on('mouseout', function(e) {
116 Y.get(target_id).setStyle('display', 'none');
117 });
118 });
119 });"/>
120 </td>
121 </tr></table>
122 <div tal:replace="structure batchnav/@@+navigation-links-lower" />
123 </div><!-- main -->
124 </body>
125</html>
0126
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2010-01-30 05:27:48 +0000
+++ lib/lp/testing/factory.py 2010-02-02 18:46:23 +0000
@@ -130,7 +130,7 @@
130from lp.soyuz.interfaces.component import IComponentSet130from lp.soyuz.interfaces.component import IComponentSet
131from lp.soyuz.interfaces.packageset import IPackagesetSet131from lp.soyuz.interfaces.packageset import IPackagesetSet
132from lp.soyuz.model.buildqueue import BuildQueue132from lp.soyuz.model.buildqueue import BuildQueue
133from lp.testing import run_with_login, time_counter133from lp.testing import run_with_login, time_counter, login, logout
134134
135SPACE = ' '135SPACE = ' '
136136
@@ -239,6 +239,21 @@
239 %s239 %s
240 ''')240 ''')
241241
242 def doAsUser(self, user, factory_method, **factory_args):
243 """Perform a factory method while temporarily logged in as a user.
244
245 :param user: The user to log in as, and then to log out from.
246 :param factory_method: The factory method to invoke while logged in.
247 :param factory_args: Keyword arguments to pass to factory_method.
248 """
249 login(user)
250 try:
251 result = factory_method(**factory_args)
252 transaction.commit()
253 finally:
254 logout()
255 return result
256
242 def makeCopyArchiveLocation(self, distribution=None, owner=None,257 def makeCopyArchiveLocation(self, distribution=None, owner=None,
243 name=None, enabled=True):258 name=None, enabled=True):
244 """Create and return a new arbitrary location for copy packages."""259 """Create and return a new arbitrary location for copy packages."""
@@ -1084,7 +1099,7 @@
10841099
1085 def makeBugAttachment(self, bug=None, owner=None, data=None,1100 def makeBugAttachment(self, bug=None, owner=None, data=None,
1086 comment=None, filename=None, content_type=None,1101 comment=None, filename=None, content_type=None,
1087 description=None):1102 description=None, is_patch=_DEFAULT):
1088 """Create and return a new bug attachment.1103 """Create and return a new bug attachment.
10891104
1090 :param bug: An `IBug` or a bug ID or name, or None, in which1105 :param bug: An `IBug` or a bug ID or name, or None, in which
@@ -1099,6 +1114,7 @@
1099 string will be used.1114 string will be used.
1100 :param content_type: The MIME-type of this file.1115 :param content_type: The MIME-type of this file.
1101 :param description: The description of the attachment.1116 :param description: The description of the attachment.
1117 :param is_patch: If true, this attachment is a patch.
1102 :return: An `IBugAttachment`.1118 :return: An `IBugAttachment`.
1103 """1119 """
1104 if bug is None:1120 if bug is None:
@@ -1115,9 +1131,16 @@
1115 comment = self.getUniqueString()1131 comment = self.getUniqueString()
1116 if filename is None:1132 if filename is None:
1117 filename = self.getUniqueString()1133 filename = self.getUniqueString()
1134 # If the default value of is_patch when creating a new
1135 # BugAttachment should ever change, we don't want to interfere
1136 # with that. So, we only override it if our caller explicitly
1137 # passed it.
1138 other_params = {}
1139 if is_patch is not _DEFAULT:
1140 other_params['is_patch'] = is_patch
1118 return bug.addAttachment(1141 return bug.addAttachment(
1119 owner, data, comment, filename, content_type=content_type,1142 owner, data, comment, filename, content_type=content_type,
1120 description=description)1143 description=description, **other_params)
11211144
1122 def makeSignedMessage(self, msgid=None, body=None, subject=None,1145 def makeSignedMessage(self, msgid=None, body=None, subject=None,
1123 attachment_contents=None, force_transfer_encoding=False,1146 attachment_contents=None, force_transfer_encoding=False,