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__.py 2010-06-12 03:37:58 +0000 +++ lib/lp/registry/browser/__init__.py 2010-08-19 23:22:43 +0000 @@ -76,7 +76,7 @@ @property def milestone_table_class(self): """The milestone table will be unseen if there are no milestones.""" - if len(self.context.all_milestones) > 0: + if self.context.has_milestones: return 'listing' else: # The page can remove the 'unseen' class to make the table === modified file 'lib/lp/registry/browser/configure.zcml' --- lib/lp/registry/browser/configure.zcml 2010-07-16 16:58:55 +0000 +++ lib/lp/registry/browser/configure.zcml 2010-08-19 17:00:32 +0000 @@ -1655,6 +1655,13 @@ + >> view.milestones + >>> list(view.milestones) [] >>> view.bugtasks [] === modified file 'lib/lp/registry/interfaces/milestone.py' --- lib/lp/registry/interfaces/milestone.py 2010-04-24 11:19:53 +0000 +++ lib/lp/registry/interfaces/milestone.py 2010-08-20 02:51:36 +0000 @@ -225,6 +225,8 @@ """An interface for classes providing milestones.""" export_as_webservice_entry() + has_milestones = Bool(title=_("Whether the object has any milestones.")) + milestones = exported( CollectionField( title=_("The visible and active milestones associated with this " === modified file 'lib/lp/registry/model/milestone.py' --- lib/lp/registry/model/milestone.py 2010-08-02 02:13:52 +0000 +++ lib/lp/registry/model/milestone.py 2010-08-20 04:00:43 +0000 @@ -21,8 +21,13 @@ from sqlobject import ( AND, BoolCol, DateCol, ForeignKey, SQLMultipleJoin, SQLObjectNotFound, StringCol) -from storm.locals import And, Store - + + +from storm.expr import And +from storm.store import Store + + +from canonical.cachedproperty import cachedproperty from canonical.database.sqlbase import SQLBase, sqlvalues from canonical.launchpad.webapp.sorting import expand_numbers from lp.app.errors import NotFoundError @@ -63,6 +68,9 @@ class HasMilestonesMixin: implements(IHasMilestones) + _milestone_order = ( + 'milestone_sort_key(Milestone.dateexpected, Milestone.name) DESC') + def _getMilestoneCondition(self): """Provides condition for milestones and all_milestones properties. @@ -74,11 +82,15 @@ "Unexpected class for mixin: %r" % self) @property + def has_milestones(self): + return self.all_milestones.any() is not None + + @property def all_milestones(self): """See `IHasMilestones`.""" store = Store.of(self) result = store.find(Milestone, self._getMilestoneCondition()) - return sorted(result, key=milestone_sort_key, reverse=True) + return result.order_by(self._milestone_order) @property def milestones(self): @@ -87,7 +99,7 @@ result = store.find(Milestone, And(self._getMilestoneCondition(), Milestone.active == True)) - return sorted(result, key=milestone_sort_key, reverse=True) + return result.order_by(self._milestone_order) class Milestone(SQLBase, StructuralSubscriptionTargetMixin, HasBugsBase): === modified file 'lib/lp/registry/model/projectgroup.py' --- lib/lp/registry/model/projectgroup.py 2010-08-02 02:13:52 +0000 +++ lib/lp/registry/model/projectgroup.py 2010-08-20 03:46:39 +0000 @@ -54,7 +54,7 @@ from lp.services.worlddata.model.language import Language from lp.registry.model.mentoringoffer import MentoringOffer from lp.registry.model.milestone import ( - Milestone, ProjectMilestone, milestone_sort_key) + Milestone, ProjectMilestone) from lp.registry.model.announcement import MakesAnnouncements from lp.registry.model.pillar import HasAliasMixin from lp.registry.model.product import Product @@ -381,10 +381,25 @@ result.group_by(Milestone.name) if only_active: result.having('BOOL_OR(Milestone.active) = TRUE') - milestones = shortlist( + # MIN(Milestone.dateexpected) has to be used to match the + # aggregate function in the `columns` variable. + result.order_by( + 'milestone_sort_key(MIN(Milestone.dateexpected), Milestone.name) ' + 'DESC') + return shortlist( [ProjectMilestone(self, name, dateexpected, active) for name, dateexpected, active in result]) - return sorted(milestones, key=milestone_sort_key, reverse=True) + + @property + def has_milestones(self): + """See `IHasMilestones`.""" + store = Store.of(self) + result = store.find( + Milestone.id, + And(Milestone.product == Product.id, + Product.project == self, + Product.active == True)) + return result.any() is not None @property def milestones(self): === 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-19 17:00:32 +0000 @@ -29,7 +29,7 @@ >>> series_trunk = find_tag_by_id(content, 'series-trunk') >>> print extract_text(series_trunk) trunk series Focus of Development - Milestones: 1.0 Releases: 0.9.2, 0.9.1, 0.9 + Latest milestones: 1.0 Latest releases: 0.9.2, 0.9.1, 0.9 Bugs targeted: None Blueprints targeted: 1 Unknown The "trunk" series represents the primary line of development rather ... @@ -46,7 +46,7 @@ >>> series_1_0 = find_tag_by_id(content, 'series-1-0') >>> print extract_text(series_1_0) 1.0 series Active Development - Releases: 1.0.0 + Latest releases: 1.0.0 Bugs targeted: 1 New Blueprints targeted: None The 1.0 branch of the Mozilla web browser. Currently, this is the ... === 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-20 04:02:09 +0000 @@ -28,7 +28,7 @@ + tal:condition="context/has_milestones"> @@ -45,7 +45,7 @@
Version
- +

There are no milestones associated with

=== 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-19 17:00:32 +0000 @@ -4,48 +4,6 @@ xmlns:i18n="http://xml.zope.org/namespaces/i18n" omit-tag=""> - - - This macro expects two variables to be defined: - - 'series': The ProductSeries - - 'is_focus': A boolean saying whether this is the series in which - development is focused. - - - - 1.0 - series - - Focus of Development - - This series' status - -
- - Milestones: - - name, - - - - - - Releases may be a list or an resultset. We cannot easily know if there - releases until the first one is returned. - -     - Releases: - - version, - -
- -
- - This macro expects two variables to be defined: === 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-19 17:00:32 +0000 @@ -8,8 +8,7 @@ is_focus context/is_development_focus; spec_count_status view/specification_status_counts;" > - -
+
-
-