288
291
raise AssertionError("Unable to determine bugtask target.")
291
def bug_target_to_key(target):
292
"""Returns the DB column values for an IBugTarget."""
298
sourcepackagename=None,
300
if IProduct.providedBy(target):
301
values['product'] = target
302
elif IProductSeries.providedBy(target):
303
values['productseries'] = target
304
elif IDistribution.providedBy(target):
305
values['distribution'] = target
306
elif IDistroSeries.providedBy(target):
307
values['distroseries'] = target
308
elif IDistributionSourcePackage.providedBy(target):
309
values['distribution'] = target.distribution
310
values['sourcepackagename'] = target.sourcepackagename
311
elif ISourcePackage.providedBy(target):
312
values['distroseries'] = target.distroseries
313
values['sourcepackagename'] = target.sourcepackagename
315
raise AssertionError("Not an IBugTarget.")
319
294
class BugTaskDelta:
320
295
"""See `IBugTaskDelta`."""
322
297
implements(IBugTaskDelta)
324
299
def __init__(self, bugtask, status=None, importance=None,
325
assignee=None, milestone=None, bugwatch=None, target=None):
300
assignee=None, milestone=None, statusexplanation=None,
301
bugwatch=None, target=None):
326
302
self.bugtask = bugtask
328
304
self.assignee = assignee
330
306
self.importance = importance
331
307
self.milestone = milestone
332
308
self.status = status
309
self.statusexplanation = statusexplanation
333
310
self.target = target
314
"""Mix-in class for some property methods of IBugTask implementations."""
317
def bug_subscribers(self):
318
"""See `IBugTask`."""
320
chain(self.bug.getDirectSubscribers(),
321
self.bug.getIndirectSubscribers()))
324
def bugtargetdisplayname(self):
325
"""See `IBugTask`."""
326
return self.target.bugtargetdisplayname
329
def bugtargetname(self):
330
"""See `IBugTask`."""
331
return self.target.bugtargetname
335
"""See `IBugTask`."""
336
# We explicitly reference attributes here (rather than, say,
337
# IDistroBugTask.providedBy(self)), because we can't assume this
338
# task has yet been marked with the correct interface.
339
return determine_target(
340
self.product, self.productseries, self.distribution,
341
self.distroseries, self.sourcepackagename)
344
def related_tasks(self):
345
"""See `IBugTask`."""
347
task for task in self.bug.bugtasks if task != self]
353
"""See `IBugTask`."""
354
if self.product is not None:
356
elif self.productseries is not None:
357
return self.productseries.product
358
elif self.distribution is not None:
359
return self.distribution
361
return self.distroseries.distribution
364
def other_affected_pillars(self):
365
"""See `IBugTask`."""
367
this_pillar = self.pillar
368
for task in self.bug.bugtasks:
369
that_pillar = task.pillar
370
if that_pillar != this_pillar:
371
result.add(that_pillar)
372
return sorted(result, key=pillar_sort_key)
336
375
def BugTaskToBugAdapter(bugtask):
337
376
"""Adapt an IBugTask to an IBug."""
338
377
return bugtask.bug
380
@block_implicit_flushes
381
def validate_target_attribute(self, attr, value):
382
"""Update the targetnamecache."""
383
# Don't update targetnamecache during _init().
384
if self._SO_creating:
386
# Determine the new target attributes.
387
target_params = dict(
388
product=self.product,
389
productseries=self.productseries,
390
sourcepackagename=self.sourcepackagename,
391
distribution=self.distribution,
392
distroseries=self.distroseries)
393
utility_iface_dict = {
394
'productID': IProductSet,
395
'productseriesID': IProductSeriesSet,
396
'sourcepackagenameID': ISourcePackageNameSet,
397
'distributionID': IDistributionSet,
398
'distroseriesID': IDistroSeriesSet,
400
utility_iface = utility_iface_dict[attr]
402
target_params[attr[:-2]] = None
404
target_params[attr[:-2]] = getUtility(utility_iface).get(value)
406
# Update the target name cache with the potential new target. The
407
# attribute changes haven't been made yet, so we need to calculate the
409
self.updateTargetNameCache(determine_target(**target_params))
341
414
class PassthroughValue:
342
415
"""A wrapper to allow setting values on conjoined bug tasks."""
353
426
if isinstance(value, PassthroughValue):
354
427
return value.value
356
# Check to see if the object is being instantiated. This test is specific
357
# to SQLBase. Checking for specific attributes (like self.bug) is
358
# insufficient and fragile.
359
if self._SO_creating:
362
# If this is a conjoined slave then call setattr on the master.
363
# Effectively this means that making a change to the slave will
364
# actually make the change to the master (which will then be passed
365
# down to the slave, of course). This helps to prevent OOPSes when
366
# people try to update the conjoined slave via the API.
367
conjoined_master = self.conjoined_master
368
if conjoined_master is not None:
369
setattr(conjoined_master, attr, value)
372
# If there is a conjoined slave, update that.
429
# If this bugtask has no bug yet, then we are probably being
434
if self._isConjoinedBugTask():
435
raise ConjoinedBugTaskEditError(
436
"This task cannot be edited directly, it should be"
437
" edited through its conjoined_master.")
438
# The conjoined slave is updated before the master one because,
439
# for distro tasks, conjoined_slave does a comparison on
440
# sourcepackagename, and the sourcepackagenames will not match
441
# if the conjoined master is altered before the conjoined slave!
373
442
conjoined_bugtask = self.conjoined_slave
374
443
if conjoined_bugtask:
375
444
setattr(conjoined_bugtask, attr, PassthroughValue(value))
390
459
return validate_person(self, attr, value)
393
def validate_target(bug, target, retarget_existing=True):
394
"""Validate a bugtask target against a bug's existing tasks.
396
Checks that no conflicting tasks already exist, and that the new
397
target's pillar supports the bug's access policy.
399
if bug.getBugTask(target):
401
"A fix for this bug has already been requested for %s"
402
% target.displayname)
404
if (IDistributionSourcePackage.providedBy(target) or
405
ISourcePackage.providedBy(target)):
406
# If the distribution has at least one series, check that the
407
# source package has been published in the distribution.
408
if (target.sourcepackagename is not None and
409
len(target.distribution.series) > 0):
411
target.distribution.guessPublishedSourcePackageName(
412
target.sourcepackagename.name)
413
except NotFoundError, e:
414
raise IllegalTarget(e[0])
416
if bug.private and not bool(features.getFeatureFlag(
417
'disclosure.allow_multipillar_private_bugs.enabled')):
418
# Perhaps we are replacing the one and only existing bugtask, in
419
# which case that's ok.
420
if retarget_existing and len(bug.bugtasks) <= 1:
422
# We can add a target so long as the pillar exists already.
423
if (len(bug.affected_pillars) > 0
424
and target.pillar not in bug.affected_pillars):
426
"This private bug already affects %s. "
427
"Private bugs cannot affect multiple projects."
428
% bug.default_bugtask.target.bugtargetdisplayname)
430
if (bug.access_policy is not None and
431
bug.access_policy.pillar != target.pillar and
432
not getUtility(IAccessPolicySource).getByPillarAndType(
433
target.pillar, bug.access_policy.type)):
435
"%s doesn't have a %s access policy."
436
% (target.pillar.displayname, bug.access_policy.type.title))
439
def validate_new_target(bug, target):
440
"""Validate a bugtask target to be added.
442
Make sure that the isn't already a distribution task without a
443
source package, or that such task is added only when the bug doesn't
444
already have any tasks for the distribution.
446
The same checks as `validate_target` does are also done.
448
if IDistribution.providedBy(target):
449
# Prevent having a task on only the distribution if there's at
450
# least one task already on the distribution, whether or not
451
# that task also has a source package.
452
distribution_tasks_for_bug = [
454
in shortlist(bug.bugtasks, longest_expected=50)
455
if bugtask.distribution == target]
457
if len(distribution_tasks_for_bug) > 0:
459
"This bug is already on %s. Please specify an "
460
"affected package in which the bug has not yet "
461
"been reported." % target.displayname)
462
elif IDistributionSourcePackage.providedBy(target):
463
# Ensure that there isn't already a generic task open on the
464
# distribution for this bug, because if there were, that task
465
# should be reassigned to the sourcepackage, rather than a new
467
if bug.getBugTask(target.distribution) is not None:
469
"This bug is already open on %s with no package "
470
"specified. You should fill in a package name for "
471
"the existing bug." % target.distribution.displayname)
473
validate_target(bug, target, retarget_existing=False)
476
class BugTask(SQLBase):
462
@block_implicit_flushes
463
def validate_sourcepackagename(self, attr, value):
464
is_passthrough = isinstance(value, PassthroughValue)
465
value = validate_conjoined_attribute(self, attr, value)
466
if not is_passthrough:
467
self._syncSourcePackages(value)
468
return validate_target_attribute(self, attr, value)
471
class BugTask(SQLBase, BugTaskMixin):
477
472
"""See `IBugTask`."""
478
473
implements(IBugTask)
479
474
_table = "BugTask"
480
475
_defaultOrder = ['distribution', 'product', 'productseries',
481
476
'distroseries', 'milestone', 'sourcepackagename']
482
477
_CONJOINED_ATTRIBUTES = (
483
"_status", "importance", "assigneeID", "milestoneID",
478
"status", "importance", "assigneeID", "milestoneID",
484
479
"date_assigned", "date_confirmed", "date_inprogress",
485
480
"date_closed", "date_incomplete", "date_left_new",
486
481
"date_triaged", "date_fix_committed", "date_fix_released",
487
482
"date_left_closed")
488
483
_NON_CONJOINED_STATUSES = (BugTaskStatus.WONTFIX, )
490
_inhibit_target_check = False
492
485
bug = ForeignKey(dbName='bug', foreignKey='Bug', notNull=True)
493
486
product = ForeignKey(
494
487
dbName='product', foreignKey='Product',
495
notNull=False, default=None)
488
notNull=False, default=None,
489
storm_validator=validate_target_attribute)
496
490
productseries = ForeignKey(
497
491
dbName='productseries', foreignKey='ProductSeries',
498
notNull=False, default=None)
492
notNull=False, default=None,
493
storm_validator=validate_target_attribute)
499
494
sourcepackagename = ForeignKey(
500
495
dbName='sourcepackagename', foreignKey='SourcePackageName',
501
notNull=False, default=None)
496
notNull=False, default=None,
497
storm_validator=validate_sourcepackagename)
502
498
distribution = ForeignKey(
503
499
dbName='distribution', foreignKey='Distribution',
504
notNull=False, default=None)
500
notNull=False, default=None,
501
storm_validator=validate_target_attribute)
505
502
distroseries = ForeignKey(
506
503
dbName='distroseries', foreignKey='DistroSeries',
507
notNull=False, default=None)
504
notNull=False, default=None,
505
storm_validator=validate_target_attribute)
508
506
milestone = ForeignKey(
509
507
dbName='milestone', foreignKey='Milestone',
510
508
notNull=False, default=None,
511
509
storm_validator=validate_conjoined_attribute)
513
511
dbName='status', notNull=True,
514
schema=(BugTaskStatus, BugTaskStatusSearch),
512
schema=BugTaskStatus,
515
513
default=BugTaskStatus.NEW,
516
514
storm_validator=validate_status)
515
statusexplanation = StringCol(dbName='statusexplanation', default=None)
517
516
importance = EnumCol(
518
517
dbName='importance', notNull=True,
519
518
schema=BugTaskImportance,
723
641
package has to be changed, as well as the corresponding
726
if self.bug is None or not (self.distribution or self.distroseries):
727
# The validator is being called on a new or non-distro task.
645
# The validator is being called on an incomplete bug task.
729
distribution = self.distribution or self.distroseries.distribution
730
for bugtask in self.related_tasks:
732
bugtask.sourcepackagename == self.sourcepackagename and
734
bugtask.distribution,
735
getattr(bugtask.distroseries, 'distribution', None)))
737
key = bug_target_to_key(bugtask.target)
738
key['sourcepackagename'] = new_spn
739
bugtask.transitionToTarget(
740
bug_target_from_key(**key),
741
_sync_sourcepackages=False)
647
if self.distroseries is not None:
648
distribution = self.distroseries.distribution
650
distribution = self.distribution
651
if distribution is not None:
652
for bugtask in self.related_tasks:
653
if bugtask.distroseries:
654
related_distribution = bugtask.distroseries.distribution
656
related_distribution = bugtask.distribution
657
if (related_distribution == distribution and
658
bugtask.sourcepackagenameID == self.sourcepackagenameID):
659
bugtask.sourcepackagenameID = PassthroughValue(new_spnid)
743
661
def getContributorInfo(self, user, person):
744
662
"""See `IBugTask`."""
870
813
raise ValueError('Unknown debbugs severity "%s".' % severity)
871
814
return self.importance
873
# START TEMPORARY BIT FOR BUGTASK AUTOCONFIRM FEATURE FLAG.
874
_parse_launchpad_names = re.compile(r"[a-z0-9][a-z0-9\+\.\-]+").findall
876
def _checkAutoconfirmFeatureFlag(self):
877
"""Does a feature flag enable automatic switching of our bugtasks?"""
878
# This method should be ripped out if we determine that we like
879
# this behavior for all projects.
880
# This is a bit of a feature flag hack, but has been discussed as
881
# a reasonable way to deploy this quickly.
883
if IDistribution.providedBy(pillar):
884
flag_name = 'bugs.autoconfirm.enabled_distribution_names'
886
assert IProduct.providedBy(pillar), 'unexpected pillar'
887
flag_name = 'bugs.autoconfirm.enabled_product_names'
888
enabled = features.getFeatureFlag(flag_name)
891
if (enabled.strip() != '*' and
892
pillar.name not in self._parse_launchpad_names(enabled)):
893
# We are not generically enabled ('*') and our pillar's name
894
# is not explicitly enabled.
897
# END TEMPORARY BIT FOR BUGTASK AUTOCONFIRM FEATURE FLAG.
899
def maybeConfirm(self):
900
"""Maybe confirm this bugtask.
901
Only call this if the bug._shouldConfirmBugtasks().
902
This adds the further constraint that the bugtask needs to be NEW,
903
and not imported from an external bug tracker.
905
if (self.status == BugTaskStatus.NEW
906
and self.bugwatch is None
907
# START TEMPORARY BIT FOR BUGTASK AUTOCONFIRM FEATURE FLAG.
908
and self._checkAutoconfirmFeatureFlag()
909
# END TEMPORARY BIT FOR BUGTASK AUTOCONFIRM FEATURE FLAG.
911
janitor = getUtility(ILaunchpadCelebrities).janitor
912
bugtask_before_modification = Snapshot(
913
self, providing=providedBy(self))
914
# Create a bug message explaining why the janitor auto-confirmed
916
msg = ("Status changed to 'Confirmed' because the bug "
917
"affects multiple users.")
918
self.bug.newMessage(owner=janitor, content=msg)
919
self.transitionToStatus(BugTaskStatus.CONFIRMED, janitor)
920
notify(ObjectModifiedEvent(
921
self, bugtask_before_modification, ['status'], user=janitor))
923
816
def canTransitionToStatus(self, new_status, user):
924
817
"""See `IBugTask`."""
925
new_status = normalize_bugtask_status(new_status)
818
celebrities = getUtility(ILaunchpadCelebrities)
926
819
if (self.status == BugTaskStatus.FIXRELEASED and
927
820
(user.id == self.bug.ownerID or user.inTeam(self.bug.owner))):
929
elif self.userHasBugSupervisorPrivileges(user):
822
elif (user.inTeam(self.pillar.bug_supervisor) or
823
user.inTeam(self.pillar.owner) or
824
user.id == celebrities.bug_watch_updater.id or
825
user.id == celebrities.bug_importer.id or
826
user.id == celebrities.janitor.id):
932
829
return (self.status not in (
1138
1038
get_property_cache(self.bug)._known_viewers = set(
1139
1039
[self.assignee.id])
1141
def validateTransitionToTarget(self, target):
1142
"""See `IBugTask`."""
1143
from lp.registry.model.distroseries import DistroSeries
1145
# Check if any series are involved. You can't retarget series
1146
# tasks. Except for DistroSeries/SourcePackage tasks, which can
1147
# only be retargetted to another SourcePackage in the same
1148
# DistroSeries, or the DistroSeries.
1149
interfaces = set(providedBy(target))
1150
interfaces.update(providedBy(self.target))
1151
if IProductSeries in interfaces:
1152
raise IllegalTarget(
1153
"Series tasks may only be created by approving nominations.")
1154
elif interfaces.intersection((IDistroSeries, ISourcePackage)):
1156
for potential_target in (target, self.target):
1157
if IDistroSeries.providedBy(potential_target):
1158
series.add(potential_target)
1159
elif ISourcePackage.providedBy(potential_target):
1160
series.add(potential_target.distroseries)
1164
if len(series) != 1:
1165
raise IllegalTarget(
1166
"Distribution series tasks may only be retargeted "
1167
"to a package within the same series.")
1168
# Because of the mildly insane way that DistroSeries nominations
1169
# work (they affect all Distributions and
1170
# DistributionSourcePackages), we can't sensibly allow
1171
# pillar changes to/from distributions with series tasks on this
1172
# bug. That would require us to create or delete tasks.
1173
# Changing just the sourcepackagename is OK, though, as a
1174
# validator on sourcepackagename will change all related tasks.
1175
elif interfaces.intersection(
1176
(IDistribution, IDistributionSourcePackage)):
1177
# Work out the involved distros (will include None if there
1178
# are product tasks).
1180
for potential_target in (target, self.target):
1181
if IDistribution.providedBy(potential_target.pillar):
1182
distros.add(potential_target.pillar)
1185
if len(distros) > 1:
1186
# Multiple distros involved. Check that none of their
1187
# series have tasks on this bug.
1188
if not Store.of(self).find(
1190
BugTask.bugID == self.bugID,
1191
BugTask.distroseriesID == DistroSeries.id,
1192
DistroSeries.distributionID.is_in(
1193
distro.id for distro in distros if distro),
1195
raise IllegalTarget(
1196
"Distribution tasks with corresponding series "
1197
"tasks may only be retargeted to a different "
1200
validate_target(self.bug, target)
1202
def transitionToTarget(self, target, _sync_sourcepackages=True):
1041
def transitionToTarget(self, target):
1203
1042
"""See `IBugTask`.
1205
If _sync_sourcepackages is True (the default) and the
1206
sourcepackagename is being changed, any other tasks for the same
1207
name in this distribution will have their names updated to
1208
match. This should only be used by _syncSourcePackages.
1044
This method allows changing the target of some bug
1045
tasks. The rules it follows are similar to the ones
1046
enforced implicitly by the code in
1047
lib/canonical/launchpad/browser/bugtask.py#BugTaskEditView.
1210
if self.target == target:
1213
self.validateTransitionToTarget(target)
1215
1050
target_before_change = self.target
1217
1052
if (self.milestone is not None and
1218
self.milestone.target != target.pillar):
1053
self.milestone.target != target):
1219
1054
# If the milestone for this bugtask is set, we
1220
1055
# have to make sure that it's a milestone of the
1221
1056
# current target, or reset it to None
1222
1057
self.milestone = None
1224
new_key = bug_target_to_key(target)
1226
# As a special case, if the sourcepackagename has changed then
1227
# we update any other tasks for the same distribution and
1228
# sourcepackagename. This keeps series tasks consistent.
1229
if (_sync_sourcepackages and
1230
new_key['sourcepackagename'] != self.sourcepackagename):
1231
self._syncSourcePackages(new_key['sourcepackagename'])
1233
for name, value in new_key.iteritems():
1234
setattr(self, name, value)
1235
self.updateTargetNameCache()
1237
# If there's a policy set and we're changing to a another
1238
# pillar, recalculate the access policy.
1239
if (self.bug.access_policy is not None and
1240
self.bug.access_policy.pillar != target.pillar):
1241
self.bug.setAccessPolicy(self.bug.access_policy.type)
1059
if IUpstreamBugTask.providedBy(self):
1060
if IProduct.providedBy(target):
1061
self.product = target
1063
raise IllegalTarget(
1064
"Upstream bug tasks may only be re-targeted "
1065
"to another project.")
1067
if (IDistributionSourcePackage.providedBy(target) and
1068
(target.distribution == self.target or
1069
target.distribution == self.target.distribution)):
1070
self.sourcepackagename = target.sourcepackagename
1072
raise IllegalTarget(
1073
"Distribution bug tasks may only be re-targeted "
1074
"to a package in the same distribution.")
1243
1076
# After the target has changed, we need to recalculate the maximum bug
1244
1077
# heat for the new and old targets.
1245
1078
if self.target != target_before_change:
1246
1079
target_before_change.recalculateBugHeatCache()
1247
1080
self.target.recalculateBugHeatCache()
1248
# START TEMPORARY BIT FOR BUGTASK AUTOCONFIRM FEATURE FLAG.
1249
# We also should see if we ought to auto-transition to the
1251
if self.bug.shouldConfirmBugtasks():
1253
# END TEMPORARY BIT FOR BUGTASK AUTOCONFIRM FEATURE FLAG.
1255
1082
def updateTargetNameCache(self, newtarget=None):
1256
1083
"""See `IBugTask`."""
1367
def userHasDriverPrivilegesContext(cls, context, user):
1368
"""Does the user have driver privileges for the given context?
1374
role = IPersonRoles(user)
1375
# Admins can always change bug details.
1379
# Similar to admins, the Bug Watch Updater, Bug Importer and
1380
# Janitor can always change bug details.
1382
role.in_bug_watch_updater or role.in_bug_importer or
1386
# If you're the owner or a driver, you can change bug details.
1388
role.isOwner(context.pillar) or role.isOneOfDrivers(context))
1391
def userHasBugSupervisorPrivilegesContext(cls, context, user):
1392
"""Does the user have bug supervisor privileges for the given
1399
role = IPersonRoles(user)
1400
# If you have driver privileges, or are the bug supervisor, you can
1401
# change bug details.
1403
cls.userHasDriverPrivilegesContext(context, user) or
1404
role.isBugSupervisor(context.pillar))
1406
def userHasDriverPrivileges(self, user):
1407
"""See `IBugTask`."""
1408
return self.userHasDriverPrivilegesContext(self.target, user)
1410
def userHasBugSupervisorPrivileges(self, user):
1411
"""See `IBugTask`."""
1412
return self.userHasBugSupervisorPrivilegesContext(self.target, user)
1212
def _userIsPillarEditor(self, user):
1213
"""Can the user edit this tasks's pillar?"""
1216
if IUpstreamBugTask.providedBy(self):
1217
pillar = self.product
1218
elif IProductSeriesBugTask.providedBy(self):
1219
pillar = self.productseries.product
1220
elif IDistroBugTask.providedBy(self):
1221
pillar = self.distribution
1223
pillar = self.distroseries.distribution
1224
return ((pillar.bug_supervisor is not None and
1225
user.inTeam(pillar.bug_supervisor)) or
1226
pillar.userCanEdit(user))
1228
def userCanEditMilestone(self, user):
1229
"""See `IBugTask`."""
1230
return self._userIsPillarEditor(user)
1232
def userCanEditImportance(self, user):
1233
"""See `IBugTask`."""
1234
celebs = getUtility(ILaunchpadCelebrities)
1235
return (self._userIsPillarEditor(user) or
1236
user == celebs.bug_watch_updater or
1237
user == celebs.bug_importer)
1414
1239
def __repr__(self):
1415
1240
return "<BugTask for bug %s on %r>" % (self.bugID, self.target)
1493
1318
# part of the WHERE condition (i.e. the bit below.) The
1494
1319
# other half of this condition (see code above) does not
1495
1320
# use TeamParticipation at all.
1496
pillar_privacy_filters = ''
1497
if features.getFeatureFlag(
1498
'disclosure.private_bug_visibility_cte.enabled'):
1499
if features.getFeatureFlag(
1500
'disclosure.private_bug_visibility_rules.enabled'):
1501
pillar_privacy_filters = """
1504
FROM BugTask, Product
1505
WHERE Product.owner IN (SELECT team FROM teams) AND
1506
BugTask.product = Product.id AND
1507
BugTask.bug = Bug.id AND
1508
Bug.security_related IS False
1511
FROM BugTask, ProductSeries
1512
WHERE ProductSeries.owner IN (SELECT team FROM teams) AND
1513
BugTask.productseries = ProductSeries.id AND
1514
BugTask.bug = Bug.id AND
1515
Bug.security_related IS False
1518
FROM BugTask, Distribution
1519
WHERE Distribution.owner IN (SELECT team FROM teams) AND
1520
BugTask.distribution = Distribution.id AND
1521
BugTask.bug = Bug.id AND
1522
Bug.security_related IS False
1525
FROM BugTask, DistroSeries, Distribution
1526
WHERE Distribution.owner IN (SELECT team FROM teams) AND
1527
DistroSeries.distribution = Distribution.id AND
1528
BugTask.distroseries = DistroSeries.id AND
1529
BugTask.bug = Bug.id AND
1530
Bug.security_related IS False
1533
(Bug.private = FALSE OR EXISTS (
1535
SELECT team from TeamParticipation
1536
WHERE person = %(personid)s
1538
SELECT BugSubscription.bug
1539
FROM BugSubscription
1540
WHERE BugSubscription.person IN (SELECT team FROM teams) AND
1541
BugSubscription.bug = Bug.id
1545
WHERE BugTask.assignee IN (SELECT team FROM teams) AND
1546
BugTask.bug = Bug.id
1550
personid=quote(user.id),
1551
extra_filters=pillar_privacy_filters)
1553
if features.getFeatureFlag(
1554
'disclosure.private_bug_visibility_rules.enabled'):
1555
pillar_privacy_filters = """
1558
FROM BugTask, TeamParticipation, Product
1559
WHERE TeamParticipation.person = %(personid)s AND
1560
TeamParticipation.team = Product.owner AND
1561
BugTask.product = Product.id AND
1562
BugTask.bug = Bug.id AND
1563
Bug.security_related IS False
1566
FROM BugTask, TeamParticipation, ProductSeries
1567
WHERE TeamParticipation.person = %(personid)s AND
1568
TeamParticipation.team = ProductSeries.owner AND
1569
BugTask.productseries = ProductSeries.id AND
1570
BugTask.bug = Bug.id AND
1571
Bug.security_related IS False
1574
FROM BugTask, TeamParticipation, Distribution
1575
WHERE TeamParticipation.person = %(personid)s AND
1576
TeamParticipation.team = Distribution.owner AND
1577
BugTask.distribution = Distribution.id AND
1578
BugTask.bug = Bug.id AND
1579
Bug.security_related IS False
1582
FROM BugTask, TeamParticipation, DistroSeries, Distribution
1583
WHERE TeamParticipation.person = %(personid)s AND
1584
TeamParticipation.team = Distribution.owner AND
1585
DistroSeries.distribution = Distribution.id AND
1586
BugTask.distroseries = DistroSeries.id AND
1587
BugTask.bug = Bug.id AND
1588
Bug.security_related IS False
1589
""" % sqlvalues(personid=user.id)
1591
(Bug.private = FALSE OR EXISTS (
1592
SELECT BugSubscription.bug
1593
FROM BugSubscription, TeamParticipation
1594
WHERE TeamParticipation.person = %(personid)s AND
1595
TeamParticipation.team = BugSubscription.person AND
1596
BugSubscription.bug = Bug.id
1599
FROM BugTask, TeamParticipation
1600
WHERE TeamParticipation.person = %(personid)s AND
1601
TeamParticipation.team = BugTask.assignee AND
1602
BugTask.bug = Bug.id
1606
personid=quote(user.id),
1607
extra_filters=pillar_privacy_filters)
1608
return query, _make_cache_user_can_view_bug(user)
1322
(Bug.private = FALSE OR EXISTS (
1323
SELECT BugSubscription.bug
1324
FROM BugSubscription, TeamParticipation
1325
WHERE TeamParticipation.person = %(personid)s AND
1326
TeamParticipation.team = BugSubscription.person AND
1327
BugSubscription.bug = Bug.id
1330
FROM BugTask, TeamParticipation
1331
WHERE TeamParticipation.person = %(personid)s AND
1332
TeamParticipation.team = BugTask.assignee AND
1333
BugTask.bug = Bug.id
1335
""" % sqlvalues(personid=user.id),
1336
_make_cache_user_can_view_bug(user))
1611
1339
def build_tag_set_query(joiner, tags):
1861
1589
summary, Bug, ' AND '.join(constraint_clauses), ['BugTask'])
1862
1590
return self.search(search_params, _noprejoins=True)
1865
def _buildStatusClause(cls, status):
1592
def _buildStatusClause(self, status):
1866
1593
"""Return the SQL query fragment for search by status.
1868
1595
Called from `buildQuery` or recursively."""
1869
1596
if zope_isinstance(status, any):
1870
values = list(status.query_values)
1871
# Since INCOMPLETE isn't stored as a single value we need to
1872
# expand it before generating the SQL.
1873
if BugTaskStatus.INCOMPLETE in values:
1874
values.remove(BugTaskStatus.INCOMPLETE)
1875
values.extend(DB_INCOMPLETE_BUGTASK_STATUSES)
1876
return '(BugTask.status {0})'.format(
1877
search_value_to_where_condition(any(*values)))
1597
return '(' + ' OR '.join(
1598
self._buildStatusClause(dbitem)
1600
in status.query_values) + ')'
1878
1601
elif zope_isinstance(status, not_equals):
1879
return '(NOT {0})'.format(cls._buildStatusClause(status.value))
1602
return '(NOT %s)' % self._buildStatusClause(status.value)
1880
1603
elif zope_isinstance(status, BaseItem):
1881
# INCOMPLETE is not stored in the DB, instead one of
1882
# DB_INCOMPLETE_BUGTASK_STATUSES is stored, so any request to
1883
# search for INCOMPLETE should instead search for those values.
1884
if status == BugTaskStatus.INCOMPLETE:
1885
return '(BugTask.status {0})'.format(
1886
search_value_to_where_condition(
1887
any(*DB_INCOMPLETE_BUGTASK_STATUSES)))
1605
status == BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE)
1606
without_response = (
1607
status == BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE)
1608
if with_response or without_response:
1610
'(BugTask.status = %s) ' %
1611
sqlvalues(BugTaskStatus.INCOMPLETE))
1613
status_clause += ("""
1614
AND (Bug.date_last_message IS NOT NULL
1615
AND BugTask.date_incomplete <=
1616
Bug.date_last_message)
1618
elif without_response:
1619
status_clause += ("""
1620
AND (Bug.date_last_message IS NULL
1621
OR BugTask.date_incomplete >
1622
Bug.date_last_message)
1625
assert with_response != without_response
1626
return status_clause
1889
1628
return '(BugTask.status = %s)' % sqlvalues(status)
1891
raise ValueError('Unrecognized status value: %r' % (status,))
1630
raise AssertionError(
1631
'Unrecognized status value: %s' % repr(status))
1893
1633
def _buildExcludeConjoinedClause(self, milestone):
1894
1634
"""Exclude bugtasks with a conjoined master.
2105
1843
sqlvalues(personid=params.subscriber.id))
2107
1845
if params.structural_subscriber is not None:
2108
# See bug 787294 for the story that led to the query elements
2109
# below. Please change with care.
2110
with_clauses.append(
2111
'''ss as (SELECT * from StructuralSubscription
2112
WHERE StructuralSubscription.subscriber = %s)'''
1846
ssub_match_product = (
1847
BugTask.productID ==
1848
StructuralSubscription.productID)
1849
ssub_match_productseries = (
1850
BugTask.productseriesID ==
1851
StructuralSubscription.productseriesID)
1852
# Prevent circular import problems.
1853
from lp.registry.model.product import Product
1854
ssub_match_project = And(
1855
Product.projectID ==
1856
StructuralSubscription.projectID,
1857
BugTask.product == Product.id)
1858
ssub_match_distribution = (
1859
BugTask.distributionID ==
1860
StructuralSubscription.distributionID)
1861
ssub_match_sourcepackagename = (
1862
BugTask.sourcepackagenameID ==
1863
StructuralSubscription.sourcepackagenameID)
1864
ssub_match_null_sourcepackagename = (
1865
StructuralSubscription.sourcepackagename == None)
1866
ssub_match_distribution_with_optional_package = And(
1867
ssub_match_distribution, Or(
1868
ssub_match_sourcepackagename,
1869
ssub_match_null_sourcepackagename))
1870
ssub_match_distribution_series = (
1871
BugTask.distroseriesID ==
1872
StructuralSubscription.distroseriesID)
1873
ssub_match_milestone = (
1874
BugTask.milestoneID ==
1875
StructuralSubscription.milestoneID)
1879
ssub_match_productseries,
1881
ssub_match_distribution_with_optional_package,
1882
ssub_match_distribution_series,
1883
ssub_match_milestone)
1886
(Product, LeftJoin(Product, And(
1887
BugTask.productID == Product.id,
1890
(StructuralSubscription,
1891
Join(StructuralSubscription, join_clause)))
1892
extra_clauses.append(
1893
'StructuralSubscription.subscriber = %s'
2113
1894
% sqlvalues(params.structural_subscriber))
2114
# Prevent circular import problems.
2115
from lp.registry.model.product import Product
2117
(Product, LeftJoin(Product, And(
2118
BugTask.productID == Product.id,
2124
BugTask.product == SQL('ss1.product'))))
2129
BugTask.productseries == SQL('ss2.productseries'))))
2134
Product.project == SQL('ss3.project'))))
2139
And(BugTask.distribution == SQL('ss4.distribution'),
2140
Or(BugTask.sourcepackagename ==
2141
SQL('ss4.sourcepackagename'),
2142
SQL('ss4.sourcepackagename IS NULL'))))))
2143
if params.distroseries is not None:
2144
parent_distro_id = params.distroseries.distributionID
2146
parent_distro_id = 0
2151
Or(BugTask.distroseries == SQL('ss5.distroseries'),
2152
# There is a mismatch between BugTask and
2153
# StructuralSubscription. SS does not support
2154
# distroseries. This clause works because other
2155
# joins ensure the match bugtask is the right
2157
And(parent_distro_id == SQL('ss5.distribution'),
2158
BugTask.sourcepackagename == SQL(
2159
'ss5.sourcepackagename'))))))
2164
BugTask.milestone == SQL('ss6.milestone'))))
2165
extra_clauses.append(
2167
"ARRAY[ss1.id, ss2.id, ss3.id, ss4.id, ss5.id, ss6.id]"
2169
1895
has_duplicate_results = True
2171
1898
# Remove bugtasks from deactivated products, if necessary.
2172
1899
# We don't have to do this if
2173
1900
# 1) We're searching on bugtasks for a specific product
2244
1969
# is not for subscription to notifications.
2245
1970
# See bug #191809
2246
1971
if params.bug_supervisor:
2247
bug_supervisor_clause = """(
2248
BugTask.product IN (
2249
SELECT id FROM Product
2250
WHERE Product.bug_supervisor = %(bug_supervisor)s)
2252
((BugTask.distribution, Bugtask.sourcepackagename) IN
2253
(SELECT distribution, sourcepackagename FROM
2254
StructuralSubscription
2255
WHERE subscriber = %(bug_supervisor)s))
2257
BugTask.distribution IN (
2258
SELECT id from Distribution WHERE
2259
Distribution.bug_supervisor = %(bug_supervisor)s)
1972
bug_supervisor_clause = """BugTask.id IN (
1973
SELECT BugTask.id FROM BugTask, Product
1974
WHERE BugTask.product = Product.id
1975
AND Product.bug_supervisor = %(bug_supervisor)s
1978
FROM BugTask, StructuralSubscription
1980
BugTask.distribution = StructuralSubscription.distribution
1981
AND BugTask.sourcepackagename =
1982
StructuralSubscription.sourcepackagename
1983
AND StructuralSubscription.subscriber = %(bug_supervisor)s
1985
SELECT BugTask.id FROM BugTask, Distribution
1986
WHERE BugTask.distribution = Distribution.id
1987
AND Distribution.bug_supervisor = %(bug_supervisor)s
2260
1988
)""" % sqlvalues(bug_supervisor=params.bug_supervisor)
2261
1989
extra_clauses.append(bug_supervisor_clause)
2267
1995
extra_clauses.append(bug_reporter_clause)
2269
1997
if params.bug_commenter:
2270
bug_commenter_clause = """
1998
bugmessage_owner = bool(features.getFeatureFlag(
1999
'malone.bugmessage_owner'))
2000
bug_commenter_old_clause = """
2002
SELECT DISTINCT BugTask.id FROM BugTask, BugMessage, Message
2003
WHERE Message.owner = %(bug_commenter)s
2004
AND Message.id = BugMessage.message
2005
AND BugTask.bug = BugMessage.bug
2006
AND BugMessage.index > 0
2008
""" % sqlvalues(bug_commenter=params.bug_commenter)
2009
bug_commenter_new_clause = """
2271
2010
Bug.id IN (SELECT DISTINCT bug FROM Bugmessage WHERE
2272
2011
BugMessage.index > 0 AND BugMessage.owner = %(bug_commenter)s)
2273
2012
""" % sqlvalues(bug_commenter=params.bug_commenter)
2013
if bugmessage_owner:
2014
bug_commenter_clause = bug_commenter_new_clause
2016
bug_commenter_clause = bug_commenter_old_clause
2274
2017
extra_clauses.append(bug_commenter_clause)
2276
2019
if params.affects_me:
2277
2020
params.affected_user = params.user
2278
2021
if params.affected_user:
2280
(BugAffectsPerson, Join(
2281
BugAffectsPerson, And(
2282
BugTask.bugID == BugAffectsPerson.bugID,
2283
BugAffectsPerson.affected,
2284
BugAffectsPerson.person == params.affected_user))))
2022
affected_user_clause = """
2024
SELECT BugTask.id FROM BugTask, BugAffectsPerson
2025
WHERE BugTask.bug = BugAffectsPerson.bug
2026
AND BugAffectsPerson.person = %(affected_user)s
2027
AND BugAffectsPerson.affected = TRUE
2029
""" % sqlvalues(affected_user=params.affected_user)
2030
extra_clauses.append(affected_user_clause)
2286
2032
if params.nominated_for:
2287
2033
mappings = sqlvalues(
2784
2524
"""See `IBugTaskSet`."""
2785
2525
return self._search(BugTask.bugID, [], None, params).result_set
2787
def countBugs(self, user, contexts, group_on):
2527
def countBugs(self, params, group_on):
2788
2528
"""See `IBugTaskSet`."""
2790
from lp.bugs.model.bugsummary import BugSummary
2794
BugSummary.status.is_in(DB_UNRESOLVED_BUGTASK_STATUSES))
2795
# BugSummary does not include duplicates so no need to exclude.
2796
context_conditions = []
2797
for context in contexts:
2798
condition = removeSecurityProxy(
2799
context.getBugSummaryContextWhereClause())
2800
if condition is not False:
2801
context_conditions.append(condition)
2802
if not context_conditions:
2804
conditions.append(Or(*context_conditions))
2805
# bugsummary by design requires either grouping by tag or excluding
2807
# This is an awkward way of saying
2808
# if BugSummary.tag not in group_on:
2810
group_on_tag = False
2811
for column in group_on:
2812
if column is BugSummary.tag:
2814
if not group_on_tag:
2815
conditions.append(BugSummary.tag == None)
2817
conditions.append(BugSummary.tag != None)
2818
store = IStore(BugSummary)
2819
admin_team = getUtility(ILaunchpadCelebrities).admin
2820
if user is not None and not user.inTeam(admin_team):
2821
# admins get to see every bug, everyone else only sees bugs
2822
# viewable by them-or-their-teams.
2823
store = store.with_(SQL(
2825
"SELECT team from TeamParticipation WHERE person=?)",
2827
# Note that because admins can see every bug regardless of
2828
# subscription they will see rather inflated counts. Admins get to
2831
conditions.append(BugSummary.viewed_by_id == None)
2832
elif not user.inTeam(admin_team):
2835
BugSummary.viewed_by_id == None,
2836
BugSummary.viewed_by_id.is_in(
2837
SQL("SELECT team FROM teams"))
2839
sum_count = Sum(BugSummary.count)
2840
resultset = store.find(group_on + (sum_count,), *conditions)
2529
resultset = self._search(
2530
group_on + (SQL("COUNT(Distinct BugTask.bug)"),),
2531
[], None, params).result_set
2532
# We group on the related field:
2841
2533
resultset.group_by(*group_on)
2842
resultset.having(sum_count != 0)
2843
# Ensure we have no order clauses.
2844
2534
resultset.order_by()
2846
2536
for row in resultset:
2869
2561
if not milestone:
2870
2562
milestone = None
2872
# Make sure there's no task for this bug already filed
2873
# against the target.
2874
validate_new_target(bug, target)
2876
target_key = bug_target_to_key(target)
2564
# Raise a WidgetError if this product bugtask already exists.
2566
stop_checking = False
2567
if sourcepackagename is not None:
2568
# A source package takes precedence over the distro series
2569
# or distribution in which the source package is found.
2570
if distroseries is not None:
2571
# We'll need to make sure there's no bug task already
2572
# filed against this source package in this
2573
# distribution series.
2574
target = distroseries.getSourcePackage(sourcepackagename)
2575
elif distribution is not None:
2576
# Make sure there's no bug task already filed against
2577
# this source package in this distribution.
2578
validate_new_distrotask(bug, distribution, sourcepackagename)
2579
stop_checking = True
2581
if target is None and not stop_checking:
2582
# This task is not being filed against a source package. Find
2583
# the prospective target.
2584
if productseries is not None:
2585
# Bug filed against a product series.
2586
target = productseries
2587
elif product is not None:
2588
# Bug filed against a product.
2590
elif distroseries is not None:
2591
# Bug filed against a distro series.
2592
target = distroseries
2593
elif distribution is not None and not stop_checking:
2594
# Bug filed against a distribution.
2595
validate_new_distrotask(bug, distribution)
2596
stop_checking = True
2598
if target is not None and not stop_checking:
2599
# Make sure there's no task for this bug already filed
2600
# against the target.
2601
valid_upstreamtask(bug, target)
2877
2603
if not bug.private and bug.security_related:
2878
product = target_key['product']
2879
distribution = target_key['distribution']
2880
2604
if product and product.security_contact:
2881
2605
bug.subscribe(product.security_contact, owner)
2882
2606
elif distribution and distribution.security_contact:
2883
2607
bug.subscribe(distribution.security_contact, owner)
2609
assert (product or productseries or distribution or distroseries), (
2610
'Got no bugtask target.')
2885
2612
non_target_create_params = dict(
2888
2615
importance=importance,
2889
2616
assignee=assignee,
2891
2618
milestone=milestone)
2892
create_params = non_target_create_params.copy()
2893
create_params.update(target_key)
2894
bugtask = BugTask(**create_params)
2895
if target_key['distribution']:
2621
productseries=productseries,
2622
distribution=distribution,
2623
distroseries=distroseries,
2624
sourcepackagename=sourcepackagename,
2625
**non_target_create_params)
2896
2628
# Create tasks for accepted nominations if this is a source
2897
2629
# package addition.
2898
2630
accepted_nominations = [
2899
nomination for nomination in
2900
bug.getNominations(target_key['distribution'])
2631
nomination for nomination in bug.getNominations(distribution)
2901
2632
if nomination.isApproved()]
2902
2633
for nomination in accepted_nominations:
2903
2634
accepted_series_task = BugTask(
2904
2635
distroseries=nomination.distroseries,
2905
sourcepackagename=target_key['sourcepackagename'],
2636
sourcepackagename=sourcepackagename,
2906
2637
**non_target_create_params)
2907
2638
accepted_series_task.updateTargetNameCache()
3209
2935
def getOrderByColumnDBName(self, col_name):
3210
2936
"""See `IBugTaskSet`."""
3211
2937
if BugTaskSet._ORDERBY_COLUMN is None:
3212
# Avoid circular imports.
3213
from lp.bugs.model.bug import (
3217
from lp.registry.model.milestone import Milestone
3218
from lp.registry.model.person import Person
3219
Assignee = ClassAlias(Person)
3220
Reporter = ClassAlias(Person)
2938
# Local import of Bug to avoid import loop.
2939
from lp.bugs.model.bug import Bug
3221
2940
BugTaskSet._ORDERBY_COLUMN = {
3222
"task": (BugTask.id, []),
3223
"id": (BugTask.bugID, []),
3224
"importance": (BugTask.importance, []),
2942
"id": BugTask.bugID,
2943
"importance": BugTask.importance,
3225
2944
# TODO: sort by their name?
3230
LeftJoin(Assignee, BugTask.assignee == Assignee.id))
3232
"targetname": (BugTask.targetnamecache, []),
3233
"status": (BugTask._status, []),
3234
"title": (Bug.title, []),
3235
"milestone": (BugTask.milestoneID, []),
3236
"dateassigned": (BugTask.date_assigned, []),
3237
"datecreated": (BugTask.datecreated, []),
3238
"date_last_updated": (Bug.date_last_updated, []),
3239
"date_closed": (BugTask.date_closed, []),
3240
"number_of_duplicates": (Bug.number_of_duplicates, []),
3241
"message_count": (Bug.message_count, []),
3242
"users_affected_count": (Bug.users_affected_count, []),
3243
"heat": (BugTask.heat, []),
3244
"latest_patch_uploaded": (Bug.latest_patch_uploaded, []),
3250
BugTask.milestone == Milestone.id))
3255
(Bug, Join(Bug, BugTask.bug == Bug.id)),
3256
(Reporter, Join(Reporter, Bug.owner == Reporter.id))
3261
(Bug, Join(Bug, BugTask.bug == Bug.id)),
3265
BugTag.bug == Bug.id and
3266
# We want at most one tag per bug. Select the
3267
# tag that comes first in alphabetic order.
3268
BugTag.id == SQL("""
3269
SELECT id FROM BugTag AS bt
3270
WHERE bt.bug=bug.id ORDER BY bt.name LIMIT 1
3277
(Bug, Join(Bug, BugTask.bug == Bug.id)),
3281
# We want at most one specification per bug.
3282
# Select the specification that comes first
3283
# in alphabetic order.
3284
Specification.id == SQL("""
3285
SELECT Specification.id
3286
FROM SpecificationBug
3288
ON SpecificationBug.specification=
3290
WHERE SpecificationBug.bug=Bug.id
3291
ORDER BY Specification.name
2945
"assignee": BugTask.assigneeID,
2946
"targetname": BugTask.targetnamecache,
2947
"status": BugTask.status,
2949
"milestone": BugTask.milestoneID,
2950
"dateassigned": BugTask.date_assigned,
2951
"datecreated": BugTask.datecreated,
2952
"date_last_updated": Bug.date_last_updated,
2953
"date_closed": BugTask.date_closed,
2954
"number_of_duplicates": Bug.number_of_duplicates,
2955
"message_count": Bug.message_count,
2956
"users_affected_count": Bug.users_affected_count,
2957
"heat": BugTask.heat,
2958
"latest_patch_uploaded": Bug.latest_patch_uploaded,
3297
2960
return BugTaskSet._ORDERBY_COLUMN[col_name]