← Back to team overview

launchpad-reviewers team mailing list archive

lp:~edwin-grubbs/launchpad/bug-663861-product-series-timeout-part2 into lp:launchpad

 

Edwin Grubbs has proposed merging lp:~edwin-grubbs/launchpad/bug-663861-product-series-timeout-part2 into lp:launchpad with lp:~edwin-grubbs/launchpad/bug-663861-product-series-timeout-part1 as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers): code
Related bugs:
  #663861 Product:+series timesout showing obsolete series
  https://bugs.launchpad.net/bugs/663861


Summary
-------

In a prerequisite branch, I added the functional index, but I
had to make a slight change to it to move the numbered versions
to the top, so I will also need a db review again.
The python sort method orders them as
[devfocus, 3, 2a, 2b, 1, a, b, c], but it makes more sense to order it
as [devfocus, 3, 2b, 2a, 1, c, b, a].

The branch fixes a timeout on the $product/+series page by batching the
results.

Implementation details
----------------------

When sorting descending, move the numbered versions above the letters.
    database/schema/trusted.sql

Changed the product.ProductSeriesView to ProductSeriesSetView in order
to avoid confusion with productseries.ProductSeriesView.
    lib/lp/registry/browser/configure.zcml

Added the getVersionSortedSeries() method in the model and added the
ProductSeriesSetView.batched_series attribute that batches the results.
The template needs the css_class attribute in the decorated series, but
I didn't want to worry about SeriesWithReleases.releases not being
populated, even though I don't think the template uses it. Therefore, I
split css_class into the DecoratedSeries class.
    lib/lp/registry/browser/product.py
    lib/lp/registry/templates/product-series.pt
    lib/lp/registry/browser/tests/product-views.txt
    lib/lp/registry/interfaces/product.py
    lib/lp/registry/model/product.py
    lib/lp/registry/tests/test_product.py

Fixed lint error and removed reference to the series attribute inside
the decorated series, since the `name` can be retrieved from the
decorated series.
    lib/lp/registry/browser/tests/product-files-views.txt

Tests
-----

./bin/test -vv -t 'test_product|product-views|product-files-views'

Demo and Q/A
------------

* Open http://qastaging.launchpad.net/obsolete-junk/+series
  * It should not timeout, and the results should be batched.
    (The timeline graph might still timeout.)

-- 
https://code.launchpad.net/~edwin-grubbs/launchpad/bug-663861-product-series-timeout-part2/+merge/39643
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~edwin-grubbs/launchpad/bug-663861-product-series-timeout-part2 into lp:launchpad.
=== modified file 'database/schema/trusted.sql'
--- database/schema/trusted.sql	2010-10-29 16:46:05 +0000
+++ database/schema/trusted.sql	2010-10-29 16:46:06 +0000
@@ -1826,10 +1826,13 @@
     [version] = args
 
     def substitute_filled_numbers(match):
-        return match.group(0).zfill(5)
+        # Prepend "~" so that version numbers will show up first
+        # when sorted descending, i.e. [3, 2c, 2b, 1, c, b, a] instead
+        # of [c, b, a, 3, 2c, 2b, 1].
+        return '~' + match.group(0).zfill(5)
 
     return re.sub(u'\d+', substitute_filled_numbers, version)
 $$;
 
 COMMENT ON FUNCTION version_sort_key(text) IS
-'Sort a field as version numbers that do not necessarily conform to debian package versions (For example, when "2-2" should be considered greater than "1:1"). debversion_sort_key() should be used for debian versions.';
+'Sort a field as version numbers that do not necessarily conform to debian package versions (For example, when "2-2" should be considered greater than "1:1"). debversion_sort_key() should be used for debian versions. Numbers will be sorted after letters unlike typical ASCII, so that a descending sort will put the latest version number that starts with a number instead of a letter will be at the top. E.g. ascending is [a, z, 1, 9] and descending is [9, 1, z, a].';

=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml	2010-10-21 03:22:06 +0000
+++ lib/lp/registry/browser/configure.zcml	2010-10-29 16:46:06 +0000
@@ -1473,7 +1473,7 @@
         template="../templates/product-portlet-packages.pt"/>
     <browser:page
         for="lp.registry.interfaces.product.IProduct"
-        class="lp.registry.browser.product.ProductSeriesView"
+        class="lp.registry.browser.product.ProductSeriesSetView"
         name="+series"
         facet="overview"
         permission="zope.Public"

=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py	2010-10-20 14:58:01 +0000
+++ lib/lp/registry/browser/product.py	2010-10-29 16:46:06 +0000
@@ -29,7 +29,7 @@
     'ProductPackagesPortletView',
     'ProductRdfView',
     'ProductReviewLicenseView',
-    'ProductSeriesView',
+    'ProductSeriesSetView',
     'ProductSetBreadcrumb',
     'ProductSetFacets',
     'ProductSetNavigation',
@@ -88,6 +88,9 @@
     MultiStepView,
     StepView,
     )
+from canonical.launchpad.components.decoratedresultset import (
+    DecoratedResultSet,
+    )
 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
 from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
 from canonical.launchpad.mail import (
@@ -123,7 +126,6 @@
     safe_action,
     )
 from canonical.launchpad.webapp.menu import NavigationMenu
-from lp.app.browser.tales import MenuAPI
 from canonical.widgets.date import DateWidget
 from canonical.widgets.itemswidgets import (
     CheckBoxMatrixWidget,
@@ -142,6 +144,7 @@
     QuestionTargetFacetMixin,
     QuestionTargetTraversalMixin,
     )
+from lp.app.browser.tales import MenuAPI
 from lp.app.enums import ServiceUsage
 from lp.app.errors import NotFoundError
 from lp.app.interfaces.headings import IEditableContextTitle
@@ -793,7 +796,25 @@
             self.release_by_id[release.id] = release_delegate
 
 
-class SeriesWithReleases:
+class DecoratedSeries:
+    """A decorated series that includes helper attributes for templates."""
+    delegates(IProductSeries, 'series')
+
+    def __init__(self, series):
+        self.series = series
+
+    @property
+    def css_class(self):
+        """The highlighted, unhighlighted, or dimmed CSS class."""
+        if self.is_development_focus:
+            return 'highlighted'
+        elif self.status == SeriesStatus.OBSOLETE:
+            return 'dimmed'
+        else:
+            return 'unhighlighted'
+
+
+class SeriesWithReleases(DecoratedSeries):
     """A decorated series that includes releases.
 
     The extra data is included in this class to avoid repeated
@@ -807,10 +828,9 @@
     # raise an AttributeError for self.parent.
     parent = None
     releases = None
-    delegates(IProductSeries, 'series')
 
     def __init__(self, series, parent):
-        self.series = series
+        super(SeriesWithReleases, self).__init__(series)
         self.parent = parent
         self.releases = []
 
@@ -824,16 +844,6 @@
                 return True
         return False
 
-    @property
-    def css_class(self):
-        """The highlighted, unhighlighted, or dimmed CSS class."""
-        if self.is_development_focus:
-            return 'highlighted'
-        elif self.status == SeriesStatus.OBSOLETE:
-            return 'dimmed'
-        else:
-            return 'unhighlighted'
-
 
 class ReleaseWithFiles:
     """A decorated release that includes product release files.
@@ -1707,12 +1717,18 @@
         return canonical_url(self.context)
 
 
-class ProductSeriesView(ProductView):
+class ProductSeriesSetView(ProductView):
     """A view for showing a product's series."""
 
     label = 'timeline'
     page_title = label
 
+    @cachedproperty
+    def batched_series(self):
+        decorated_result = DecoratedResultSet(
+            self.context.getVersionSortedSeries(), DecoratedSeries)
+        return BatchNavigator(decorated_result, self.request)
+
 
 class ProductRdfView(BaseRdfView):
     """A view that sets its mime-type to application/rdf+xml"""

=== modified file 'lib/lp/registry/browser/tests/product-files-views.txt'
--- lib/lp/registry/browser/tests/product-files-views.txt	2010-08-03 14:51:31 +0000
+++ lib/lp/registry/browser/tests/product-files-views.txt	2010-10-29 16:46:06 +0000
@@ -21,7 +21,7 @@
 
     >>> def print_series_release(sr):
     ...     print "%s from the %s series" % (sr.release.name_with_codename,
-    ...                                      sr.series.series.name)
+    ...                                      sr.series.name)
 
     >>> for sr in batch:
     ...     print_series_release(sr)
@@ -35,7 +35,8 @@
     ...         milestone = factory.makeMilestone(productseries=productseries,
     ...                                           name="%d.%d"%(i,j))
     ...         release_file = factory.makeProductReleaseFile(
-    ...             product=product, productseries=productseries, milestone=milestone)
+    ...             product=product, productseries=productseries,
+    ...             milestone=milestone)
     >>> view = create_initialized_view(product, '+download')
     >>> batch = view.series_and_releases_batch.currentBatch()
     >>> print len(batch)

=== modified file 'lib/lp/registry/browser/tests/product-views.txt'
--- lib/lp/registry/browser/tests/product-views.txt	2010-10-18 22:24:59 +0000
+++ lib/lp/registry/browser/tests/product-views.txt	2010-10-29 16:46:06 +0000
@@ -417,6 +417,27 @@
     ftp://mozilla.org/firefox.*bz2
 
 
+Viewing series for a product
+============================
+
+All the product series can be viewed in batches.
+
+    >>> product = factory.makeProduct()
+    >>> for name in ('stable', 'testing', '1.1', '1.2', 'extra'):
+    ...     series = factory.makeProductSeries(product=product, name=name)
+    >>> view = create_view(product, name='+series')
+    >>> batch = view.batched_series.currentBatch()
+    >>> print batch.total()
+    6
+    >>> for series in batch:
+    ...     print series.name
+    trunk
+    1.2
+    1.1
+    testing
+    stable
+
+
 Product index view
 ==================
 

=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py	2010-09-21 09:37:06 +0000
+++ lib/lp/registry/interfaces/product.py	2010-10-29 16:46:06 +0000
@@ -733,6 +733,16 @@
                 "Some bug trackers host multiple projects at the same URL "
                 "and require an identifier for the specific project.")))
 
+    def getVersionSortedSeries(filter_obsolete=False):
+        """Return all the series sorted by the name field as a version.
+
+        The development focus field is an exception. It will always
+        be sorted first.
+
+        :param filter_obsolete: If true, do not include any series with
+                                SeriesStatus.OBSOLETE in the results.
+        """
+
     def redeemSubscriptionVoucher(voucher, registrant, purchaser,
                                   subscription_months, whiteboard=None,
                                   current_datetime=None):

=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py	2010-10-24 13:02:07 +0000
+++ lib/lp/registry/model/product.py	2010-10-29 16:46:06 +0000
@@ -137,6 +137,7 @@
     License,
     LicenseStatus,
     )
+from lp.registry.interfaces.series import SeriesStatus
 from lp.registry.model.announcement import MakesAnnouncements
 from lp.registry.model.commercialsubscription import CommercialSubscription
 from lp.registry.model.distribution import Distribution
@@ -980,6 +981,30 @@
             translatable_product_series,
             key=operator.attrgetter('datecreated'))
 
+    def getVersionSortedSeries(self, filter_obsolete=False):
+        """See `IProduct`."""
+        store = Store.of(self)
+        dev_focus = store.find(
+            ProductSeries,
+            ProductSeries.id == self.development_focus.id)
+        other_series_conditions = [
+            ProductSeries.product == self,
+            ProductSeries.id != self.development_focus.id,
+            ]
+        if filter_obsolete is True:
+            other_series_conditions.append(
+                ProductSeries.status != SeriesStatus.OBSOLETE)
+        other_series = store.find(ProductSeries, other_series_conditions)
+        # The query will be much slower if the version_sort_key is not
+        # the first thing that is sorted, since it won't be able to use
+        # the productseries_name_sort index.
+        other_series.order_by(SQL('version_sort_key(name) DESC'))
+        # UNION ALL must be used to preserve the sort order from the
+        # separate queries. The sorting should not be done after
+        # unioning the two queries, because that will prevent it from
+        # being able to use the productseries_name_sort index.
+        return dev_focus.union(other_series, all=True)
+
     @property
     def obsolete_translatable_series(self):
         """See `IProduct`."""

=== modified file 'lib/lp/registry/templates/product-series.pt'
--- lib/lp/registry/templates/product-series.pt	2010-05-17 17:29:08 +0000
+++ lib/lp/registry/templates/product-series.pt	2010-10-29 16:46:06 +0000
@@ -21,17 +21,27 @@
       </li>
     </ul>
 
-    <tal:cache content="cache:public, 1 hour">
-      <div tal:repeat="series view/sorted_series_list">
-        <div style="margin-top: 1em;
-                    border-bottom: 1px solid #ccc; max-width: 60em;"
-          tal:define="is_focus series/is_development_focus"
-          tal:attributes="class string:${series/css_class} series;
-                          id series/name/fmt:css-id/series-;">
-          <tal:status replace="structure series/series/@@+status" />
-        </div>
-      </div>
-    </tal:cache>
+    <tal:series-list condition="view/batched_series/currentBatch">
+      <div class="lesser" id="active-top-navigation">
+        <tal:navigation
+          content="structure view/batched_series/@@+navigation-links-upper" />
+      </div>
+      <div tal:repeat="series view/batched_series/currentBatch">
+        <tal:cache content="cache:public, 1 hour, series/name">
+          <div style="margin-top: 1em;
+                      border-bottom: 1px solid #ccc; max-width: 60em;"
+            tal:define="is_focus series/is_development_focus"
+            tal:attributes="class string:${series/css_class} series;
+                            id series/name/fmt:css-id/series-;">
+            <tal:status replace="structure series/series/@@+status" />
+          </div>
+        </tal:cache>
+      </div>
+      <div class="lesser">
+        <tal:navigation
+          content="structure view/batched_series/@@+navigation-links-lower" />
+      </div>
+    </tal:series-list>
   </div>
 </body>
 </html>

=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py	2010-10-04 19:50:45 +0000
+++ lib/lp/registry/tests/test_product.py	2010-10-29 16:46:06 +0000
@@ -27,8 +27,12 @@
     IProduct,
     License,
     )
+from lp.registry.interfaces.series import SeriesStatus
 from lp.registry.model.commercialsubscription import CommercialSubscription
-from lp.registry.model.product import Product, UnDeactivateable
+from lp.registry.model.product import (
+    Product,
+    UnDeactivateable,
+    )
 from lp.registry.model.productlicense import ProductLicense
 from lp.testing import TestCaseWithFactory
 
@@ -136,6 +140,32 @@
             expected_milestones,
             timeline_milestones)
 
+    def test_getVersionSortedSeries(self):
+        # The product series should be sorted with the development focus
+        # series first, the series starting with a number in descending
+        # order, and then the series starting with a letter in
+        # descending order.
+        product = self.factory.makeProduct()
+        for name in ('1', '2', '3', '3a', '3b', 'alpha', 'beta'):
+            self.factory.makeProductSeries(product=product, name=name)
+        self.assertEqual(
+            [u'trunk', u'3b', u'3a', u'3', u'2', u'1', u'beta', u'alpha'],
+            [series.name for series in product.getVersionSortedSeries()])
+
+    def test_getVersionSortedSeries_filter_obsolete(self):
+        # The obsolete series should not be included in the results if
+        # the filter_obsolete argument is set to True.
+        login('admin@xxxxxxxxxxxxx')
+        product = self.factory.makeProduct()
+        self.factory.makeProductSeries(product=product, name='active-series')
+        obsolete_series = self.factory.makeProductSeries(
+            product=product, name='obsolete-series')
+        obsolete_series.status = SeriesStatus.OBSOLETE
+        active_series = product.getVersionSortedSeries(filter_obsolete=True)
+        self.assertEqual(
+            [u'trunk', u'active-series'],
+            [series.name for series in active_series])
+
 
 class TestProductFiles(unittest.TestCase):
     """Tests for downloadable product files."""