Merge lp:~danilo/launchpad/bug-1021196 into lp:launchpad

Proposed by Данило Шеган
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: 15597
Proposed branch: lp:~danilo/launchpad/bug-1021196
Merge into: lp:launchpad
Diff against target: 255 lines (+151/-25)
4 files modified
lib/lp/registry/browser/tests/test_milestone.py (+19/-2)
lib/lp/registry/model/milestone.py (+63/-23)
lib/lp/registry/templates/milestone-index.pt (+10/-0)
lib/lp/registry/tests/test_milestone.py (+59/-0)
To merge this branch: bzr merge lp:~danilo/launchpad/bug-1021196
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+114157@code.launchpad.net

Commit message

List blueprints with work items for a milestone in the milestone page (take 2 with an extra fix).

Description of the change

= Bug 1021196: take 2 =

Original MP up at https://code.launchpad.net/~danilo/launchpad/milestone-all-bps/+merge/113531

This fixes the OOPS as noticed by Steven in QA (https://oops.canonical.com/oops/?oopsid=OOPS-7ec83b58aab8ad14b1b194bf93116da6).

Only revisions r15585 and r15586 are relevant (r15587 is lint fixes) — diff containing both on https://pastebin.canonical.com/69731/.

== Tests ==

bin/test -cvvt TestMilestoneViews.test_distroseries_milestone

(or "bin/test -cvvt milestone" for all milestone related tests)

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/tests/test_milestone.py'
2--- lib/lp/registry/browser/tests/test_milestone.py 2012-06-11 00:47:38 +0000
3+++ lib/lp/registry/browser/tests/test_milestone.py 2012-07-10 10:43:25 +0000
4@@ -18,8 +18,6 @@
5 from lp.services.config import config
6 from lp.services.webapp import canonical_url
7 from lp.testing import (
8- ANONYMOUS,
9- login,
10 login_person,
11 login_team,
12 logout,
13@@ -40,6 +38,25 @@
14
15 layer = DatabaseFunctionalLayer
16
17+ def test_distroseries_milestone(self):
18+ # Distribution milestone with an untargeted blueprint containing
19+ # work items targeted to the milestone lists this blueprint
20+ # with a special note.
21+ distro_series = self.factory.makeDistroSeries()
22+ distribution = distro_series.distribution
23+ milestone = self.factory.makeMilestone(distroseries=distro_series)
24+ specification = self.factory.makeSpecification(
25+ distribution=distribution)
26+ self.factory.makeSpecificationWorkItem(
27+ specification=specification, milestone=milestone)
28+ view = create_initialized_view(milestone, '+index')
29+ self.assertIn('some work for this milestone', view.render())
30+
31+
32+class TestAddMilestoneViews(TestCaseWithFactory):
33+
34+ layer = DatabaseFunctionalLayer
35+
36 def setUp(self):
37 TestCaseWithFactory.setUp(self)
38 self.product = self.factory.makeProduct()
39
40=== modified file 'lib/lp/registry/model/milestone.py'
41--- lib/lp/registry/model/milestone.py 2012-07-09 03:15:32 +0000
42+++ lib/lp/registry/model/milestone.py 2012-07-10 10:43:25 +0000
43@@ -23,12 +23,15 @@
44 BoolCol,
45 DateCol,
46 ForeignKey,
47- SQLMultipleJoin,
48 StringCol,
49 )
50-from storm.locals import (
51+from storm.locals import Store
52+from storm.expr import (
53 And,
54- Store,
55+ Desc,
56+ Join,
57+ LeftJoin,
58+ Or,
59 )
60 from storm.zope import IResultSet
61 from zope.component import getUtility
62@@ -36,6 +39,7 @@
63
64 from lp.app.errors import NotFoundError
65 from lp.blueprints.model.specification import Specification
66+from lp.blueprints.model.specificationworkitem import SpecificationWorkItem
67 from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
68 from lp.bugs.interfaces.bugtarget import IHasBugs
69 from lp.bugs.interfaces.bugtask import (
70@@ -55,11 +59,9 @@
71 IProjectGroupMilestone,
72 )
73 from lp.registry.model.productrelease import ProductRelease
74+from lp.services.database.decoratedresultset import DecoratedResultSet
75 from lp.services.database.lpstorm import IStore
76-from lp.services.database.sqlbase import (
77- SQLBase,
78- sqlvalues,
79- )
80+from lp.services.database.sqlbase import SQLBase
81 from lp.services.webapp.sorting import expand_numbers
82
83
84@@ -186,10 +188,31 @@
85 summary = StringCol(notNull=False, default=None)
86 code_name = StringCol(dbName='codename', notNull=False, default=None)
87
88- specifications = SQLMultipleJoin('Specification', joinColumn='milestone',
89- orderBy=['-priority', 'definition_status',
90- 'implementation_status', 'title'],
91- prejoins=['assignee'])
92+ @property
93+ def specifications(self):
94+ from lp.registry.model.person import Person
95+ store = Store.of(self)
96+ origin = [
97+ Specification,
98+ LeftJoin(
99+ SpecificationWorkItem,
100+ SpecificationWorkItem.specification_id == Specification.id),
101+ LeftJoin(Person, Specification.assigneeID == Person.id),
102+ ]
103+
104+ results = store.using(*origin).find(
105+ (Specification, Person),
106+ Or(Specification.milestoneID == self.id,
107+ SpecificationWorkItem.milestone_id == self.id),
108+ Or(SpecificationWorkItem.deleted == None,
109+ SpecificationWorkItem.deleted == False))
110+ results.config(distinct=True)
111+ ordered_results = results.order_by(Desc(Specification.priority),
112+ Specification.definition_status,
113+ Specification.implementation_status,
114+ Specification.title)
115+ mapper = lambda row: row[0]
116+ return DecoratedResultSet(ordered_results, mapper)
117
118 @property
119 def target(self):
120@@ -397,18 +420,35 @@
121
122 @property
123 def specifications(self):
124- """See `IMilestone`."""
125- return Specification.select(
126- """milestone IN
127- (SELECT milestone.id
128- FROM Milestone, Product
129- WHERE Milestone.Product = Product.id
130- AND Milestone.name = %s
131- AND Product.project = %s)
132- """ % sqlvalues(self.name, self.target),
133- orderBy=['-priority', 'definition_status',
134- 'implementation_status', 'title'],
135- prejoins=['assignee'])
136+ """See `IMilestoneData`."""
137+ from lp.registry.model.person import Person
138+ from lp.registry.model.product import Product
139+ store = Store.of(self.target)
140+ origin = [
141+ Specification,
142+ LeftJoin(
143+ SpecificationWorkItem,
144+ SpecificationWorkItem.specification_id == Specification.id),
145+ Join(Milestone,
146+ Or(Milestone.id == Specification.milestoneID,
147+ Milestone.id == SpecificationWorkItem.milestone_id)),
148+ Join(Product, Product.id == Milestone.productID),
149+ LeftJoin(Person, Specification.assigneeID == Person.id),
150+ ]
151+
152+ results = store.using(*origin).find(
153+ (Specification, Person),
154+ Product.projectID == self.target.id,
155+ Milestone.name == self.name,
156+ Or(SpecificationWorkItem.deleted == None,
157+ SpecificationWorkItem.deleted == False))
158+ results.config(distinct=True)
159+ ordered_results = results.order_by(Desc(Specification.priority),
160+ Specification.definition_status,
161+ Specification.implementation_status,
162+ Specification.title)
163+ mapper = lambda row: row[0]
164+ return DecoratedResultSet(ordered_results, mapper)
165
166 @property
167 def displayname(self):
168
169=== modified file 'lib/lp/registry/templates/milestone-index.pt'
170--- lib/lp/registry/templates/milestone-index.pt 2012-07-09 03:15:32 +0000
171+++ lib/lp/registry/templates/milestone-index.pt 2012-07-10 10:43:25 +0000
172@@ -252,6 +252,16 @@
173 title spec/summary/fmt:shorten/400">Foo Bar Baz</a>
174 <img src="/@@/info" alt="Informational"
175 tal:condition="spec/informational" />
176+ <tal:comment condition="nothing">
177+ Compare milestone names to see if a blueprint is only
178+ partially targeted to this milestone.
179+
180+ If a blueprint is untargeted, then it's partial as well.
181+ </tal:comment>
182+ <span tal:condition="
183+ python:(not spec.milestone or
184+ spec.milestone.name != context.name)">
185+ (some work for this milestone)</span>
186 </td>
187 <td tal:condition="view/is_project_milestone">
188 <span class="sortkey" tal:content="spec/product/displayname" />
189
190=== modified file 'lib/lp/registry/tests/test_milestone.py'
191--- lib/lp/registry/tests/test_milestone.py 2012-07-09 03:15:32 +0000
192+++ lib/lp/registry/tests/test_milestone.py 2012-07-10 10:43:25 +0000
193@@ -175,3 +175,62 @@
194 product=self.product,
195 )
196 self.assertContentEqual(specifications, self.milestone.specifications)
197+
198+
199+class MilestonesContainsPartialSpecifications(TestCaseWithFactory):
200+ """Milestones list specifications with some workitems targeted to it."""
201+
202+ layer = DatabaseFunctionalLayer
203+
204+ def _create_milestones_on_target(self, **kwargs):
205+ """Create a milestone on a target with work targeted to it.
206+
207+ Target should be specified using either product or distribution
208+ argument which is directly passed into makeMilestone call.
209+ """
210+ other_milestone = self.factory.makeMilestone(**kwargs)
211+ target_milestone = self.factory.makeMilestone(**kwargs)
212+ specification = self.factory.makeSpecification(
213+ milestone=other_milestone, **kwargs)
214+ # Create two workitems to ensure this doesn't cause
215+ # two specifications to be returned.
216+ self.factory.makeSpecificationWorkItem(
217+ specification=specification, milestone=target_milestone)
218+ self.factory.makeSpecificationWorkItem(
219+ specification=specification, milestone=target_milestone)
220+ return specification, target_milestone
221+
222+ def test_milestones_on_product(self):
223+ specification, target_milestone = self._create_milestones_on_target(
224+ product=self.factory.makeProduct())
225+ self.assertEqual([specification],
226+ list(target_milestone.specifications))
227+
228+ def test_milestones_on_distribution(self):
229+ specification, target_milestone = self._create_milestones_on_target(
230+ distribution=self.factory.makeDistribution())
231+ self.assertEqual([specification],
232+ list(target_milestone.specifications))
233+
234+ def test_milestones_on_project(self):
235+ # A Project (Project Group) milestone contains all specifications
236+ # targetted to contained Products (Projects) for milestones of
237+ # a certain name.
238+ projectgroup = self.factory.makeProject()
239+ product = self.factory.makeProduct(project=projectgroup)
240+ specification, target_milestone = self._create_milestones_on_target(
241+ product=product)
242+ milestone = projectgroup.getMilestone(name=target_milestone.name)
243+ self.assertEqual([specification],
244+ list(milestone.specifications))
245+
246+ def test_milestones_with_deleted_workitems(self):
247+ # Deleted work items do not cause the specification to show up
248+ # in the milestone page.
249+ milestone = self.factory.makeMilestone(
250+ product=self.factory.makeProduct())
251+ specification = self.factory.makeSpecification(
252+ milestone=milestone, product=milestone.product)
253+ self.factory.makeSpecificationWorkItem(
254+ specification=specification, milestone=milestone, deleted=True)
255+ self.assertEqual([], list(milestone.specifications))