Merge lp:~edwin-grubbs/launchpad/bug-490659-series-timeout into lp:launchpad/db-devel

Proposed by Edwin Grubbs
Status: Merged
Approved by: Stuart Bishop
Approved revision: no longer in the source branch.
Merged at revision: 9696
Proposed branch: lp:~edwin-grubbs/launchpad/bug-490659-series-timeout
Merge into: lp:launchpad/db-devel
Diff against target: 429 lines (+135/-62)
17 files modified
database/schema/patch-2208-01-0.sql (+9/-0)
database/schema/security.cfg (+1/-0)
database/schema/trusted.sql (+27/-0)
lib/canonical/launchpad/webapp/sorting.py (+3/-2)
lib/lp/registry/browser/__init__.py (+1/-1)
lib/lp/registry/browser/configure.zcml (+7/-0)
lib/lp/registry/browser/productseries.py (+14/-0)
lib/lp/registry/browser/tests/productseries-views.txt (+1/-1)
lib/lp/registry/browser/tests/test_milestone.py (+1/-1)
lib/lp/registry/interfaces/milestone.py (+2/-0)
lib/lp/registry/model/milestone.py (+9/-2)
lib/lp/registry/model/projectgroup.py (+17/-5)
lib/lp/registry/stories/productseries/xx-productseries-series.txt (+2/-2)
lib/lp/registry/templates/object-milestones.pt (+2/-2)
lib/lp/registry/templates/productseries-detailed-display.pt (+38/-0)
lib/lp/registry/templates/productseries-macros.pt (+0/-42)
lib/lp/registry/templates/productseries-status.pt (+1/-4)
To merge this branch: bzr merge lp:~edwin-grubbs/launchpad/bug-490659-series-timeout
Reviewer Review Type Date Requested Status
Stuart Bishop (community) db Approve
Robert Collins (community) Approve
Brad Crittenden (community) code Approve
Review via email: mp+32555@code.launchpad.net

Description of the change

Summary
-------

Bug 490659 reports a timeout caused by a project with over 700 releases.
The ProductSeries' milestones and releases attributes both used a python
function to expand the version numbers in the milestone name so that
milestone version numbers such as 1.1, 1.10, and 1.100 would sort
correctly. To avoid loading all 700 releases from the database just to
sort them in python to find the most recent ones, I added the
milestone_sort_key(timestamp dateexpected, text name) postgresql
function, so that the sorting can take place in the db. I also added an
index to the Milestone table using this function.

I have never submitted a db function for review before, so I'm sure that
I'm doing something wrong. For some reason, the configuration I added to
security.cfg doesn't seem to have any effect, so I have to run the
following sql on launchpad_ftest_template and laucnhpad_dev.

GRANT EXECUTE ON FUNCTION milestone_sort_key(timestamp, text) TO PUBLIC;

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

Added milestone_sort_key(timestamp, text) db function.
    database/schema/patch-2207-99-0.sql
    database/schema/security.cfg

Added __len__ to DecoratedResultSet, so that code expecting the model to
return a list won't blow up on the result set.
    lib/canonical/launchpad/components/decoratedresultset.py
    lib/canonical/launchpad/zcml/decoratedresultset.zcml

Changed the productseries detailed-display macro into its own page since
it makes it cleaner to limit the number of milestones and releases in
the view attributes.
    lib/lp/registry/browser/configure.zcml
    lib/lp/registry/browser/productseries.py
    lib/lp/registry/templates/productseries-macros.pt
    lib/lp/registry/templates/productseries-status.pt
    lib/lp/registry/stories/productseries/xx-productseries-series.txt

Converted HasMilestones' milestones and releases attributes to return a
DecoratedResultSet instead of alist.
    lib/lp/registry/model/milestone.py
    lib/lp/registry/browser/tests/productseries-views.txt

Tests
-----

./bin/test -vv -t 'xx-productseries-series.txt|productseries-views.txt'

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

Create a bunch of sample milestones and releases on launchpad.dev:
    INSERT INTO Milestone (name, product, productseries)
    SELECT 'a' || i, (select id from product where name = 'bzr'),
        (select id from productseries where name='trunk' and product = (
                select id from product where name = 'bzr'))
    FROM generate_series(1, 100) as i;

    INSERT INTO Milestone (name, product, productseries, active)
    SELECT
        'rel-' || i,
        (select id from product where name = 'bzr'),
        (select id from productseries where name='trunk' and product = (
                select id from product where name = 'bzr')),
        FALSE
    FROM generate_series(1, 100) as i;

    INSERT INTO ProductRelease (owner, milestone, datereleased)
    SELECT 1, id, now()
    FROM Milestone
    WHERE name ~ 'rel-.*';

* Open http://launchpad.dev/bzr/+series
  * Only 12 milestones and 12 releases should be listed in the gray box
    for trunk.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

I don't like the __len__ introduction as it blurs the line between lists and resultsets more : and we suffer because the line is blurry already. I realise you probably have a stack of stuff to put on top of this that is doing lens : however I'm worried about the knock on effects of the change : we should do it in IResultSet if we're doing it at all, not piecemeal on DecoratedResultSet. You could use a lazr.delegates to add __len__ just to your specific case (or, and perhaps better?) work up the stack replacing listified stuff with resultset aware code.

Other than that I think this is conceptually fine and an appropriate solution.

I'm a tad rusty on my plpython gotchas, so I'll let Stuart review that for you.

review: Needs Fixing
Revision history for this message
Stuart Bishop (stub) wrote :

The function and a database comment should be added directly to trusted.sql. You will still need the database patch to add the index. I'll allocate a number shortly.

The function should return """ '%s %s' % (str(date_expected), name) """, rather than the implicit cast of repr(date_expected, name).

You are importing plpy but no longer using it.

I agree with Robert on __len__ - it is deliberately left out to avoid it accidentally being called, as it can be very expensive. In particular, Python invoked __len__ if it exists when casting something to a list, doubling the amount of database time materializing a result set (we reported this as a Python bug - not sure if it is still open).

review: Approve (db)
Revision history for this message
Stuart Bishop (stub) wrote :

patch-2208-01-0.sql

Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (14.4 KiB)

I got rid of __len__ on DecoratedResultSet. Instead, I added IHasMilestones.has_milestones, since this is what len() was being used to test for, and a database count is much more intensive than finding a single unsorted matching row.

I made the changes to to the db function, and I discovered why my configuration in security.cfg wasn't granting execute to public. It expects me to specify "timestamp without time zone" instead of just "timestamp". A GRANT statement with just "timestamp" works, so it must just be the introspection that security.py does to figure out whether it is dealing with a function or a table.

I have included a new full diff below, since it is less confusing than the incremental diff and not much bigger.

-Edwin

=== added file 'database/schema/patch-2208-01-0.sql'
--- database/schema/patch-2208-01-0.sql 1970-01-01 00:00:00 +0000
+++ database/schema/patch-2208-01-0.sql 2010-08-19 17:15:22 +0000
@@ -0,0 +1,9 @@
+-- Copyright 2010 Canonical Ltd. This software is licensed under the
+-- GNU Affero General Public License version 3 (see the file LICENSE).
+SET client_min_messages=ERROR;
+
+CREATE INDEX milestone_dateexpected_name_sort
+ON Milestone
+USING btree (milestone_sort_key(dateexpected, name));
+
+INSERT INTO LaunchpadDatabaseRevision VALUES (2208, 01, 0);

=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2010-08-10 05:49:16 +0000
+++ database/schema/security.cfg 2010-08-19 23:52:51 +0000
@@ -17,6 +17,7 @@
 public.person_sort_key(text, text) = EXECUTE
 public.calculate_bug_heat(integer) = EXECUTE
 public.debversion_sort_key(text) = EXECUTE
+public.milestone_sort_key(timestamp without time zone, text) = EXECUTE
 public.null_count(anyarray) = EXECUTE
 public.valid_name(text) = EXECUTE
 public.valid_bug_name(text) = EXECUTE

=== modified file 'database/schema/trusted.sql'
--- database/schema/trusted.sql 2010-08-06 09:32:02 +0000
+++ database/schema/trusted.sql 2010-08-19 19:50:52 +0000
@@ -1705,3 +1705,30 @@

     return int(total_heat)
 $$;
+
+-- This function is not STRICT, since it needs to handle
+-- dateexpected when it is NULL.
+CREATE OR REPLACE FUNCTION milestone_sort_key(
+ dateexpected timestamp, name text)
+ RETURNS text
+AS $_$
+ # If this method is altered, then any functional indexes using it
+ # need to be rebuilt.
+ import re
+ import datetime
+
+ date_expected, name = args
+
+ def substitude_filled_numbers(match):
+ return match.group(0).zfill(5)
+
+ name = re.sub(u'\d+', substitude_filled_numbers, name)
+ if date_expected is None:
+ # NULL dates are considered to be in the future.
+ date_expected = datetime.datetime(datetime.MAXYEAR, 1, 1)
+ return '%s %s' % (date_expected, name)
+$_$
+LANGUAGE plpythonu IMMUTABLE;
+
+COMMENT ON FUNCTION milestone_sort_key(timestamp, text) IS
+'Sort by the Milestone dateexpected and name. If the dateexpected is NULL, then it is converted to a date far in the future, so it will be sorted as a milestone in the future.';

=== modified file 'lib/lp/registry/browser/__init__.py'
--- lib/lp/registry/browser/__init__....

Revision history for this message
Brad Crittenden (bac) wrote :

Hi Edwin,

Interesting branch!

typo: substitude -> substitute

Should has_milestones be exported?

Any reason you cannot use ISlaveStore for the queries?

review: Approve (code)
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :

> Hi Edwin,
>
> Interesting branch!
>
> typo: substitude -> substitute

Fixed both the db function and the code that I stole that from.

> Should has_milestones be exported?

I think it would be better not to slow down every query for the pillars and series by adding another attribute that is hardly ever used. The other milestones attributes are references to collections, so you don't actually invoke the extra query unless you follow that link.

> Any reason you cannot use ISlaveStore for the queries?

IStore() chooses the slave or master based on whether the user has modified anything lately. If I forced it to use ISlaveStore, it would not show the user any changes they made until they propagated to the slave servers.

Revision history for this message
Robert Collins (lifeless) wrote :

69- def substitude_filled_numbers(match):
70+
71+ def substitute_filled_numbers(match):

That new VWS breaks up a small function - closures like that are more readable when they look like one conceptual thing rather than two IMO; please consider removing the new VWS.

I think the index is new? Needs a new stamp from stuart if so.

Lastly, it seems to me that LBYL isn't needed here: surely doing *neither* a .count() nor a .any() is appropriate: rather just iterate the latest_milestones, and if the iterator outputs no rows don't show the table? Perhaps we don't have a construct for doing that; if thats the case I'm happy with this approach, but suggest that you file a bug saying we should have such a construct - it will be the least work of all and thus fastest.

review: Approve
Revision history for this message
Stuart Bishop (stub) wrote :

db still approved.

review: Approve (db)
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :

> 69- def substitude_filled_numbers(match):
> 70+
> 71+ def substitute_filled_numbers(match):

Brad also pointed that out, and it has been fixed.

> That new VWS breaks up a small function - closures like that are more readable
> when they look like one conceptual thing rather than two IMO; please consider
> removing the new VWS.

I actually disagree that it makes it less readable. Secondly, pocketlint complains when you don't have a blank line above a function definition.

> I think the index is new? Needs a new stamp from stuart if so.

The index isn't new. Since I included a full diff after the first set of changes instead of an incremental diff, it may have appeared new, but it was in the patch file below the db function before the db function was moved to trusted.sql.

> Lastly, it seems to me that LBYL isn't needed here: surely doing *neither* a
> .count() nor a .any() is appropriate: rather just iterate the
> latest_milestones, and if the iterator outputs no rows don't show the table?

The problem with not checking .any() before iterating is that a storm ResultSet object will query the database every time you iterate over it; therefore, you would have to create a new cached property on the view that would hold a list in order to eliminate the query. We can't have the model just cache the property as a list for us, since the whole point of this refactoring was to allow the view code to slice the model's attribute without the model first querying a really large number of rows.

> Perhaps we don't have a construct for doing that; if thats the case I'm happy
> with this approach, but suggest that you file a bug saying we should have such
> a construct - it will be the least work of all and thus fastest.

I definitely think there is a better universal solution to this, but it will need more discussion on the mailing list, since either template/view coding practices or storm's ResultSet will have to change significantly. Also, to put it into perspective, we are talking about eliminating one or two single-row queries, compared to the 700-rows of results that this branch eliminates, so improving this might not be as urgent as other optimizations.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'database/schema/patch-2208-01-0.sql'
2--- database/schema/patch-2208-01-0.sql 1970-01-01 00:00:00 +0000
3+++ database/schema/patch-2208-01-0.sql 2010-08-25 04:29:43 +0000
4@@ -0,0 +1,9 @@
5+-- Copyright 2010 Canonical Ltd. This software is licensed under the
6+-- GNU Affero General Public License version 3 (see the file LICENSE).
7+SET client_min_messages=ERROR;
8+
9+CREATE INDEX milestone_dateexpected_name_sort
10+ON Milestone
11+USING btree (milestone_sort_key(dateexpected, name));
12+
13+INSERT INTO LaunchpadDatabaseRevision VALUES (2208, 01, 0);
14
15=== modified file 'database/schema/security.cfg'
16--- database/schema/security.cfg 2010-08-15 11:29:23 +0000
17+++ database/schema/security.cfg 2010-08-25 04:29:43 +0000
18@@ -17,6 +17,7 @@
19 public.person_sort_key(text, text) = EXECUTE
20 public.calculate_bug_heat(integer) = EXECUTE
21 public.debversion_sort_key(text) = EXECUTE
22+public.milestone_sort_key(timestamp without time zone, text) = EXECUTE
23 public.null_count(anyarray) = EXECUTE
24 public.valid_name(text) = EXECUTE
25 public.valid_bug_name(text) = EXECUTE
26
27=== modified file 'database/schema/trusted.sql'
28--- database/schema/trusted.sql 2010-08-06 09:32:02 +0000
29+++ database/schema/trusted.sql 2010-08-25 04:29:43 +0000
30@@ -1705,3 +1705,30 @@
31
32 return int(total_heat)
33 $$;
34+
35+-- This function is not STRICT, since it needs to handle
36+-- dateexpected when it is NULL.
37+CREATE OR REPLACE FUNCTION milestone_sort_key(
38+ dateexpected timestamp, name text)
39+ RETURNS text
40+AS $_$
41+ # If this method is altered, then any functional indexes using it
42+ # need to be rebuilt.
43+ import re
44+ import datetime
45+
46+ date_expected, name = args
47+
48+ def substitute_filled_numbers(match):
49+ return match.group(0).zfill(5)
50+
51+ name = re.sub(u'\d+', substitute_filled_numbers, name)
52+ if date_expected is None:
53+ # NULL dates are considered to be in the future.
54+ date_expected = datetime.datetime(datetime.MAXYEAR, 1, 1)
55+ return '%s %s' % (date_expected, name)
56+$_$
57+LANGUAGE plpythonu IMMUTABLE;
58+
59+COMMENT ON FUNCTION milestone_sort_key(timestamp, text) IS
60+'Sort by the Milestone dateexpected and name. If the dateexpected is NULL, then it is converted to a date far in the future, so it will be sorted as a milestone in the future.';
61
62=== modified file 'lib/canonical/launchpad/webapp/sorting.py'
63--- lib/canonical/launchpad/webapp/sorting.py 2009-06-25 05:30:52 +0000
64+++ lib/canonical/launchpad/webapp/sorting.py 2010-08-25 04:29:43 +0000
65@@ -25,9 +25,10 @@
66
67 """
68 assert(isinstance(unicode_text, unicode))
69- def substitude_filled_numbers(match):
70+
71+ def substitute_filled_numbers(match):
72 return match.group(0).zfill(fill_digits)
73- return re.sub(u'\d+', substitude_filled_numbers, unicode_text)
74+ return re.sub(u'\d+', substitute_filled_numbers, unicode_text)
75
76
77 # Create translation table for numeric ordinals to their
78
79=== modified file 'lib/lp/registry/browser/__init__.py'
80--- lib/lp/registry/browser/__init__.py 2010-08-20 20:31:18 +0000
81+++ lib/lp/registry/browser/__init__.py 2010-08-25 04:29:43 +0000
82@@ -80,7 +80,7 @@
83 @property
84 def milestone_table_class(self):
85 """The milestone table will be unseen if there are no milestones."""
86- if len(self.context.all_milestones) > 0:
87+ if self.context.has_milestones:
88 return 'listing'
89 else:
90 # The page can remove the 'unseen' class to make the table
91
92=== modified file 'lib/lp/registry/browser/configure.zcml'
93--- lib/lp/registry/browser/configure.zcml 2010-08-13 21:30:24 +0000
94+++ lib/lp/registry/browser/configure.zcml 2010-08-25 04:29:43 +0000
95@@ -1655,6 +1655,13 @@
96 </browser:pages>
97 <browser:page
98 for="lp.registry.interfaces.productseries.IProductSeries"
99+ class="lp.registry.browser.productseries.ProductSeriesDetailedDisplayView"
100+ template="../templates/productseries-detailed-display.pt"
101+ facet="overview"
102+ permission="zope.Public"
103+ name="+detailed-display"/>
104+ <browser:page
105+ for="lp.registry.interfaces.productseries.IProductSeries"
106 class="lp.registry.browser.productseries.ProductSeriesRdfView"
107 facet="overview"
108 permission="zope.Public"
109
110=== modified file 'lib/lp/registry/browser/productseries.py'
111--- lib/lp/registry/browser/productseries.py 2010-08-23 04:48:17 +0000
112+++ lib/lp/registry/browser/productseries.py 2010-08-25 04:29:43 +0000
113@@ -10,6 +10,7 @@
114 'ProductSeriesBreadcrumb',
115 'ProductSeriesBugsMenu',
116 'ProductSeriesDeleteView',
117+ 'ProductSeriesDetailedDisplayView',
118 'ProductSeriesEditView',
119 'ProductSeriesFacets',
120 'ProductSeriesFileBugRedirect',
121@@ -482,6 +483,19 @@
122 return None
123
124
125+class ProductSeriesDetailedDisplayView(ProductSeriesView):
126+
127+ @cachedproperty
128+ def latest_milestones(self):
129+ # Convert to list to avoid the query being run multiple times.
130+ return list(self.context.milestones[:12])
131+
132+ @cachedproperty
133+ def latest_releases(self):
134+ # Convert to list to avoid the query being run multiple times.
135+ return list(self.context.releases[:12])
136+
137+
138 class ProductSeriesUbuntuPackagingView(LaunchpadFormView):
139
140 schema = IPackaging
141
142=== modified file 'lib/lp/registry/browser/tests/productseries-views.txt'
143--- lib/lp/registry/browser/tests/productseries-views.txt 2010-08-23 04:48:17 +0000
144+++ lib/lp/registry/browser/tests/productseries-views.txt 2010-08-25 04:29:43 +0000
145@@ -307,7 +307,7 @@
146 specifications that will be unassigned, and release files that will be
147 deleted are available.
148
149- >>> view.milestones
150+ >>> list(view.milestones)
151 []
152 >>> view.bugtasks
153 []
154
155=== modified file 'lib/lp/registry/browser/tests/test_milestone.py'
156--- lib/lp/registry/browser/tests/test_milestone.py 2010-08-22 18:31:30 +0000
157+++ lib/lp/registry/browser/tests/test_milestone.py 2010-08-25 04:29:43 +0000
158@@ -160,7 +160,7 @@
159 }
160 view = create_initialized_view(milestone, '+delete', form=form)
161 self.assertEqual([], view.errors)
162- self.assertEqual(0, len(product.all_milestones))
163+ self.assertEqual([], list(product.all_milestones))
164 self.assertEqual(0, product.development_focus.all_bugtasks.count())
165
166
167
168=== modified file 'lib/lp/registry/interfaces/milestone.py'
169--- lib/lp/registry/interfaces/milestone.py 2010-08-20 20:31:18 +0000
170+++ lib/lp/registry/interfaces/milestone.py 2010-08-25 04:29:43 +0000
171@@ -253,6 +253,8 @@
172 """An interface for classes providing milestones."""
173 export_as_webservice_entry()
174
175+ has_milestones = Bool(title=_("Whether the object has any milestones."))
176+
177 milestones = exported(
178 CollectionField(
179 title=_("The visible and active milestones associated with this "
180
181=== modified file 'lib/lp/registry/model/milestone.py'
182--- lib/lp/registry/model/milestone.py 2010-08-20 20:31:18 +0000
183+++ lib/lp/registry/model/milestone.py 2010-08-25 04:29:43 +0000
184@@ -82,6 +82,9 @@
185 class HasMilestonesMixin:
186 implements(IHasMilestones)
187
188+ _milestone_order = (
189+ 'milestone_sort_key(Milestone.dateexpected, Milestone.name) DESC')
190+
191 def _getMilestoneCondition(self):
192 """Provides condition for milestones and all_milestones properties.
193
194@@ -93,11 +96,15 @@
195 "Unexpected class for mixin: %r" % self)
196
197 @property
198+ def has_milestones(self):
199+ return self.all_milestones.any() is not None
200+
201+ @property
202 def all_milestones(self):
203 """See `IHasMilestones`."""
204 store = Store.of(self)
205 result = store.find(Milestone, self._getMilestoneCondition())
206- return sorted(result, key=milestone_sort_key, reverse=True)
207+ return result.order_by(self._milestone_order)
208
209 @property
210 def milestones(self):
211@@ -106,7 +113,7 @@
212 result = store.find(Milestone,
213 And(self._getMilestoneCondition(),
214 Milestone.active == True))
215- return sorted(result, key=milestone_sort_key, reverse=True)
216+ return result.order_by(self._milestone_order)
217
218
219 class Milestone(SQLBase, StructuralSubscriptionTargetMixin, HasBugsBase):
220
221=== modified file 'lib/lp/registry/model/projectgroup.py'
222--- lib/lp/registry/model/projectgroup.py 2010-08-20 20:31:18 +0000
223+++ lib/lp/registry/model/projectgroup.py 2010-08-25 04:29:43 +0000
224@@ -94,7 +94,6 @@
225 from lp.registry.model.mentoringoffer import MentoringOffer
226 from lp.registry.model.milestone import (
227 Milestone,
228- milestone_sort_key,
229 ProjectMilestone,
230 )
231 from lp.registry.model.pillar import HasAliasMixin
232@@ -164,8 +163,6 @@
233 bug_reported_acknowledgement = StringCol(default=None)
234 max_bug_heat = Int()
235
236- # convenient joins
237-
238 @property
239 def products(self):
240 return Product.selectBy(project=self, active=True, orderBy='name')
241@@ -419,10 +416,25 @@
242 result.group_by(Milestone.name)
243 if only_active:
244 result.having('BOOL_OR(Milestone.active) = TRUE')
245- milestones = shortlist(
246+ # MIN(Milestone.dateexpected) has to be used to match the
247+ # aggregate function in the `columns` variable.
248+ result.order_by(
249+ 'milestone_sort_key(MIN(Milestone.dateexpected), Milestone.name) '
250+ 'DESC')
251+ return shortlist(
252 [ProjectMilestone(self, name, dateexpected, active)
253 for name, dateexpected, active in result])
254- return sorted(milestones, key=milestone_sort_key, reverse=True)
255+
256+ @property
257+ def has_milestones(self):
258+ """See `IHasMilestones`."""
259+ store = Store.of(self)
260+ result = store.find(
261+ Milestone.id,
262+ And(Milestone.product == Product.id,
263+ Product.project == self,
264+ Product.active == True))
265+ return result.any() is not None
266
267 @property
268 def milestones(self):
269
270=== modified file 'lib/lp/registry/stories/productseries/xx-productseries-series.txt'
271--- lib/lp/registry/stories/productseries/xx-productseries-series.txt 2010-05-24 20:23:19 +0000
272+++ lib/lp/registry/stories/productseries/xx-productseries-series.txt 2010-08-25 04:29:43 +0000
273@@ -29,7 +29,7 @@
274 >>> series_trunk = find_tag_by_id(content, 'series-trunk')
275 >>> print extract_text(series_trunk)
276 trunk series Focus of Development
277- Milestones: 1.0 Releases: 0.9.2, 0.9.1, 0.9
278+ Latest milestones: 1.0 Latest releases: 0.9.2, 0.9.1, 0.9
279 Bugs targeted: None
280 Blueprints targeted: 1 Unknown
281 The "trunk" series represents the primary line of development rather ...
282@@ -46,7 +46,7 @@
283 >>> series_1_0 = find_tag_by_id(content, 'series-1-0')
284 >>> print extract_text(series_1_0)
285 1.0 series Active Development
286- Releases: 1.0.0
287+ Latest releases: 1.0.0
288 Bugs targeted: 1 New
289 Blueprints targeted: None
290 The 1.0 branch of the Mozilla web browser. Currently, this is the ...
291
292=== modified file 'lib/lp/registry/templates/object-milestones.pt'
293--- lib/lp/registry/templates/object-milestones.pt 2009-11-07 00:49:26 +0000
294+++ lib/lp/registry/templates/object-milestones.pt 2010-08-25 04:29:43 +0000
295@@ -28,7 +28,7 @@
296 <tal:milestones define="milestones context/all_milestones">
297 <table id="milestones" class="listing"
298 tal:define="has_series context/series|nothing"
299- tal:condition="milestones">
300+ tal:condition="context/has_milestones">
301 <thead>
302 <tr>
303 <th>Version</th>
304@@ -45,7 +45,7 @@
305 </tbody>
306 </table>
307
308- <tal:no-milestones condition="not: milestones">
309+ <tal:no-milestones condition="not: context/has_milestones">
310 <p>There are no milestones associated with
311 <span tal:replace="context/title" />
312 </p>
313
314=== added file 'lib/lp/registry/templates/productseries-detailed-display.pt'
315--- lib/lp/registry/templates/productseries-detailed-display.pt 1970-01-01 00:00:00 +0000
316+++ lib/lp/registry/templates/productseries-detailed-display.pt 2010-08-25 04:29:43 +0000
317@@ -0,0 +1,38 @@
318+<div
319+ xmlns="http://www.w3.org/1999/xhtml"
320+ xmlns:tal="http://xml.zope.org/namespaces/tal"
321+ xmlns:metal="http://xml.zope.org/namespaces/metal"
322+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
323+ >
324+ <strong><a tal:attributes="href context/fmt:url"
325+ tal:content="context/name" >
326+ 1.0
327+ </a> series
328+ </strong>
329+ <em tal:condition="context/is_development_focus">Focus of Development</em>
330+ <em tal:condition="not: context/is_development_focus"
331+ tal:content="context/status/title">
332+ This series' status
333+ </em>
334+ <div>
335+ <tal:milestones condition="view/latest_milestones">
336+ Latest milestones:
337+ <tal:milestone repeat="milestone view/latest_milestones">
338+ <a tal:attributes="href milestone/fmt:url" tal:content="milestone/name"
339+ >name</a><tal:comma condition="not:repeat/milestone/end">,</tal:comma>
340+ </tal:milestone>
341+ </tal:milestones>
342+ <tal:release repeat="release view/latest_releases">
343+ <tal:release-start condition="repeat/release/start">
344+ <tal:comment condition="nothing">
345+ Releases may be a list or an resultset. We cannot easily know if there
346+ releases until the first one is returned.
347+ </tal:comment>
348+ <tal:space condition="view/latest_milestones"> &nbsp &nbsp </tal:space>
349+ Latest releases:
350+ </tal:release-start>
351+ <a tal:attributes="href release/fmt:url" tal:content="release/version"
352+ >version</a><tal:comma condition="not:repeat/release/end">,</tal:comma>
353+ </tal:release>
354+ </div>
355+</div>
356
357=== modified file 'lib/lp/registry/templates/productseries-macros.pt'
358--- lib/lp/registry/templates/productseries-macros.pt 2010-05-24 20:23:19 +0000
359+++ lib/lp/registry/templates/productseries-macros.pt 2010-08-25 04:29:43 +0000
360@@ -4,48 +4,6 @@
361 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
362 omit-tag="">
363
364-<metal:detailed_display define-macro="detailed_display">
365- <tal:comment replace="nothing">
366- This macro expects two variables to be defined:
367- - 'series': The ProductSeries
368- - 'is_focus': A boolean saying whether this is the series in which
369- development is focused.
370- </tal:comment>
371-
372- <strong><a tal:attributes="href series/fmt:url"
373- tal:content="series/name" >
374- 1.0
375- </a> series
376- </strong>
377- <em tal:condition="is_focus">Focus of Development</em>
378- <em tal:condition="not: is_focus" tal:content="series/status/title">
379- This series' status
380- </em>
381- <div>
382- <tal:milestones condition="series/milestones">
383- Milestones:
384- <tal:milestone repeat="milestone series/milestones">
385- <a tal:attributes="href milestone/fmt:url" tal:content="milestone/name"
386- >name</a><tal:comma condition="not:repeat/milestone/end">,</tal:comma>
387- </tal:milestone>
388- </tal:milestones>
389- <tal:release repeat="release series/releases">
390- <tal:release-start condition="repeat/release/start">
391- <tal:comment condition="nothing">
392- Releases may be a list or an resultset. We cannot easily know if there
393- releases until the first one is returned.
394- </tal:comment>
395- <tal:space condition="series/milestones"> &nbsp &nbsp </tal:space>
396- Releases:
397- </tal:release-start>
398- <a tal:attributes="href release/fmt:url" tal:content="release/version"
399- >version</a><tal:comma condition="not:repeat/release/end">,</tal:comma>
400- </tal:release>
401- </div>
402- <metal:extra define-slot="extra" />
403-</metal:detailed_display>
404-
405-
406 <metal:branch_display define-macro="branch_display">
407 <tal:comment condition="nothing">
408 This macro expects two variables to be defined:
409
410=== modified file 'lib/lp/registry/templates/productseries-status.pt'
411--- lib/lp/registry/templates/productseries-status.pt 2009-07-21 18:17:49 +0000
412+++ lib/lp/registry/templates/productseries-status.pt 2010-08-25 04:29:43 +0000
413@@ -8,8 +8,7 @@
414 is_focus context/is_development_focus;
415 spec_count_status view/specification_status_counts;"
416 >
417- <metal:series use-macro="series/@@+macros/detailed_display">
418- <div metal:fill-slot="extra">
419+ <div tal:replace="structure context/@@+detailed-display"/>
420 <div>
421 <tal:not-obsolete
422 condition="not: view/is_obsolete"
423@@ -42,6 +41,4 @@
424 <tal:summary
425 condition="series/summary"
426 content="structure context/summary/fmt:text-to-html" />
427- </div>
428- </metal:series>
429 </div>

Subscribers

People subscribed via source and target branches

to status/vote changes: