Hi there, This branch is pretty cool to see, I'm sure it will be very useful! I have quite a few comments below though, so I'm going to have to vote "Needs Fixing". Cheers, mwh > === 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 02:15:24 +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.""" > @@ -7,6 +7,7 @@ > > __all__ = [ > "BugsVHostBreadcrumb", > + "BugsPatchesView", > "BugTargetBugListingView", > "BugTargetBugTagsView", > "BugTargetBugsView", > @@ -19,8 +20,10 @@ > > 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) > @@ -48,7 +52,8 @@ > IBugTarget, IOfficialBugTagTargetPublic, IOfficialBugTagTargetRestricted) > from lp.bugs.interfaces.bug import IBugSet > from lp.bugs.interfaces.bugtask import ( > - BugTaskStatus, IBugTaskSet, UNRESOLVED_BUGTASK_STATUSES) > + BugTaskStatus, IBugTaskSet, UNRESOLVED_BUGTASK_STATUSES, > + UNRESOLVED_PLUS_FIXRELEASED_BUGTASK_STATUSES) > from canonical.launchpad.interfaces.launchpad import ( > IHasExternalBugTracker, ILaunchpadUsage) > from canonical.launchpad.interfaces.hwdb import IHWSubmissionSet > @@ -73,6 +78,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 > @@ -1384,3 +1390,59 @@ > 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.""" > + # XXX: Karl Fogel 2010-02-01 bug=515584: we should be using a > + # Zope form instead of validating the values by hand in the > + # code. Doing it the Zope form way would specify rendering > + # and validation from the same enum, and thus observe DRY. Did you try to do this? It would be much nicer, I think... > + orderby = self.request.get("orderby") > + if (orderby is not None and > + orderby not in ["-importance", "status", "targetname", > + "datecreated", "-datecreated"]): > + raise AssertionError("patch task batch navigator ordered by " > + "invalid value '%s'" % orderby) We shouldn't raise AssertionError in the face of unexpected user input -- going to a URL like https://bugs.launchpad.dev/patches-view-test/+patches?orderby=xxx shouldn't oops. UnexpectedFormData would be better. > + return BatchNavigator( > + self.context.searchTasks( > + None, user=self.user, order_by=orderby, > + status=UNRESOLVED_PLUS_FIXRELEASED_BUGTASK_STATUSES, > + omit_duplicates=True, has_patch=True), > + self.request) > + Trailing whitespace here. > + def shouldShowTargetName(self): > + """Return True if current context can have different bugtargets.""" > + return (IDistribution.providedBy(self.context) or > + IDistroSeries.providedBy(self.context) or > + IProject.providedBy(self.context)) > + > + def youngestPatch(self, bug): > + """Return the youngest patch attached to a bug, else error.""" > + youngest = None > + # Loop over bugtasks, gathering youngest patch for each's bug. > + for attachment in bug.attachments: > + if attachment.is_patch: > + if youngest is None: > + youngest = attachment > + elif (attachment.message.datecreated > > + youngest.message.datecreated): > + youngest = attachment > + if youngest is None: > + # This is the patches view, so every bug under > + # consideration should have at least one patch attachment. > + raise AssertionError("bug %i has no patch attachments" % bug.id) > + return youngest > + > + def patchAge(self, patch): > + """Return a timedelta object for the age of a patch attachment.""" > + now = datetime.now(timezone('UTC')) > + return now - patch.message.datecreated > === 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 02:15:24 +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). > > # pylint: disable-msg=E0211,E0213 Seems odd to have this be the only change to the file... > === modified file 'lib/lp/bugs/interfaces/bugtask.py' > --- lib/lp/bugs/interfaces/bugtask.py 2010-01-23 21:42:36 +0000 > +++ lib/lp/bugs/interfaces/bugtask.py 2010-02-02 02:15:24 +0000 > @@ -36,6 +36,7 @@ > 'IUpstreamProductBugTaskSearch', > 'RESOLVED_BUGTASK_STATUSES', > 'UNRESOLVED_BUGTASK_STATUSES', > + 'UNRESOLVED_PLUS_FIXRELEASED_BUGTASK_STATUSES', > 'UserCannotEditBugTaskImportance', > 'UserCannotEditBugTaskMilestone', > 'UserCannotEditBugTaskStatus', > @@ -287,6 +288,9 @@ > BugTaskStatus.INPROGRESS, > BugTaskStatus.FIXCOMMITTED) > > +UNRESOLVED_PLUS_FIXRELEASED_BUGTASK_STATUSES = ( > + UNRESOLVED_BUGTASK_STATUSES + (BugTaskStatus.FIXRELEASED,)) > + > RESOLVED_BUGTASK_STATUSES = ( > BugTaskStatus.FIXRELEASED, > BugTaskStatus.INVALID, > === 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 02:15:24 +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). > > # pylint: disable-msg=E0611,W0212 Same. > === 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 02:15:24 +0000 > @@ -0,0 +1,325 @@ > +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. > + > + >>> patchy_product = factory.doAsUser( > + ... '