~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/bugs/model/bugtask.py

  • Committer: Curtis Hovey
  • Date: 2011-05-12 18:25:06 UTC
  • mto: This revision was merged to the branch mainline in revision 13038.
  • Revision ID: curtis.hovey@canonical.com-20110512182506-098n1wovp9m1av59
Renamed licence_reviewed to project_reviewed.

Show diffs side-by-side

added added

removed removed

Lines of Context:
10
10
__all__ = [
11
11
    'BugTaskDelta',
12
12
    'BugTaskToBugAdapter',
 
13
    'BugTaskMixin',
13
14
    'BugTask',
14
15
    'BugTaskSet',
15
16
    'bugtask_sort_key',
16
 
    'bug_target_from_key',
17
 
    'bug_target_to_key',
 
17
    'determine_target',
18
18
    'get_bug_privacy_filter',
19
19
    'get_related_bugtasks_search_params',
20
20
    'search_value_to_where_condition',
21
 
    'validate_new_target',
22
 
    'validate_target',
23
21
    ]
24
22
 
25
23
 
26
24
import datetime
27
25
from itertools import chain
28
26
from operator import attrgetter
29
 
import re
30
27
 
31
28
from lazr.enum import BaseItem
32
 
from lazr.lifecycle.event import (
33
 
    ObjectDeletedEvent,
34
 
    ObjectModifiedEvent,
35
 
    )
36
 
from lazr.lifecycle.snapshot import Snapshot
37
29
import pytz
38
30
from sqlobject import (
39
31
    ForeignKey,
53
45
    Or,
54
46
    Select,
55
47
    SQL,
56
 
    Sum,
57
48
    )
58
49
from storm.info import ClassAlias
59
50
from storm.store import (
61
52
    Store,
62
53
    )
63
54
from zope.component import getUtility
64
 
from zope.event import notify
65
55
from zope.interface import (
 
56
    alsoProvides,
66
57
    implements,
67
 
    providedBy,
68
58
    )
 
59
from zope.interface.interfaces import IMethod
69
60
from zope.security.proxy import (
70
61
    isinstance as zope_isinstance,
71
62
    removeSecurityProxy,
89
80
    DecoratedResultSet,
90
81
    )
91
82
from canonical.launchpad.helpers import shortlist
 
83
from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
92
84
from canonical.launchpad.interfaces.lpstorm import IStore
 
85
from canonical.launchpad.interfaces.validation import (
 
86
    validate_new_distrotask,
 
87
    valid_upstreamtask,
 
88
    )
93
89
from canonical.launchpad.searchbuilder import (
94
90
    all,
95
91
    any,
105
101
    )
106
102
from lp.app.enums import ServiceUsage
107
103
from lp.app.errors import NotFoundError
108
 
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
109
104
from lp.bugs.interfaces.bug import IBugSet
110
105
from lp.bugs.interfaces.bugattachment import BugAttachmentType
111
106
from lp.bugs.interfaces.bugnomination import BugNominationStatus
117
112
    BugTaskSearchParams,
118
113
    BugTaskStatus,
119
114
    BugTaskStatusSearch,
120
 
    CannotDeleteBugtask,
121
 
    DB_INCOMPLETE_BUGTASK_STATUSES,
122
 
    DB_UNRESOLVED_BUGTASK_STATUSES,
123
 
    get_bugtask_status,
 
115
    ConjoinedBugTaskEditError,
124
116
    IBugTask,
125
117
    IBugTaskDelta,
126
118
    IBugTaskSet,
 
119
    IDistroBugTask,
 
120
    IDistroSeriesBugTask,
127
121
    IllegalRelatedBugTasksParams,
128
122
    IllegalTarget,
129
 
    normalize_bugtask_status,
 
123
    IProductSeriesBugTask,
 
124
    IUpstreamBugTask,
130
125
    RESOLVED_BUGTASK_STATUSES,
 
126
    UNRESOLVED_BUGTASK_STATUSES,
131
127
    UserCannotEditBugTaskAssignee,
132
128
    UserCannotEditBugTaskImportance,
133
129
    UserCannotEditBugTaskMilestone,
135
131
    )
136
132
from lp.bugs.model.bugnomination import BugNomination
137
133
from lp.bugs.model.bugsubscription import BugSubscription
138
 
from lp.registry.interfaces.accesspolicy import IAccessPolicySource
 
134
from lp.bugs.model.structuralsubscription import StructuralSubscription
139
135
from lp.registry.interfaces.distribution import (
140
136
    IDistribution,
141
137
    IDistributionSet,
143
139
from lp.registry.interfaces.distributionsourcepackage import (
144
140
    IDistributionSourcePackage,
145
141
    )
146
 
from lp.registry.interfaces.distroseries import IDistroSeries
 
142
from lp.registry.interfaces.distroseries import (
 
143
    IDistroSeries,
 
144
    IDistroSeriesSet,
 
145
    )
147
146
from lp.registry.interfaces.milestone import (
148
147
    IMilestoneSet,
149
148
    IProjectGroupMilestone,
153
152
    validate_person,
154
153
    validate_public_person,
155
154
    )
156
 
from lp.registry.interfaces.product import IProduct
157
 
from lp.registry.interfaces.productseries import IProductSeries
 
155
from lp.registry.interfaces.product import (
 
156
    IProduct,
 
157
    IProductSet,
 
158
    )
 
159
from lp.registry.interfaces.productseries import (
 
160
    IProductSeries,
 
161
    IProductSeriesSet,
 
162
    )
158
163
from lp.registry.interfaces.projectgroup import IProjectGroup
159
 
from lp.registry.interfaces.role import IPersonRoles
160
164
from lp.registry.interfaces.sourcepackage import ISourcePackage
161
165
from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
162
166
from lp.registry.model.pillar import pillar_sort_key
164
168
from lp.services import features
165
169
from lp.services.propertycache import get_property_cache
166
170
from lp.soyuz.enums import PackagePublishingStatus
167
 
from lp.blueprints.model.specification import Specification
168
171
 
169
172
 
170
173
debbugsseveritymap = {
261
264
        raise IllegalRelatedBugTasksParams(
262
265
            ('Cannot search for related tasks to \'%s\', at least one '
263
266
             'of these parameter has to be empty: %s'
264
 
                % (context.name, ", ".join(relevant_fields))))
 
267
                %(context.name, ", ".join(relevant_fields))))
265
268
    return search_params
266
269
 
267
270
 
268
 
def bug_target_from_key(product, productseries, distribution, distroseries,
269
 
                        sourcepackagename):
270
 
    """Returns the IBugTarget defined by the given DB column values."""
 
271
def determine_target(product, productseries, distribution, distroseries,
 
272
                     sourcepackagename):
 
273
    """Returns the IBugTarget defined by the given arguments."""
271
274
    if product:
272
275
        return product
273
276
    elif productseries:
288
291
        raise AssertionError("Unable to determine bugtask target.")
289
292
 
290
293
 
291
 
def bug_target_to_key(target):
292
 
    """Returns the DB column values for an IBugTarget."""
293
 
    values = dict(
294
 
                product=None,
295
 
                productseries=None,
296
 
                distribution=None,
297
 
                distroseries=None,
298
 
                sourcepackagename=None,
299
 
                )
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
314
 
    else:
315
 
        raise AssertionError("Not an IBugTarget.")
316
 
    return values
317
 
 
318
 
 
319
294
class BugTaskDelta:
320
295
    """See `IBugTaskDelta`."""
321
296
 
322
297
    implements(IBugTaskDelta)
323
298
 
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
327
303
 
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
334
311
 
335
312
 
 
313
class BugTaskMixin:
 
314
    """Mix-in class for some property methods of IBugTask implementations."""
 
315
 
 
316
    @property
 
317
    def bug_subscribers(self):
 
318
        """See `IBugTask`."""
 
319
        return tuple(
 
320
            chain(self.bug.getDirectSubscribers(),
 
321
                  self.bug.getIndirectSubscribers()))
 
322
 
 
323
    @property
 
324
    def bugtargetdisplayname(self):
 
325
        """See `IBugTask`."""
 
326
        return self.target.bugtargetdisplayname
 
327
 
 
328
    @property
 
329
    def bugtargetname(self):
 
330
        """See `IBugTask`."""
 
331
        return self.target.bugtargetname
 
332
 
 
333
    @property
 
334
    def target(self):
 
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)
 
342
 
 
343
    @property
 
344
    def related_tasks(self):
 
345
        """See `IBugTask`."""
 
346
        other_tasks = [
 
347
            task for task in self.bug.bugtasks if task != self]
 
348
 
 
349
        return other_tasks
 
350
 
 
351
    @property
 
352
    def pillar(self):
 
353
        """See `IBugTask`."""
 
354
        if self.product is not None:
 
355
            return self.product
 
356
        elif self.productseries is not None:
 
357
            return self.productseries.product
 
358
        elif self.distribution is not None:
 
359
            return self.distribution
 
360
        else:
 
361
            return self.distroseries.distribution
 
362
 
 
363
    @property
 
364
    def other_affected_pillars(self):
 
365
        """See `IBugTask`."""
 
366
        result = set()
 
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)
 
373
 
 
374
 
336
375
def BugTaskToBugAdapter(bugtask):
337
376
    """Adapt an IBugTask to an IBug."""
338
377
    return bugtask.bug
339
378
 
340
379
 
 
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:
 
385
        return value
 
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,
 
399
        }
 
400
    utility_iface = utility_iface_dict[attr]
 
401
    if value is None:
 
402
        target_params[attr[:-2]] = None
 
403
    else:
 
404
        target_params[attr[:-2]] = getUtility(utility_iface).get(value)
 
405
 
 
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
 
408
    # target manually.
 
409
    self.updateTargetNameCache(determine_target(**target_params))
 
410
 
 
411
    return value
 
412
 
 
413
 
341
414
class PassthroughValue:
342
415
    """A wrapper to allow setting values on conjoined bug tasks."""
343
416
 
353
426
    if isinstance(value, PassthroughValue):
354
427
        return value.value
355
428
 
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:
360
 
        return value
361
 
 
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)
370
 
        return value
371
 
 
372
 
    # If there is a conjoined slave, update that.
 
429
    # If this bugtask has no bug yet, then we are probably being
 
430
    # instantiated.
 
431
    if self.bug is None:
 
432
        return value
 
433
 
 
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)
391
460
 
392
461
 
393
 
def validate_target(bug, target, retarget_existing=True):
394
 
    """Validate a bugtask target against a bug's existing tasks.
395
 
 
396
 
    Checks that no conflicting tasks already exist, and that the new
397
 
    target's pillar supports the bug's access policy.
398
 
    """
399
 
    if bug.getBugTask(target):
400
 
        raise IllegalTarget(
401
 
            "A fix for this bug has already been requested for %s"
402
 
            % target.displayname)
403
 
 
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):
410
 
            try:
411
 
                target.distribution.guessPublishedSourcePackageName(
412
 
                    target.sourcepackagename.name)
413
 
            except NotFoundError, e:
414
 
                raise IllegalTarget(e[0])
415
 
 
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:
421
 
            return
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):
425
 
            raise IllegalTarget(
426
 
                "This private bug already affects %s. "
427
 
                "Private bugs cannot affect multiple projects."
428
 
                    % bug.default_bugtask.target.bugtargetdisplayname)
429
 
 
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)):
434
 
        raise IllegalTarget(
435
 
            "%s doesn't have a %s access policy."
436
 
            % (target.pillar.displayname, bug.access_policy.type.title))
437
 
 
438
 
 
439
 
def validate_new_target(bug, target):
440
 
    """Validate a bugtask target to be added.
441
 
 
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.
445
 
 
446
 
    The same checks as `validate_target` does are also done.
447
 
    """
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 = [
453
 
            bugtask for bugtask
454
 
            in shortlist(bug.bugtasks, longest_expected=50)
455
 
            if bugtask.distribution == target]
456
 
 
457
 
        if len(distribution_tasks_for_bug) > 0:
458
 
            raise IllegalTarget(
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
466
 
        # task opened.
467
 
        if bug.getBugTask(target.distribution) is not None:
468
 
            raise IllegalTarget(
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)
472
 
 
473
 
    validate_target(bug, target, retarget_existing=False)
474
 
 
475
 
 
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)
 
469
 
 
470
 
 
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, )
489
484
 
490
 
    _inhibit_target_check = False
491
 
 
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)
512
 
    _status = EnumCol(
 
510
    status = EnumCol(
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,
562
561
        dbName='targetnamecache', notNull=False, default=None)
563
562
 
564
563
    @property
565
 
    def status(self):
566
 
        if self._status in DB_INCOMPLETE_BUGTASK_STATUSES:
567
 
            return BugTaskStatus.INCOMPLETE
568
 
        return self._status
569
 
 
570
 
    @property
571
564
    def title(self):
572
565
        """See `IBugTask`."""
573
566
        return 'Bug #%s in %s: "%s"' % (
574
567
            self.bug.id, self.bugtargetdisplayname, self.bug.title)
575
568
 
576
569
    @property
577
 
    def bug_subscribers(self):
578
 
        """See `IBugTask`."""
579
 
        return tuple(
580
 
            chain(self.bug.getDirectSubscribers(),
581
 
                  self.bug.getIndirectSubscribers()))
582
 
 
583
 
    @property
584
 
    def bugtargetname(self):
585
 
        """See `IBugTask`."""
586
 
        return self.target.bugtargetname
587
 
 
588
 
    @property
589
 
    def target(self):
590
 
        """See `IBugTask`."""
591
 
        return bug_target_from_key(
592
 
            self.product, self.productseries, self.distribution,
593
 
            self.distroseries, self.sourcepackagename)
594
 
 
595
 
    @property
596
 
    def related_tasks(self):
597
 
        """See `IBugTask`."""
598
 
        other_tasks = [
599
 
            task for task in self.bug.bugtasks if task != self]
600
 
 
601
 
        return other_tasks
602
 
 
603
 
    @property
604
 
    def pillar(self):
605
 
        """See `IBugTask`."""
606
 
        return self.target.pillar
607
 
 
608
 
    @property
609
 
    def other_affected_pillars(self):
610
 
        """See `IBugTask`."""
611
 
        result = set()
612
 
        this_pillar = self.pillar
613
 
        for task in self.bug.bugtasks:
614
 
            that_pillar = task.pillar
615
 
            if that_pillar != this_pillar:
616
 
                result.add(that_pillar)
617
 
        return sorted(result, key=pillar_sort_key)
618
 
 
619
 
    @property
620
570
    def bugtargetdisplayname(self):
621
571
        """See `IBugTask`."""
622
572
        return self.targetnamecache
624
574
    @property
625
575
    def age(self):
626
576
        """See `IBugTask`."""
627
 
        now = datetime.datetime.now(pytz.UTC)
 
577
        UTC = pytz.timezone('UTC')
 
578
        now = datetime.datetime.now(UTC)
628
579
 
629
580
        return now - self.datecreated
630
581
 
648
599
        Note that this should be kept in sync with the completeness_clause
649
600
        above.
650
601
        """
651
 
        return self._status in RESOLVED_BUGTASK_STATUSES
652
 
 
653
 
    def canBeDeleted(self):
654
 
        try:
655
 
            self.checkCanBeDeleted()
656
 
        except Exception:
657
 
            return False
658
 
        return True
659
 
 
660
 
    def checkCanBeDeleted(self):
661
 
        num_bugtasks = Store.of(self).find(
662
 
            BugTask, bug=self.bug).count()
663
 
 
664
 
        if num_bugtasks < 2:
665
 
            raise CannotDeleteBugtask(
666
 
                "Cannot delete only bugtask affecting: %s."
667
 
                % self.target.bugtargetdisplayname)
668
 
 
669
 
    def delete(self, who=None):
670
 
        """See `IBugTask`."""
671
 
        if who is None:
672
 
            who = getUtility(ILaunchBag).user
673
 
 
674
 
        # Raise an error if the bugtask cannot be deleted.
675
 
        self.checkCanBeDeleted()
676
 
 
677
 
        bug = self.bug
678
 
        target = self.target
679
 
        notify(ObjectDeletedEvent(self, who))
680
 
        self.destroySelf()
681
 
        del get_property_cache(bug).bugtasks
682
 
 
683
 
        # When a task is deleted the bug's heat needs to be recalculated.
684
 
        target.recalculateBugHeatCache()
 
602
        return self.status in RESOLVED_BUGTASK_STATUSES
685
603
 
686
604
    def findSimilarBugs(self, user, limit=10):
687
605
        """See `IBugTask`."""
715
633
        """See `IBugTask`."""
716
634
        return self.bug.isSubscribed(person)
717
635
 
718
 
    def _syncSourcePackages(self, new_spn):
 
636
    def _syncSourcePackages(self, new_spnid):
719
637
        """Synchronize changes to source packages with other distrotasks.
720
638
 
721
639
        If one distroseriestask's source package is changed, all the
723
641
        package has to be changed, as well as the corresponding
724
642
        distrotask.
725
643
        """
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.
 
644
        if self.bug is None:
 
645
            # The validator is being called on an incomplete bug task.
728
646
            return
729
 
        distribution = self.distribution or self.distroseries.distribution
730
 
        for bugtask in self.related_tasks:
731
 
            relevant = (
732
 
                bugtask.sourcepackagename == self.sourcepackagename and
733
 
                distribution in (
734
 
                    bugtask.distribution,
735
 
                    getattr(bugtask.distroseries, 'distribution', None)))
736
 
            if relevant:
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
 
649
        else:
 
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
 
655
                else:
 
656
                    related_distribution = bugtask.distribution
 
657
                if (related_distribution == distribution and
 
658
                    bugtask.sourcepackagenameID == self.sourcepackagenameID):
 
659
                    bugtask.sourcepackagenameID = PassthroughValue(new_spnid)
742
660
 
743
661
    def getContributorInfo(self, user, person):
744
662
        """See `IBugTask`."""
752
670
    def getConjoinedMaster(self, bugtasks, bugtasks_by_package=None):
753
671
        """See `IBugTask`."""
754
672
        conjoined_master = None
755
 
        if self.distribution:
 
673
        if IDistroBugTask.providedBy(self):
756
674
            if bugtasks_by_package is None:
757
675
                bugtasks_by_package = (
758
676
                    self.bug.getBugTasksByPackageName(bugtasks))
770
688
                if bugtask.distroseries == current_series:
771
689
                    conjoined_master = bugtask
772
690
                    break
773
 
        elif self.product:
 
691
        elif IUpstreamBugTask.providedBy(self):
774
692
            assert self.product.development_focusID is not None, (
775
693
                'A product should always have a development series.')
776
694
            devel_focusID = self.product.development_focusID
796
714
    def conjoined_slave(self):
797
715
        """See `IBugTask`."""
798
716
        conjoined_slave = None
799
 
        if self.distroseries:
 
717
        if IDistroSeriesBugTask.providedBy(self):
800
718
            distribution = self.distroseries.distribution
801
719
            if self.distroseries != distribution.currentseries:
802
720
                # Only current series tasks are conjoined.
806
724
                    bugtask.sourcepackagename == self.sourcepackagename):
807
725
                    conjoined_slave = bugtask
808
726
                    break
809
 
        elif self.productseries:
 
727
        elif IProductSeriesBugTask.providedBy(self):
810
728
            product = self.productseries.product
811
729
            if self.productseries != product.development_focus:
812
730
                # Only development focus tasks are conjoined.
821
739
            conjoined_slave = None
822
740
        return conjoined_slave
823
741
 
 
742
    def _isConjoinedBugTask(self):
 
743
        """Return True when conjoined_master is not None, otherwise False."""
 
744
        return self.conjoined_master is not None
 
745
 
824
746
    def _syncFromConjoinedSlave(self):
825
747
        """Ensure the conjoined master is synched from its slave.
826
748
 
837
759
            # setter methods directly.
838
760
            setattr(self, synched_attr, PassthroughValue(slave_attr_value))
839
761
 
 
762
    def _init(self, *args, **kw):
 
763
        """Marks the task when it's created or fetched from the database."""
 
764
        SQLBase._init(self, *args, **kw)
 
765
 
 
766
        # We check both the foreign key column and the reference so we
 
767
        # can detect unflushed references.  The reference check will
 
768
        # only be made if the FK is None, so no additional queries
 
769
        # will be executed.
 
770
        if self.productID is not None or self.product is not None:
 
771
            alsoProvides(self, IUpstreamBugTask)
 
772
        elif (self.productseriesID is not None or
 
773
              self.productseries is not None):
 
774
            alsoProvides(self, IProductSeriesBugTask)
 
775
        elif self.distroseriesID is not None or self.distroseries is not None:
 
776
            alsoProvides(self, IDistroSeriesBugTask)
 
777
        elif self.distributionID is not None or self.distribution is not None:
 
778
            # If nothing else, this is a distro task.
 
779
            alsoProvides(self, IDistroBugTask)
 
780
        else:
 
781
            raise AssertionError("Task %d is floating." % self.id)
 
782
 
840
783
    @property
841
784
    def target_uses_malone(self):
842
785
        """See `IBugTask`"""
846
789
 
847
790
    def transitionToMilestone(self, new_milestone, user):
848
791
        """See `IBugTask`."""
849
 
        if not self.userHasBugSupervisorPrivileges(user):
 
792
        if not self.userCanEditMilestone(user):
850
793
            raise UserCannotEditBugTaskMilestone(
851
794
                "User does not have sufficient permissions "
852
795
                "to edit the bug task milestone.")
855
798
 
856
799
    def transitionToImportance(self, new_importance, user):
857
800
        """See `IBugTask`."""
858
 
        if not self.userHasBugSupervisorPrivileges(user):
 
801
        if not self.userCanEditImportance(user):
859
802
            raise UserCannotEditBugTaskImportance(
860
803
                "User does not have sufficient permissions "
861
804
                "to edit the bug task importance.")
870
813
            raise ValueError('Unknown debbugs severity "%s".' % severity)
871
814
        return self.importance
872
815
 
873
 
    # START TEMPORARY BIT FOR BUGTASK AUTOCONFIRM FEATURE FLAG.
874
 
    _parse_launchpad_names = re.compile(r"[a-z0-9][a-z0-9\+\.\-]+").findall
875
 
 
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.
882
 
        pillar = self.pillar
883
 
        if IDistribution.providedBy(pillar):
884
 
            flag_name = 'bugs.autoconfirm.enabled_distribution_names'
885
 
        else:
886
 
            assert IProduct.providedBy(pillar), 'unexpected pillar'
887
 
            flag_name = 'bugs.autoconfirm.enabled_product_names'
888
 
        enabled = features.getFeatureFlag(flag_name)
889
 
        if enabled is None:
890
 
            return False
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.
895
 
            return False
896
 
        return True
897
 
    # END TEMPORARY BIT FOR BUGTASK AUTOCONFIRM FEATURE FLAG.
898
 
 
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.
904
 
        """
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.
910
 
            ):
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
915
 
            # the bugtask.
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))
922
 
 
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))):
928
821
            return True
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):
930
827
            return True
931
828
        else:
932
829
            return (self.status not in (
941
838
            # testing the edit form.
942
839
            return
943
840
 
944
 
        new_status = normalize_bugtask_status(new_status)
945
 
 
946
841
        if not self.canTransitionToStatus(new_status, user):
947
842
            raise UserCannotEditBugTaskStatus(
948
843
                "Only Bug Supervisors may change status to %s." % (
949
844
                    new_status.title,))
950
845
 
951
 
        if new_status == BugTaskStatus.INCOMPLETE:
952
 
            # We store INCOMPLETE as INCOMPLETE_WITHOUT_RESPONSE so that it
953
 
            # can be queried on efficiently.
954
 
            if (when is None or self.bug.date_last_message is None or
955
 
                when > self.bug.date_last_message):
956
 
                new_status = BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE
957
 
            else:
958
 
                new_status = BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE
959
 
 
960
 
        if self._status == new_status:
 
846
        if self.status == new_status:
961
847
            # No change in the status, so nothing to do.
962
848
            return
963
849
 
964
850
        old_status = self.status
965
 
        self._status = new_status
 
851
        self.status = new_status
966
852
 
967
853
        if new_status == BugTaskStatus.UNKNOWN:
968
854
            # Ensure that all status-related dates are cleared,
980
866
            return
981
867
 
982
868
        if when is None:
983
 
            when = datetime.datetime.now(pytz.UTC)
 
869
            UTC = pytz.timezone('UTC')
 
870
            when = datetime.datetime.now(UTC)
984
871
 
985
872
        # Record the date of the particular kinds of transitions into
986
873
        # certain states.
1035
922
        # Bugs can jump in and out of 'incomplete' status
1036
923
        # and for just as long as they're marked incomplete
1037
924
        # we keep a date_incomplete recorded for them.
1038
 
        if new_status in DB_INCOMPLETE_BUGTASK_STATUSES:
 
925
        if new_status == BugTaskStatus.INCOMPLETE:
1039
926
            self.date_incomplete = when
1040
927
        else:
1041
928
            self.date_incomplete = None
1042
929
 
1043
 
        if ((old_status in DB_UNRESOLVED_BUGTASK_STATUSES) and
 
930
        if ((old_status in UNRESOLVED_BUGTASK_STATUSES) and
1044
931
            (new_status in RESOLVED_BUGTASK_STATUSES)):
1045
932
            self.date_closed = when
1046
933
 
1047
934
        if ((old_status in RESOLVED_BUGTASK_STATUSES) and
1048
 
            (new_status in DB_UNRESOLVED_BUGTASK_STATUSES)):
 
935
            (new_status in UNRESOLVED_BUGTASK_STATUSES)):
1049
936
            self.date_left_closed = when
1050
937
 
1051
938
        # Ensure that we don't have dates recorded for state
1053
940
        # workflow state. We want to ensure that, for example, a
1054
941
        # bugtask that went New => Confirmed => New
1055
942
        # has a dateconfirmed value of None.
1056
 
        if new_status in DB_UNRESOLVED_BUGTASK_STATUSES:
 
943
        if new_status in UNRESOLVED_BUGTASK_STATUSES:
1057
944
            self.date_closed = None
1058
945
 
1059
946
        if new_status < BugTaskStatus.CONFIRMED:
1071
958
        if new_status < BugTaskStatus.FIXRELEASED:
1072
959
            self.date_fix_released = None
1073
960
 
 
961
    def _userCanSetAssignee(self, user):
 
962
        """Used by methods to check if user can assign or unassign bugtask."""
 
963
        celebrities = getUtility(ILaunchpadCelebrities)
 
964
        return (
 
965
            user.inTeam(self.pillar.bug_supervisor) or
 
966
            user.inTeam(self.pillar.owner) or
 
967
            user.inTeam(self.pillar.driver) or
 
968
            (self.distroseries is not None and
 
969
             user.inTeam(self.distroseries.driver)) or
 
970
            (self.productseries is not None and
 
971
             user.inTeam(self.productseries.driver)) or
 
972
            user.inTeam(celebrities.admin)
 
973
            or user == celebrities.bug_importer)
 
974
 
1074
975
    def userCanSetAnyAssignee(self, user):
1075
976
        """See `IBugTask`."""
1076
977
        if user is None:
1078
979
        elif self.pillar.bug_supervisor is None:
1079
980
            return True
1080
981
        else:
1081
 
            return self.userHasBugSupervisorPrivileges(user)
 
982
            return self._userCanSetAssignee(user)
1082
983
 
1083
984
    def userCanUnassign(self, user):
1084
985
        """True if user can set the assignee to None.
1088
989
        Launchpad admins can always unassign.
1089
990
        """
1090
991
        return user is not None and (
1091
 
            user.inTeam(self.assignee) or
1092
 
            self.userHasBugSupervisorPrivileges(user))
 
992
            user.inTeam(self.assignee) or self._userCanSetAssignee(user))
1093
993
 
1094
994
    def canTransitionToAssignee(self, assignee):
1095
995
        """See `IBugTask`."""
1138
1038
            get_property_cache(self.bug)._known_viewers = set(
1139
1039
                [self.assignee.id])
1140
1040
 
1141
 
    def validateTransitionToTarget(self, target):
1142
 
        """See `IBugTask`."""
1143
 
        from lp.registry.model.distroseries import DistroSeries
1144
 
 
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)):
1155
 
            series = set()
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)
1161
 
                else:
1162
 
                    series = set()
1163
 
                    break
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).
1179
 
            distros = set()
1180
 
            for potential_target in (target, self.target):
1181
 
                if IDistribution.providedBy(potential_target.pillar):
1182
 
                    distros.add(potential_target.pillar)
1183
 
                else:
1184
 
                    distros.add(None)
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(
1189
 
                    BugTask,
1190
 
                    BugTask.bugID == self.bugID,
1191
 
                    BugTask.distroseriesID == DistroSeries.id,
1192
 
                    DistroSeries.distributionID.is_in(
1193
 
                        distro.id for distro in distros if distro),
1194
 
                    ).is_empty():
1195
 
                    raise IllegalTarget(
1196
 
                        "Distribution tasks with corresponding series "
1197
 
                        "tasks may only be retargeted to a different "
1198
 
                        "package.")
1199
 
 
1200
 
        validate_target(self.bug, target)
1201
 
 
1202
 
    def transitionToTarget(self, target, _sync_sourcepackages=True):
 
1041
    def transitionToTarget(self, target):
1203
1042
        """See `IBugTask`.
1204
1043
 
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.
1209
1048
        """
1210
 
        if self.target == target:
1211
 
            return
1212
 
 
1213
 
        self.validateTransitionToTarget(target)
1214
1049
 
1215
1050
        target_before_change = self.target
1216
1051
 
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
1223
1058
 
1224
 
        new_key = bug_target_to_key(target)
1225
 
 
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'])
1232
 
 
1233
 
        for name, value in new_key.iteritems():
1234
 
            setattr(self, name, value)
1235
 
        self.updateTargetNameCache()
1236
 
 
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
 
1062
            else:
 
1063
                raise IllegalTarget(
 
1064
                    "Upstream bug tasks may only be re-targeted "
 
1065
                    "to another project.")
 
1066
        else:
 
1067
            if (IDistributionSourcePackage.providedBy(target) and
 
1068
                (target.distribution == self.target or
 
1069
                 target.distribution == self.target.distribution)):
 
1070
                self.sourcepackagename = target.sourcepackagename
 
1071
            else:
 
1072
                raise IllegalTarget(
 
1073
                    "Distribution bug tasks may only be re-targeted "
 
1074
                    "to a package in the same distribution.")
1242
1075
 
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
1250
 
            # CONFIRMED status.
1251
 
            if self.bug.shouldConfirmBugtasks():
1252
 
                self.maybeConfirm()
1253
 
            # END TEMPORARY BIT FOR BUGTASK AUTOCONFIRM FEATURE FLAG.
1254
1081
 
1255
1082
    def updateTargetNameCache(self, newtarget=None):
1256
1083
        """See `IBugTask`."""
1303
1130
        else:
1304
1131
            component_name = component.name
1305
1132
 
1306
 
        if self.product:
 
1133
        if IUpstreamBugTask.providedBy(self):
1307
1134
            header_value = 'product=%s;' % self.target.name
1308
 
        elif self.productseries:
 
1135
        elif IProductSeriesBugTask.providedBy(self):
1309
1136
            header_value = 'product=%s; productseries=%s;' % (
1310
1137
                self.productseries.product.name, self.productseries.name)
1311
 
        elif self.distribution:
 
1138
        elif IDistroBugTask.providedBy(self):
1312
1139
            header_value = ((
1313
1140
                'distribution=%(distroname)s; '
1314
1141
                'sourcepackage=%(sourcepackagename)s; '
1316
1143
                {'distroname': self.distribution.name,
1317
1144
                 'sourcepackagename': sourcepackagename_value,
1318
1145
                 'componentname': component_name})
1319
 
        elif self.distroseries:
 
1146
        elif IDistroSeriesBugTask.providedBy(self):
1320
1147
            header_value = ((
1321
1148
                'distribution=%(distroname)s; '
1322
1149
                'distroseries=%(distroseriesname)s; '
1345
1172
 
1346
1173
    def getDelta(self, old_task):
1347
1174
        """See `IBugTask`."""
 
1175
        valid_interfaces = [
 
1176
            IUpstreamBugTask,
 
1177
            IProductSeriesBugTask,
 
1178
            IDistroBugTask,
 
1179
            IDistroSeriesBugTask,
 
1180
            ]
 
1181
 
 
1182
        # This tries to find a matching pair of bug tasks, i.e. where
 
1183
        # both provide IUpstreamBugTask, or both IDistroBugTask.
 
1184
        # Failing that, it drops off the bottom of the loop and raises
 
1185
        # the TypeError.
 
1186
        for interface in valid_interfaces:
 
1187
            if interface.providedBy(self) and interface.providedBy(old_task):
 
1188
                break
 
1189
        else:
 
1190
            raise TypeError(
 
1191
                "Can't calculate delta on bug tasks of incompatible types: "
 
1192
                "[%s, %s]." % (repr(old_task), repr(self)))
 
1193
 
1348
1194
        # calculate the differences in the fields that both types of tasks
1349
1195
        # have in common
1350
1196
        changes = {}
1363
1209
        else:
1364
1210
            return None
1365
1211
 
1366
 
    @classmethod
1367
 
    def userHasDriverPrivilegesContext(cls, context, user):
1368
 
        """Does the user have driver privileges for the given context?
1369
 
 
1370
 
        :return: a boolean.
1371
 
        """
1372
 
        if not user:
1373
 
            return False
1374
 
        role = IPersonRoles(user)
1375
 
        # Admins can always change bug details.
1376
 
        if role.in_admin:
1377
 
            return True
1378
 
 
1379
 
        # Similar to admins, the Bug Watch Updater, Bug Importer and
1380
 
        # Janitor can always change bug details.
1381
 
        if (
1382
 
            role.in_bug_watch_updater or role.in_bug_importer or
1383
 
            role.in_janitor):
1384
 
            return True
1385
 
 
1386
 
        # If you're the owner or a driver, you can change bug details.
1387
 
        return (
1388
 
            role.isOwner(context.pillar) or role.isOneOfDrivers(context))
1389
 
 
1390
 
    @classmethod
1391
 
    def userHasBugSupervisorPrivilegesContext(cls, context, user):
1392
 
        """Does the user have bug supervisor privileges for the given
1393
 
        context?
1394
 
 
1395
 
        :return: a boolean.
1396
 
        """
1397
 
        if not user:
1398
 
            return False
1399
 
        role = IPersonRoles(user)
1400
 
        # If you have driver privileges, or are the bug supervisor, you can
1401
 
        # change bug details.
1402
 
        return (
1403
 
            cls.userHasDriverPrivilegesContext(context, user) or
1404
 
            role.isBugSupervisor(context.pillar))
1405
 
 
1406
 
    def userHasDriverPrivileges(self, user):
1407
 
        """See `IBugTask`."""
1408
 
        return self.userHasDriverPrivilegesContext(self.target, user)
1409
 
 
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?"""
 
1214
        if user is None:
 
1215
            return False
 
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
 
1222
        else:
 
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))
 
1227
 
 
1228
    def userCanEditMilestone(self, user):
 
1229
        """See `IBugTask`."""
 
1230
        return self._userIsPillarEditor(user)
 
1231
 
 
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)
1413
1238
 
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 = """
1502
 
                UNION
1503
 
                SELECT BugTask.bug
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
1509
 
                UNION
1510
 
                SELECT BugTask.bug
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
1516
 
                UNION
1517
 
                SELECT BugTask.bug
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
1523
 
                UNION
1524
 
                SELECT BugTask.bug
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
1531
 
            """
1532
 
        query = """
1533
 
            (Bug.private = FALSE OR EXISTS (
1534
 
                WITH teams AS (
1535
 
                    SELECT team from TeamParticipation
1536
 
                    WHERE person = %(personid)s
1537
 
                )
1538
 
                SELECT BugSubscription.bug
1539
 
                FROM BugSubscription
1540
 
                WHERE BugSubscription.person IN (SELECT team FROM teams) AND
1541
 
                    BugSubscription.bug = Bug.id
1542
 
                UNION
1543
 
                SELECT BugTask.bug
1544
 
                FROM BugTask
1545
 
                WHERE BugTask.assignee IN (SELECT team FROM teams) AND
1546
 
                    BugTask.bug = Bug.id
1547
 
                %(extra_filters)s
1548
 
                    ))
1549
 
            """ % dict(
1550
 
                    personid=quote(user.id),
1551
 
                    extra_filters=pillar_privacy_filters)
1552
 
    else:
1553
 
        if features.getFeatureFlag(
1554
 
            'disclosure.private_bug_visibility_rules.enabled'):
1555
 
            pillar_privacy_filters = """
1556
 
                UNION
1557
 
                SELECT BugTask.bug
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
1564
 
                UNION
1565
 
                SELECT BugTask.bug
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
1572
 
                UNION
1573
 
                SELECT BugTask.bug
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
1580
 
                UNION
1581
 
                SELECT BugTask.bug
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)
1590
 
        query = """
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
1597
 
                UNION
1598
 
                SELECT BugTask.bug
1599
 
                FROM BugTask, TeamParticipation
1600
 
                WHERE TeamParticipation.person = %(personid)s AND
1601
 
                    TeamParticipation.team = BugTask.assignee AND
1602
 
                    BugTask.bug = Bug.id
1603
 
                %(extra_filters)s
1604
 
                    ))
1605
 
            """ % dict(
1606
 
                    personid=quote(user.id),
1607
 
                    extra_filters=pillar_privacy_filters)
1608
 
    return query, _make_cache_user_can_view_bug(user)
 
1321
    return ("""
 
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
 
1328
             UNION
 
1329
             SELECT BugTask.bug
 
1330
             FROM BugTask, TeamParticipation
 
1331
             WHERE TeamParticipation.person = %(personid)s AND
 
1332
                   TeamParticipation.team = BugTask.assignee AND
 
1333
                   BugTask.bug = Bug.id
 
1334
                   ))
 
1335
                     """ % sqlvalues(personid=user.id),
 
1336
        _make_cache_user_can_view_bug(user))
1609
1337
 
1610
1338
 
1611
1339
def build_tag_set_query(joiner, tags):
1746
1474
        """See `IBugTaskSet`."""
1747
1475
        return BugTaskSearchParams(
1748
1476
            user=getUtility(ILaunchBag).user,
1749
 
            status=any(*DB_UNRESOLVED_BUGTASK_STATUSES),
 
1477
            status=any(*UNRESOLVED_BUGTASK_STATUSES),
1750
1478
            omit_dupes=True)
1751
1479
 
1752
1480
    def get(self, task_id):
1861
1589
            summary, Bug, ' AND '.join(constraint_clauses), ['BugTask'])
1862
1590
        return self.search(search_params, _noprejoins=True)
1863
1591
 
1864
 
    @classmethod
1865
 
    def _buildStatusClause(cls, status):
 
1592
    def _buildStatusClause(self, status):
1866
1593
        """Return the SQL query fragment for search by status.
1867
1594
 
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)
 
1599
                for 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)))
 
1604
            with_response = (
 
1605
                status == BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE)
 
1606
            without_response = (
 
1607
                status == BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE)
 
1608
            if with_response or without_response:
 
1609
                status_clause = (
 
1610
                    '(BugTask.status = %s) ' %
 
1611
                    sqlvalues(BugTaskStatus.INCOMPLETE))
 
1612
                if with_response:
 
1613
                    status_clause += ("""
 
1614
                        AND (Bug.date_last_message IS NOT NULL
 
1615
                             AND BugTask.date_incomplete <=
 
1616
                                 Bug.date_last_message)
 
1617
                        """)
 
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)
 
1623
                        """)
 
1624
                else:
 
1625
                    assert with_response != without_response
 
1626
                return status_clause
1888
1627
            else:
1889
1628
                return '(BugTask.status = %s)' % sqlvalues(status)
1890
1629
        else:
1891
 
            raise ValueError('Unrecognized status value: %r' % (status,))
 
1630
            raise AssertionError(
 
1631
                'Unrecognized status value: %s' % repr(status))
1892
1632
 
1893
1633
    def _buildExcludeConjoinedClause(self, milestone):
1894
1634
        """Exclude bugtasks with a conjoined master.
1920
1660
                And(ConjoinedMaster.bugID == BugTask.bugID,
1921
1661
                    BugTask.distributionID == milestone.distribution.id,
1922
1662
                    ConjoinedMaster.distroseriesID == current_series.id,
1923
 
                    Not(ConjoinedMaster._status.is_in(
 
1663
                    Not(ConjoinedMaster.status.is_in(
1924
1664
                            BugTask._NON_CONJOINED_STATUSES))))
1925
1665
            join_tables = [(ConjoinedMaster, join)]
1926
1666
        else:
1940
1680
                        And(ConjoinedMaster.bugID == BugTask.bugID,
1941
1681
                            ConjoinedMaster.productseriesID
1942
1682
                                == Product.development_focusID,
1943
 
                            Not(ConjoinedMaster._status.is_in(
 
1683
                            Not(ConjoinedMaster.status.is_in(
1944
1684
                                    BugTask._NON_CONJOINED_STATUSES)))),
1945
1685
                    ]
1946
1686
                # join.right is the table name.
1953
1693
                    And(ConjoinedMaster.bugID == BugTask.bugID,
1954
1694
                        BugTask.productID == milestone.product.id,
1955
1695
                        ConjoinedMaster.productseriesID == dev_focus_id,
1956
 
                        Not(ConjoinedMaster._status.is_in(
 
1696
                        Not(ConjoinedMaster.status.is_in(
1957
1697
                                BugTask._NON_CONJOINED_STATUSES))))
1958
1698
                join_tables = [(ConjoinedMaster, join)]
1959
1699
            else:
1979
1719
            decorator to call on each returned row.
1980
1720
        """
1981
1721
        params = self._require_params(params)
1982
 
        from lp.bugs.model.bug import (
1983
 
            Bug,
1984
 
            BugAffectsPerson,
1985
 
            )
 
1722
        from lp.bugs.model.bug import Bug
1986
1723
        extra_clauses = ['Bug.id = BugTask.bug']
1987
1724
        clauseTables = [BugTask, Bug]
1988
1725
        join_tables = []
2053
1790
                "BugTaskSearchParam.exclude_conjoined cannot be True if "
2054
1791
                "BugTaskSearchParam.milestone is not set")
2055
1792
 
 
1793
 
2056
1794
        if params.project:
2057
1795
            # Prevent circular import problems.
2058
1796
            from lp.registry.model.product import Product
2105
1843
                    sqlvalues(personid=params.subscriber.id))
2106
1844
 
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)
 
1876
 
 
1877
            join_clause = Or(
 
1878
                ssub_match_product,
 
1879
                ssub_match_productseries,
 
1880
                ssub_match_project,
 
1881
                ssub_match_distribution_with_optional_package,
 
1882
                ssub_match_distribution_series,
 
1883
                ssub_match_milestone)
 
1884
 
 
1885
            join_tables.append(
 
1886
                (Product, LeftJoin(Product, And(
 
1887
                                BugTask.productID == Product.id,
 
1888
                                Product.active))))
 
1889
            join_tables.append(
 
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
2116
 
            join_tables.append(
2117
 
                (Product, LeftJoin(Product, And(
2118
 
                                BugTask.productID == Product.id,
2119
 
                                Product.active))))
2120
 
            join_tables.append(
2121
 
                (None,
2122
 
                 LeftJoin(
2123
 
                    SQL('ss ss1'),
2124
 
                    BugTask.product == SQL('ss1.product'))))
2125
 
            join_tables.append(
2126
 
                (None,
2127
 
                 LeftJoin(
2128
 
                    SQL('ss ss2'),
2129
 
                    BugTask.productseries == SQL('ss2.productseries'))))
2130
 
            join_tables.append(
2131
 
                (None,
2132
 
                 LeftJoin(
2133
 
                    SQL('ss ss3'),
2134
 
                    Product.project == SQL('ss3.project'))))
2135
 
            join_tables.append(
2136
 
                (None,
2137
 
                 LeftJoin(
2138
 
                    SQL('ss ss4'),
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
2145
 
            else:
2146
 
                parent_distro_id = 0
2147
 
            join_tables.append(
2148
 
                (None,
2149
 
                 LeftJoin(
2150
 
                    SQL('ss ss5'),
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
2156
 
                        # series.
2157
 
                        And(parent_distro_id == SQL('ss5.distribution'),
2158
 
                            BugTask.sourcepackagename == SQL(
2159
 
                                'ss5.sourcepackagename'))))))
2160
 
            join_tables.append(
2161
 
                (None,
2162
 
                 LeftJoin(
2163
 
                    SQL('ss ss6'),
2164
 
                    BugTask.milestone == SQL('ss6.milestone'))))
2165
 
            extra_clauses.append(
2166
 
                "NULL_COUNT("
2167
 
                "ARRAY[ss1.id, ss2.id, ss3.id, ss4.id, ss5.id, ss6.id]"
2168
 
                ") < 6")
2169
1895
            has_duplicate_results = True
2170
1896
 
 
1897
 
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
2195
1922
                distroseries = params.distribution.currentseries
2196
1923
            elif params.distroseries:
2197
1924
                distroseries = params.distroseries
2198
 
            if distroseries is None:
2199
 
                raise ValueError(
2200
 
                    "Search by component requires a context with a "
2201
 
                    "distribution or distroseries.")
 
1925
            assert distroseries, (
 
1926
                "Search by component requires a context with a distribution "
 
1927
                "or distroseries.")
2202
1928
 
2203
1929
            if zope_isinstance(params.component, any):
2204
1930
                component_ids = sqlvalues(*params.component.query_values)
2209
1935
                archive.id
2210
1936
                for archive in distroseries.distribution.all_distro_archives]
2211
1937
            with_clauses.append("""spns as (
2212
 
                SELECT spr.sourcepackagename
2213
 
                FROM SourcePackagePublishingHistory
2214
 
                JOIN SourcePackageRelease AS spr ON spr.id =
 
1938
                SELECT sourcepackagename from SourcePackagePublishingHistory
 
1939
                JOIN SourcePackageRelease on SourcePackageRelease.id =
2215
1940
                    SourcePackagePublishingHistory.sourcepackagerelease AND
2216
1941
                SourcePackagePublishingHistory.distroseries = %s AND
2217
1942
                SourcePackagePublishingHistory.archive IN %s AND
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)
2251
 
                OR
2252
 
                ((BugTask.distribution, Bugtask.sourcepackagename) IN
2253
 
                    (SELECT distribution,  sourcepackagename FROM
2254
 
                     StructuralSubscription
2255
 
                     WHERE subscriber = %(bug_supervisor)s))
2256
 
                OR
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
 
1976
                UNION ALL
 
1977
                SELECT BugTask.id
 
1978
                FROM BugTask, StructuralSubscription
 
1979
                WHERE
 
1980
                  BugTask.distribution = StructuralSubscription.distribution
 
1981
                    AND BugTask.sourcepackagename =
 
1982
                        StructuralSubscription.sourcepackagename
 
1983
                    AND StructuralSubscription.subscriber = %(bug_supervisor)s
 
1984
                UNION ALL
 
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)
2262
1990
 
2267
1995
            extra_clauses.append(bug_reporter_clause)
2268
1996
 
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 = """
 
2001
            BugTask.id IN (
 
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
 
2007
            )
 
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
 
2015
            else:
 
2016
                bug_commenter_clause = bug_commenter_old_clause
2274
2017
            extra_clauses.append(bug_commenter_clause)
2275
2018
 
2276
2019
        if params.affects_me:
2277
2020
            params.affected_user = params.user
2278
2021
        if params.affected_user:
2279
 
            join_tables.append(
2280
 
                (BugAffectsPerson, Join(
2281
 
                    BugAffectsPerson, And(
2282
 
                        BugTask.bugID == BugAffectsPerson.bugID,
2283
 
                        BugAffectsPerson.affected,
2284
 
                        BugAffectsPerson.person == params.affected_user))))
 
2022
            affected_user_clause = """
 
2023
            BugTask.id IN (
 
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
 
2028
            )
 
2029
            """ % sqlvalues(affected_user=params.affected_user)
 
2030
            extra_clauses.append(affected_user_clause)
2285
2031
 
2286
2032
        if params.nominated_for:
2287
2033
            mappings = sqlvalues(
2345
2091
                "BugTask.datecreated > %s" % (
2346
2092
                    sqlvalues(params.created_since,)))
2347
2093
 
2348
 
        orderby_arg, extra_joins = self._processOrderBy(params)
2349
 
        join_tables.extend(extra_joins)
 
2094
        orderby_arg = self._processOrderBy(params)
2350
2095
 
2351
2096
        query = " AND ".join(extra_clauses)
2352
2097
 
2455
2200
            statuses_for_open_tasks = [
2456
2201
                BugTaskStatus.NEW,
2457
2202
                BugTaskStatus.INCOMPLETE,
2458
 
                BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE,
2459
 
                BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE,
2460
2203
                BugTaskStatus.CONFIRMED,
2461
2204
                BugTaskStatus.INPROGRESS,
2462
2205
                BugTaskStatus.UNKNOWN]
2643
2386
        origin = [BugTask]
2644
2387
        already_joined = set(origin)
2645
2388
        for table, join in join_tables:
2646
 
            if table is None or table not in already_joined:
 
2389
            if table not in already_joined:
2647
2390
                origin.append(join)
2648
 
                if table is not None:
2649
 
                    already_joined.add(table)
 
2391
                already_joined.add(table)
2650
2392
        for table, join in prejoin_tables:
2651
2393
            if table not in already_joined:
2652
2394
                origin.append(join)
2700
2442
            [query, clauseTables, ignore, decorator, join_tables,
2701
2443
             has_duplicate_results, with_clause] = self.buildQuery(arg)
2702
2444
            origin = self.buildOrigin(join_tables, [], clauseTables)
2703
 
            localstore = store
2704
2445
            if with_clause:
2705
 
                localstore = orig_store.with_(with_clause)
2706
 
            next_result = localstore.using(*origin).find(
2707
 
                inner_resultrow, query)
 
2446
                store = orig_store.with_(with_clause)
 
2447
            next_result = store.using(*origin).find(inner_resultrow, query)
2708
2448
            resultset = resultset.union(next_result)
2709
2449
            # NB: assumes the decorators are all compatible.
2710
2450
            # This may need revisiting if e.g. searches on behalf of different
2784
2524
        """See `IBugTaskSet`."""
2785
2525
        return self._search(BugTask.bugID, [], None, params).result_set
2786
2526
 
2787
 
    def countBugs(self, user, contexts, group_on):
 
2527
    def countBugs(self, params, group_on):
2788
2528
        """See `IBugTaskSet`."""
2789
 
        # Circular fail.
2790
 
        from lp.bugs.model.bugsummary import BugSummary
2791
 
        conditions = []
2792
 
        # Open bug statuses
2793
 
        conditions.append(
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:
2803
 
            return {}
2804
 
        conditions.append(Or(*context_conditions))
2805
 
        # bugsummary by design requires either grouping by tag or excluding
2806
 
        # non-null tags.
2807
 
        # This is an awkward way of saying
2808
 
        # if BugSummary.tag not in group_on:
2809
 
        # - see bug 799602
2810
 
        group_on_tag = False
2811
 
        for column in group_on:
2812
 
            if column is BugSummary.tag:
2813
 
                group_on_tag = True
2814
 
        if not group_on_tag:
2815
 
            conditions.append(BugSummary.tag == None)
2816
 
        else:
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(
2824
 
                "teams AS ("
2825
 
                "SELECT team from TeamParticipation WHERE person=?)",
2826
 
                (user.id,)))
2827
 
        # Note that because admins can see every bug regardless of
2828
 
        # subscription they will see rather inflated counts. Admins get to
2829
 
        # deal.
2830
 
        if user is None:
2831
 
            conditions.append(BugSummary.viewed_by_id == None)
2832
 
        elif not user.inTeam(admin_team):
2833
 
            conditions.append(
2834
 
                Or(
2835
 
                    BugSummary.viewed_by_id == None,
2836
 
                    BugSummary.viewed_by_id.is_in(
2837
 
                        SQL("SELECT team FROM teams"))
2838
 
                    ))
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()
2845
2535
        result = {}
2846
2536
        for row in resultset:
2855
2545
            omit_dupes=True, exclude_conjoined_tasks=True)
2856
2546
        return self.search(params)
2857
2547
 
2858
 
    def createTask(self, bug, owner, target,
 
2548
    def createTask(self, bug, owner, product=None, productseries=None,
 
2549
                   distribution=None, distroseries=None,
 
2550
                   sourcepackagename=None,
2859
2551
                   status=IBugTask['status'].default,
2860
2552
                   importance=IBugTask['importance'].default,
2861
2553
                   assignee=None, milestone=None):
2869
2561
        if not milestone:
2870
2562
            milestone = None
2871
2563
 
2872
 
        # Make sure there's no task for this bug already filed
2873
 
        # against the target.
2874
 
        validate_new_target(bug, target)
2875
 
 
2876
 
        target_key = bug_target_to_key(target)
 
2564
        # Raise a WidgetError if this product bugtask already exists.
 
2565
        target = None
 
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
 
2580
 
 
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.
 
2589
                target = 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
 
2597
 
 
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)
 
2602
 
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)
2884
2608
 
 
2609
        assert (product or productseries or distribution or distroseries), (
 
2610
            'Got no bugtask target.')
 
2611
 
2885
2612
        non_target_create_params = dict(
2886
2613
            bug=bug,
2887
 
            _status=status,
 
2614
            status=status,
2888
2615
            importance=importance,
2889
2616
            assignee=assignee,
2890
2617
            owner=owner,
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']:
 
2619
        bugtask = BugTask(
 
2620
            product=product,
 
2621
            productseries=productseries,
 
2622
            distribution=distribution,
 
2623
            distroseries=distroseries,
 
2624
            sourcepackagename=sourcepackagename,
 
2625
            **non_target_create_params)
 
2626
 
 
2627
        if distribution:
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()
2908
2639
 
2926
2657
            # since the get_bug_privacy_filter() check for non-admins is
2927
2658
            # costly, don't filter those bugs at all.
2928
2659
            bug_privacy_filter = ''
 
2660
        cur = cursor()
 
2661
 
2929
2662
        # The union is actually much faster than a LEFT JOIN with the
2930
2663
        # Milestone table, since postgres optimizes it to perform index
2931
2664
        # scans instead of sequential scans on the BugTask table.
2932
2665
        query = """
2933
 
            SELECT
2934
 
                status, COUNT(*)
 
2666
            SELECT status, count(*)
2935
2667
            FROM (
2936
2668
                SELECT BugTask.status
2937
2669
                FROM BugTask
2939
2671
                WHERE
2940
2672
                    BugTask.productseries = %(series)s
2941
2673
                    %(privacy)s
 
2674
 
2942
2675
                UNION ALL
 
2676
 
2943
2677
                SELECT BugTask.status
2944
2678
                FROM BugTask
2945
2679
                    JOIN Bug ON BugTask.bug = Bug.id
2950
2684
                    %(privacy)s
2951
2685
                ) AS subquery
2952
2686
            GROUP BY status
2953
 
            """
2954
 
        query %= dict(
2955
 
            series=quote(product_series),
2956
 
            privacy=bug_privacy_filter)
2957
 
        cur = cursor()
 
2687
            """ % dict(series=quote(product_series),
 
2688
                       privacy=bug_privacy_filter)
 
2689
 
2958
2690
        cur.execute(query)
2959
 
        return dict(
2960
 
            (get_bugtask_status(status_id), count)
2961
 
            for (status_id, count) in cur.fetchall())
 
2691
        return cur.fetchall()
2962
2692
 
2963
2693
    def findExpirableBugTasks(self, min_days_old, user,
2964
2694
                              bug=None, target=None, limit=None):
3016
2746
                """ + target_clause + """
3017
2747
                """ + bug_clause + """
3018
2748
                """ + bug_privacy_filter + """
3019
 
                    AND BugTask.status in (%s, %s, %s)
 
2749
                    AND BugTask.status = %s
3020
2750
                    AND BugTask.assignee IS NULL
3021
2751
                    AND BugTask.milestone IS NULL
3022
2752
                    AND Bug.duplicateof IS NULL
3023
2753
                    AND Bug.date_last_updated < CURRENT_TIMESTAMP
3024
2754
                        AT TIME ZONE 'UTC' - interval '%s days'
3025
2755
                    AND BugWatch.id IS NULL
3026
 
            )""" % sqlvalues(BugTaskStatus.INCOMPLETE,
3027
 
                BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE,
3028
 
                BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE, min_days_old)
 
2756
            )""" % sqlvalues(BugTaskStatus.INCOMPLETE, min_days_old)
3029
2757
        expirable_bugtasks = BugTask.select(
3030
2758
            query + unconfirmed_bug_condition,
3031
2759
            clauseTables=['Bug'],
3043
2771
        """
3044
2772
        statuses_not_preventing_expiration = [
3045
2773
            BugTaskStatus.INVALID, BugTaskStatus.INCOMPLETE,
3046
 
            BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE,
3047
2774
            BugTaskStatus.WONTFIX]
3048
2775
 
3049
2776
        unexpirable_status_list = [
3189
2916
            ]
3190
2917
 
3191
2918
        product_ids = [product.id for product in products]
3192
 
        conditions = And(
3193
 
            BugTask._status.is_in(DB_UNRESOLVED_BUGTASK_STATUSES),
3194
 
            Bug.duplicateof == None,
3195
 
            BugTask.productID.is_in(product_ids))
 
2919
        conditions = And(BugTask.status.is_in(UNRESOLVED_BUGTASK_STATUSES),
 
2920
                         Bug.duplicateof == None,
 
2921
                         BugTask.productID.is_in(product_ids))
3196
2922
 
3197
2923
        privacy_filter = get_bug_privacy_filter(user)
3198
2924
        if privacy_filter != '':
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 (
3214
 
                Bug,
3215
 
                BugTag,
3216
 
                )
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, []),
 
2941
                "task": BugTask.id,
 
2942
                "id": BugTask.bugID,
 
2943
                "importance": BugTask.importance,
3225
2944
                # TODO: sort by their name?
3226
 
                "assignee": (
3227
 
                    Assignee.name,
3228
 
                    [
3229
 
                        (Assignee,
3230
 
                         LeftJoin(Assignee, BugTask.assignee == Assignee.id))
3231
 
                        ]),
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, []),
3245
 
                "milestone_name": (
3246
 
                    Milestone.name,
3247
 
                    [
3248
 
                        (Milestone,
3249
 
                         LeftJoin(Milestone,
3250
 
                                  BugTask.milestone == Milestone.id))
3251
 
                        ]),
3252
 
                "reporter": (
3253
 
                    Reporter.name,
3254
 
                    [
3255
 
                        (Bug, Join(Bug, BugTask.bug == Bug.id)),
3256
 
                        (Reporter, Join(Reporter, Bug.owner == Reporter.id))
3257
 
                        ]),
3258
 
                "tag": (
3259
 
                    BugTag.tag,
3260
 
                    [
3261
 
                        (Bug, Join(Bug, BugTask.bug == Bug.id)),
3262
 
                        (BugTag,
3263
 
                         LeftJoin(
3264
 
                             BugTag,
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
3271
 
                                 """))),
3272
 
                        ]
3273
 
                    ),
3274
 
                "specification": (
3275
 
                    Specification.name,
3276
 
                    [
3277
 
                        (Bug, Join(Bug, BugTask.bug == Bug.id)),
3278
 
                        (Specification,
3279
 
                         LeftJoin(
3280
 
                             Specification,
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
3287
 
                                 JOIN Specification
3288
 
                                     ON SpecificationBug.specification=
3289
 
                                         Specification.id
3290
 
                                 WHERE SpecificationBug.bug=Bug.id
3291
 
                                 ORDER BY Specification.name
3292
 
                                 LIMIT 1
3293
 
                                 """))),
3294
 
                        ]
3295
 
                    ),
 
2945
                "assignee": BugTask.assigneeID,
 
2946
                "targetname": BugTask.targetnamecache,
 
2947
                "status": BugTask.status,
 
2948
                "title": Bug.title,
 
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,
3296
2959
                }
3297
2960
        return BugTaskSet._ORDERBY_COLUMN[col_name]
3298
2961
 
3341
3004
 
3342
3005
        # Translate orderby keys into corresponding Table.attribute
3343
3006
        # strings.
3344
 
        extra_joins = []
3345
3007
        ambiguous = True
3346
 
        # Sorting by milestone only is a very "coarse" sort order.
3347
 
        # If no additional sort order is specified, add the bug task
3348
 
        # importance as a secondary sort order.
3349
 
        if len(orderby) == 1:
3350
 
            if orderby[0] == 'milestone_name':
3351
 
                # We want the most important bugtasks first; these have
3352
 
                # larger integer values.
3353
 
                orderby.append('-importance')
3354
 
            elif orderby[0] == '-milestone_name':
3355
 
                orderby.append('importance')
3356
 
            else:
3357
 
                # Other sort orders don't need tweaking.
3358
 
                pass
3359
 
 
3360
3008
        for orderby_col in orderby:
3361
3009
            if isinstance(orderby_col, SQLConstant):
3362
3010
                orderby_arg.append(orderby_col)
3363
3011
                continue
3364
3012
            if orderby_col.startswith("-"):
3365
 
                col, sort_joins = self.getOrderByColumnDBName(orderby_col[1:])
3366
 
                extra_joins.extend(sort_joins)
 
3013
                col = self.getOrderByColumnDBName(orderby_col[1:])
3367
3014
                order_clause = Desc(col)
3368
3015
            else:
3369
 
                col, sort_joins = self.getOrderByColumnDBName(orderby_col)
3370
 
                extra_joins.extend(sort_joins)
 
3016
                col = self.getOrderByColumnDBName(orderby_col)
3371
3017
                order_clause = col
3372
3018
            if col in unambiguous_cols:
3373
3019
                ambiguous = False
3379
3025
            else:
3380
3026
                orderby_arg.append(BugTask.id)
3381
3027
 
3382
 
        return tuple(orderby_arg), extra_joins
 
3028
        return tuple(orderby_arg)
3383
3029
 
3384
3030
    def getBugCountsForPackages(self, user, packages):
3385
3031
        """See `IBugTaskSet`."""
3405
3051
 
3406
3052
        open_bugs_cond = (
3407
3053
            'BugTask.status %s' % search_value_to_where_condition(
3408
 
                any(*DB_UNRESOLVED_BUGTASK_STATUSES)))
 
3054
                any(*UNRESOLVED_BUGTASK_STATUSES)))
3409
3055
 
3410
3056
        sum_template = "SUM(CASE WHEN %s THEN 1 ELSE 0 END) AS %s"
3411
3057
        sums = [
3495
3141
        distro_series_ids = set()
3496
3142
        product_ids = set()
3497
3143
        product_series_ids = set()
3498
 
 
 
3144
        
3499
3145
        # Gather all the ids that might have milestones to preload for the
3500
3146
        # for the milestone vocabulary
3501
3147
        for task in bugtasks:
3505
3151
            product_ids.add(task.productID)
3506
3152
            product_series_ids.add(task.productseriesID)
3507
3153
 
3508
 
        distro_ids.discard(None)
3509
 
        distro_series_ids.discard(None)
3510
 
        product_ids.discard(None)
3511
 
        product_series_ids.discard(None)
3512
 
 
 
3154
        distro_ids.discard(None) 
 
3155
        distro_series_ids.discard(None) 
 
3156
        product_ids.discard(None) 
 
3157
        product_series_ids.discard(None) 
 
3158
        
3513
3159
        milestones = store.find(
3514
3160
            Milestone,
3515
3161
            Or(
3527
3173
            Product, Product.id.is_in(product_ids)))
3528
3174
        list(store.find(
3529
3175
            ProductSeries, ProductSeries.id.is_in(product_series_ids)))
3530
 
 
 
3176
            
3531
3177
        return milestones
 
3178
 
 
3179