← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~edwin-grubbs/launchpad/bug-671014-timeline-graph-timeout into lp:launchpad/devel

 

Edwin Grubbs has proposed merging lp:~edwin-grubbs/launchpad/bug-671014-timeline-graph-timeout into lp:launchpad/devel with lp:~edwin-grubbs/launchpad/bug-667900-dsp-page-upstream-link-form as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #671014 Product:EntryResource:get_timeline timeline graph timeout on obsolete-junk project
  https://bugs.launchpad.net/bugs/671014


Summary
-------

This branch fixes a timeout displaying the timeline graph on the
$project/+series page when there is a huge number of series. It will now
batch the series in the timeline graph in the same way that the series
are batched in the rest of the page.


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

Fixed a bug when viewing the last batch in the list. lazr.restful is
smart enough to realize that if the batch isn't full, then it can
calculate the total size without an extra query. This causes
representation.total_size to be defined instead of
representation.total_size_link.
    lib/canonical/launchpad/javascript/client/client.js

I created the new ITimelineProductSeries interface, since lazr.restful
has a bug that prevents batching of plain objects instead of entries.
    lib/canonical/launchpad/security.py
    lib/lp/registry/browser/configure.zcml
    lib/lp/registry/configure.zcml
    lib/lp/registry/interfaces/productseries.py
    lib/lp/registry/interfaces/webservice.py
    lib/lp/registry/model/productseries.py

I used a DecoratedResultSet to allow batching, and I added more flexible
parameters to getVersionSortedSeries().
    lib/lp/registry/interfaces/product.py
    lib/lp/registry/model/product.py
    lib/lp/registry/model/series.py

The templates now enable the start and size parameters to be passed into
the REST API request. Since the javascript LP.client encapsulates entry
JSON in YUI objects, I had to extract the fields with .get() so that I
could pass plain javascript objects into the TimelineGraph.
    lib/lp/registry/templates/object-timeline-graph.pt
    lib/lp/registry/templates/product-series.pt
    lib/lp/registry/templates/timeline-macros.pt

Fixed tests.
    lib/lp/registry/stories/webservice/xx-project-registry.txt
    lib/lp/registry/tests/test_product.py

Tests
-----

./bin/test -vv -t 'test_product|xx-project-registry'

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

* Open http://launchpad.dev/firefox/+series
    * Register enough series so that it batches the result.
    * The series shown in the timeline graph should match those shown on
      the page when you hit the Next and Previous links.
-- 
https://code.launchpad.net/~edwin-grubbs/launchpad/bug-671014-timeline-graph-timeout/+merge/41522
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~edwin-grubbs/launchpad/bug-671014-timeline-graph-timeout into lp:launchpad/devel.
=== modified file 'lib/canonical/launchpad/javascript/client/client.js'
--- lib/canonical/launchpad/javascript/client/client.js	2010-07-23 05:41:36 +0000
+++ lib/canonical/launchpad/javascript/client/client.js	2010-11-22 22:13:24 +0000
@@ -504,8 +504,10 @@
         if (representation.resource_type_link === undefined) {
             // This is a non-entry object returned by a named operation.
             // It's either a list or a random JSON object.
-            if (representation.total_size !== undefined) {
-                // It's a list. Treat it as a collection; it should be slicable.
+            if (representation.total_size !== undefined
+                || representation.total_size_link !== undefined) {
+                // It's a list. Treat it as a collection;
+                // it should be slicable.
                 return new LP.client.Collection(this, representation, uri);
             } else {
                 // It's a random JSON object. Leave it alone.

=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py	2010-11-12 23:30:57 +0000
+++ lib/canonical/launchpad/security.py	2010-11-22 22:13:24 +0000
@@ -140,7 +140,10 @@
     IProductRelease,
     IProductReleaseFile,
     )
-from lp.registry.interfaces.productseries import IProductSeries
+from lp.registry.interfaces.productseries import (
+    IProductSeries,
+    ITimelineProductSeries,
+    )
 from lp.registry.interfaces.projectgroup import (
     IProjectGroup,
     IProjectGroupSet,
@@ -424,6 +427,11 @@
             user)
 
 
+class ViewTimelineProductSeries(AnonymousAuthorization):
+    """Anyone can view an ITimelineProductSeries."""
+    usedfor = ITimelineProductSeries
+
+
 class ViewProductReleaseFile(AnonymousAuthorization):
     """Anyone can view an IProductReleaseFile."""
     usedfor = IProductReleaseFile

=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml	2010-11-12 23:30:57 +0000
+++ lib/lp/registry/browser/configure.zcml	2010-11-22 22:13:24 +0000
@@ -1684,6 +1684,10 @@
         for="lp.registry.interfaces.productseries.IProductSeries"
         path_expression="name"
         attribute_to_parent="product"/>
+    <browser:url
+        for="lp.registry.interfaces.productseries.ITimelineProductSeries"
+        path_expression="name"
+        attribute_to_parent="product"/>
     <adapter
         provides="canonical.launchpad.webapp.interfaces.INavigationMenu"
         for="lp.registry.browser.productseries.IProductSeriesInvolved"

=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml	2010-11-11 03:19:18 +0000
+++ lib/lp/registry/configure.zcml	2010-11-22 22:13:24 +0000
@@ -1442,6 +1442,14 @@
         factory=".model.structuralsubscription.ProductSeriesTargetHelper"
         permission="zope.Public"/>
 
+    <!-- This is class TimelineProductSeries -->
+
+    <class
+        class="lp.registry.model.productseries.TimelineProductSeries">
+        <allow
+            interface="lp.registry.interfaces.productseries.ITimelineProductSeries"/>
+    </class>
+
     <!-- ProductSeriesSet -->
 
     <class

=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py	2010-11-11 11:55:53 +0000
+++ lib/lp/registry/interfaces/product.py	2010-11-22 22:13:24 +0000
@@ -736,14 +736,16 @@
                 "Some bug trackers host multiple projects at the same URL "
                 "and require an identifier for the specific project.")))
 
-    def getVersionSortedSeries(filter_obsolete=False):
+    def getVersionSortedSeries(statuses=None, filter_statuses=None):
         """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.
+        :param statuses: If statuses is not None, only include series
+                         which are in the given statuses.
+        :param filter_statuses: Filter out any series with statuses listed in
+                                filter_statuses.
         """
 
     def redeemSubscriptionVoucher(voucher, registrant, purchaser,

=== modified file 'lib/lp/registry/interfaces/productseries.py'
--- lib/lp/registry/interfaces/productseries.py	2010-11-08 12:09:21 +0000
+++ lib/lp/registry/interfaces/productseries.py	2010-11-22 22:13:24 +0000
@@ -13,6 +13,7 @@
     'IProductSeriesPublic',
     'IProductSeriesSet',
     'NoSuchProductSeries',
+    'ITimelineProductSeries',
     ]
 
 from lazr.restful.declarations import (
@@ -37,6 +38,7 @@
     Bool,
     Choice,
     Datetime,
+    Field,
     Int,
     TextLine,
     )
@@ -322,6 +324,32 @@
     export_as_webservice_entry('project_series')
 
 
+class ITimelineProductSeries(Interface):
+    """Minimal product series info for the timeline."""
+
+    # XXX: EdwinGrubbs 2010-11-18 bug=677671
+    # lazr.restful can't batch a DecoratedResultSet returning basic
+    # python types such as dicts, so this interface is necessary.
+    export_as_webservice_entry('timeline_project_series')
+
+    name = IProductSeries['name']
+
+    status = IProductSeries['status']
+
+    product = IProductSeries['product']
+
+    is_development_focus = exported(
+        Bool(title=_("Is series the development focus of the project"),
+             required=True))
+
+    uri = exported(
+        TextLine(title=_("Series URI"), required=False,
+            description=_('foo')))
+
+    landmarks = exported(
+        Field(title=_("List of milestones and releases")))
+
+
 class IProductSeriesSet(Interface):
     """Interface representing the set of ProductSeries."""
 

=== modified file 'lib/lp/registry/interfaces/webservice.py'
--- lib/lp/registry/interfaces/webservice.py	2010-11-12 05:37:05 +0000
+++ lib/lp/registry/interfaces/webservice.py	2010-11-22 22:13:24 +0000
@@ -36,6 +36,7 @@
     'IStructuralSubscriptionTarget',
     'ITeam',
     'ITeamMembership',
+    'ITimelineProductSeries',
     'IWikiName',
     ]
 
@@ -84,7 +85,10 @@
     IProductRelease,
     IProductReleaseFile,
     )
-from lp.registry.interfaces.productseries import IProductSeries
+from lp.registry.interfaces.productseries import (
+    IProductSeries,
+    ITimelineProductSeries,
+    )
 from lp.registry.interfaces.projectgroup import (
     IProjectGroup,
     IProjectGroupSet,

=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py	2010-11-19 17:27:35 +0000
+++ lib/lp/registry/model/product.py	2010-11-22 22:13:24 +0000
@@ -34,6 +34,7 @@
     Desc,
     Int,
     Join,
+    Not,
     Select,
     SQL,
     Store,
@@ -47,6 +48,9 @@
     )
 from zope.security.proxy import removeSecurityProxy
 
+from canonical.launchpad.components.decoratedresultset import (
+    DecoratedResultSet,
+    )
 from canonical.database.constants import UTC_NOW
 from canonical.database.datetimecol import UtcDateTimeCol
 from canonical.database.enumcol import EnumCol
@@ -55,9 +59,6 @@
     SQLBase,
     sqlvalues,
     )
-from canonical.launchpad.components.decoratedresultset import (
-    DecoratedResultSet,
-    )
 from canonical.launchpad.interfaces.launchpad import (
     IHasIcon,
     IHasLogo,
@@ -73,7 +74,7 @@
     IStoreSelector,
     MAIN_STORE,
     )
-from canonical.launchpad.webapp.sorting import sorted_version_numbers
+from lp.registry.model.series import ACTIVE_STATUSES
 from lp.answers.interfaces.faqtarget import IFAQTarget
 from lp.answers.interfaces.questioncollection import (
     QUESTION_STATUS_DEFAULT_SEARCH,
@@ -140,7 +141,6 @@
     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
@@ -982,7 +982,7 @@
             translatable_product_series,
             key=operator.attrgetter('datecreated'))
 
-    def getVersionSortedSeries(self, filter_obsolete=False):
+    def getVersionSortedSeries(self, statuses=None, filter_statuses=None):
         """See `IProduct`."""
         store = Store.of(self)
         dev_focus = store.find(
@@ -992,9 +992,12 @@
             ProductSeries.product == self,
             ProductSeries.id != self.development_focus.id,
             ]
-        if filter_obsolete is True:
-            other_series_conditions.append(
-                ProductSeries.status != SeriesStatus.OBSOLETE)
+        if statuses is not None:
+            other_series_conditions.append(
+                ProductSeries.status.is_in(statuses))
+        if filter_statuses is not None:
+            other_series_conditions.append(
+                Not(ProductSeries.status.is_in(filter_statuses)))
         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
@@ -1262,17 +1265,15 @@
 
     def getTimeline(self, include_inactive=False):
         """See `IProduct`."""
-        series_list = sorted_version_numbers(self.series,
-                                             key=operator.attrgetter('name'))
-        if self.development_focus in series_list:
-            series_list.remove(self.development_focus)
-        series_list.insert(0, self.development_focus)
-        series_list.reverse()
-        return [
-            series.getTimeline(include_inactive=include_inactive)
-            for series in series_list
-            if include_inactive or series.active or
-               series == self.development_focus]
+
+        def decorate(series):
+            return series.getTimeline(include_inactive=include_inactive)
+        if include_inactive is True:
+            statuses = None
+        else:
+            statuses = ACTIVE_STATUSES
+        return DecoratedResultSet(
+            self.getVersionSortedSeries(statuses=statuses), decorate)
 
     def getRecipes(self):
         """See `IHasRecipes`."""

=== modified file 'lib/lp/registry/model/productseries.py'
--- lib/lp/registry/model/productseries.py	2010-11-08 12:12:29 +0000
+++ lib/lp/registry/model/productseries.py	2010-11-22 22:13:24 +0000
@@ -9,6 +9,7 @@
 __all__ = [
     'ProductSeries',
     'ProductSeriesSet',
+    'TimelineProductSeries',
     ]
 
 import datetime
@@ -73,6 +74,7 @@
 from lp.registry.interfaces.productseries import (
     IProductSeries,
     IProductSeriesSet,
+    ITimelineProductSeries,
     )
 from lp.registry.interfaces.series import SeriesStatus
 from lp.registry.model.milestone import (
@@ -637,12 +639,27 @@
 
         landmarks = sorted_dotted_numbers(landmarks, key=landmark_key)
         landmarks.reverse()
-        return dict(
+        return TimelineProductSeries(
             name=self.name,
             is_development_focus=self.is_development_focus,
-            status=self.status.title,
+            status=self.status,
             uri=canonical_url(self, path_only_if_possible=True),
-            landmarks=landmarks)
+            landmarks=landmarks,
+            product=self.product)
+
+
+class TimelineProductSeries:
+    """See `ITimelineProductSeries`."""
+    implements(ITimelineProductSeries)
+
+    def __init__(self, name, status, is_development_focus, uri, landmarks,
+                 product):
+        self.name = name
+        self.status = status
+        self.is_development_focus = is_development_focus
+        self.uri = uri
+        self.landmarks = landmarks
+        self.product = product
 
 
 class ProductSeriesSet:

=== modified file 'lib/lp/registry/model/series.py'
--- lib/lp/registry/model/series.py	2010-08-20 20:31:18 +0000
+++ lib/lp/registry/model/series.py	2010-11-22 22:13:24 +0000
@@ -6,6 +6,7 @@
 __metaclass__ = type
 
 __all__ = [
+    'ACTIVE_STATUSES',
     'SeriesMixin',
     ]
 
@@ -19,6 +20,13 @@
     SeriesStatus,
     )
 
+ACTIVE_STATUSES = [
+    SeriesStatus.DEVELOPMENT,
+    SeriesStatus.FROZEN,
+    SeriesStatus.CURRENT,
+    SeriesStatus.SUPPORTED,
+    ]
+
 
 class SeriesMixin:
     """See `ISeriesMixin`."""
@@ -29,12 +37,7 @@
 
     @property
     def active(self):
-        return self.status in [
-            SeriesStatus.DEVELOPMENT,
-            SeriesStatus.FROZEN,
-            SeriesStatus.CURRENT,
-            SeriesStatus.SUPPORTED,
-            ]
+        return self.status in ACTIVE_STATUSES
 
     @property
     def bug_supervisor(self):

=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
--- lib/lp/registry/stories/webservice/xx-project-registry.txt	2010-10-09 16:36:22 +0000
+++ lib/lp/registry/stories/webservice/xx-project-registry.txt	2010-11-22 22:13:24 +0000
@@ -472,44 +472,58 @@
     ...     "get_timeline",
     ...     include_inactive=True).jsonBody()
     >>> print pretty(timeline)
-    [{u'is_development_focus': False,
-     u'landmarks': [],
-     u'name': u'experimental',
-     u'status': u'Active Development',
-     u'uri': u'/firefox/experimental'},
-    {u'is_development_focus': False,
-     u'landmarks': [{u'code_name': u'First Stable Release',
-                      u'date': u'2004-06-28',
-                      u'name': u'1.0.0',
-                      u'type': u'release',
-                      u'uri': u'/firefox/1.0/1.0.0'}],
-      u'name': u'1.0',
-      u'status': u'Active Development',
-      u'uri': u'/firefox/1.0'},
-     {u'is_development_focus': True,
-      u'landmarks': [{u'code_name': None,
-                      u'date': u'2056-10-16',
-                      u'name': u'1.0',
-                      u'type': u'milestone',
-                      u'uri': u'/firefox/+milestone/1.0'},
-                     {u'code_name': u'One (secure) Tree Hill',
-                      u'date': u'2004-10-15',
-                      u'name': u'0.9.2',
-                      u'type': u'release',
-                      u'uri': u'/firefox/trunk/0.9.2'},
-                     {u'code_name': u'One Tree Hill (v2)',
-                      u'date': u'2004-10-15',
-                      u'name': u'0.9.1',
-                      u'type': u'release',
-                      u'uri': u'/firefox/trunk/0.9.1'},
-                     {u'code_name': u'One Tree Hill',
-                      u'date': u'2004-10-15',
-                      u'name': u'0.9',
-                      u'type': u'release',
-                      u'uri': u'/firefox/trunk/0.9'}],
-      u'name': u'trunk',
-      u'status': u'Obsolete',
-      u'uri': u'/firefox/trunk'}]
+    {u'entries': [{u'http_etag': ...
+                   u'is_development_focus': True,
+                   u'landmarks': [{u'code_name': None,
+                                   u'date': u'2056-10-16',
+                                   u'name': u'1.0',
+                                   u'type': u'milestone',
+                                   u'uri': u'/firefox/+milestone/1.0'},
+                                  {u'code_name': u'One (secure) Tree Hill',
+                                   u'date': u'2004-10-15',
+                                   u'name': u'0.9.2',
+                                   u'type': u'release',
+                                   u'uri': u'/firefox/trunk/0.9.2'},
+                                  {u'code_name': u'One Tree Hill (v2)',
+                                   u'date': u'2004-10-15',
+                                   u'name': u'0.9.1',
+                                   u'type': u'release',
+                                   u'uri': u'/firefox/trunk/0.9.1'},
+                                  {u'code_name': u'One Tree Hill',
+                                   u'date': u'2004-10-15',
+                                   u'name': u'0.9',
+                                   u'type': u'release',
+                                   u'uri': u'/firefox/trunk/0.9'}],
+                   u'name': u'trunk',
+                   u'project_link': u'http://.../firefox',
+                   u'resource_type_link': u'.../#timeline_project_series',
+                   u'self_link': u'http://.../firefox/trunk',
+                   u'status': u'Obsolete',
+                   u'uri': u'/firefox/trunk'},
+                  {u'http_etag': ...
+                   u'is_development_focus': False,
+                   u'landmarks': [{u'code_name': u'First Stable Release',
+                                   u'date': u'2004-06-28',
+                                   u'name': u'1.0.0',
+                                   u'type': u'release',
+                                   u'uri': u'/firefox/1.0/1.0.0'}],
+                   u'name': u'1.0',
+                   u'project_link': u'http://.../firefox',
+                   u'resource_type_link': u'.../#timeline_project_series',
+                   u'self_link': u'http://.../firefox/1.0',
+                   u'status': u'Active Development',
+                   u'uri': u'/firefox/1.0'},
+                  {u'http_etag': ...
+                   u'is_development_focus': False,
+                   u'landmarks': [],
+                   u'name': u'experimental',
+                   u'project_link': u'http://.../firefox',
+                   u'resource_type_link': u'.../#timeline_project_series',
+                   u'self_link': u'http://.../firefox/experimental',
+                   u'status': u'Active Development',
+                   u'uri': u'/firefox/experimental'}],
+     u'start': 0,
+     u'total_size': 3}
 
 
 Project collection
@@ -872,9 +886,13 @@
     >>> timeline = webservice.named_get(
     ...     babadoo_foobadoo['self_link'], "get_timeline").jsonBody()
     >>> print pretty(timeline)
-    {u'is_development_focus': False,
+    {u'http_etag': ...
+     u'is_development_focus': False,
      u'landmarks': [],
      u'name': u'foobadoo',
+     u'project_link': u'http://.../babadoo',
+     u'resource_type_link': u'http://.../#timeline_project_series',
+     u'self_link': u'http://.../babadoo/foobadoo',
      u'status': u'Active Development',
      u'uri': u'/babadoo/foobadoo'}
 

=== modified file 'lib/lp/registry/templates/object-timeline-graph.pt'
--- lib/lp/registry/templates/object-timeline-graph.pt	2010-07-14 15:00:47 +0000
+++ lib/lp/registry/templates/object-timeline-graph.pt	2010-11-22 22:13:24 +0000
@@ -27,13 +27,26 @@
     </div>
   </div>
   <script>
-  function show_timeline_graph(include_inactive, resize_frame) {
+  function show_timeline_graph(include_inactive, resize_frame, start, size) {
       if (include_inactive == 'true') {
           include_inactive = true;
       } else {
           include_inactive = false;
       }
 
+      var get_timeline_config = {
+          parameters: {include_inactive: include_inactive},
+      };
+
+      start = parseInt(start);
+      size = parseInt(size);
+      if (start != NaN && start >= 0) {
+          get_timeline_config.start = start;
+      }
+      if (size != NaN && size >= 1) {
+          get_timeline_config.size = size;
+      }
+
       LPS.use('lp.registry.timeline', 'node', 'lp.app.dragscroll', function(Y) {
           Y.on('domready', function(e) {
               if (Y.UA.ie) {
@@ -41,25 +54,53 @@
               }
 
               var display_graph = function(result) {
-                  var timeline = result;
-                  // The timeline for a productseries needs to be wrapped
-                  // in an array.
-                  if (!Y.Lang.isArray(timeline)) {
-                      timeline = [timeline];
-                  }
+                  // The result for a single productseries needs to be
+                  // wrapped in an array just as all the series for a
+                  // product would be.
+                  var entries = null;
+                  if (result.entries === undefined) {
+                      entries = [result];
+                  }
+                  else {
+                      entries = result.entries;
+                  }
+
+                  // XXX: EdwinGrubbs 2010-11-18 bug=677671
+                  // The get_timeline() REST method used to return an
+                  // arbitrary chunk of JSON. Since lazr.restful can only
+                  // batch entries, the LP.client is now wrapping in
+                  // objects that provide more functionality for the
+                  // entries, so we need to unwrap it before providing
+                  // the info to the TimelineGraph object.
+                  var timeline_data = [];
+                  Y.each(entries.reverse(), function(series, i) {
+                      var plain_series = {};
+                      var fields = [
+                          'is_development_focus',
+                          'landmarks',
+                          'name',
+                          'status',
+                          'uri'
+                          ];
+                      Y.each(fields, function(field, j) {
+                          plain_series[field] = series.get(field);
+                      });
+                      timeline_data.push(plain_series);
+                  });
 
                   // Don't display graph if there are zero milestones or
                   // releases.
                   var container = Y.one('#timeline-container');
                   container.setStyle('display', 'block');
                   var config = {
-                      timeline: timeline,
+                      timeline: timeline_data,
                       boundingBox: container
                   };
                   if (resize_frame !== '') {
                       config.resize_frame = resize_frame;
                   }
-                  var graph = new Y.lp.registry.timeline.TimelineGraph(config);
+                  var graph = new Y.lp.registry.timeline.TimelineGraph(
+                      config);
                   graph.render();
                   Y.one('#spinner').setStyle('display', 'none');
                   // Scroll to the most recent milestones or
@@ -67,26 +108,25 @@
                   graph.scroll_to_last_development_focus_landmark();
               }
 
+              get_timeline_config.on = {
+                  success: display_graph,
+                  failure: function(ignore, response, args) {
+                      // XXX: EdwinGrubbs 2009-07-02 bug=394912
+                      // Firefox triggers the failure handler with
+                      // status=0 if the page load is interrupted.
+                      if (response.status !== 0) {
+                          alert(
+                              response.status +
+                              ' Error retrieving series data.\n' +
+                              response.responseText);
+                      }
+                  }
+              };
+
               var client = new LP.client.Launchpad();
-              var timeline = client.named_get(
+              client.named_get(
                   LP.client.cache['context']['self_link'],
-                  'get_timeline', {
-                      parameters: {include_inactive: include_inactive},
-                      on: {
-                          success: display_graph,
-                          failure: function(ignore, response, args) {
-                              // XXX: EdwinGrubbs 2009-07-02 bug=394912
-                              // Firefox triggers the failure handler with
-                              // status=0 if the page load is interrupted.
-                              if (response.status !== 0) {
-                                  alert(
-                                      response.status +
-                                      ' Error retrieving series data.\n' +
-                                      response.responseText);
-                              }
-                          }
-                      }
-                  });
+                  'get_timeline', get_timeline_config);
 
               var drag_scroll_handler =
                   new Y.lp.app.dragscroll.DragScrollEventHandler();
@@ -98,9 +138,12 @@
   <script
     tal:define="
       include_inactive request/form/include_inactive | string:false;
-      resize_frame request/form/resize_frame | string:"
+      resize_frame request/form/resize_frame | string:;
+      start request/form/start | string:;
+      size request/form/size | string:"
     tal:content="
-      string: show_timeline_graph('${include_inactive}', '${resize_frame}');"/>
+      string: show_timeline_graph(
+        '${include_inactive}', '${resize_frame}', '${start}', '${size}');"/>
 
 </body>
 </html>

=== modified file 'lib/lp/registry/templates/product-series.pt'
--- lib/lp/registry/templates/product-series.pt	2010-10-27 00:33:29 +0000
+++ lib/lp/registry/templates/product-series.pt	2010-11-22 22:13:24 +0000
@@ -8,8 +8,12 @@
 >
 <body>
 
-  <div metal:fill-slot="main">
-    <tal:vars define="auto_resize string:true; include_inactive string:true">
+  <div metal:fill-slot="main"
+       tal:define="batch view/batched_series/currentBatch">
+    <tal:vars define="auto_resize string:true;
+                      include_inactive string:true;
+                      start batch/start | nothing;
+                      size batch/size | nothing">
       <iframe metal:use-macro="context/@@+timeline-macros/timeline-iframe"/>
     </tal:vars>
 
@@ -21,12 +25,12 @@
       </li>
     </ul>
 
-    <tal:series-list condition="view/batched_series/currentBatch">
+    <tal:series-list condition="batch">
       <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">
+      <div tal:repeat="series batch">
         <tal:cache content="cache:public, 1 hour, series/name">
           <div style="margin-top: 1em;
                       border-bottom: 1px solid #ccc; max-width: 60em;"

=== modified file 'lib/lp/registry/templates/timeline-macros.pt'
--- lib/lp/registry/templates/timeline-macros.pt	2009-12-03 18:33:22 +0000
+++ lib/lp/registry/templates/timeline-macros.pt	2010-11-22 22:13:24 +0000
@@ -26,7 +26,7 @@
           scrolling="no"
           width="100%" height="216px"></iframe>
   <script>
-  function timeline_iframe(auto_resize, include_inactive) {
+  function timeline_iframe(auto_resize, include_inactive, start, size) {
       var timeline_url = "+timeline-graph?";
       if (include_inactive == 'true') {
           timeline_url += "include_inactive=true&";
@@ -35,6 +35,15 @@
       if (auto_resize == 'true') {
           timeline_url += 'resize_frame=timeline-iframe&';
       }
+
+      start = parseInt(start);
+      size = parseInt(size);
+      if (start != NaN && start >= 0) {
+          timeline_url += "start=" + start + "&";
+      }
+      if (size != NaN && size >= 1) {
+          timeline_url += "size=" + size + "&";
+      }
       LPS.use('node', function(Y) {
           if (Y.UA.ie) {
               return;
@@ -53,11 +62,16 @@
   <script
     tal:define="
       auto_resize auto_resize | nothing;
-      include_inactive include_inactive | nothing"
+      include_inactive include_inactive | nothing;
+      start start | nothing;
+      size size | nothing"
     tal:content="string:
       timeline_iframe(
         '${auto_resize}',
-        '${include_inactive}');
+        '${include_inactive}',
+        '${start}',
+        '${size}'
+        );
       "/>
 </metal:timeline-iframe>
 

=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py	2010-10-29 15:47:09 +0000
+++ lib/lp/registry/tests/test_product.py	2010-11-22 22:13:24 +0000
@@ -21,7 +21,10 @@
     get_feedback_messages,
     setupBrowser,
     )
-from canonical.testing.layers import LaunchpadFunctionalLayer
+from canonical.testing.layers import (
+    DatabaseFunctionalLayer,
+    LaunchpadFunctionalLayer,
+    )
 from lp.registry.interfaces.person import IPersonSet
 from lp.registry.interfaces.product import (
     IProduct,
@@ -40,7 +43,7 @@
 class TestProduct(TestCaseWithFactory):
     """Tests product object."""
 
-    layer = LaunchpadFunctionalLayer
+    layer = DatabaseFunctionalLayer
 
     def test_deactivation_failure(self):
         # Ensure that a product cannot be deactivated if
@@ -81,16 +84,6 @@
         release_2 = self.factory.makeProductRelease(
             product=product,
             milestone=milestone_0_2)
-        release_file1 = self.factory.makeProductReleaseFile(
-            product=product,
-            release=release_1,
-            productseries=series,
-            milestone=milestone_0_1)
-        release_file2 = self.factory.makeProductReleaseFile(
-            product=product,
-            release=release_2,
-            productseries=series,
-            milestone=milestone_0_2)
         expected = [(milestone_0_2, release_2), (milestone_0_1, release_1)]
         self.assertEqual(
             expected,
@@ -135,7 +128,7 @@
         [series] = product.getTimeline()
         timeline_milestones = [
             landmark['uri']
-            for landmark in series['landmarks']]
+            for landmark in series.landmarks]
         self.assertEqual(
             expected_milestones,
             timeline_milestones)
@@ -152,16 +145,36 @@
             [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):
+    def test_getVersionSortedSeries_with_specific_statuses(self):
+        # The obsolete series should be included in the results if
+        # statuses=[SeriesStatus.OBSOLETE]. The development focus will
+        # also be included since it does not get filtered.
+        login('admin@xxxxxxxxxxxxx')
+        product = self.factory.makeProduct()
+        self.factory.makeProductSeries(
+            product=product, name='frozen-series')
+        obsolete_series = self.factory.makeProductSeries(
+            product=product, name='obsolete-series')
+        obsolete_series.status = SeriesStatus.OBSOLETE
+        active_series = product.getVersionSortedSeries(
+            statuses=[SeriesStatus.OBSOLETE])
+        self.assertEqual(
+            [u'trunk', u'obsolete-series'],
+            [series.name for series in active_series])
+
+    def test_getVersionSortedSeries_without_specific_statuses(self):
         # The obsolete series should not be included in the results if
-        # the filter_obsolete argument is set to True.
+        # filter_statuses=[SeriesStatus.OBSOLETE]. The development focus will
+        # always be included since it does not get filtered.
         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)
+        product.development_focus.status = SeriesStatus.OBSOLETE
+        active_series = product.getVersionSortedSeries(
+            filter_statuses=[SeriesStatus.OBSOLETE])
         self.assertEqual(
             [u'trunk', u'active-series'],
             [series.name for series in active_series])
@@ -224,7 +237,7 @@
 class ProductAttributeCacheTestCase(unittest.TestCase):
     """Cached attributes must be cleared at the end of a transaction."""
 
-    layer = LaunchpadFunctionalLayer
+    layer = DatabaseFunctionalLayer
 
     def setUp(self):
         self.product = Product.selectOneBy(name='tomcat')
@@ -294,7 +307,7 @@
 class ProductSnapshotTestCase(TestCaseWithFactory):
     """A TestCase for product snapshots."""
 
-    layer = LaunchpadFunctionalLayer
+    layer = DatabaseFunctionalLayer
 
     def setUp(self):
         super(ProductSnapshotTestCase, self).setUp()
@@ -320,7 +333,7 @@
 class BugSupervisorTestCase(TestCaseWithFactory):
     """A TestCase for bug supervisor management."""
 
-    layer = LaunchpadFunctionalLayer
+    layer = DatabaseFunctionalLayer
 
     def setUp(self):
         super(BugSupervisorTestCase, self).setUp()