> === 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.FIXRELEASED,)) Also, since this value doesn't ever change I think it would be appropriate to move it to the module's topmost level and use an ALL_CAPITAL identifier. > +        return BatchNavigator( > +            self.context.searchTasks(None, user=self.user, > +                                     order_by=self.request.get("orderby"), Why do you need to read from the request directly? If there's a zope form covering this, you can get the value from self.request.form. If there isn't a form then maybe we should consider having one, since it gives us validation "for free" (here it seems there's no validation at all). > +                                     status=any_unresolved_plus_fixreleased, > +                                     omit_duplicates=True, has_patch=True), > +            self.request) > + > +    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)) I would prefer the combining operator to be on the end of the previous lines, so that the constituents being combined align nicely to the left. I don't think this is a requirement, though, so only change this if you agree with me. > + > +    def youngest_patch(self, bug): Following ZOPE, we deviate from PEP-8 by using camelCase for method names. This one should be called youngestPatch. > +        """Return the youngest patch attached to a bug, else error.""" > +        youngest = None > +        # Loop over bugtasks, gathering youngest patch for each's bug. > +        for a in bug.attachments: Let's avoid a single-character identifier. 'attachment' takes a bit longer to write, but is much easier to read. > +            if a.type == BugAttachmentType.PATCH: If you want you can use the helper property is_patch, instead of comparing the patch type. > +                if youngest is None: > +                    youngest = a > +                else: > +                    if (a.message.datecreated > youngest.message.datecreated): > +                        youngest = a > +        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 Another way to express this would be: (achtung: untested code) patches = sorted( [attachment for attachment in bug.attachments if attachment.is_patch], operator.itemgetter('message.datecreated')) youngest = patches[-1] if len(patches) > 0 or None Both ways are fine, so you don't need to change this unless you feel like. > + > +    def patch_age(self, patch): camelCase > +        """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/browser/configure.zcml' > --- lib/lp/bugs/browser/configure.zcml  2009-12-09 20:48:01 +0000 > +++ lib/lp/bugs/browser/configure.zcml  2010-01-28 17:24:20 +0000 > @@ -1,4 +1,4 @@ > - > > @@ -108,6 +108,13 @@ >             facet="bugs" >             permission="launchpad.Edit" >             template="../templates/official-bug-target-manage-tags.pt"/> > +         +            name="+patches" > +            for="lp.bugs.interfaces.bugtarget.IHasBugs" > +            class="lp.bugs.browser.bugtarget.BugsPatchesView" > +            facet="bugs" > +            permission="zope.Public" > +            template="../templates/bugtarget-patches.pt"/> >     >             name="+bugtarget-macros-search" > > === 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-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). > >  # pylint: disable-msg=E0211,E0213 > > === 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-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). > >  # pylint: disable-msg=E0611,W0212 > > === 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('