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
=== 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-25 04:29:43 +0000
@@ -0,0 +1,9 @@
1-- Copyright 2010 Canonical Ltd. This software is licensed under the
2-- GNU Affero General Public License version 3 (see the file LICENSE).
3SET client_min_messages=ERROR;
4
5CREATE INDEX milestone_dateexpected_name_sort
6ON Milestone
7USING btree (milestone_sort_key(dateexpected, name));
8
9INSERT INTO LaunchpadDatabaseRevision VALUES (2208, 01, 0);
010
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2010-08-15 11:29:23 +0000
+++ database/schema/security.cfg 2010-08-25 04:29:43 +0000
@@ -17,6 +17,7 @@
17public.person_sort_key(text, text) = EXECUTE17public.person_sort_key(text, text) = EXECUTE
18public.calculate_bug_heat(integer) = EXECUTE18public.calculate_bug_heat(integer) = EXECUTE
19public.debversion_sort_key(text) = EXECUTE19public.debversion_sort_key(text) = EXECUTE
20public.milestone_sort_key(timestamp without time zone, text) = EXECUTE
20public.null_count(anyarray) = EXECUTE21public.null_count(anyarray) = EXECUTE
21public.valid_name(text) = EXECUTE22public.valid_name(text) = EXECUTE
22public.valid_bug_name(text) = EXECUTE23public.valid_bug_name(text) = EXECUTE
2324
=== modified file 'database/schema/trusted.sql'
--- database/schema/trusted.sql 2010-08-06 09:32:02 +0000
+++ database/schema/trusted.sql 2010-08-25 04:29:43 +0000
@@ -1705,3 +1705,30 @@
17051705
1706 return int(total_heat)1706 return int(total_heat)
1707$$;1707$$;
1708
1709-- This function is not STRICT, since it needs to handle
1710-- dateexpected when it is NULL.
1711CREATE OR REPLACE FUNCTION milestone_sort_key(
1712 dateexpected timestamp, name text)
1713 RETURNS text
1714AS $_$
1715 # If this method is altered, then any functional indexes using it
1716 # need to be rebuilt.
1717 import re
1718 import datetime
1719
1720 date_expected, name = args
1721
1722 def substitute_filled_numbers(match):
1723 return match.group(0).zfill(5)
1724
1725 name = re.sub(u'\d+', substitute_filled_numbers, name)
1726 if date_expected is None:
1727 # NULL dates are considered to be in the future.
1728 date_expected = datetime.datetime(datetime.MAXYEAR, 1, 1)
1729 return '%s %s' % (date_expected, name)
1730$_$
1731LANGUAGE plpythonu IMMUTABLE;
1732
1733COMMENT ON FUNCTION milestone_sort_key(timestamp, text) IS
1734'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.';
17081735
=== modified file 'lib/canonical/launchpad/webapp/sorting.py'
--- lib/canonical/launchpad/webapp/sorting.py 2009-06-25 05:30:52 +0000
+++ lib/canonical/launchpad/webapp/sorting.py 2010-08-25 04:29:43 +0000
@@ -25,9 +25,10 @@
2525
26 """26 """
27 assert(isinstance(unicode_text, unicode))27 assert(isinstance(unicode_text, unicode))
28 def substitude_filled_numbers(match):28
29 def substitute_filled_numbers(match):
29 return match.group(0).zfill(fill_digits)30 return match.group(0).zfill(fill_digits)
30 return re.sub(u'\d+', substitude_filled_numbers, unicode_text)31 return re.sub(u'\d+', substitute_filled_numbers, unicode_text)
3132
3233
33# Create translation table for numeric ordinals to their34# Create translation table for numeric ordinals to their
3435
=== modified file 'lib/lp/registry/browser/__init__.py'
--- lib/lp/registry/browser/__init__.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/browser/__init__.py 2010-08-25 04:29:43 +0000
@@ -80,7 +80,7 @@
80 @property80 @property
81 def milestone_table_class(self):81 def milestone_table_class(self):
82 """The milestone table will be unseen if there are no milestones."""82 """The milestone table will be unseen if there are no milestones."""
83 if len(self.context.all_milestones) > 0:83 if self.context.has_milestones:
84 return 'listing'84 return 'listing'
85 else:85 else:
86 # The page can remove the 'unseen' class to make the table86 # The page can remove the 'unseen' class to make the table
8787
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml 2010-08-13 21:30:24 +0000
+++ lib/lp/registry/browser/configure.zcml 2010-08-25 04:29:43 +0000
@@ -1655,6 +1655,13 @@
1655 </browser:pages>1655 </browser:pages>
1656 <browser:page1656 <browser:page
1657 for="lp.registry.interfaces.productseries.IProductSeries"1657 for="lp.registry.interfaces.productseries.IProductSeries"
1658 class="lp.registry.browser.productseries.ProductSeriesDetailedDisplayView"
1659 template="../templates/productseries-detailed-display.pt"
1660 facet="overview"
1661 permission="zope.Public"
1662 name="+detailed-display"/>
1663 <browser:page
1664 for="lp.registry.interfaces.productseries.IProductSeries"
1658 class="lp.registry.browser.productseries.ProductSeriesRdfView"1665 class="lp.registry.browser.productseries.ProductSeriesRdfView"
1659 facet="overview"1666 facet="overview"
1660 permission="zope.Public"1667 permission="zope.Public"
16611668
=== modified file 'lib/lp/registry/browser/productseries.py'
--- lib/lp/registry/browser/productseries.py 2010-08-23 04:48:17 +0000
+++ lib/lp/registry/browser/productseries.py 2010-08-25 04:29:43 +0000
@@ -10,6 +10,7 @@
10 'ProductSeriesBreadcrumb',10 'ProductSeriesBreadcrumb',
11 'ProductSeriesBugsMenu',11 'ProductSeriesBugsMenu',
12 'ProductSeriesDeleteView',12 'ProductSeriesDeleteView',
13 'ProductSeriesDetailedDisplayView',
13 'ProductSeriesEditView',14 'ProductSeriesEditView',
14 'ProductSeriesFacets',15 'ProductSeriesFacets',
15 'ProductSeriesFileBugRedirect',16 'ProductSeriesFileBugRedirect',
@@ -482,6 +483,19 @@
482 return None483 return None
483484
484485
486class ProductSeriesDetailedDisplayView(ProductSeriesView):
487
488 @cachedproperty
489 def latest_milestones(self):
490 # Convert to list to avoid the query being run multiple times.
491 return list(self.context.milestones[:12])
492
493 @cachedproperty
494 def latest_releases(self):
495 # Convert to list to avoid the query being run multiple times.
496 return list(self.context.releases[:12])
497
498
485class ProductSeriesUbuntuPackagingView(LaunchpadFormView):499class ProductSeriesUbuntuPackagingView(LaunchpadFormView):
486500
487 schema = IPackaging501 schema = IPackaging
488502
=== modified file 'lib/lp/registry/browser/tests/productseries-views.txt'
--- lib/lp/registry/browser/tests/productseries-views.txt 2010-08-23 04:48:17 +0000
+++ lib/lp/registry/browser/tests/productseries-views.txt 2010-08-25 04:29:43 +0000
@@ -307,7 +307,7 @@
307specifications that will be unassigned, and release files that will be307specifications that will be unassigned, and release files that will be
308deleted are available.308deleted are available.
309309
310 >>> view.milestones310 >>> list(view.milestones)
311 []311 []
312 >>> view.bugtasks312 >>> view.bugtasks
313 []313 []
314314
=== modified file 'lib/lp/registry/browser/tests/test_milestone.py'
--- lib/lp/registry/browser/tests/test_milestone.py 2010-08-22 18:31:30 +0000
+++ lib/lp/registry/browser/tests/test_milestone.py 2010-08-25 04:29:43 +0000
@@ -160,7 +160,7 @@
160 }160 }
161 view = create_initialized_view(milestone, '+delete', form=form)161 view = create_initialized_view(milestone, '+delete', form=form)
162 self.assertEqual([], view.errors)162 self.assertEqual([], view.errors)
163 self.assertEqual(0, len(product.all_milestones))163 self.assertEqual([], list(product.all_milestones))
164 self.assertEqual(0, product.development_focus.all_bugtasks.count())164 self.assertEqual(0, product.development_focus.all_bugtasks.count())
165165
166166
167167
=== modified file 'lib/lp/registry/interfaces/milestone.py'
--- lib/lp/registry/interfaces/milestone.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/interfaces/milestone.py 2010-08-25 04:29:43 +0000
@@ -253,6 +253,8 @@
253 """An interface for classes providing milestones."""253 """An interface for classes providing milestones."""
254 export_as_webservice_entry()254 export_as_webservice_entry()
255255
256 has_milestones = Bool(title=_("Whether the object has any milestones."))
257
256 milestones = exported(258 milestones = exported(
257 CollectionField(259 CollectionField(
258 title=_("The visible and active milestones associated with this "260 title=_("The visible and active milestones associated with this "
259261
=== modified file 'lib/lp/registry/model/milestone.py'
--- lib/lp/registry/model/milestone.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/model/milestone.py 2010-08-25 04:29:43 +0000
@@ -82,6 +82,9 @@
82class HasMilestonesMixin:82class HasMilestonesMixin:
83 implements(IHasMilestones)83 implements(IHasMilestones)
8484
85 _milestone_order = (
86 'milestone_sort_key(Milestone.dateexpected, Milestone.name) DESC')
87
85 def _getMilestoneCondition(self):88 def _getMilestoneCondition(self):
86 """Provides condition for milestones and all_milestones properties.89 """Provides condition for milestones and all_milestones properties.
8790
@@ -93,11 +96,15 @@
93 "Unexpected class for mixin: %r" % self)96 "Unexpected class for mixin: %r" % self)
9497
95 @property98 @property
99 def has_milestones(self):
100 return self.all_milestones.any() is not None
101
102 @property
96 def all_milestones(self):103 def all_milestones(self):
97 """See `IHasMilestones`."""104 """See `IHasMilestones`."""
98 store = Store.of(self)105 store = Store.of(self)
99 result = store.find(Milestone, self._getMilestoneCondition())106 result = store.find(Milestone, self._getMilestoneCondition())
100 return sorted(result, key=milestone_sort_key, reverse=True)107 return result.order_by(self._milestone_order)
101108
102 @property109 @property
103 def milestones(self):110 def milestones(self):
@@ -106,7 +113,7 @@
106 result = store.find(Milestone,113 result = store.find(Milestone,
107 And(self._getMilestoneCondition(),114 And(self._getMilestoneCondition(),
108 Milestone.active == True))115 Milestone.active == True))
109 return sorted(result, key=milestone_sort_key, reverse=True)116 return result.order_by(self._milestone_order)
110117
111118
112class Milestone(SQLBase, StructuralSubscriptionTargetMixin, HasBugsBase):119class Milestone(SQLBase, StructuralSubscriptionTargetMixin, HasBugsBase):
113120
=== modified file 'lib/lp/registry/model/projectgroup.py'
--- lib/lp/registry/model/projectgroup.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/model/projectgroup.py 2010-08-25 04:29:43 +0000
@@ -94,7 +94,6 @@
94from lp.registry.model.mentoringoffer import MentoringOffer94from lp.registry.model.mentoringoffer import MentoringOffer
95from lp.registry.model.milestone import (95from lp.registry.model.milestone import (
96 Milestone,96 Milestone,
97 milestone_sort_key,
98 ProjectMilestone,97 ProjectMilestone,
99 )98 )
100from lp.registry.model.pillar import HasAliasMixin99from lp.registry.model.pillar import HasAliasMixin
@@ -164,8 +163,6 @@
164 bug_reported_acknowledgement = StringCol(default=None)163 bug_reported_acknowledgement = StringCol(default=None)
165 max_bug_heat = Int()164 max_bug_heat = Int()
166165
167 # convenient joins
168
169 @property166 @property
170 def products(self):167 def products(self):
171 return Product.selectBy(project=self, active=True, orderBy='name')168 return Product.selectBy(project=self, active=True, orderBy='name')
@@ -419,10 +416,25 @@
419 result.group_by(Milestone.name)416 result.group_by(Milestone.name)
420 if only_active:417 if only_active:
421 result.having('BOOL_OR(Milestone.active) = TRUE')418 result.having('BOOL_OR(Milestone.active) = TRUE')
422 milestones = shortlist(419 # MIN(Milestone.dateexpected) has to be used to match the
420 # aggregate function in the `columns` variable.
421 result.order_by(
422 'milestone_sort_key(MIN(Milestone.dateexpected), Milestone.name) '
423 'DESC')
424 return shortlist(
423 [ProjectMilestone(self, name, dateexpected, active)425 [ProjectMilestone(self, name, dateexpected, active)
424 for name, dateexpected, active in result])426 for name, dateexpected, active in result])
425 return sorted(milestones, key=milestone_sort_key, reverse=True)427
428 @property
429 def has_milestones(self):
430 """See `IHasMilestones`."""
431 store = Store.of(self)
432 result = store.find(
433 Milestone.id,
434 And(Milestone.product == Product.id,
435 Product.project == self,
436 Product.active == True))
437 return result.any() is not None
426438
427 @property439 @property
428 def milestones(self):440 def milestones(self):
429441
=== modified file 'lib/lp/registry/stories/productseries/xx-productseries-series.txt'
--- lib/lp/registry/stories/productseries/xx-productseries-series.txt 2010-05-24 20:23:19 +0000
+++ lib/lp/registry/stories/productseries/xx-productseries-series.txt 2010-08-25 04:29:43 +0000
@@ -29,7 +29,7 @@
29 >>> series_trunk = find_tag_by_id(content, 'series-trunk')29 >>> series_trunk = find_tag_by_id(content, 'series-trunk')
30 >>> print extract_text(series_trunk)30 >>> print extract_text(series_trunk)
31 trunk series Focus of Development31 trunk series Focus of Development
32 Milestones: 1.0 Releases: 0.9.2, 0.9.1, 0.932 Latest milestones: 1.0 Latest releases: 0.9.2, 0.9.1, 0.9
33 Bugs targeted: None33 Bugs targeted: None
34 Blueprints targeted: 1 Unknown34 Blueprints targeted: 1 Unknown
35 The "trunk" series represents the primary line of development rather ...35 The "trunk" series represents the primary line of development rather ...
@@ -46,7 +46,7 @@
46 >>> series_1_0 = find_tag_by_id(content, 'series-1-0')46 >>> series_1_0 = find_tag_by_id(content, 'series-1-0')
47 >>> print extract_text(series_1_0)47 >>> print extract_text(series_1_0)
48 1.0 series Active Development48 1.0 series Active Development
49 Releases: 1.0.049 Latest releases: 1.0.0
50 Bugs targeted: 1 New50 Bugs targeted: 1 New
51 Blueprints targeted: None51 Blueprints targeted: None
52 The 1.0 branch of the Mozilla web browser. Currently, this is the ...52 The 1.0 branch of the Mozilla web browser. Currently, this is the ...
5353
=== modified file 'lib/lp/registry/templates/object-milestones.pt'
--- lib/lp/registry/templates/object-milestones.pt 2009-11-07 00:49:26 +0000
+++ lib/lp/registry/templates/object-milestones.pt 2010-08-25 04:29:43 +0000
@@ -28,7 +28,7 @@
28 <tal:milestones define="milestones context/all_milestones">28 <tal:milestones define="milestones context/all_milestones">
29 <table id="milestones" class="listing"29 <table id="milestones" class="listing"
30 tal:define="has_series context/series|nothing"30 tal:define="has_series context/series|nothing"
31 tal:condition="milestones">31 tal:condition="context/has_milestones">
32 <thead>32 <thead>
33 <tr>33 <tr>
34 <th>Version</th>34 <th>Version</th>
@@ -45,7 +45,7 @@
45 </tbody>45 </tbody>
46 </table>46 </table>
4747
48 <tal:no-milestones condition="not: milestones">48 <tal:no-milestones condition="not: context/has_milestones">
49 <p>There are no milestones associated with49 <p>There are no milestones associated with
50 <span tal:replace="context/title" />50 <span tal:replace="context/title" />
51 </p>51 </p>
5252
=== added file 'lib/lp/registry/templates/productseries-detailed-display.pt'
--- lib/lp/registry/templates/productseries-detailed-display.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/templates/productseries-detailed-display.pt 2010-08-25 04:29:43 +0000
@@ -0,0 +1,38 @@
1<div
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 >
7 <strong><a tal:attributes="href context/fmt:url"
8 tal:content="context/name" >
9 1.0
10 </a> series
11 </strong>
12 <em tal:condition="context/is_development_focus">Focus of Development</em>
13 <em tal:condition="not: context/is_development_focus"
14 tal:content="context/status/title">
15 This series' status
16 </em>
17 <div>
18 <tal:milestones condition="view/latest_milestones">
19 Latest milestones:
20 <tal:milestone repeat="milestone view/latest_milestones">
21 <a tal:attributes="href milestone/fmt:url" tal:content="milestone/name"
22 >name</a><tal:comma condition="not:repeat/milestone/end">,</tal:comma>
23 </tal:milestone>
24 </tal:milestones>
25 <tal:release repeat="release view/latest_releases">
26 <tal:release-start condition="repeat/release/start">
27 <tal:comment condition="nothing">
28 Releases may be a list or an resultset. We cannot easily know if there
29 releases until the first one is returned.
30 </tal:comment>
31 <tal:space condition="view/latest_milestones"> &nbsp &nbsp </tal:space>
32 Latest releases:
33 </tal:release-start>
34 <a tal:attributes="href release/fmt:url" tal:content="release/version"
35 >version</a><tal:comma condition="not:repeat/release/end">,</tal:comma>
36 </tal:release>
37 </div>
38</div>
039
=== modified file 'lib/lp/registry/templates/productseries-macros.pt'
--- lib/lp/registry/templates/productseries-macros.pt 2010-05-24 20:23:19 +0000
+++ lib/lp/registry/templates/productseries-macros.pt 2010-08-25 04:29:43 +0000
@@ -4,48 +4,6 @@
4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
5 omit-tag="">5 omit-tag="">
66
7<metal:detailed_display define-macro="detailed_display">
8 <tal:comment replace="nothing">
9 This macro expects two variables to be defined:
10 - 'series': The ProductSeries
11 - 'is_focus': A boolean saying whether this is the series in which
12 development is focused.
13 </tal:comment>
14
15 <strong><a tal:attributes="href series/fmt:url"
16 tal:content="series/name" >
17 1.0
18 </a> series
19 </strong>
20 <em tal:condition="is_focus">Focus of Development</em>
21 <em tal:condition="not: is_focus" tal:content="series/status/title">
22 This series' status
23 </em>
24 <div>
25 <tal:milestones condition="series/milestones">
26 Milestones:
27 <tal:milestone repeat="milestone series/milestones">
28 <a tal:attributes="href milestone/fmt:url" tal:content="milestone/name"
29 >name</a><tal:comma condition="not:repeat/milestone/end">,</tal:comma>
30 </tal:milestone>
31 </tal:milestones>
32 <tal:release repeat="release series/releases">
33 <tal:release-start condition="repeat/release/start">
34 <tal:comment condition="nothing">
35 Releases may be a list or an resultset. We cannot easily know if there
36 releases until the first one is returned.
37 </tal:comment>
38 <tal:space condition="series/milestones"> &nbsp &nbsp </tal:space>
39 Releases:
40 </tal:release-start>
41 <a tal:attributes="href release/fmt:url" tal:content="release/version"
42 >version</a><tal:comma condition="not:repeat/release/end">,</tal:comma>
43 </tal:release>
44 </div>
45 <metal:extra define-slot="extra" />
46</metal:detailed_display>
47
48
49<metal:branch_display define-macro="branch_display">7<metal:branch_display define-macro="branch_display">
50 <tal:comment condition="nothing">8 <tal:comment condition="nothing">
51 This macro expects two variables to be defined:9 This macro expects two variables to be defined:
5210
=== modified file 'lib/lp/registry/templates/productseries-status.pt'
--- lib/lp/registry/templates/productseries-status.pt 2009-07-21 18:17:49 +0000
+++ lib/lp/registry/templates/productseries-status.pt 2010-08-25 04:29:43 +0000
@@ -8,8 +8,7 @@
8 is_focus context/is_development_focus;8 is_focus context/is_development_focus;
9 spec_count_status view/specification_status_counts;"9 spec_count_status view/specification_status_counts;"
10 >10 >
11 <metal:series use-macro="series/@@+macros/detailed_display">11 <div tal:replace="structure context/@@+detailed-display"/>
12 <div metal:fill-slot="extra">
13 <div>12 <div>
14 <tal:not-obsolete13 <tal:not-obsolete
15 condition="not: view/is_obsolete"14 condition="not: view/is_obsolete"
@@ -42,6 +41,4 @@
42 <tal:summary41 <tal:summary
43 condition="series/summary"42 condition="series/summary"
44 content="structure context/summary/fmt:text-to-html" />43 content="structure context/summary/fmt:text-to-html" />
45 </div>
46 </metal:series>
47</div>44</div>

Subscribers

People subscribed via source and target branches

to status/vote changes: