1
# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
1
# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
2
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
4
# pylint: disable-msg=E0211,E0213,E0602
18
18
'BugTaskStatusSearch',
19
19
'BugTaskStatusSearchDisplay',
20
'CannotDeleteBugtask',
21
'DB_INCOMPLETE_BUGTASK_STATUSES',
22
'DB_UNRESOLVED_BUGTASK_STATUSES',
23
20
'DEFAULT_SEARCH_BUGTASK_STATUSES_FOR_DISPLAY',
26
22
'IAddBugTaskWithProductCreationForm',
32
27
'ICreateQuestionFromBugTaskForm',
29
'IDistroSeriesBugTask',
33
30
'IFrontPageBugTaskSearch',
34
'IllegalRelatedBugTasksParams',
36
31
'INominationsReviewTableBatchNavigator',
37
32
'IPersonBugTaskSearch',
33
'IProductSeriesBugTask',
38
34
'IRemoveQuestionFromBugTaskForm',
39
36
'IUpstreamProductBugTaskSearch',
40
'normalize_bugtask_status',
37
'IllegalRelatedBugTasksParams',
41
39
'RESOLVED_BUGTASK_STATUSES',
42
40
'UNRESOLVED_BUGTASK_STATUSES',
43
41
'UserCannotEditBugTaskAssignee',
99
96
from zope.security.interfaces import Unauthorized
100
97
from zope.security.proxy import isinstance as zope_isinstance
103
from lp.app.interfaces.launchpad import IHasDateCreated
99
from canonical.launchpad import _
100
from canonical.launchpad.interfaces.launchpad import (
104
from canonical.launchpad.searchbuilder import (
109
from canonical.launchpad.webapp.interfaces import ITableBatchNavigator
104
110
from lp.app.validators import LaunchpadValidationError
105
111
from lp.app.validators.name import name_validator
106
112
from lp.bugs.interfaces.bugwatch import (
201
200
this product or source package.
204
# INCOMPLETE is never actually stored now: INCOMPLETE_WITH_RESPONSE and
205
# INCOMPLETE_WITHOUT_RESPONSE are mapped to INCOMPLETE on read, and on
206
# write INCOMPLETE is mapped to INCOMPLETE_WITHOUT_RESPONSE. This permits
207
# An index on the INCOMPLETE_WITH*_RESPONSE queries that the webapp
209
203
INCOMPLETE = DBItem(15, """
294
292
use_template(BugTaskStatus, exclude=('UNKNOWN'))
296
INCOMPLETE_WITH_RESPONSE = DBItem(13, """
295
'NEW', 'INCOMPLETE_WITH_RESPONSE', 'INCOMPLETE_WITHOUT_RESPONSE',
296
'INCOMPLETE', 'OPINION', 'INVALID', 'WONTFIX', 'EXPIRED',
297
'CONFIRMED', 'TRIAGED', 'INPROGRESS', 'FIXCOMMITTED', 'FIXRELEASED')
299
INCOMPLETE_WITH_RESPONSE = DBItem(35, """
297
300
Incomplete (with response)
299
302
This bug has new information since it was last marked
300
303
as requiring a response.
303
INCOMPLETE_WITHOUT_RESPONSE = DBItem(14, """
306
INCOMPLETE_WITHOUT_RESPONSE = DBItem(40, """
304
307
Incomplete (without response)
306
309
This bug requires more information, but no additional
311
def get_bugtask_status(status_id):
312
"""Get a member of `BugTaskStatus` or `BugTaskStatusSearch` by value.
314
`BugTaskStatus` and `BugTaskStatusSearch` intersect, but neither is a
315
subset of the other, so this searches first in `BugTaskStatus` then in
316
`BugTaskStatusSearch` for a member with the given ID.
319
return BugTaskStatus.items[status_id]
321
return BugTaskStatusSearch.items[status_id]
324
def normalize_bugtask_status(status):
325
"""Normalize `status`.
327
It might be a member of any of three related enums: `BugTaskStatus`,
328
`BugTaskStatusSearch`, or `BugTaskStatusSearchDisplay`. This tries to
329
normalize by value back to the first of those three enums in which the
333
return BugTaskStatus.items[status.value]
335
return BugTaskStatusSearch.items[status.value]
338
314
class BugTagsSearchCombinator(EnumeratedType):
339
315
"""Bug Tags Search Combinator
399
375
BugTaskStatus.INPROGRESS,
400
376
BugTaskStatus.FIXCOMMITTED)
402
# Actual values stored in the DB:
403
DB_INCOMPLETE_BUGTASK_STATUSES = (
404
BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE,
405
BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE,
408
DB_UNRESOLVED_BUGTASK_STATUSES = (
409
UNRESOLVED_BUGTASK_STATUSES +
410
DB_INCOMPLETE_BUGTASK_STATUSES
413
378
RESOLVED_BUGTASK_STATUSES = (
414
379
BugTaskStatus.FIXRELEASED,
415
380
BugTaskStatus.OPINION,
436
401
for item in DEFAULT_SEARCH_BUGTASK_STATUSES]
439
@error_status(httplib.BAD_REQUEST)
440
class CannotDeleteBugtask(Exception):
441
"""The bugtask cannot be deleted.
443
Raised when a user tries to delete a bugtask but the deletion cannot
444
proceed because of a model constraint or other business rule violation.
448
404
@error_status(httplib.UNAUTHORIZED)
449
405
class UserCannotEditBugTaskStatus(Unauthorized):
450
406
"""User not permitted to change status.
492
448
in a search for related bug tasks"""
495
class IBugTaskDelete(Interface):
496
"""An interface for operations allowed with the Delete permission."""
497
@export_destructor_operation()
498
@call_with(who=REQUEST_USER)
499
@operation_for_version('devel')
501
"""Delete this bugtask.
503
:param who: the user who is removing the bugtask.
504
:raises: CannotDeleteBugtask if the bugtask cannot be deleted due to a
505
business rule or other model constraint.
506
:raises: Unauthorized if the user does not have permission
507
to delete the bugtask.
511
class IBugTask(IHasDateCreated, IHasBug, IBugTaskDelete):
451
class IBugTask(IHasDateCreated, IHasBug):
512
452
"""A bug needing fixing in a particular product or package."""
513
453
export_as_webservice_entry()
540
480
schema=Interface)) # IMilestone
541
481
milestoneID = Attribute('The id of the milestone.')
483
# XXX kiko 2006-03-23:
543
484
# The status and importance's vocabularies do not
544
485
# contain an UNKNOWN item in bugtasks that aren't linked to a remote
545
486
# bugwatch; this would be better described in a separate interface,
546
487
# but adding a marker interface during initialization is expensive,
547
488
# and adding it post-initialization is not trivial.
548
# Note that status is a property because the model only exposes INCOMPLETE
549
# but the DB stores INCOMPLETE_WITH_RESPONSE and
550
# INCOMPLETE_WITHOUT_RESPONSE for query efficiency.
551
489
status = exported(
552
490
Choice(title=_('Status'), vocabulary=BugTaskStatus,
553
491
default=BugTaskStatus.NEW, readonly=True))
554
_status = Attribute('The actual status DB column used in queries.')
555
492
importance = exported(
556
493
Choice(title=_('Importance'), vocabulary=BugTaskImportance,
557
494
default=BugTaskImportance.UNDECIDED, readonly=True))
495
statusexplanation = Text(
496
title=_("Status notes (optional)"), required=False)
558
497
assignee = exported(
560
499
title=_('Assigned to'), required=False,
860
799
value is set to None, date_assigned is also set to None.
863
def validateTransitionToTarget(target):
864
"""Check whether a transition to this target is legal.
866
:raises IllegalTarget: if the new target is not allowed.
869
802
@mutator_for(target)
870
803
@operation_parameters(
871
804
target=copy_field(target))
899
832
def getDelta(old_task):
900
833
"""Compute the delta from old_task to this task.
835
old_task and this task are either both IDistroBugTask's or both
836
IUpstreamBugTask's, otherwise a TypeError is raised.
902
838
Returns an IBugTaskDelta or None if there were no changes between
903
839
old_task and this task.
911
847
not a package task, returns None.
914
def userHasDriverPrivileges(user):
915
"""Does the user have driver privledges on the current bugtask?
920
def userHasBugSupervisorPrivileges(user):
921
"""Is the user a privledged one, allowed to changed details on a
850
def userCanEditMilestone(user):
851
"""Can the user edit the Milestone field?"""
853
def userCanEditImportance(user):
854
"""Can the user edit the Importance field?"""
928
857
# Set schemas that were impossible to specify during the definition of
993
922
omit_targeted = Bool(
994
923
title=_('Omit bugs targeted to a series'), required=False,
925
statusexplanation = TextLine(
926
title=_("Status notes"), required=False)
996
927
has_patch = Bool(
997
928
title=_('Show only bugs with patches available.'), required=False,
1023
954
title=_('Show only bugs associated with a CVE'), required=False)
1024
structural_subscriber = Choice(
1025
title=_('Structural Subscriber'), vocabulary='ValidPersonOrTeam',
1027
'Show only bugs in projects, series, distributions, and packages '
1028
'that this person or team is subscribed to.'),
955
bug_supervisor = Choice(
956
title=_('Bug supervisor'), vocabulary='ValidPersonOrTeam',
957
description=_('Show only bugs in packages this person or team '
958
'is subscribed to.'),
1030
960
bug_commenter = Choice(
1031
961
title=_('Bug commenter'), vocabulary='ValidPersonOrTeam',
1102
1032
required=False)
1105
class IFrontPageBugTaskSearch(IBugTaskSearch):
1035
class IFrontPageBugTaskSearch(IBugTaskSearchBase):
1106
1036
"""Additional search options for the front page of bugs."""
1107
1037
scope = Choice(
1108
1038
title=u"Search Scope", required=False,
1143
1073
The value is a dict like {'old' : IPerson, 'new' : IPerson}, or None,
1144
1074
if no change was made to the assignee.
1076
statusexplanation = Attribute("The new value of the status notes.")
1146
1077
bugwatch = Attribute("The bugwatch which governs this task.")
1147
1078
milestone = Attribute("The milestone for which this task is scheduled.")
1081
class IUpstreamBugTask(IBugTask):
1082
"""A bug needing fixing in a product."""
1083
# XXX Brad Bollenbach 2006-08-03 bugs=55089:
1084
# This interface should be renamed.
1085
product = Choice(title=_('Project'), required=True, vocabulary='Product')
1088
class IDistroBugTask(IBugTask):
1089
"""A bug needing fixing in a distribution, possibly a specific package."""
1090
sourcepackagename = Choice(
1091
title=_("Source Package Name"), required=False,
1092
description=_("The source package in which the bug occurs. "
1093
"Leave blank if you are not sure."),
1094
vocabulary='SourcePackageName')
1095
distribution = Choice(
1096
title=_("Distribution"), required=True, vocabulary='Distribution')
1099
class IDistroSeriesBugTask(IBugTask):
1100
"""A bug needing fixing in a distrorelease, or a specific package."""
1101
sourcepackagename = Choice(
1102
title=_("Source Package Name"), required=True,
1103
vocabulary='SourcePackageName')
1104
distroseries = Choice(
1105
title=_("Series"), required=True,
1106
vocabulary='DistroSeries')
1109
class IProductSeriesBugTask(IBugTask):
1110
"""A bug needing fixing a productseries."""
1111
productseries = Choice(
1112
title=_("Series"), required=True,
1113
vocabulary='ProductSeries')
1150
1116
class BugTaskSearchParams:
1151
1117
"""Encapsulates search parameters for BugTask.search()
1481
1447
class IBugTaskSet(Interface):
1482
1448
"""A utility to retrieving BugTasks."""
1483
1449
title = Attribute('Title')
1484
orderby_expression = Attribute(
1485
"The SQL expression for a sort key")
1487
1451
def get(task_id):
1488
1452
"""Retrieve a BugTask with the given id.
1556
1520
:param params: the BugTaskSearchParams to search on.
1559
def countBugs(user, contexts, group_on):
1560
"""Count open bugs that match params, grouping by group_on.
1562
This serves results from the bugsummary fact table: it is fast but not
1563
completely precise. See the bug summary documentation for more detail.
1565
:param user: The user to query on behalf of.
1566
:param contexts: A list of contexts to search. Contexts must support
1567
the IBugSummaryDimension interface.
1523
def countBugs(params, group_on):
1524
"""Count bugs that match params, grouping by group_on.
1526
:param param: A BugTaskSearchParams object.
1568
1527
:param group_on: The column(s) group on - .e.g (
1569
BugSummary.distroseries_id, BugSummary.milestone_id) will cause
1528
Bugtask.distroseriesID, BugTask.milestoneID) will cause
1570
1529
grouping by distro series and then milestone.
1571
1530
:return: A dict {group_instance: count, ...}
1581
1540
:return: A list of tuples containing (status_id, count).
1584
def createTask(bug, owner, target, status=None, importance=None,
1585
assignee=None, milestone=None):
1543
def createTask(bug, product=None, productseries=None, distribution=None,
1544
distroseries=None, sourcepackagename=None, status=None,
1545
importance=None, assignee=None, owner=None,
1586
1547
"""Create a bug task on a bug and return it.
1588
1549
If the bug is public, bug supervisors will be automatically
1591
1552
If the bug has any accepted series nominations for a supplied
1592
1553
distribution, series tasks will be created for them.
1555
Exactly one of product, distribution or distroseries must be provided.
1595
1558
def findExpirableBugTasks(min_days_old, user, bug=None, target=None,
1639
1602
<user> is None, no private bugtasks will be returned.
1605
def getOrderByColumnDBName(col_name):
1606
"""Get the database name for col_name.
1608
If the col_name is unrecognized, a KeyError is raised.
1642
1611
def getBugCountsForPackages(user, packages):
1643
1612
"""Return open bug counts for the list of packages.
1664
1633
The assignee and the assignee's validity are precached.
1667
def getBugTaskTargetMilestones(bugtasks):
1636
def getBugTaskTargetMilestones(self, bugtasks, eager=False):
1668
1637
"""Get all the milestones for the selected bugtasks' targets."""
1670
1639
open_bugtask_search = Attribute("A search returning open bugTasks.")