625
621
return self._status in RESOLVED_BUGTASK_STATUSES
627
def canBeDeleted(self):
628
num_bugtasks = Store.of(self).find(
629
BugTask, bug=self.bug).count()
631
return num_bugtasks > 1
633
def delete(self, who=None):
634
"""See `IBugTask`."""
636
who = getUtility(ILaunchBag).user
638
if not self.canBeDeleted():
639
raise CannotDeleteBugtask(
640
"Cannot delete bugtask: %s" % self.title)
643
notify(ObjectDeletedEvent(self, who))
645
del get_property_cache(bug).bugtasks
647
# When a task is deleted the bug's heat needs to be recalculated.
648
target.recalculateBugHeatCache()
650
623
def findSimilarBugs(self, user, limit=10):
651
624
"""See `IBugTask`."""
652
625
if self.product is not None:
1753
1726
summary, Bug, ' AND '.join(constraint_clauses), ['BugTask'])
1754
1727
return self.search(search_params, _noprejoins=True)
1757
def _buildStatusClause(cls, status):
1729
def _buildStatusClause(self, status):
1758
1730
"""Return the SQL query fragment for search by status.
1760
1732
Called from `buildQuery` or recursively."""
1761
1733
if zope_isinstance(status, any):
1762
values = list(status.query_values)
1763
# Since INCOMPLETE isn't stored as a single value we need to
1764
# expand it before generating the SQL.
1765
if BugTaskStatus.INCOMPLETE in values:
1766
values.remove(BugTaskStatus.INCOMPLETE)
1767
values.extend(DB_INCOMPLETE_BUGTASK_STATUSES)
1768
return '(BugTask.status {0})'.format(
1769
search_value_to_where_condition(any(*values)))
1734
return '(' + ' OR '.join(
1735
self._buildStatusClause(dbitem)
1737
in status.query_values) + ')'
1770
1738
elif zope_isinstance(status, not_equals):
1771
return '(NOT {0})'.format(cls._buildStatusClause(status.value))
1739
return '(NOT %s)' % self._buildStatusClause(status.value)
1772
1740
elif zope_isinstance(status, BaseItem):
1773
# INCOMPLETE is not stored in the DB, instead one of
1774
# DB_INCOMPLETE_BUGTASK_STATUSES is stored, so any request to
1775
# search for INCOMPLETE should instead search for those values.
1776
if status == BugTaskStatus.INCOMPLETE:
1777
return '(BugTask.status {0})'.format(
1778
search_value_to_where_condition(
1779
any(*DB_INCOMPLETE_BUGTASK_STATUSES)))
1741
incomplete_response = (
1742
status == BugTaskStatus.INCOMPLETE)
1744
status == BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE)
1745
without_response = (
1746
status == BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE)
1747
# TODO: bug 759467 tracks the migration of INCOMPLETE in the db to
1748
# INCOMPLETE_WITH_RESPONSE and INCOMPLETE_WITHOUT_RESPONSE. When
1749
# the migration is complete, we can convert status lookups to a
1751
if with_response or without_response:
1754
BugTask.status = %s OR
1755
(BugTask.status = %s
1756
AND (Bug.date_last_message IS NOT NULL
1757
AND BugTask.date_incomplete <=
1758
Bug.date_last_message)))
1760
BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE,
1761
BugTaskStatus.INCOMPLETE)
1762
elif without_response:
1764
BugTask.status = %s OR
1765
(BugTask.status = %s
1766
AND (Bug.date_last_message IS NULL
1767
OR BugTask.date_incomplete >
1768
Bug.date_last_message)))
1770
BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE,
1771
BugTaskStatus.INCOMPLETE)
1772
assert with_response != without_response
1773
elif incomplete_response:
1774
# search for any of INCOMPLETE (being migrated from),
1775
# INCOMPLETE_WITH_RESPONSE or INCOMPLETE_WITHOUT_RESPONSE
1776
return 'BugTask.status %s' % search_value_to_where_condition(
1777
any(BugTaskStatus.INCOMPLETE,
1778
BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE,
1779
BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE))
1781
1781
return '(BugTask.status = %s)' % sqlvalues(status)
1783
raise ValueError('Unrecognized status value: %r' % (status,))
1783
raise AssertionError(
1784
'Unrecognized status value: %s' % repr(status))
1785
1786
def _buildExcludeConjoinedClause(self, milestone):
1786
1787
"""Exclude bugtasks with a conjoined master.