launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #01994
[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()