104
95
LibraryFileContent,
97
from lp.services.messages.model.message import (
106
102
from canonical.launchpad.helpers import shortlist
107
from canonical.launchpad.interfaces.launchpad import IHasBug
103
from canonical.launchpad.interfaces.launchpad import (
105
ILaunchpadCelebrities,
108
108
from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
109
109
from canonical.launchpad.interfaces.lpstorm import IStore
110
from lp.services.messages.interfaces.message import (
110
114
from canonical.launchpad.webapp.authorization import check_permission
111
115
from canonical.launchpad.webapp.interfaces import (
186
188
from lp.registry.interfaces.distroseries import IDistroSeries
187
189
from lp.registry.interfaces.person import (
190
191
validate_public_person,
192
193
from lp.registry.interfaces.product import IProduct
193
194
from lp.registry.interfaces.productseries import IProductSeries
194
from lp.registry.interfaces.role import IPersonRoles
195
195
from lp.registry.interfaces.series import SeriesStatus
196
196
from lp.registry.interfaces.sourcepackage import ISourcePackage
197
197
from lp.registry.model.person import (
262
257
return shortlist([row[0] for row in cur.fetchall()])
265
def get_bug_tags_open_count(context_condition, user, tag_limit=0,
267
"""Worker for IBugTarget.getUsedBugTagsWithOpenCounts.
269
See `IBugTarget` for details.
271
The only change is that this function takes a SQL expression for limiting
260
def get_bug_tags_open_count(context_condition, user, wanted_tags=None):
261
"""Return all the used bug tags with their open bug count.
273
263
:param context_condition: A Storm SQL expression, limiting the
274
264
used tags to a specific context. Only the BugTask table may be
275
265
used to choose the context.
266
:param user: The user performing the search.
267
:param wanted_tags: A set of tags within which to restrict the search.
269
:return: A list of tuples, (tag name, open bug count).
278
from lp.bugs.model.bugsummary import BugSummary
281
tags = dict((tag, 0) for tag in include_tags)
282
store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
283
admin_team = getUtility(ILaunchpadCelebrities).admin
284
if user is not None and not user.inTeam(admin_team):
285
store = store.with_(SQL(
287
"SELECT team from TeamParticipation WHERE person=?)", (user.id,)))
273
Join(BugTask, BugTask.bugID == BugTag.bugID),
288
275
where_conditions = [
289
BugSummary.status.is_in(UNRESOLVED_BUGTASK_STATUSES),
290
BugSummary.tag != None,
276
BugTask.status.is_in(UNRESOLVED_BUGTASK_STATUSES),
291
277
context_condition,
294
where_conditions.append(BugSummary.viewed_by_id == None)
295
elif not user.inTeam(admin_team):
279
if wanted_tags is not None:
280
where_conditions.append(BugTag.tag.is_in(wanted_tags))
281
privacy_filter = get_bug_privacy_filter(user)
283
# The EXISTS sub-select avoids a join against Bug, improving
284
# performance significantly.
296
285
where_conditions.append(
298
BugSummary.viewed_by_id == None,
299
BugSummary.viewed_by_id.is_in(SQL("SELECT team FROM teams"))
301
sum_count = Sum(BugSummary.count)
302
tag_count_columns = (BugSummary.tag, sum_count)
303
# Always query for used
305
return store.find(tag_count_columns, *(where_conditions + list(args))
306
).group_by(BugSummary.tag).having(sum_count != 0).order_by(
307
Desc(Sum(BugSummary.count)), BugSummary.tag)
310
used = used[:tag_limit]
312
# Union in a query for just include_tags.
313
used = used.union(_query(BugSummary.tag.is_in(include_tags)))
314
tags.update(dict(used))
287
columns=[True], tables=[Bug],
288
where=And(Bug.id == BugTag.bugID, SQLRaw(privacy_filter)))))
289
store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
290
return store.using(*tables).find(
291
tag_count_columns, *where_conditions).group_by(BugTag.tag).order_by(
292
Desc(Count()), BugTag.tag)
318
295
class BugBecameQuestionEvent:
866
843
"""See `IBug`."""
867
844
return self.personIsSubscribedToDuplicate(person)
869
def _getMutes(self, person):
870
store = Store.of(self)
874
BugMute.person == person)
877
846
def isMuted(self, person):
878
847
"""See `IBug`."""
879
mutes = self._getMutes(person)
880
return not mutes.is_empty()
848
store = Store.of(self)
849
subscriptions = store.find(
851
BugSubscription.bug == self,
852
BugSubscription.person == person,
853
BugSubscription.bug_notification_level ==
854
BugNotificationLevel.NOTHING)
855
return not subscriptions.is_empty()
882
857
def mute(self, person, muted_by):
883
858
"""See `IBug`."""
884
859
if person is None:
885
860
# This may be a webservice request.
886
861
person = muted_by
887
assert not person.is_team, (
888
"Muting a subscription for entire team is not allowed.")
890
# If it's already muted, ignore the request.
891
mutes = self._getMutes(person)
893
mute = BugMute(person, self)
894
Store.of(mute).flush()
862
# If there's an existing subscription, update it.
863
store = Store.of(self)
864
subscriptions = store.find(
866
BugSubscription.bug == self,
867
BugSubscription.person == person)
868
if subscriptions.is_empty():
869
return self.subscribe(
870
person, muted_by, level=BugNotificationLevel.NOTHING)
896
# It's already muted, pass.
872
subscription = subscriptions.one()
873
subscription.bug_notification_level = (
874
BugNotificationLevel.NOTHING)
899
877
def unmute(self, person, unmuted_by):
900
878
"""See `IBug`."""
901
store = Store.of(self)
903
# This may be a webservice request.
905
mutes = self._getMutes(person)
906
store.remove(mutes.one())
907
return self.getSubscriptionForPerson(person)
879
self.unsubscribe(person, unmuted_by)
910
882
def subscriptions(self):
1464
1436
# 1 bugmessage -> 1 message -> small N chunks. For now, using a wide
1465
1437
# query seems fine as we have to join out from bugmessage anyway.
1466
1438
result = Store.of(self).find((BugMessage, Message, MessageChunk),
1467
Message.id == MessageChunk.messageID,
1468
BugMessage.messageID == Message.id,
1469
BugMessage.bug == self.id,
1439
Message.id==MessageChunk.messageID,
1440
BugMessage.messageID==Message.id,
1441
BugMessage.bug==self.id,
1471
1443
result.order_by(BugMessage.index, MessageChunk.sequence)
1861
1833
def userCanView(self, user):
1864
This method is called by security adapters but only in the case for
1865
authenticated users. It is also called in other contexts where the
1866
user may be anonymous.
1836
Note that Editing is also controlled by this check,
1837
because we permit editing of any bug one can see.
1868
1839
If bug privacy rights are changed here, corresponding changes need
1869
1840
to be made to the queries which screen for privacy. See
1870
1841
Bug.searchAsUser and BugTask.get_bug_privacy_filter_with_decorator.
1843
assert user is not None, "User may not be None"
1845
if user.id in self._known_viewers:
1872
1847
if not self.private:
1873
1848
# This is a public bug.
1875
# This method may be called for anonymous users. For private bugs
1876
# always return false for anonymous.
1879
if user.id in self._known_viewers:
1882
1850
elif IPersonRoles(user).in_admin:
1883
1851
# Admins can view all bugs.
2267
2235
return IStore(BugSubscription).find(
2268
2236
BugSubscription,
2269
2237
BugSubscription.bug_notification_level >= self.level,
2270
BugSubscription.bug == self.bug,
2271
Not(In(BugSubscription.person_id,
2272
Select(BugMute.person_id, BugMute.bug_id == self.bug.id))))
2238
BugSubscription.bug == self.bug)
2274
2240
@cachedproperty
2275
2241
@freeze(BugSubscriptionSet)
2282
2248
BugSubscription,
2283
2249
BugSubscription.bug_notification_level >= self.level,
2284
2250
BugSubscription.bug_id == Bug.id,
2285
Bug.duplicateof == self.bug,
2286
Not(In(BugSubscription.person_id,
2287
Select(BugMute.person_id, BugMute.bug_id == Bug.id))))
2251
Bug.duplicateof == self.bug)
2289
2253
@cachedproperty
2290
2254
@freeze(BugSubscriptionSet)
2291
2255
def duplicate_only_subscriptions(self):
2292
"""Subscriptions to duplicates of the bug.
2256
"""Subscripitions to duplicates of the bug.
2294
2258
Excludes subscriptions for people who have a direct subscription or
2295
2259
are also notified for another reason.
2297
self.duplicate_subscriptions.subscribers # Pre-load subscribers.
2261
self.duplicate_subscriptions.subscribers # Pre-load subscribers.
2298
2262
higher_precedence = (
2299
2263
self.direct_subscriptions.subscribers.union(
2300
2264
self.also_notified_subscribers))
2330
2294
if self.bug.private:
2331
2295
return BugSubscriberSet()
2333
muted = IStore(BugMute).find(
2335
BugMute.person_id == Person.id,
2336
BugMute.bug == self.bug)
2337
2297
return BugSubscriberSet().union(
2338
2298
self.structural_subscriptions.subscribers,
2339
2299
self.all_pillar_owners_without_bug_supervisors,
2340
2300
self.all_assignees).difference(
2341
self.direct_subscriptions.subscribers).difference(muted)
2301
self.direct_subscriptions.subscribers)
2343
2303
@cachedproperty
2344
2304
def indirect_subscribers(self):
2586
2546
# Transaction.iterSelect() will try to listify the results.
2587
2547
# This can be fixed by selecting from Bugs directly, but
2588
2548
# that's non-trivial.
2589
# ---: Robert Collins 2010-08-18: if bug_tasks implements IResultSet
2549
# ---: Robert Collins 20100818: if bug_tasks implements IResultSset
2590
2550
# then it should be very possible to improve on it, though
2591
2551
# DecoratedResultSets would need careful handling (e.g. type
2592
2552
# driven callbacks on columns)
2677
2637
def asDict(self):
2678
2638
"""Return the FileBugData instance as a dict."""
2679
2639
return self.__dict__.copy()
2682
class BugMute(StormBase):
2683
"""Contains bugs a person has decided to block notifications from."""
2685
implements(IBugMute)
2687
__storm_table__ = "BugMute"
2689
def __init__(self, person=None, bug=None):
2690
if person is not None:
2691
self.person = person
2693
self.bug_id = bug.id
2695
person_id = Int("person", allow_none=False, validator=validate_person)
2696
person = Reference(person_id, "Person.id")
2698
bug_id = Int("bug", allow_none=False)
2699
bug = Reference(bug_id, "Bug.id")
2701
__storm_primary__ = 'person_id', 'bug_id'
2703
date_created = DateTime(
2704
"date_created", allow_none=False, default=UTC_NOW,