Merge lp:~lifeless/launchpad/bugtask+index into lp:launchpad

Proposed by Robert Collins
Status: Merged
Approved by: Jonathan Lange
Approved revision: no longer in the source branch.
Merged at revision: 11611
Proposed branch: lp:~lifeless/launchpad/bugtask+index
Merge into: lp:launchpad
Diff against target: 1096 lines (+283/-141)
23 files modified
lib/lp/bugs/browser/bug.py (+32/-26)
lib/lp/bugs/browser/bugtask.py (+14/-11)
lib/lp/bugs/browser/tests/test_bugtask.py (+39/-32)
lib/lp/bugs/configure.zcml (+3/-0)
lib/lp/bugs/interfaces/bug.py (+3/-0)
lib/lp/bugs/interfaces/bugtarget.py (+3/-0)
lib/lp/bugs/interfaces/bugtask.py (+1/-0)
lib/lp/bugs/model/bug.py (+106/-28)
lib/lp/bugs/model/bugtarget.py (+16/-15)
lib/lp/bugs/model/bugtask.py (+13/-5)
lib/lp/bugs/templates/bug-portlet-actions.pt (+2/-2)
lib/lp/bugs/templates/bugtask-index.pt (+1/-1)
lib/lp/registry/configure.zcml (+1/-0)
lib/lp/registry/interfaces/product.py (+1/-0)
lib/lp/registry/interfaces/projectgroup.py (+2/-0)
lib/lp/registry/model/distributionsourcepackage.py (+3/-0)
lib/lp/registry/model/distroseries.py (+3/-0)
lib/lp/registry/model/milestone.py (+3/-2)
lib/lp/registry/model/person.py (+2/-0)
lib/lp/registry/model/productseries.py (+3/-0)
lib/lp/registry/model/projectgroup.py (+20/-1)
lib/lp/registry/model/sourcepackage.py (+3/-0)
lib/lp/registry/vocabularies.py (+9/-18)
To merge this branch: bzr merge lp:~lifeless/launchpad/bugtask+index
Reviewer Review Type Date Requested Status
Jonathan Lange (community) Approve
Review via email: mp+36117@code.launchpad.net

Commit message

Less queries in BugTask:+index.

Description of the change

Take a basic bugtask:+index down from 110 queries to 64. Thats 46 gone! Woo!

Probably broken, but lets throw it at ec2land and see what happens.

To post a comment you must log in.
Revision history for this message
Jonathan Lange (jml) wrote :

Hey Rob,

Looks good to me. Very pleased with the improvements to performance & API clarity.

Only a few minor code clarity & style issues. Please fix them & land.

jml

In def _bug_attachments(self):
  * Spaces after colons
  * Proper sentence case for the docstring.

The comment:
+ # Unwrap the security proxy.
confuses me. I don't see how it's unwrapping the security proxy, or why you would need to. Could you please either delete the comment or expand it?

The "sort and group" comments don't help. Please delete them.

"# Note that this query and the tp query above can be consolidated" in model/person.py has a spelling mistake. ITYM "top query".

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/browser/bug.py'
--- lib/lp/bugs/browser/bug.py 2010-08-31 11:11:09 +0000
+++ lib/lp/bugs/browser/bug.py 2010-09-22 21:01:52 +0000
@@ -299,7 +299,7 @@
299299
300 def unlinkcve(self):300 def unlinkcve(self):
301 """Return 'Remove CVE link' Link."""301 """Return 'Remove CVE link' Link."""
302 enabled = bool(self.context.bug.cves)302 enabled = self.context.bug.has_cves
303 text = 'Remove CVE link'303 text = 'Remove CVE link'
304 return Link('+unlinkcve', text, icon='remove', enabled=enabled)304 return Link('+unlinkcve', text, icon='remove', enabled=enabled)
305305
@@ -307,31 +307,30 @@
307 """Return the 'Offer mentorship' Link."""307 """Return the 'Offer mentorship' Link."""
308 text = 'Offer mentorship'308 text = 'Offer mentorship'
309 user = getUtility(ILaunchBag).user309 user = getUtility(ILaunchBag).user
310 enabled = self.context.bug.canMentor(user)310 enabled = False
311 return Link('+mentor', text, icon='add', enabled=enabled)311 return Link('+mentor', text, icon='add', enabled=enabled)
312312
313 def retractmentoring(self):313 def retractmentoring(self):
314 """Return the 'Retract mentorship' Link."""314 """Return the 'Retract mentorship' Link."""
315 text = 'Retract mentorship'315 text = 'Retract mentorship'
316 user = getUtility(ILaunchBag).user316 user = getUtility(ILaunchBag).user
317 # We should really only allow people to retract mentoring if the317 enabled = False
318 # bug's open and the user's already a mentor.
319 if user and not self.context.bug.is_complete:
320 enabled = self.context.bug.isMentor(user)
321 else:
322 enabled = False
323 return Link('+retractmentoring', text, icon='remove', enabled=enabled)318 return Link('+retractmentoring', text, icon='remove', enabled=enabled)
324319
320 @property
321 def _bug_question(self):
322 return self.context.bug.getQuestionCreatedFromBug()
323
325 def createquestion(self):324 def createquestion(self):
326 """Create a question from this bug."""325 """Create a question from this bug."""
327 text = 'Convert to a question'326 text = 'Convert to a question'
328 enabled = self.context.bug.getQuestionCreatedFromBug() is None327 enabled = self._bug_question is None
329 return Link('+create-question', text, enabled=enabled, icon='add')328 return Link('+create-question', text, enabled=enabled, icon='add')
330329
331 def removequestion(self):330 def removequestion(self):
332 """Remove the created question from this bug."""331 """Remove the created question from this bug."""
333 text = 'Convert back to a bug'332 text = 'Convert back to a bug'
334 enabled = self.context.bug.getQuestionCreatedFromBug() is not None333 enabled = self._bug_question is not None
335 return Link('+remove-question', text, enabled=enabled, icon='remove')334 return Link('+remove-question', text, enabled=enabled, icon='remove')
336335
337 def activitylog(self):336 def activitylog(self):
@@ -510,29 +509,36 @@
510 else:509 else:
511 return 'subscribed-false %s' % dup_class510 return 'subscribed-false %s' % dup_class
512511
512 @cachedproperty
513 def _bug_attachments(self):
514 """Get a dict of attachment type -> attachments list."""
515 # Note that this is duplicated with get_comments_for_bugtask
516 # if you are looking to consolidate things.
517 result = {BugAttachmentType.PATCH: [],
518 'other': []
519 }
520 for attachment in self.context.attachments_unpopulated:
521 info = {
522 'attachment': attachment,
523 'file': ProxiedLibraryFileAlias(
524 attachment.libraryfile, attachment),
525 }
526 if attachment.type == BugAttachmentType.PATCH:
527 key = attachment.type
528 else:
529 key = 'other'
530 result[key].append(info)
531 return result
532
513 @property533 @property
514 def regular_attachments(self):534 def regular_attachments(self):
515 """The list of bug attachments that are not patches."""535 """The list of bug attachments that are not patches."""
516 return [536 return self._bug_attachments['other']
517 {
518 'attachment': attachment,
519 'file': ProxiedLibraryFileAlias(
520 attachment.libraryfile, attachment),
521 }
522 for attachment in self.context.attachments_unpopulated
523 if attachment.type != BugAttachmentType.PATCH]
524537
525 @property538 @property
526 def patches(self):539 def patches(self):
527 """The list of bug attachments that are patches."""540 """The list of bug attachments that are patches."""
528 return [541 return self._bug_attachments[BugAttachmentType.PATCH]
529 {
530 'attachment': attachment,
531 'file': ProxiedLibraryFileAlias(
532 attachment.libraryfile, attachment),
533 }
534 for attachment in self.context.attachments_unpopulated
535 if attachment.type == BugAttachmentType.PATCH]
536542
537543
538class BugView(LaunchpadView, BugViewMixin):544class BugView(LaunchpadView, BugViewMixin):
539545
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2010-09-09 17:02:33 +0000
+++ lib/lp/bugs/browser/bugtask.py 2010-09-22 21:01:52 +0000
@@ -921,6 +921,9 @@
921 This is particularly useful for views that may render a921 This is particularly useful for views that may render a
922 NullBugTask.922 NullBugTask.
923 """923 """
924 if self.context.id is not None:
925 # Fast path for real bugtasks: they have a DB id.
926 return True
924 params = BugTaskSearchParams(user=self.user, bug=self.context.bug)927 params = BugTaskSearchParams(user=self.user, bug=self.context.bug)
925 matching_bugtasks = self.context.target.searchTasks(params)928 matching_bugtasks = self.context.target.searchTasks(params)
926 if self.context.productseries is not None:929 if self.context.productseries is not None:
@@ -1194,14 +1197,14 @@
1194 @property1197 @property
1195 def official_tags(self):1198 def official_tags(self):
1196 """The list of official tags for this bug."""1199 """The list of official tags for this bug."""
1197 target_official_tags = self.context.target.official_bug_tags1200 target_official_tags = set(self.context.bug.official_tags)
1198 return [tag for tag in self.context.bug.tags1201 return [tag for tag in self.context.bug.tags
1199 if tag in target_official_tags]1202 if tag in target_official_tags]
12001203
1201 @property1204 @property
1202 def unofficial_tags(self):1205 def unofficial_tags(self):
1203 """The list of unofficial tags for this bug."""1206 """The list of unofficial tags for this bug."""
1204 target_official_tags = self.context.target.official_bug_tags1207 target_official_tags = set(self.context.bug.official_tags)
1205 return [tag for tag in self.context.bug.tags1208 return [tag for tag in self.context.bug.tags
1206 if tag not in target_official_tags]1209 if tag not in target_official_tags]
12071210
@@ -1213,11 +1216,10 @@
1213 bug has a task. It is returned as Javascript snippet, to be embedded1216 bug has a task. It is returned as Javascript snippet, to be embedded
1214 in the bug page.1217 in the bug page.
1215 """1218 """
1216 available_tags = set()1219 # Unwrap the security proxy. - official_tags is a security proxy
1217 for task in self.context.bug.bugtasks:1220 # wrapped list.
1218 available_tags.update(task.target.official_bug_tags)1221 available_tags = list(self.context.bug.official_tags)
1219 return 'var available_official_tags = %s;' % dumps(list(sorted(1222 return 'var available_official_tags = %s;' % dumps(available_tags)
1220 available_tags)))
12211223
1222 @property1224 @property
1223 def user_is_admin(self):1225 def user_is_admin(self):
@@ -3247,6 +3249,7 @@
3247 """Cache the list of bugtasks and set up the release mapping."""3249 """Cache the list of bugtasks and set up the release mapping."""
3248 # Cache some values, so that we don't have to recalculate them3250 # Cache some values, so that we don't have to recalculate them
3249 # for each bug task.3251 # for each bug task.
3252 # This query is redundant: the publisher also queries all the bugtasks.
3250 self.bugtasks = list(self.context.bugtasks)3253 self.bugtasks = list(self.context.bugtasks)
3251 self.many_bugtasks = len(self.bugtasks) >= 103254 self.many_bugtasks = len(self.bugtasks) >= 10
3252 self.cached_milestone_source = CachedMilestoneSourceFactory()3255 self.cached_milestone_source = CachedMilestoneSourceFactory()
@@ -3338,7 +3341,6 @@
33383341
3339 upstream_tasks.sort(key=_by_targetname)3342 upstream_tasks.sort(key=_by_targetname)
3340 distro_tasks.sort(key=_by_targetname)3343 distro_tasks.sort(key=_by_targetname)
3341
3342 all_bugtasks = upstream_tasks + distro_tasks3344 all_bugtasks = upstream_tasks + distro_tasks
33433345
3344 # Cache whether the bug was converted to a question, since3346 # Cache whether the bug was converted to a question, since
@@ -3402,7 +3404,7 @@
3402 # Hide the links when the bug is viewed in a CVE context3404 # Hide the links when the bug is viewed in a CVE context
3403 return self.request.getNearest(ICveSet) == (None, None)3405 return self.request.getNearest(ICveSet) == (None, None)
34043406
3405 @property3407 @cachedproperty
3406 def current_user_affected_status(self):3408 def current_user_affected_status(self):
3407 """Is the current user marked as affected by this bug?"""3409 """Is the current user marked as affected by this bug?"""
3408 return self.context.isUserAffected(self.user)3410 return self.context.isUserAffected(self.user)
@@ -3623,10 +3625,11 @@
36233625
3624 return items3626 return items
36253627
3626 @property3628 @cachedproperty
3627 def target_has_milestones(self):3629 def target_has_milestones(self):
3628 """Are there any milestones we can target?"""3630 """Are there any milestones we can target?"""
3629 return list(MilestoneVocabulary(self.context)) != []3631 return MilestoneVocabulary.getMilestoneTarget(
3632 self.context).has_milestones
36303633
3631 def bugtask_canonical_url(self):3634 def bugtask_canonical_url(self):
3632 """Return the canonical url for the bugtask."""3635 """Return the canonical url for the bugtask."""
36333636
=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py 2010-09-01 01:57:37 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2010-09-22 21:01:52 +0000
@@ -28,6 +28,7 @@
28 setUp,28 setUp,
29 tearDown,29 tearDown,
30 )30 )
31from canonical.launchpad.webapp import canonical_url
31from canonical.launchpad.webapp.servers import LaunchpadTestRequest32from canonical.launchpad.webapp.servers import LaunchpadTestRequest
32from canonical.testing import LaunchpadFunctionalLayer33from canonical.testing import LaunchpadFunctionalLayer
33from lp.bugs.browser import bugtask34from lp.bugs.browser import bugtask
@@ -37,11 +38,8 @@
37 BugTasksAndNominationsView,38 BugTasksAndNominationsView,
38 )39 )
39from lp.bugs.interfaces.bugtask import BugTaskStatus40from lp.bugs.interfaces.bugtask import BugTaskStatus
40from lp.testing import (41from lp.testing import TestCaseWithFactory
41 person_logged_in,42from lp.testing._webservice import QueryCollector
42 StormStatementRecorder,
43 TestCaseWithFactory,
44 )
45from lp.testing.matchers import HasQueryCount43from lp.testing.matchers import HasQueryCount
46from lp.testing.sampledata import (44from lp.testing.sampledata import (
47 ADMIN_EMAIL,45 ADMIN_EMAIL,
@@ -54,16 +52,6 @@
5452
55 layer = LaunchpadFunctionalLayer53 layer = LaunchpadFunctionalLayer
5654
57 def record_view_initialization(self, bugtask, person):
58 self.invalidate_caches(bugtask)
59 # Login first because logging in triggers queries.
60 with nested(person_logged_in(person), StormStatementRecorder()) as (
61 _,
62 recorder):
63 view = BugTaskView(bugtask, LaunchpadTestRequest())
64 view.initialize()
65 return recorder
66
67 def invalidate_caches(self, obj):55 def invalidate_caches(self, obj):
68 store = Store.of(obj)56 store = Store.of(obj)
69 # Make sure everything is in the database.57 # Make sure everything is in the database.
@@ -72,24 +60,31 @@
72 # the domain objects)60 # the domain objects)
73 store.invalidate()61 store.invalidate()
7462
75 def test_query_counts_constant_with_team_memberships(self):63 def test_rendered_query_counts_constant_with_team_memberships(self):
76 login(ADMIN_EMAIL)64 login(ADMIN_EMAIL)
77 bugtask = self.factory.makeBugTask()65 bugtask = self.factory.makeBugTask()
78 person_no_teams = self.factory.makePerson()66 person_no_teams = self.factory.makePerson(password='test')
79 person_with_teams = self.factory.makePerson()67 person_with_teams = self.factory.makePerson(password='test')
80 for _ in range(10):68 for _ in range(10):
81 self.factory.makeTeam(members=[person_with_teams])69 self.factory.makeTeam(members=[person_with_teams])
82 # count with no teams70 # count with no teams
83 recorder = self.record_view_initialization(bugtask, person_no_teams)71 url = canonical_url(bugtask)
84 self.assertThat(recorder, HasQueryCount(LessThan(14)))72 recorder = QueryCollector()
73 recorder.register()
74 self.addCleanup(recorder.unregister)
75 self.invalidate_caches(bugtask)
76 self.getUserBrowser(url, person_no_teams)
77 # This may seem large: it is; there is easily another 30% fat in there.
78 self.assertThat(recorder, HasQueryCount(LessThan(64)))
85 count_with_no_teams = recorder.count79 count_with_no_teams = recorder.count
86 # count with many teams80 # count with many teams
87 recorder2 = self.record_view_initialization(bugtask, person_with_teams)81 self.invalidate_caches(bugtask)
82 self.getUserBrowser(url, person_with_teams)
88 # Allow an increase of one because storm bug 619017 causes additional83 # Allow an increase of one because storm bug 619017 causes additional
89 # queries, revalidating things unnecessarily. An increase which is84 # queries, revalidating things unnecessarily. An increase which is
90 # less than the number of new teams shows it is definitely not85 # less than the number of new teams shows it is definitely not
91 # growing per-team.86 # growing per-team.
92 self.assertThat(recorder2, HasQueryCount(87 self.assertThat(recorder, HasQueryCount(
93 LessThan(count_with_no_teams + 3),88 LessThan(count_with_no_teams + 3),
94 ))89 ))
9590
@@ -105,23 +100,32 @@
105 self.view = BugTasksAndNominationsView(100 self.view = BugTasksAndNominationsView(
106 self.bug, LaunchpadTestRequest())101 self.bug, LaunchpadTestRequest())
107102
103 def refresh(self):
104 # The view caches, to see different scenarios, a refresh is needed.
105 self.view = BugTasksAndNominationsView(
106 self.bug, LaunchpadTestRequest())
107
108 def test_current_user_affected_status(self):108 def test_current_user_affected_status(self):
109 self.failUnlessEqual(109 self.failUnlessEqual(
110 None, self.view.current_user_affected_status)110 None, self.view.current_user_affected_status)
111 self.view.context.markUserAffected(self.view.user, True)111 self.bug.markUserAffected(self.view.user, True)
112 self.refresh()
112 self.failUnlessEqual(113 self.failUnlessEqual(
113 True, self.view.current_user_affected_status)114 True, self.view.current_user_affected_status)
114 self.view.context.markUserAffected(self.view.user, False)115 self.bug.markUserAffected(self.view.user, False)
116 self.refresh()
115 self.failUnlessEqual(117 self.failUnlessEqual(
116 False, self.view.current_user_affected_status)118 False, self.view.current_user_affected_status)
117119
118 def test_current_user_affected_js_status(self):120 def test_current_user_affected_js_status(self):
119 self.failUnlessEqual(121 self.failUnlessEqual(
120 'null', self.view.current_user_affected_js_status)122 'null', self.view.current_user_affected_js_status)
121 self.view.context.markUserAffected(self.view.user, True)123 self.bug.markUserAffected(self.view.user, True)
124 self.refresh()
122 self.failUnlessEqual(125 self.failUnlessEqual(
123 'true', self.view.current_user_affected_js_status)126 'true', self.view.current_user_affected_js_status)
124 self.view.context.markUserAffected(self.view.user, False)127 self.bug.markUserAffected(self.view.user, False)
128 self.refresh()
125 self.failUnlessEqual(129 self.failUnlessEqual(
126 'false', self.view.current_user_affected_js_status)130 'false', self.view.current_user_affected_js_status)
127131
@@ -148,10 +152,12 @@
148 # logged-in user marked him or herself as affected or not.152 # logged-in user marked him or herself as affected or not.
149 self.failUnlessEqual(153 self.failUnlessEqual(
150 1, self.view.other_users_affected_count)154 1, self.view.other_users_affected_count)
151 self.view.context.markUserAffected(self.view.user, True)155 self.bug.markUserAffected(self.view.user, True)
156 self.refresh()
152 self.failUnlessEqual(157 self.failUnlessEqual(
153 1, self.view.other_users_affected_count)158 1, self.view.other_users_affected_count)
154 self.view.context.markUserAffected(self.view.user, False)159 self.bug.markUserAffected(self.view.user, False)
160 self.refresh()
155 self.failUnlessEqual(161 self.failUnlessEqual(
156 1, self.view.other_users_affected_count)162 1, self.view.other_users_affected_count)
157163
@@ -161,17 +167,18 @@
161 self.failUnlessEqual(167 self.failUnlessEqual(
162 1, self.view.other_users_affected_count)168 1, self.view.other_users_affected_count)
163 other_user_1 = self.factory.makePerson()169 other_user_1 = self.factory.makePerson()
164 self.view.context.markUserAffected(other_user_1, True)170 self.bug.markUserAffected(other_user_1, True)
165 self.failUnlessEqual(171 self.failUnlessEqual(
166 2, self.view.other_users_affected_count)172 2, self.view.other_users_affected_count)
167 other_user_2 = self.factory.makePerson()173 other_user_2 = self.factory.makePerson()
168 self.view.context.markUserAffected(other_user_2, True)174 self.bug.markUserAffected(other_user_2, True)
169 self.failUnlessEqual(175 self.failUnlessEqual(
170 3, self.view.other_users_affected_count)176 3, self.view.other_users_affected_count)
171 self.view.context.markUserAffected(other_user_1, False)177 self.bug.markUserAffected(other_user_1, False)
172 self.failUnlessEqual(178 self.failUnlessEqual(
173 2, self.view.other_users_affected_count)179 2, self.view.other_users_affected_count)
174 self.view.context.markUserAffected(self.view.user, True)180 self.bug.markUserAffected(self.view.user, True)
181 self.refresh()
175 self.failUnlessEqual(182 self.failUnlessEqual(
176 2, self.view.other_users_affected_count)183 2, self.view.other_users_affected_count)
177184
178185
=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml 2010-09-10 16:21:21 +0000
+++ lib/lp/bugs/configure.zcml 2010-09-22 21:01:52 +0000
@@ -191,6 +191,7 @@
191 date_fix_released191 date_fix_released
192 date_left_closed192 date_left_closed
193 date_closed193 date_closed
194 productseriesID
194 task_age195 task_age
195 bug_subscribers196 bug_subscribers
196 is_complete197 is_complete
@@ -592,6 +593,7 @@
592 isMentor593 isMentor
593 getNullBugTask594 getNullBugTask
594 is_complete595 is_complete
596 official_tags
595 who_made_private597 who_made_private
596 date_made_private598 date_made_private
597 userCanView599 userCanView
@@ -624,6 +626,7 @@
624 initial_message626 initial_message
625 linked_branches627 linked_branches
626 watches628 watches
629 has_cves
627 cves630 cves
628 cve_links631 cve_links
629 duplicates632 duplicates
630633
=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py 2010-08-30 23:50:41 +0000
+++ lib/lp/bugs/interfaces/bug.py 2010-09-22 21:01:52 +0000
@@ -269,6 +269,7 @@
269 title=_('CVE entries related to this bug.'),269 title=_('CVE entries related to this bug.'),
270 value_type=Reference(schema=ICve),270 value_type=Reference(schema=ICve),
271 readonly=True))271 readonly=True))
272 has_cves = Bool(title=u"True if the bug has cve entries.")
272 cve_links = Attribute('Links between this bug and CVE entries.')273 cve_links = Attribute('Links between this bug and CVE entries.')
273 subscriptions = exported(274 subscriptions = exported(
274 doNotSnapshot(CollectionField(275 doNotSnapshot(CollectionField(
@@ -406,6 +407,8 @@
406407
407 latest_patch = Attribute("The most recent patch of this bug.")408 latest_patch = Attribute("The most recent patch of this bug.")
408409
410 official_tags = Attribute("The official bug tags relevant to this bug.")
411
409 @operation_parameters(412 @operation_parameters(
410 subject=optional_message_subject_field(),413 subject=optional_message_subject_field(),
411 content=copy_field(IMessage['content']))414 content=copy_field(IMessage['content']))
412415
=== modified file 'lib/lp/bugs/interfaces/bugtarget.py'
--- lib/lp/bugs/interfaces/bugtarget.py 2010-08-26 20:22:48 +0000
+++ lib/lp/bugs/interfaces/bugtarget.py 2010-09-22 21:01:52 +0000
@@ -363,6 +363,9 @@
363 bugs will be returned.363 bugs will be returned.
364 """364 """
365365
366 def _getOfficialTagClause():
367 """Get the storm clause for finding this targets tags."""
368
366369
367class IOfficialBugTagTargetPublic(IHasOfficialBugTags):370class IOfficialBugTagTargetPublic(IHasOfficialBugTags):
368 """Public attributes for `IOfficialBugTagTarget`."""371 """Public attributes for `IOfficialBugTagTarget`."""
369372
=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py 2010-08-30 19:30:45 +0000
+++ lib/lp/bugs/interfaces/bugtask.py 2010-09-22 21:01:52 +0000
@@ -454,6 +454,7 @@
454 title=_('Project'), required=False, vocabulary='Product')454 title=_('Project'), required=False, vocabulary='Product')
455 productseries = Choice(455 productseries = Choice(
456 title=_('Series'), required=False, vocabulary='ProductSeries')456 title=_('Series'), required=False, vocabulary='ProductSeries')
457 productseriesID = Attribute('The product series ID')
457 sourcepackagename = Choice(458 sourcepackagename = Choice(
458 title=_("Package"), required=False,459 title=_("Package"), required=False,
459 vocabulary='SourcePackageName')460 vocabulary='SourcePackageName')
460461
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2010-09-15 23:40:13 +0000
+++ lib/lp/bugs/model/bug.py 2010-09-22 21:01:52 +0000
@@ -70,6 +70,7 @@
70 implements,70 implements,
71 providedBy,71 providedBy,
72 )72 )
73from zope.security.proxy import removeSecurityProxy
7374
74from canonical.config import config75from canonical.config import config
75from canonical.database.constants import UTC_NOW76from canonical.database.constants import UTC_NOW
@@ -161,6 +162,7 @@
161 get_bug_privacy_filter,162 get_bug_privacy_filter,
162 NullBugTask,163 NullBugTask,
163 )164 )
165from lp.bugs.model.bugtarget import OfficialBugTag
164from lp.bugs.model.bugwatch import BugWatch166from lp.bugs.model.bugwatch import BugWatch
165from lp.hardwaredb.interfaces.hwdb import IHWSubmissionBugSet167from lp.hardwaredb.interfaces.hwdb import IHWSubmissionBugSet
166from lp.registry.enum import BugNotificationLevel168from lp.registry.enum import BugNotificationLevel
@@ -191,6 +193,7 @@
191from lp.services.propertycache import (193from lp.services.propertycache import (
192 cachedproperty,194 cachedproperty,
193 IPropertyCache,195 IPropertyCache,
196 IPropertyCacheManager,
194 )197 )
195198
196199
@@ -348,6 +351,21 @@
348 heat_last_updated = UtcDateTimeCol(default=None)351 heat_last_updated = UtcDateTimeCol(default=None)
349 latest_patch_uploaded = UtcDateTimeCol(default=None)352 latest_patch_uploaded = UtcDateTimeCol(default=None)
350353
354 @cachedproperty
355 def _subscriber_cache(self):
356 """Caches known subscribers."""
357 return set()
358
359 @cachedproperty
360 def _subscriber_dups_cache(self):
361 """Caches known subscribers to dupes."""
362 return set()
363
364 @cachedproperty
365 def _unsubscribed_cache(self):
366 """Cache known non-subscribers."""
367 return set()
368
351 @property369 @property
352 def latest_patch(self):370 def latest_patch(self):
353 """See `IBug`."""371 """See `IBug`."""
@@ -531,7 +549,7 @@
531 dn += ' ('+self.name+')'549 dn += ' ('+self.name+')'
532 return dn550 return dn
533551
534 @property552 @cachedproperty
535 def bugtasks(self):553 def bugtasks(self):
536 """See `IBug`."""554 """See `IBug`."""
537 result = BugTask.select('BugTask.bug = %s' % sqlvalues(self.id))555 result = BugTask.select('BugTask.bug = %s' % sqlvalues(self.id))
@@ -541,7 +559,8 @@
541 # Do not use the default orderBy as the prejoins cause ambiguities559 # Do not use the default orderBy as the prejoins cause ambiguities
542 # across the tables.560 # across the tables.
543 result = result.orderBy("id")561 result = result.orderBy("id")
544 return sorted(result, key=bugtask_sort_key)562 result = sorted(result, key=bugtask_sort_key)
563 return result
545564
546 @property565 @property
547 def default_bugtask(self):566 def default_bugtask(self):
@@ -665,6 +684,33 @@
665 BugMessage.message == Message.id).order_by('id')684 BugMessage.message == Message.id).order_by('id')
666 return messages.first()685 return messages.first()
667686
687 @cachedproperty
688 def official_tags(self):
689 """See `IBug`."""
690 # Da circle of imports forces the locals.
691 from lp.registry.model.distribution import Distribution
692 from lp.registry.model.product import Product
693 table = OfficialBugTag
694 table = LeftJoin(
695 table,
696 Distribution,
697 OfficialBugTag.distribution_id==Distribution.id)
698 table = LeftJoin(
699 table,
700 Product,
701 OfficialBugTag.product_id==Product.id)
702 # When this method is typically called it already has the necessary
703 # info in memory, so rather than rejoin with Product etc, we do this
704 # bit in Python. If reviewing performance here feel free to change.
705 clauses = []
706 for task in self.bugtasks:
707 clauses.append(
708 # Storm cannot compile proxied objects.
709 removeSecurityProxy(task.target._getOfficialTagClause()))
710 clause = Or(*clauses)
711 return list(Store.of(self).using(table).find(OfficialBugTag.tag,
712 clause).order_by(OfficialBugTag.tag).config(distinct=True))
713
668 def followup_subject(self):714 def followup_subject(self):
669 """See `IBug`."""715 """See `IBug`."""
670 return 'Re: '+ self.title716 return 'Re: '+ self.title
@@ -698,6 +744,8 @@
698744
699 def unsubscribe(self, person, unsubscribed_by):745 def unsubscribe(self, person, unsubscribed_by):
700 """See `IBug`."""746 """See `IBug`."""
747 # Drop cached subscription info.
748 IPropertyCacheManager(self).clear()
701 if person is None:749 if person is None:
702 person = unsubscribed_by750 person = unsubscribed_by
703751
@@ -737,21 +785,11 @@
737785
738 def isSubscribed(self, person):786 def isSubscribed(self, person):
739 """See `IBug`."""787 """See `IBug`."""
740 if person is None:788 return self.personIsDirectSubscriber(person)
741 return False
742
743 bs = BugSubscription.selectBy(bug=self, person=person)
744 return bool(bs)
745789
746 def isSubscribedToDupes(self, person):790 def isSubscribedToDupes(self, person):
747 """See `IBug`."""791 """See `IBug`."""
748 if person is None:792 return self.personIsSubscribedToDuplicate(person)
749 return False
750
751 return bool(
752 BugSubscription.select("""
753 bug IN (SELECT id FROM Bug WHERE duplicateof = %d) AND
754 person = %d""" % (self.id, person.id)))
755793
756 def getDirectSubscriptions(self):794 def getDirectSubscriptions(self):
757 """See `IBug`."""795 """See `IBug`."""
@@ -868,9 +906,23 @@
868 def getSubscribersForPerson(self, person):906 def getSubscribersForPerson(self, person):
869 """See `IBug."""907 """See `IBug."""
870 assert person is not None908 assert person is not None
871 return Store.of(self).find(909 def cache_unsubscribed(rows):
872 # return people910 if not rows:
873 Person,911 self._unsubscribed_cache.add(person)
912 def cache_subscriber(row):
913 _, subscriber, subscription = row
914 if subscription.bugID == self.id:
915 self._subscriber_cache.add(subscriber)
916 else:
917 self._subscriber_dups_cache.add(subscriber)
918 return subscriber
919 return DecoratedResultSet(Store.of(self).find(
920 # XXX: RobertCollins 2010-09-22 bug=374777: This SQL(...) is a
921 # hack; it does not seem to be possible to express DISTINCT ON
922 # with Storm.
923 (SQL("DISTINCT ON (Person.name, BugSubscription.person) 0 AS ignore"),
924 # return people and subscribptions
925 Person, BugSubscription),
874 # For this bug or its duplicates926 # For this bug or its duplicates
875 Or(927 Or(
876 Bug.id == self.id,928 Bug.id == self.id,
@@ -890,7 +942,8 @@
890 # bug=https://bugs.edge.launchpad.net/storm/+bug/627137942 # bug=https://bugs.edge.launchpad.net/storm/+bug/627137
891 # RBC 20100831943 # RBC 20100831
892 SQL("""Person.id = TeamParticipation.team"""),944 SQL("""Person.id = TeamParticipation.team"""),
893 ).order_by(Person.name).config(distinct=True)945 ).order_by(Person.name),
946 cache_subscriber, pre_iter_hook=cache_unsubscribed)
894947
895 def getAlsoNotifiedSubscribers(self, recipients=None, level=None):948 def getAlsoNotifiedSubscribers(self, recipients=None, level=None):
896 """See `IBug`.949 """See `IBug`.
@@ -1216,6 +1269,11 @@
1216 notify(ObjectDeletedEvent(bug_branch, user=user))1269 notify(ObjectDeletedEvent(bug_branch, user=user))
1217 bug_branch.destroySelf()1270 bug_branch.destroySelf()
12181271
1272 @cachedproperty
1273 def has_cves(self):
1274 """See `IBug`."""
1275 return bool(self.cves)
1276
1219 def linkCVE(self, cve, user):1277 def linkCVE(self, cve, user):
1220 """See `IBug`."""1278 """See `IBug`."""
1221 if cve not in self.cves:1279 if cve not in self.cves:
@@ -1314,18 +1372,23 @@
1314 question_target = IQuestionTarget(bugtask.target)1372 question_target = IQuestionTarget(bugtask.target)
1315 question = question_target.createQuestionFromBug(self)1373 question = question_target.createQuestionFromBug(self)
1316 self.addChange(BugConvertedToQuestion(UTC_NOW, person, question))1374 self.addChange(BugConvertedToQuestion(UTC_NOW, person, question))
1375 IPropertyCache(self)._question_from_bug = question
13171376
1318 notify(BugBecameQuestionEvent(self, question, person))1377 notify(BugBecameQuestionEvent(self, question, person))
1319 return question1378 return question
13201379
1321 def getQuestionCreatedFromBug(self):1380 @cachedproperty
1322 """See `IBug`."""1381 def _question_from_bug(self):
1323 for question in self.questions:1382 for question in self.questions:
1324 if (question.owner == self.owner1383 if (question.ownerID == self.ownerID
1325 and question.datecreated == self.datecreated):1384 and question.datecreated == self.datecreated):
1326 return question1385 return question
1327 return None1386 return None
13281387
1388 def getQuestionCreatedFromBug(self):
1389 """See `IBug`."""
1390 return self._question_from_bug
1391
1329 def canMentor(self, user):1392 def canMentor(self, user):
1330 """See `ICanBeMentored`."""1393 """See `ICanBeMentored`."""
1331 if user is None:1394 if user is None:
@@ -1624,10 +1687,13 @@
16241687
1625 def _getTags(self):1688 def _getTags(self):
1626 """Get the tags as a sorted list of strings."""1689 """Get the tags as a sorted list of strings."""
1627 tags = [1690 return self._cached_tags
1628 bugtag.tag1691
1629 for bugtag in BugTag.selectBy(bug=self, orderBy='tag')]1692 @cachedproperty
1630 return tags1693 def _cached_tags(self):
1694 return list(Store.of(self).find(
1695 BugTag.tag,
1696 BugTag.bugID==self.id).order_by(BugTag.tag))
16311697
1632 def _setTags(self, tags):1698 def _setTags(self, tags):
1633 """Set the tags from a list of strings."""1699 """Set the tags from a list of strings."""
@@ -1635,6 +1701,7 @@
1635 # and insert the new ones.1701 # and insert the new ones.
1636 new_tags = set([tag.lower() for tag in tags])1702 new_tags = set([tag.lower() for tag in tags])
1637 old_tags = set(self.tags)1703 old_tags = set(self.tags)
1704 del IPropertyCache(self)._cached_tags
1638 added_tags = new_tags.difference(old_tags)1705 added_tags = new_tags.difference(old_tags)
1639 removed_tags = old_tags.difference(new_tags)1706 removed_tags = old_tags.difference(new_tags)
1640 for removed_tag in removed_tags:1707 for removed_tag in removed_tags:
@@ -1782,12 +1849,17 @@
17821849
1783 def personIsDirectSubscriber(self, person):1850 def personIsDirectSubscriber(self, person):
1784 """See `IBug`."""1851 """See `IBug`."""
1852 if person in self._subscriber_cache:
1853 return True
1854 if person in self._unsubscribed_cache:
1855 return False
1856 if person is None:
1857 return False
1785 store = Store.of(self)1858 store = Store.of(self)
1786 subscriptions = store.find(1859 subscriptions = store.find(
1787 BugSubscription,1860 BugSubscription,
1788 BugSubscription.bug == self,1861 BugSubscription.bug == self,
1789 BugSubscription.person == person)1862 BugSubscription.person == person)
1790
1791 return not subscriptions.is_empty()1863 return not subscriptions.is_empty()
17921864
1793 def personIsAlsoNotifiedSubscriber(self, person):1865 def personIsAlsoNotifiedSubscriber(self, person):
@@ -1803,6 +1875,12 @@
18031875
1804 def personIsSubscribedToDuplicate(self, person):1876 def personIsSubscribedToDuplicate(self, person):
1805 """See `IBug`."""1877 """See `IBug`."""
1878 if person in self._subscriber_dups_cache:
1879 return True
1880 if person in self._unsubscribed_cache:
1881 return False
1882 if person is None:
1883 return False
1806 store = Store.of(self)1884 store = Store.of(self)
1807 subscriptions_from_dupes = store.find(1885 subscriptions_from_dupes = store.find(
1808 BugSubscription,1886 BugSubscription,
@@ -2012,13 +2090,13 @@
20122090
2013 # Create the task on a product if one was passed.2091 # Create the task on a product if one was passed.
2014 if params.product:2092 if params.product:
2015 BugTaskSet().createTask(2093 getUtility(IBugTaskSet).createTask(
2016 bug=bug, product=params.product, owner=params.owner,2094 bug=bug, product=params.product, owner=params.owner,
2017 status=params.status)2095 status=params.status)
20182096
2019 # Create the task on a source package name if one was passed.2097 # Create the task on a source package name if one was passed.
2020 if params.distribution:2098 if params.distribution:
2021 BugTaskSet().createTask(2099 getUtility(IBugTaskSet).createTask(
2022 bug=bug, distribution=params.distribution,2100 bug=bug, distribution=params.distribution,
2023 sourcepackagename=params.sourcepackagename,2101 sourcepackagename=params.sourcepackagename,
2024 owner=params.owner, status=params.status)2102 owner=params.owner, status=params.status)
20252103
=== modified file 'lib/lp/bugs/model/bugtarget.py'
--- lib/lp/bugs/model/bugtarget.py 2010-09-02 22:08:12 +0000
+++ lib/lp/bugs/model/bugtarget.py 2010-09-22 21:01:52 +0000
@@ -338,22 +338,26 @@
338338
339 Using this call in ProjectGroup requires a fix of bug 341203, see339 Using this call in ProjectGroup requires a fix of bug 341203, see
340 below, class OfficialBugTag.340 below, class OfficialBugTag.
341
342 See also `Bug.official_bug_tags` which calculates this efficiently for
343 a single bug.
341 """344 """
342345
346 def _getOfficialTagClause(self):
347 if IDistribution.providedBy(self):
348 return (OfficialBugTag.distribution == self)
349 elif IProduct.providedBy(self):
350 return (OfficialBugTag.product == self)
351 else:
352 raise AssertionError(
353 '%s is not a valid official bug target' % self)
354
343 def _getOfficialTags(self):355 def _getOfficialTags(self):
344 """Get the official bug tags as a sorted list of strings."""356 """Get the official bug tags as a sorted list of strings."""
345 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)357 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
346 if IDistribution.providedBy(self):358 target_clause = self._getOfficialTagClause()
347 target_clause = (OfficialBugTag.distribution == self)359 return list(store.find(
348 elif IProduct.providedBy(self):360 OfficialBugTag.tag, target_clause).order_by(OfficialBugTag.tag))
349 target_clause = (OfficialBugTag.product == self)
350 else:
351 raise AssertionError(
352 '%s is not a valid official bug target' % self)
353 tags = [
354 obt.tag for obt
355 in store.find(OfficialBugTag, target_clause).order_by('tag')]
356 return tags
357361
358 def _setOfficialTags(self, tags):362 def _setOfficialTags(self, tags):
359 """Set the official bug tags from a list of strings."""363 """Set the official bug tags from a list of strings."""
@@ -374,10 +378,7 @@
374 If the tag is not defined for this target, None is returned.378 If the tag is not defined for this target, None is returned.
375 """379 """
376 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)380 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
377 if IDistribution.providedBy(self):381 target_clause = self._getOfficialTagClause()
378 target_clause = (OfficialBugTag.distribution == self)
379 else:
380 target_clause = (OfficialBugTag.product == self)
381 return store.find(382 return store.find(
382 OfficialBugTag, OfficialBugTag.tag==tag, target_clause).one()383 OfficialBugTag, OfficialBugTag.tag==tag, target_clause).one()
383384
384385
=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py 2010-09-09 17:02:33 +0000
+++ lib/lp/bugs/model/bugtask.py 2010-09-22 21:01:52 +0000
@@ -43,7 +43,10 @@
43 Or,43 Or,
44 SQL,44 SQL,
45 )45 )
46from storm.store import EmptyResultSet46from storm.store import (
47 EmptyResultSet,
48 Store,
49 )
47from storm.zope.interfaces import (50from storm.zope.interfaces import (
48 IResultSet,51 IResultSet,
49 ISQLObjectResultSet,52 ISQLObjectResultSet,
@@ -399,6 +402,7 @@
399 sourcepackagename=None, distribution=None,402 sourcepackagename=None, distribution=None,
400 distroseries=None):403 distroseries=None):
401 """Initialize a NullBugTask."""404 """Initialize a NullBugTask."""
405 self.id = None
402 self.bug = bug406 self.bug = bug
403 self.product = product407 self.product = product
404 self.productseries = productseries408 self.productseries = productseries
@@ -756,11 +760,11 @@
756 conjoined_master = bugtask760 conjoined_master = bugtask
757 break761 break
758 elif IUpstreamBugTask.providedBy(self):762 elif IUpstreamBugTask.providedBy(self):
759 assert self.product.development_focus is not None, (763 assert self.product.development_focusID is not None, (
760 'A product should always have a development series.')764 'A product should always have a development series.')
761 devel_focus = self.product.development_focus765 devel_focusID = self.product.development_focusID
762 for bugtask in bugtasks:766 for bugtask in bugtasks:
763 if bugtask.productseries == devel_focus:767 if bugtask.productseriesID == devel_focusID:
764 conjoined_master = bugtask768 conjoined_master = bugtask
765 break769 break
766770
@@ -2367,7 +2371,11 @@
2367 bugtask._syncFromConjoinedSlave()2371 bugtask._syncFromConjoinedSlave()
23682372
2369 bugtask.updateTargetNameCache()2373 bugtask.updateTargetNameCache()
23702374 del IPropertyCache(bug).bugtasks
2375 # Because of block_implicit_flushes, it is possible for a new bugtask
2376 # to be queued in appropriately, which leads to Bug.bugtasks not
2377 # finding the bugtask.
2378 Store.of(bugtask).flush()
2371 return bugtask2379 return bugtask
23722380
2373 def getStatusCountsForProductSeries(self, user, product_series):2381 def getStatusCountsForProductSeries(self, user, product_series):
23742382
=== modified file 'lib/lp/bugs/templates/bug-portlet-actions.pt'
--- lib/lp/bugs/templates/bug-portlet-actions.pt 2010-08-02 17:49:45 +0000
+++ lib/lp/bugs/templates/bug-portlet-actions.pt 2010-09-22 21:01:52 +0000
@@ -18,8 +18,8 @@
18 class="menu-link-mark-dupe">Mark as duplicate</a>18 class="menu-link-mark-dupe">Mark as duplicate</a>
19 </span>19 </span>
20 <tal:block20 <tal:block
21 tal:condition="context/duplicates"21 tal:condition="context/number_of_duplicates"
22 tal:define="number_of_dupes context/duplicates/count"22 tal:define="number_of_dupes context/number_of_duplicates"
23 >23 >
24 </tal:block>24 </tal:block>
25 <tal:block25 <tal:block
2626
=== modified file 'lib/lp/bugs/templates/bugtask-index.pt'
--- lib/lp/bugs/templates/bugtask-index.pt 2010-09-07 16:29:19 +0000
+++ lib/lp/bugs/templates/bugtask-index.pt 2010-09-22 21:01:52 +0000
@@ -195,7 +195,7 @@
195 </tal:branches>195 </tal:branches>
196 </div><!-- bug-branch-container -->196 </div><!-- bug-branch-container -->
197197
198 <div tal:condition="context/bug/cves" class="cves">198 <div tal:condition="context/bug/has_cves" class="cves">
199 <h2>CVE References</h2>199 <h2>CVE References</h2>
200 <ul>200 <ul>
201 <li class="sprite cve" tal:repeat="cve context/bug/cves">201 <li class="sprite cve" tal:repeat="cve context/bug/cves">
202202
=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml 2010-09-21 13:39:31 +0000
+++ lib/lp/registry/configure.zcml 2010-09-22 21:01:52 +0000
@@ -452,6 +452,7 @@
452 getReleasesAndPublishingHistory452 getReleasesAndPublishingHistory
453 upstream_product453 upstream_product
454 target_type_display454 target_type_display
455 _getOfficialTagClause
455 official_bug_tags456 official_bug_tags
456 findRelatedArchives457 findRelatedArchives
457 findRelatedArchivePublications458 findRelatedArchivePublications
458459
=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py 2010-09-09 17:02:33 +0000
+++ lib/lp/registry/interfaces/product.py 2010-09-22 21:01:52 +0000
@@ -660,6 +660,7 @@
660 'The series that represents the master or trunk branch. '660 'The series that represents the master or trunk branch. '
661 'The Bazaar URL lp:<project> points to the development focus '661 'The Bazaar URL lp:<project> points to the development focus '
662 'series branch.')))662 'series branch.')))
663 development_focusID = Attribute("The development focus ID.")
663664
664 name_with_project = Attribute(_("Returns the product name prefixed "665 name_with_project = Attribute(_("Returns the product name prefixed "
665 "by the project name, if a project is associated with this "666 "by the project name, if a project is associated with this "
666667
=== modified file 'lib/lp/registry/interfaces/projectgroup.py'
--- lib/lp/registry/interfaces/projectgroup.py 2010-08-21 18:41:57 +0000
+++ lib/lp/registry/interfaces/projectgroup.py 2010-09-22 21:01:52 +0000
@@ -328,6 +328,8 @@
328 def getSeries(series_name):328 def getSeries(series_name):
329 """Return a ProjectGroupSeries object with name `series_name`."""329 """Return a ProjectGroupSeries object with name `series_name`."""
330330
331 product_milestones = Attribute('all the milestones for all the products.')
332
331333
332class IProjectGroup(IProjectGroupPublic,334class IProjectGroup(IProjectGroupPublic,
333 IProjectGroupModerate,335 IProjectGroupModerate,
334336
=== modified file 'lib/lp/registry/model/distributionsourcepackage.py'
--- lib/lp/registry/model/distributionsourcepackage.py 2010-09-12 17:43:49 +0000
+++ lib/lp/registry/model/distributionsourcepackage.py 2010-09-22 21:01:52 +0000
@@ -494,6 +494,9 @@
494 BugTask.sourcepackagename == self.sourcepackagename),494 BugTask.sourcepackagename == self.sourcepackagename),
495 user)495 user)
496496
497 def _getOfficialTagClause(self):
498 return self.distribution._getOfficialTagClause()
499
497 @property500 @property
498 def official_bug_tags(self):501 def official_bug_tags(self):
499 """See `IHasBugs`."""502 """See `IHasBugs`."""
500503
=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py 2010-09-21 19:34:45 +0000
+++ lib/lp/registry/model/distroseries.py 2010-09-22 21:01:52 +0000
@@ -743,6 +743,9 @@
743 """Customize `search_params` for this distribution series."""743 """Customize `search_params` for this distribution series."""
744 search_params.setDistroSeries(self)744 search_params.setDistroSeries(self)
745745
746 def _getOfficialTagClause(self):
747 return self.distribution._getOfficialTagClause()
748
746 @property749 @property
747 def official_bug_tags(self):750 def official_bug_tags(self):
748 """See `IHasBugs`."""751 """See `IHasBugs`."""
749752
=== modified file 'lib/lp/registry/model/milestone.py'
--- lib/lp/registry/model/milestone.py 2010-09-14 15:04:06 +0000
+++ lib/lp/registry/model/milestone.py 2010-09-22 21:01:52 +0000
@@ -107,8 +107,7 @@
107 result = store.find(Milestone, self._getMilestoneCondition())107 result = store.find(Milestone, self._getMilestoneCondition())
108 return result.order_by(self._milestone_order)108 return result.order_by(self._milestone_order)
109109
110 @property110 def _get_milestones(self):
111 def milestones(self):
112 """See `IHasMilestones`."""111 """See `IHasMilestones`."""
113 store = Store.of(self)112 store = Store.of(self)
114 result = store.find(Milestone,113 result = store.find(Milestone,
@@ -116,6 +115,8 @@
116 Milestone.active == True))115 Milestone.active == True))
117 return result.order_by(self._milestone_order)116 return result.order_by(self._milestone_order)
118117
118 milestones = property(_get_milestones)
119
119120
120class MultipleProductReleases(Exception):121class MultipleProductReleases(Exception):
121 """Raised when a second ProductRelease is created for a milestone."""122 """Raised when a second ProductRelease is created for a milestone."""
122123
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2010-09-10 20:27:19 +0000
+++ lib/lp/registry/model/person.py 2010-09-22 21:01:52 +0000
@@ -1265,6 +1265,8 @@
1265 # The owner is not a member but must retain his rights over1265 # The owner is not a member but must retain his rights over
1266 # this team. This person may be a member of the owner, and in this1266 # this team. This person may be a member of the owner, and in this
1267 # case it'll also have rights over this team.1267 # case it'll also have rights over this team.
1268 # Note that this query and the tp query above can be consolidated
1269 # when we get to a finer grained level of optimisations.
1268 in_team = self.inTeam(team.teamowner)1270 in_team = self.inTeam(team.teamowner)
1269 else:1271 else:
1270 in_team = False1272 in_team = False
12711273
=== modified file 'lib/lp/registry/model/productseries.py'
--- lib/lp/registry/model/productseries.py 2010-09-21 19:34:45 +0000
+++ lib/lp/registry/model/productseries.py 2010-09-22 21:01:52 +0000
@@ -423,6 +423,9 @@
423 """Customize `search_params` for this product series."""423 """Customize `search_params` for this product series."""
424 search_params.setProductSeries(self)424 search_params.setProductSeries(self)
425425
426 def _getOfficialTagClause(self):
427 return self.product._getOfficialTagClause()
428
426 @property429 @property
427 def official_bug_tags(self):430 def official_bug_tags(self):
428 """See `IHasBugs`."""431 """See `IHasBugs`."""
429432
=== modified file 'lib/lp/registry/model/projectgroup.py'
--- lib/lp/registry/model/projectgroup.py 2010-09-09 15:26:11 +0000
+++ lib/lp/registry/model/projectgroup.py 2010-09-22 21:01:52 +0000
@@ -93,6 +93,7 @@
93from lp.registry.model.karma import KarmaContextMixin93from lp.registry.model.karma import KarmaContextMixin
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 HasMilestonesMixin,
96 Milestone,97 Milestone,
97 ProjectMilestone,98 ProjectMilestone,
98 )99 )
@@ -110,7 +111,8 @@
110 MakesAnnouncements, HasSprintsMixin, HasAliasMixin,111 MakesAnnouncements, HasSprintsMixin, HasAliasMixin,
111 KarmaContextMixin, BranchVisibilityPolicyMixin,112 KarmaContextMixin, BranchVisibilityPolicyMixin,
112 StructuralSubscriptionTargetMixin,113 StructuralSubscriptionTargetMixin,
113 HasBranchesMixin, HasMergeProposalsMixin, HasBugHeatMixin):114 HasBranchesMixin, HasMergeProposalsMixin, HasBugHeatMixin,
115 HasMilestonesMixin):
114 """A ProjectGroup"""116 """A ProjectGroup"""
115117
116 implements(IProjectGroup, IFAQCollection, IHasBugHeat, IHasIcon, IHasLogo,118 implements(IProjectGroup, IFAQCollection, IHasBugHeat, IHasIcon, IHasLogo,
@@ -309,6 +311,11 @@
309 """Customize `search_params` for this milestone."""311 """Customize `search_params` for this milestone."""
310 search_params.setProject(self)312 search_params.setProject(self)
311313
314 def _getOfficialTagClause(self):
315 """See `OfficialBugTagTargetMixin`."""
316 And(ProjectGroup.id == Product.projectID,
317 Product.id == OfficialBugTag.productID)
318
312 @property319 @property
313 def official_bug_tags(self):320 def official_bug_tags(self):
314 """See `IHasBugs`."""321 """See `IHasBugs`."""
@@ -396,6 +403,11 @@
396 """403 """
397 return self.products.count() != 0404 return self.products.count() != 0
398405
406 def _getMilestoneCondition(self):
407 """See `HasMilestonesMixin`."""
408 return And(Milestone.productID == Product.id,
409 Product.projectID == self.id)
410
399 def _getMilestones(self, only_active):411 def _getMilestones(self, only_active):
400 """Return a list of milestones for this project group.412 """Return a list of milestones for this project group.
401413
@@ -445,6 +457,13 @@
445 return self._getMilestones(True)457 return self._getMilestones(True)
446458
447 @property459 @property
460 def product_milestones(self):
461 """Hack to avoid the ProjectMilestone in MilestoneVocabulary."""
462 # XXX: bug=644977 Robert Collins - this is a workaround for
463 # insconsistency in project group milestone use.
464 return self._get_milestones()
465
466 @property
448 def all_milestones(self):467 def all_milestones(self):
449 """See `IProjectGroup`."""468 """See `IProjectGroup`."""
450 return self._getMilestones(False)469 return self._getMilestones(False)
451470
=== modified file 'lib/lp/registry/model/sourcepackage.py'
--- lib/lp/registry/model/sourcepackage.py 2010-09-07 00:51:44 +0000
+++ lib/lp/registry/model/sourcepackage.py 2010-09-22 21:01:52 +0000
@@ -481,6 +481,9 @@
481 """Customize `search_params` for this source package."""481 """Customize `search_params` for this source package."""
482 search_params.setSourcePackage(self)482 search_params.setSourcePackage(self)
483483
484 def _getOfficialTagClause(self):
485 return self.distroseries._getOfficialTagClause()
486
484 @property487 @property
485 def official_bug_tags(self):488 def official_bug_tags(self):
486 """See `IHasBugs`."""489 """See `IHasBugs`."""
487490
=== modified file 'lib/lp/registry/vocabularies.py'
--- lib/lp/registry/vocabularies.py 2010-08-31 11:11:09 +0000
+++ lib/lp/registry/vocabularies.py 2010-09-22 21:01:52 +0000
@@ -1161,16 +1161,18 @@
1161 elif IDistroSeriesBugTask.providedBy(milestone_context):1161 elif IDistroSeriesBugTask.providedBy(milestone_context):
1162 target = milestone_context.distroseries1162 target = milestone_context.distroseries
1163 elif IProductSeriesBugTask.providedBy(milestone_context):1163 elif IProductSeriesBugTask.providedBy(milestone_context):
1164 target = milestone_context.productseries1164 target = milestone_context.productseries.product
1165 elif IDistributionSourcePackage.providedBy(milestone_context):1165 elif IDistributionSourcePackage.providedBy(milestone_context):
1166 target = milestone_context.distribution1166 target = milestone_context.distribution
1167 elif ISourcePackage.providedBy(milestone_context):1167 elif ISourcePackage.providedBy(milestone_context):
1168 target = milestone_context.distroseries1168 target = milestone_context.distroseries
1169 elif ISpecification.providedBy(milestone_context):1169 elif ISpecification.providedBy(milestone_context):
1170 target = milestone_context.target1170 target = milestone_context.target
1171 elif IProductSeries.providedBy(milestone_context):
1172 # Show all the milestones of the product for a product series.
1173 target = milestone_context.product
1171 elif (IProjectGroup.providedBy(milestone_context) or1174 elif (IProjectGroup.providedBy(milestone_context) or
1172 IProduct.providedBy(milestone_context) or1175 IProduct.providedBy(milestone_context) or
1173 IProductSeries.providedBy(milestone_context) or
1174 IDistribution.providedBy(milestone_context) or1176 IDistribution.providedBy(milestone_context) or
1175 IDistroSeries.providedBy(milestone_context)):1177 IDistroSeries.providedBy(milestone_context)):
1176 target = milestone_context1178 target = milestone_context
@@ -1196,23 +1198,10 @@
1196 # should be revisited after we've unblocked users.1198 # should be revisited after we've unblocked users.
1197 if target is not None:1199 if target is not None:
1198 if IProjectGroup.providedBy(target):1200 if IProjectGroup.providedBy(target):
1199 milestones = shortlist(1201 milestones_source = target.product_milestones
1200 (milestone for product in target.products
1201 for milestone in product.milestones),
1202 longest_expected=40)
1203 elif IProductSeries.providedBy(target):
1204 # While some milestones may be associated with a
1205 # productseries, we want to show all milestones for
1206 # the product. Since the database constraint
1207 # "valid_target" ensures that a milestone associated
1208 # with a series is also associated with the product
1209 # itself, we don't need to look up series-related
1210 # milestones.
1211 milestones = shortlist(target.product.milestones,
1212 longest_expected=40)
1213 else:1202 else:
1214 milestones = shortlist(1203 milestones_source = target.milestones
1215 target.milestones, longest_expected=40)1204 milestones = shortlist(milestones_source, longest_expected=40)
1216 else:1205 else:
1217 # We can't use context to reasonably filter the1206 # We can't use context to reasonably filter the
1218 # milestones, so let's either just grab all of them,1207 # milestones, so let's either just grab all of them,
@@ -1249,9 +1238,11 @@
1249 product_ids = set(1238 product_ids = set(
1250 removeSecurityProxy(milestone).productID1239 removeSecurityProxy(milestone).productID
1251 for milestone in milestones)1240 for milestone in milestones)
1241 product_ids.discard(None)
1252 distro_ids = set(1242 distro_ids = set(
1253 removeSecurityProxy(milestone).distributionID1243 removeSecurityProxy(milestone).distributionID
1254 for milestone in milestones)1244 for milestone in milestones)
1245 distro_ids.discard(None)
1255 if len(product_ids) > 0:1246 if len(product_ids) > 0:
1256 list(Product.select("id IN %s" % sqlvalues(product_ids)))1247 list(Product.select("id IN %s" % sqlvalues(product_ids)))
1257 if len(distro_ids) > 0:1248 if len(distro_ids) > 0: