← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rharding/launchpad/editstatus_timeout_874250 into lp:launchpad

 

Richard Harding has proposed merging lp:~rharding/launchpad/editstatus_timeout_874250 into lp:launchpad.

Requested reviews:
  Deryck Hodge (deryck)
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #874250 in Launchpad itself: "BugNomination:+editstatus timeout for bugs with many tasks"
  https://bugs.launchpad.net/launchpad/+bug/874250

For more details, see:
https://code.launchpad.net/~rharding/launchpad/editstatus_timeout_874250/+merge/103912

= Summary =
This takes another hack at improving the performance of approving the
bug nomination when the bug is listed across many source packages and several
series.

See:
https://lp-oops.canonical.com/oops.py/?oopsid=OOPS-6e6b4b29ef635939095468528e92c7ec

== Pre Implementation ==
Had a chat with Deryck. The goal is to fix the queries doing the 'INSERT INTO
bugtask' by avoiding the loop

== Implementation Notes ==
To do this we add a createManyTasks method and using that when approving bug

== Q/A ==
We're going to have to try to get a bug nomination such as the bug we're linked against and approve it. It'll still probably oops,but we need to check the oops out and make sure we're only seeing one instance of the 'INSERT INTO bugtask' query that is all over the oops linked in the `Summary`.

== Tests ==

lib/lp/bugs/stories/bugtask-management/xx-change-milestone.txt
lib/lp/bugs/doc/bugtask.txt

== Lint ==
All clean


== LoC Qualification ==
I've moved the doc/bugtask.txt into the unit tests of tests/test_bugtaskset.py.

I tried to split out the bugtask vs bugtaskset, but it's a bit of a grey area and kept it together for now. 

Since this is just a moving of tests to the unit tests for the most part, Deryck agreed to review even though it was over the line count limitations. In the future, I need to think ahead more about possible places to split work into multiple smaller branches and to make this more manageable. Many apologies to Deryck and owe him one for taking this on.
-- 
https://code.launchpad.net/~rharding/launchpad/editstatus_timeout_874250/+merge/103912
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rharding/launchpad/editstatus_timeout_874250 into lp:launchpad.
=== removed file 'lib/lp/bugs/doc/bugtask.txt'
--- lib/lp/bugs/doc/bugtask.txt	2012-05-04 05:48:00 +0000
+++ lib/lp/bugs/doc/bugtask.txt	1970-01-01 00:00:00 +0000
@@ -1,1252 +0,0 @@
-Introduction
-===================================
-
-Bugs are problems in software. When a bug gets assigned to a specific
-upstream or distro/sourcepackagename, a bug /task/ is created. In
-essence, a bug task is a bug that needs to be fixed in a specific
-place. Where a bug has things like a title, comments and subscribers,
-it's the bug task that tracks importance, assignee, etc.
-
-Working with Bug Tasks in Launchpad
-===================================
-
-
-Creating Bug Tasks
-------------------
-
-All BugTask creation and retrieval is done through an IBugTaskSet utility.
-
-    >>> from zope.component import getUtility
-    >>> import transaction
-    >>> from lp.bugs.interfaces.bugtask import IBugTaskSet
-    >>> bugtaskset = getUtility(IBugTaskSet)
-
-To create a bug task, you have to be logged in:
-
-    >>> from lp.testing import login, ANONYMOUS
-    >>> login('foo.bar@xxxxxxxxxxxxx')
-
-There are three kinds of bug tasks. We need to pass the bug task creation
-methods some other objects to create a task, so lets get the utilities we need
-to access those other objects:
-
-    >>> from lp.bugs.interfaces.bug import IBugSet
-    >>> from lp.registry.interfaces.distribution import IDistributionSet
-    >>> from lp.registry.interfaces.distroseries import IDistroSeriesSet
-    >>> from lp.registry.interfaces.person import IPersonSet
-    >>> from lp.registry.interfaces.product import IProductSet
-    >>> from lp.registry.interfaces.sourcepackagename import (
-    ...     ISourcePackageNameSet)
-    >>> productset = getUtility(IProductSet)
-    >>> distroset = getUtility(IDistributionSet)
-    >>> distoseriesset = getUtility(IDistroSeriesSet)
-    >>> sourcepackagenameset = getUtility(ISourcePackageNameSet)
-    >>> bugset = getUtility(IBugSet)
-    >>> personset = getUtility(IPersonSet)
-    >>> bug_one = bugset.get(1)
-    >>> mark = personset.getByEmail('mark@xxxxxxxxxxx')
-
-Next, we need to grab some values to provide for importance and status.
-
-    >>> from lp.bugs.interfaces.bugtask import (
-    ...     BugTaskImportance,
-    ...     BugTaskStatus,
-    ...     )
-    >>> STATUS_NEW = BugTaskStatus.NEW
-    >>> STATUS_CONFIRMED = BugTaskStatus.CONFIRMED
-    >>> STATUS_FIXRELEASED = BugTaskStatus.FIXRELEASED
-    >>> IMPORTANCE_MEDIUM = BugTaskImportance.MEDIUM
-
-  i. Upstream -- a bug that has to be fixed in an upstream product
-
-    >>> evolution = productset.get(5)
-    >>> upstream_task = bugtaskset.createTask(
-    ...     bug_one, mark, evolution,
-    ...     status=STATUS_NEW, importance=IMPORTANCE_MEDIUM)
-    >>> upstream_task.product == evolution
-    True
-
-  ii. Distro -- a bug that has to be fixed in a specific distro
-
-    >>> ubuntu = distroset.get(1)
-    >>> a_distro = factory.makeDistribution(name='tubuntu')
-    >>> distro_task = bugtaskset.createTask(
-    ...     bug_one, mark, a_distro,
-    ...     status=STATUS_NEW, importance=IMPORTANCE_MEDIUM)
-    >>> distro_task.distribution == a_distro
-    True
-
-  ii. Distro Series -- a bug that has to be fixed in a specific distro
-  series. These tasks are used for release management and backporting.
-
-    >>> warty = distoseriesset.get(1)
-    >>> distro_series_task = bugtaskset.createTask(
-    ...     bug_one, mark, warty,
-    ...     status=STATUS_NEW, importance=IMPORTANCE_MEDIUM)
-    >>> distro_series_task.distroseries == warty
-    True
-
-# XXX: Brad Bollenbach 2005-02-24: See the bottom of this file for a chunk of
-# test documentation that is missing from here, due to problems with resetting
-# the connection after a ProgrammingError is raised. ARGH.
-
-
-Bug Task Targets
-----------------
-
-    >>> from lp.registry.interfaces.distributionsourcepackage \
-    ...     import IDistributionSourcePackage
-
-The "target" of an IBugTask can be one of the items in the following
-list.
-
-  * an upstream product
-
-    >>> upstream_task.target == evolution
-    True
-
-  * a product series
-
-
-    >>> firefox = productset['firefox']
-    >>> firefox_1_0 = firefox.getSeries("1.0")
-
-    >>> productseries_task = bugtaskset.createTask(bug_one, mark, firefox_1_0)
-
-    >>> productseries_task.target == firefox_1_0
-    True
-
-  * a distribution
-
-    >>> distro_task.target == a_distro
-    True
-
-  * a distroseries
-
-    >>> distro_series_task.target == warty
-    True
-
-  * a distribution sourcepackage
-
-    >>> def get_expected_target(distro_sp_task):
-    ...      return distro_sp_task.target
-
-    >>> debian_ff_task = bugtaskset.get(4)
-    >>> IDistributionSourcePackage.providedBy(debian_ff_task.target)
-    True
-    >>> target = get_expected_target(debian_ff_task)
-    >>> target.distribution.name, target.sourcepackagename.name
-    (u'debian', u'mozilla-firefox')
-
-    >>> ubuntu_linux_task = bugtaskset.get(25)
-    >>> IDistributionSourcePackage.providedBy(ubuntu_linux_task.target)
-    True
-    >>> target = get_expected_target(ubuntu_linux_task)
-    >>> target.distribution.name, target.sourcepackagename.name
-    (u'ubuntu', u'linux-source-2.6.15')
-
-  * a distroseries sourcepackage
-
-    >>> from lp.registry.interfaces.sourcepackage import ISourcePackage
-    >>> from lp.registry.model.sourcepackage import SourcePackage
-    >>> distro_series_sp_task = bugtaskset.get(16)
-    >>> expected_target = SourcePackage(
-    ...     distroseries=distro_series_sp_task.distroseries,
-    ...     sourcepackagename=distro_series_sp_task.sourcepackagename)
-    >>> got_target = distro_series_sp_task.target
-    >>> ISourcePackage.providedBy(distro_series_sp_task.target)
-    True
-    >>> got_target.distroseries == expected_target.distroseries
-    True
-    >>> got_target.sourcepackagename == expected_target.sourcepackagename
-    True
-
-Each task has a "bugtargetdisplayname" and a "bugtargetname", strings
-describing the site of the task. They concatenate the names of the
-distribution,
-
-    >>> bugtask = bugtaskset.get(17)
-    >>> bugtask.bugtargetdisplayname
-    u'mozilla-firefox (Ubuntu)'
-    >>> bugtask.bugtargetname
-    u'mozilla-firefox (Ubuntu)'
-
-distro series, or product;
-
-    >>> bugtask = bugtaskset.get(2)
-    >>> bugtask.bugtargetdisplayname
-    u'Mozilla Firefox'
-    >>> bugtask.bugtargetname
-    u'firefox'
-
-the name of the source package (if any); and the name of the binary
-package (but only if it's named differently from the source
-package).
-
-
-getPackageComponent
-...................
-
-We offer a convenience method on IBugTask which allows you to look up
-the archive component associated to the bugtask's target. Obviously, it
-only applies to tasks that specify package information:
-
-    >>> print upstream_task.getPackageComponent()
-    None
-    >>> print productseries_task.getPackageComponent()
-    None
-    >>> print distro_task.getPackageComponent()
-    None
-    >>> print distro_series_task.getPackageComponent()
-    None
-
-And it only applies to tasks whose packages which are published in
-IDistribution.currentseries (for bugtasks on IDistributions) or the
-bugtask's series (for bugtasks on IDistroSeries)
-
-    >>> print debian_ff_task.getPackageComponent()
-    None
-    >>> print ubuntu_linux_task.getPackageComponent().name
-    main
-    >>> print distro_series_sp_task.getPackageComponent().name
-    main
-
-
-Editing Bug Tasks
------------------
-
-When changing status we must pass the user making the change. Some
-statuses are restricted to Bug Supervisors only.
-
-
-Upstream Bug Tasks
-..................
-
-To edit an upstream task, you must be logged in. Anonymous users
-cannot edit upstream tasks.
-
-    >>> login(ANONYMOUS)
-    >>> upstream_task.transitionToStatus(
-    ...     STATUS_CONFIRMED, getUtility(ILaunchBag).user)
-    Traceback (most recent call last):
-      ...
-    Unauthorized: (..., 'transitionToStatus', 'launchpad.Edit')
-
-Let's login and try again.
-
-    >>> login('jeff.waugh@xxxxxxxxxxxxxxx')
-    >>> upstream_task.transitionToStatus(
-    ...     STATUS_FIXRELEASED, getUtility(ILaunchBag).user)
-
-
-Distro and Distro Series Bug Tasks
-..................................
-
-Any logged-in user can edit tasks filed on distros as long as the bug
-is not marked private. So, as an anonymous user, we cannot edit
-anything:
-
-    >>> login(ANONYMOUS)
-    >>> distro_task.transitionToStatus(
-    ...     STATUS_FIXRELEASED, getUtility(ILaunchBag).user)
-    Traceback (most recent call last):
-      ...
-    Unauthorized: (..., 'transitionToStatus', 'launchpad.Edit')
-
-    >>> sample_person = personset.getByEmail('test@xxxxxxxxxxxxx')
-    >>> distro_series_task.transitionToAssignee(sample_person)
-    Traceback (most recent call last):
-      ...
-    Unauthorized: (..., 'transitionToAssignee', 'launchpad.Edit')
-
-But once authenticated:
-
-    >>> login('test@xxxxxxxxxxxxx')
-
-We can edit the task:
-
-    >>> distro_task.transitionToStatus(
-    ...     STATUS_FIXRELEASED, getUtility(ILaunchBag).user)
-    >>> distro_series_task.transitionToAssignee(sample_person)
-
-
-Conjoined Bug Tasks
-...................
-
-A bugtask open on the current development series for a distro is kept
-in sync with the "generic" bugtask for that distro, because they
-represent the same piece of work. The same is true for product and
-productseries tasks, when the productseries task is targeted to the
-IProduct.developmentfocus. The following attributes are synced:
-
-    * status
-    * assignee
-    * importance
-    * milestone
-    * sourcepackagename
-    * date_confirmed
-    * date_inprogress
-    * date_assigned
-    * date_closed
-    * date_left_new
-    * date_triaged
-    * date_fix_committed
-    * date_fix_released
-
-We'll open a bug on just the distribution, and also a bug on a specific
-package.
-
-    >>> from lp.services.webapp.interfaces import ILaunchBag
-    >>> from lp.bugs.interfaces.bug import CreateBugParams
-    >>> launchbag = getUtility(ILaunchBag)
-    >>> params = CreateBugParams(
-    ...     owner=launchbag.user,
-    ...     title="a test bug",
-    ...     comment="test bug description")
-    >>> ubuntu_netapplet = ubuntu.getSourcePackage("netapplet")
-    >>> ubuntu_netapplet_bug = ubuntu_netapplet.createBug(params)
-    >>> generic_netapplet_task = ubuntu_netapplet_bug.bugtasks[0]
-
-    >>> ubuntu_bug = ubuntu.createBug(params)
-    >>> generic_ubuntu_task = ubuntu_bug.bugtasks[0]
-
-First, we'll target the bug for the current Ubuntu series, Hoary. Note
-that the synched attributes are copied when the series-specific tasks are
-created. We'll set non-default attribute values for each generic task to
-demonstrate.
-
-    >>> print ubuntu.currentseries.name
-    hoary
-
-    # Only owners, experts, or admins can create a milestone.
-    >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> ubuntu_edgy_milestone = ubuntu.currentseries.newMilestone("knot1")
-    >>> login('test@xxxxxxxxxxxxx')
-
-    >>> generic_netapplet_task.transitionToStatus(
-    ...     BugTaskStatus.INPROGRESS, getUtility(ILaunchBag).user)
-    >>> generic_netapplet_task.transitionToAssignee(sample_person)
-    >>> generic_netapplet_task.milestone = ubuntu_edgy_milestone
-    >>> generic_netapplet_task.transitionToImportance(
-    ...     BugTaskImportance.CRITICAL, ubuntu.owner)
-
-    >>> current_series_ubuntu_task = bugtaskset.createTask(
-    ...     ubuntu_bug, launchbag.user, ubuntu.currentseries)
-
-    >>> current_series_netapplet_task = bugtaskset.createTask(
-    ...     ubuntu_netapplet_bug, launchbag.user,
-    ...     ubuntu_netapplet.development_version)
-
-(The attributes were synched with the generic task.)
-
-    >>> print current_series_netapplet_task.status.title
-    In Progress
-    >>> print current_series_netapplet_task.assignee.displayname
-    Sample Person
-    >>> print current_series_netapplet_task.milestone.name
-    knot1
-    >>> print current_series_netapplet_task.importance.title
-    Critical
-
-    >>> current_series_netapplet_task.date_assigned == (
-    ...     generic_netapplet_task.date_assigned)
-    True
-
-    >>> current_series_netapplet_task.date_confirmed == (
-    ...     generic_netapplet_task.date_confirmed)
-    True
-
-    >>> current_series_netapplet_task.date_inprogress == (
-    ...     generic_netapplet_task.date_inprogress)
-    True
-
-    >>> current_series_netapplet_task.date_closed == (
-    ...     generic_netapplet_task.date_closed)
-    True
-
-We'll also add some product and productseries tasks.
-
-    >>> alsa_utils = productset['alsa-utils']
-
-    >>> print alsa_utils.development_focus.name
-    trunk
-
-    >>> generic_alsa_utils_task = bugtaskset.createTask(
-    ...     ubuntu_netapplet_bug, launchbag.user, alsa_utils)
-
-    >>> devel_focus_alsa_utils_task = bugtaskset.createTask(
-    ...     ubuntu_netapplet_bug, launchbag.user,
-    ...     alsa_utils.getSeries("trunk"))
-
-A conjoined bugtask involves a master and slave in the conjoined
-relationship. The slave is the generic product or distribution task; the
-master is the series-specific task. These tasks are accessed through the
-conjoined_master and conjoined_slave properties.
-
-    >>> current_series_netapplet_task.conjoined_slave == (
-    ...     generic_netapplet_task)
-    True
-    >>> current_series_netapplet_task.conjoined_master is None
-    True
-
-    >>> generic_netapplet_task.conjoined_slave is None
-    True
-    >>> generic_netapplet_task.conjoined_master == (
-    ...     current_series_netapplet_task)
-    True
-
-    >>> current_series_ubuntu_task.conjoined_slave == (
-    ...     generic_ubuntu_task)
-    True
-    >>> current_series_ubuntu_task.conjoined_master is None
-    True
-    >>> generic_ubuntu_task.conjoined_master == (
-    ...     current_series_ubuntu_task)
-    True
-    >>> generic_ubuntu_task.conjoined_slave is None
-    True
-
-    >>> devel_focus_alsa_utils_task.conjoined_slave == (
-    ...     generic_alsa_utils_task)
-    True
-    >>> devel_focus_alsa_utils_task.conjoined_master is None
-    True
-
-    >>> generic_alsa_utils_task.conjoined_slave is None
-    True
-    >>> generic_alsa_utils_task.conjoined_master == (
-    ...     devel_focus_alsa_utils_task)
-    True
-
-A distroseries/productseries task that isn't the current development
-focus, doesn't have any conjoined masters or slaves.
-
-    >>> from storm.store import Store
-
-    # Only owners, experts, or admins can create a series.
-    >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> alsa_utils_stable = alsa_utils.newSeries(
-    ...     launchbag.user, 'stable', 'The stable series.')
-    >>> login('test@xxxxxxxxxxxxx')
-    >>> Store.of(alsa_utils_stable).flush()
-    >>> alsa_utils.development_focus == alsa_utils_stable
-    False
-    >>> stable_netapplet_task = bugtaskset.createTask(
-    ...     ubuntu_netapplet_bug, launchbag.user, alsa_utils_stable)
-    >>> stable_netapplet_task.conjoined_master is None
-    True
-    >>> stable_netapplet_task.conjoined_slave is None
-    True
-
-    >>> warty = ubuntu.getSeries('warty')
-    >>> warty == ubuntu.currentseries
-    False
-    >>> warty_netapplet_task = bugtaskset.createTask(
-    ...     ubuntu_netapplet_bug, launchbag.user,
-    ...     warty.getSourcePackage(ubuntu_netapplet.sourcepackagename))
-    >>> warty_netapplet_task.conjoined_master is None
-    True
-    >>> warty_netapplet_task.conjoined_slave is None
-    True
-
-If a distribution doesn't have a current series, its tasks don't have a
-conjoined master or slave.
-
-    >>> gentoo = getUtility(IDistributionSet).getByName('gentoo')
-    >>> gentoo.currentseries is None
-    True
-
-    >>> gentoo_netapplet_task = bugtaskset.createTask(
-    ...     ubuntu_netapplet_bug, launchbag.user,
-    ...     gentoo.getSourcePackage(ubuntu_netapplet.sourcepackagename))
-    >>> gentoo_netapplet_task.conjoined_master is None
-    True
-    >>> gentoo_netapplet_task.conjoined_slave is None
-    True
-
-
-Now the attributes are kept in sync. Here are examples of each:
-
-(Login as Foo Bar, because the milestone and importance examples
-require extra privileges.)
-
-    >>> login("foo.bar@xxxxxxxxxxxxx")
-
-1. Status
-
-    >>> print generic_netapplet_task.status.title
-    In Progress
-    >>> print current_series_netapplet_task.status.title
-    In Progress
-    >>> generic_netapplet_task.date_closed is None
-    True
-    >>> current_series_netapplet_task.date_closed is None
-    True
-
-    >>> current_series_netapplet_task.transitionToStatus(
-    ...     BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
-
-    >>> generic_netapplet_task.date_left_new
-    datetime.datetime...
-    >>> generic_netapplet_task.date_left_new == (
-    ...     current_series_netapplet_task.date_left_new)
-    True
-
-    >>> generic_netapplet_task.date_triaged
-    datetime.datetime...
-    >>> generic_netapplet_task.date_triaged == (
-    ...     current_series_netapplet_task.date_triaged)
-    True
-
-    >>> generic_netapplet_task.date_fix_committed
-    datetime.datetime...
-    >>> generic_netapplet_task.date_fix_committed == (
-    ...     current_series_netapplet_task.date_fix_committed)
-    True
-
-    >>> print generic_netapplet_task.status.title
-    Fix Released
-    >>> print current_series_netapplet_task.status.title
-    Fix Released
-
-    >>> generic_netapplet_task.date_closed
-    datetime.datetime...
-    >>> generic_netapplet_task.date_closed == (
-    ...     current_series_netapplet_task.date_closed)
-    True
-
-    >>> generic_netapplet_task.date_fix_released
-    datetime.datetime...
-    >>> generic_netapplet_task.date_fix_released == (
-    ...     current_series_netapplet_task.date_fix_released)
-    True
-
-2. Assignee
-
-    >>> no_priv = personset.getByEmail('no-priv@xxxxxxxxxxxxx')
-
-    >>> generic_alsa_utils_task.assignee is None
-    True
-    >>> devel_focus_alsa_utils_task.assignee is None
-    True
-    >>> generic_alsa_utils_task.date_assigned is None
-    True
-    >>> devel_focus_alsa_utils_task.date_assigned is None
-    True
-
-    >>> devel_focus_alsa_utils_task.transitionToAssignee(no_priv)
-
-    >>> print generic_alsa_utils_task.assignee.displayname
-    No Privileges Person
-    >>> print devel_focus_alsa_utils_task.assignee.displayname
-    No Privileges Person
-
-    >>> generic_alsa_utils_task.date_assigned
-    datetime.datetime...
-    >>> generic_alsa_utils_task.date_assigned == (
-    ...     devel_focus_alsa_utils_task.date_assigned)
-    True
-
-3. Importance
-
-    >>> print generic_netapplet_task.importance.title
-    Critical
-    >>> print current_series_netapplet_task.importance.title
-    Critical
-
-    >>> current_series_netapplet_task.transitionToImportance(
-    ...     BugTaskImportance.MEDIUM, ubuntu.owner)
-
-    >>> print generic_netapplet_task.importance.title
-    Medium
-    >>> print current_series_netapplet_task.importance.title
-    Medium
-
-Not everyone can edit the importance, though. If an unauthorised user
-is passed to transitionToImportance an exception is raised.
-
-    >>> current_series_netapplet_task.transitionToImportance(
-    ...     BugTaskImportance.LOW, no_priv)
-    Traceback (most recent call last):
-    ...
-    UserCannotEditBugTaskImportance:
-      User does not have sufficient permissions to edit the
-      bug task importance.
-
-    >>> print generic_netapplet_task.importance.title
-    Medium
-
-4. Milestone
-
-    >>> test_milestone = alsa_utils.development_focus.newMilestone("test")
-    >>> noway_milestone = alsa_utils.development_focus.newMilestone("noway")
-    >>> Store.of(test_milestone).flush()
-
-    >>> generic_alsa_utils_task.milestone is None
-    True
-    >>> devel_focus_alsa_utils_task.milestone is None
-    True
-
-    >>> devel_focus_alsa_utils_task.transitionToMilestone(
-    ...     test_milestone, alsa_utils.owner)
-
-    >>> print generic_alsa_utils_task.milestone.name
-    test
-    >>> print devel_focus_alsa_utils_task.milestone.name
-    test
-
-But a normal unprivileged user can't set the milestone.
-
-    >>> devel_focus_alsa_utils_task.transitionToMilestone(
-    ...     noway_milestone, no_priv)
-    Traceback (most recent call last):
-    ...
-    UserCannotEditBugTaskMilestone:
-      User does not have sufficient permissions to edit the bug
-      task milestone.
-
-    >>> print devel_focus_alsa_utils_task.milestone.name
-    test
-
-5. Source package name
-
-    >>> ubuntu_pmount = ubuntu.getSourcePackage("pmount")
-
-    >>> print generic_netapplet_task.sourcepackagename.name
-    netapplet
-    >>> print current_series_netapplet_task.sourcepackagename.name
-    netapplet
-
-    >>> current_series_netapplet_task.transitionToTarget(
-    ...     ubuntu_pmount.development_version)
-
-    >>> print generic_netapplet_task.sourcepackagename.name
-    pmount
-    >>> print current_series_netapplet_task.sourcepackagename.name
-    pmount
-
-A conjoined relationship can be broken, though. If the development
-task (i.e the conjoined master) is Won't Fix, it means that the bug is
-deferred to the next series. In this case the development task should
-be Won't Fix, while the generic task keeps the value it had before,
-allowing it to stay open.
-
-First let's change the status from Fix Released, since it doesn't make
-sense to reject such a task.
-
-    >>> current_series_netapplet_task.transitionToStatus(
-    ...     BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
-    >>> print generic_netapplet_task.status.title
-    Confirmed
-    >>> print current_series_netapplet_task.status.title
-    Confirmed
-    >>> generic_netapplet_task.date_closed is None
-    True
-    >>> current_series_netapplet_task.date_closed is None
-    True
-
-
-Now, if we set the current series task to Won't Fix, the generic task
-will still be confirmed.
-
-    >>> netapplet_owner = current_series_netapplet_task.pillar.owner
-
-    >>> current_series_netapplet_task.transitionToStatus(
-    ...     BugTaskStatus.WONTFIX, netapplet_owner)
-
-    >>> print generic_netapplet_task.status.title
-    Confirmed
-    >>> print current_series_netapplet_task.status.title
-    Won't Fix
-
-    >>> generic_netapplet_task.date_closed is None
-    True
-    >>> current_series_netapplet_task.date_closed is None
-    False
-
-And the bugtasks are no longer conjoined:
-
-    >>> generic_netapplet_task.conjoined_master is None
-    True
-    >>> current_series_netapplet_task.conjoined_slave is None
-    True
-
-If the current development release is marked as Invalid, then the
-bug is invalid for all future series too, and so the general bugtask
-is therefore Invalid also. In other words, conjoined again.
-
-    >>> current_series_netapplet_task.transitionToStatus(
-    ...     BugTaskStatus.NEW, getUtility(ILaunchBag).user)
-
-    # XXX Gavin Panella 2007-06-06 bug=112746:
-    # We must make two transitions.
-    >>> current_series_netapplet_task.transitionToStatus(
-    ...     BugTaskStatus.INVALID, getUtility(ILaunchBag).user)
-
-    >>> print generic_netapplet_task.status.title
-    Invalid
-    >>> print current_series_netapplet_task.status.title
-    Invalid
-
-    >>> generic_netapplet_task.date_closed is None
-    False
-    >>> current_series_netapplet_task.date_closed is None
-    False
-
-
-Bug Privacy
-===========
-
-A bug is either private or public. Private bugs are only visible
-(e.g. in search listings) to explicit subscribers and Launchpad
-admins. Public bugs are visible to anyone.
-
-    >>> from zope.event import notify
-    >>> from lazr.lifecycle.event import ObjectModifiedEvent
-
-
-Privacy and Unprivileged Users
-------------------------------
-
-Let's log in as the user Foo Bar (to be allowed to edit bugs):
-
-    >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> foobar = launchbag.user
-
-and mark one of the Firefox bugs private. While we do this, we're also
-going to subscribe the Ubuntu team to the bug report to help demonstrate
-later on the interaction between privacy and teams (see the section
-entitled _Privacy and Team Awareness_):
-
-    >>> from lazr.lifecycle.snapshot import Snapshot
-    >>> from lp.bugs.interfaces.bug import IBug
-
-    >>> bug_upstream_firefox_crashes = bugtaskset.get(15)
-
-    >>> ubuntu_team = personset.getByEmail('support@xxxxxxxxxx')
-    >>> subscription = bug_upstream_firefox_crashes.bug.subscribe(
-    ...     ubuntu_team, ubuntu_team)
-
-    >>> old_state = Snapshot(
-    ...     bug_upstream_firefox_crashes.bug, providing=IBug)
-    >>> bug_upstream_firefox_crashes.bug.setPrivate(
-    ...     True, getUtility(ILaunchBag).user)
-    True
-    >>> bug_set_private = ObjectModifiedEvent(
-    ...     bug_upstream_firefox_crashes.bug, old_state,
-    ...     ["id", "title", "private"])
-    >>> notify(bug_set_private)
-
-    >>> from lp.services.database.sqlbase import flush_database_updates
-    >>> flush_database_updates()
-
-If we now login as someone who was neither implicitly nor explicitly
-subscribed to this bug, e.g. No Privileges Person, they will not be
-able to access or set properties of the bugtask.
-
-    >>> login("no-priv@xxxxxxxxxxxxx")
-    >>> mr_no_privs = launchbag.user
-
-    >>> bug_upstream_firefox_crashes.status
-    Traceback (most recent call last):
-      ...
-    Unauthorized: (..., 'status', 'launchpad.View')
-
-    >>> bug_upstream_firefox_crashes.transitionToStatus(
-    ...     BugTaskStatus.FIXCOMMITTED, getUtility(ILaunchBag).user)
-    Traceback (most recent call last):
-      ...
-    Unauthorized: (..., 'transitionToStatus', 'launchpad.Edit')
-
-The private bugs will be invisible to No Privileges Person in the search
-results:
-
-    >>> from lp.services.searchbuilder import any
-    >>> from lp.bugs.interfaces.bugtask import BugTaskSearchParams
-    >>> params = BugTaskSearchParams(
-    ...     status=any(STATUS_NEW, STATUS_CONFIRMED),
-    ...     orderby="id", user=mr_no_privs)
-    >>> upstream_mozilla = productset.getByName('firefox')
-    >>> bugtasks = upstream_mozilla.searchTasks(params)
-    >>> print bugtasks.count()
-    3
-    >>> bug_ids = [bt.bug.id for bt in bugtasks]
-    >>> print sorted(bug_ids)
-    [1, 4, 5]
-
-We can create an access policy grant on the pillar to which the bug is
-targeted and No Privileges Person will have access to the private bug.
-
-    >>> from lp.registry.enums import InformationType
-    >>> from lp.registry.interfaces.accesspolicy import (
-    ...     IAccessPolicyGrantSource,
-    ...     IAccessPolicySource,
-    ...     )
-    >>> aps = getUtility(IAccessPolicySource)
-    >>> [policy] = aps.find(
-    ...     [(upstream_mozilla, InformationType.USERDATA)])
-    >>> apgs = getUtility(IAccessPolicyGrantSource)
-    >>> grant = apgs.grant([(policy, mr_no_privs, ubuntu_team)])
-    >>> bugtasks = upstream_mozilla.searchTasks(params)
-    >>> print bugtasks.count()
-    4
-    >>> bug_ids = [bt.bug.id for bt in bugtasks]
-    >>> print sorted(bug_ids)
-    [1, 4, 5, 6]
-    >>> apgs.revoke([(policy, mr_no_privs)])
-
-
-Open bugtask count for a given list of projects
------------------------------------------------
-
-IBugTaskSet.getOpenBugTasksPerProduct() will return a dictionary
-of product_id:count entries for bugs in an open status that
-the user given as a parameter is allowed to see. If a product,
-such as id=3 does not have any open bugs, it will not appear
-in the result.
-
-    >>> products = [productset.get(id) for id in (3, 5, 20)]
-    >>> bugtask_counts = bugtaskset.getOpenBugTasksPerProduct(
-    ...     sample_person, products)
-    >>> for product_id, count in sorted(bugtask_counts.items()):
-    ...     print 'product_id=%d count=%d' % (product_id, count)
-    product_id=5 count=1
-    product_id=20 count=2
-
-A Launchpad admin will get a higher count for the product with id=20
-because he can see the private bug.
-
-    >>> bugtask_counts = bugtaskset.getOpenBugTasksPerProduct(
-    ...     foobar, products)
-    >>> for product_id, count in sorted(bugtask_counts.items()):
-    ...     print 'product_id=%d count=%d' % (product_id, count)
-    product_id=5 count=1
-    product_id=20 count=3
-
-Someone subscribed to the private bug on the product with id=20
-will also have it added to the count.
-
-    >>> karl = personset.getByName('karl')
-    >>> bugtask_counts = bugtaskset.getOpenBugTasksPerProduct(
-    ...     karl, products)
-    >>> for product_id, count in sorted(bugtask_counts.items()):
-    ...     print 'product_id=%d count=%d' % (product_id, count)
-    product_id=5 count=1
-    product_id=20 count=3
-
-
-Privacy and Priviledged Users
------------------------------
-
-Now, we'll log in as Mark Shuttleworth, who was assigned to this bug
-when it was marked private:
-
-    >>> login("mark@xxxxxxxxxxx")
-
-And note that he can access and set the bugtask attributes:
-
-    >>> bug_upstream_firefox_crashes.status.title
-    'New'
-
-    >>> bug_upstream_firefox_crashes.transitionToStatus(
-    ...     BugTaskStatus.NEW, getUtility(ILaunchBag).user)
-
-
-Privacy and Team Awareness
---------------------------
-
-No Privileges Person can't see the private bug, because he's not a subscriber:
-
-    >>> login("no-priv@xxxxxxxxxxxxx")
-    >>> params = BugTaskSearchParams(
-    ...     status=any(STATUS_NEW, STATUS_CONFIRMED), user=no_priv)
-    >>> firefox_bugtasks = firefox.searchTasks(params)
-    >>> [bugtask.bug.id for bugtask in firefox_bugtasks]
-    [1, 4, 5]
-
-
-But if we add No Privileges Person to the Ubuntu Team, and because the
-Ubuntu Team *is* subscribed to the bug, No Privileges Person will see
-the private bug.
-
-    >>> login("mark@xxxxxxxxxxx")
-    >>> ignored = ubuntu_team.addMember(
-    ...     no_priv, reviewer=ubuntu_team.teamowner)
-
-    >>> login("no-priv@xxxxxxxxxxxxx")
-    >>> params = BugTaskSearchParams(
-    ...     status=any(STATUS_NEW, STATUS_CONFIRMED), user=foobar)
-    >>> firefox_bugtasks = firefox.searchTasks(params)
-    >>> [bugtask.bug.id for bugtask in firefox_bugtasks]
-    [1, 4, 5, 6]
-
-
-Privacy and Launchpad Admins
-----------------------------
-
-Let's log in as Daniel Henrique Debonzi:
-
-    >>> login("daniel.debonzi@xxxxxxxxxxxxx")
-    >>> debonzi = launchbag.user
-
-The same search as above yields the same result, because Daniel Debonzi is an
-administrator.
-
-    >>> firefox = productset.get(4)
-    >>> params = BugTaskSearchParams(status=any(STATUS_NEW,
-    ...                                         STATUS_CONFIRMED),
-    ...                              user=debonzi)
-    >>> firefox_bugtasks = firefox.searchTasks(params)
-    >>> [bugtask.bug.id for bugtask in firefox_bugtasks]
-    [1, 4, 5, 6]
-
-Trying to retrieve the bug directly will work fine:
-
-    >>> bug_upstream_firefox_crashes = bugtaskset.get(15)
-
-As will attribute access:
-
-    >>> bug_upstream_firefox_crashes.status.title
-    'New'
-
-And attribute setting:
-
-    >>> bug_upstream_firefox_crashes.transitionToStatus(
-    ...     BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
-    >>> bug_upstream_firefox_crashes.transitionToStatus(
-    ...     BugTaskStatus.NEW, getUtility(ILaunchBag).user)
-
-
-
-Sorting Bug Tasks
------------------
-
-Bug tasks need to sort in a very particular order. We want product tasks
-first, then ubuntu tasks, then other distro-related tasks. In the
-distro-related tasks we want a distribution-task first, then
-distroseries-tasks for that same distribution. The distroseries tasks
-should be sorted by distroseries version.
-
-Phew.
-
-Let's just make sure that the tasks on bug_one sort correctly.
-
-    >>> tasks = bug_one.bugtasks
-    >>> for task in tasks:
-    ...     print task.bugtargetdisplayname
-    Evolution
-    Mozilla Firefox
-    Mozilla Firefox 1.0
-    mozilla-firefox (Ubuntu)
-    Ubuntu Warty
-    mozilla-firefox (Debian)
-    Tubuntu
-
-
-BugTask Adaptation
-------------------
-
-An IBugTask can be adapted to an IBug.
-
-    >>> from lp.bugs.interfaces.bug import IBug
-
-    >>> bugtask_four = bugtaskset.get(4)
-    >>> bug = IBug(bugtask_four)
-    >>> bug.title
-    u'Firefox does not support SVG'
-
-
-The targetnamecache attribute of BugTask
-----------------------------------------
-
-The BugTask table has this targetnamecache attribute which stores a
-computed value to allow us to sort and search on that value without
-having to do lots of SQL joins. This cached value gets updated daily
-by the update-bugtask-targetnamecaches cronscript and whenever the
-bugtask is changed.  Of course, it's also computed and set when a
-bugtask is created.
-
-`BugTask.bugtargetdisplayname` simply returns `targetnamecache`, and
-the latter is not exposed in `IBugTask`, so the `bugtargetdisplayname`
-is used here.
-
-    >>> netapplet = productset.get(11)
-    >>> upstream_task = bugtaskset.createTask(
-    ...     bug_one, mark, netapplet,
-    ...     status=STATUS_NEW, importance=IMPORTANCE_MEDIUM)
-    >>> upstream_task.bugtargetdisplayname
-    u'NetApplet'
-
-    >>> thunderbird = productset.get(8)
-    >>> upstream_task_id = upstream_task.id
-    >>> upstream_task.transitionToTarget(thunderbird)
-    >>> upstream_task.bugtargetdisplayname
-    u'Mozilla Thunderbird'
-
-    >>> thunderbird.name = 'thunderbird-ng'
-    >>> thunderbird.displayname = 'Mozilla Thunderbird NG'
-
-    # XXX Guilherme Salgado 2005-11-07 bug=3989:
-    # This flush_database_updates() shouldn't be needed because we
-    # already have the transaction.commit() here, but without it
-    # (flush_database_updates), the cronscript won't see the thunderbird name
-    # change.
-    >>> flush_database_updates()
-    >>> transaction.commit()
-
-    >>> import subprocess
-    >>> process = subprocess.Popen(
-    ...     'cronscripts/update-bugtask-targetnamecaches.py', shell=True,
-    ...     stdin=subprocess.PIPE, stdout=subprocess.PIPE,
-    ...     stderr=subprocess.PIPE)
-    >>> (out, err) = process.communicate()
-
-    >>> print err
-    INFO Creating lockfile:
-        /var/lock/launchpad-launchpad-targetnamecacheupdater.lock
-    INFO Updating targetname cache of bugtasks.
-    INFO Calculating targets.
-    INFO Will check ... targets.
-    ...
-    INFO Updating (u'Mozilla Thunderbird',) to 'Mozilla Thunderbird NG'.
-    ...
-    INFO Updated 1 target names.
-    INFO Finished updating targetname cache of bugtasks.
-
-    >>> process.returncode
-    0
-
-    # XXX Guilherme Salgado 2005-11-07:
-    # If we don't call flush_database_caches() here, we won't see the
-    # changes made by the cronscript in objects we already have cached.
-    >>> from lp.services.database.sqlbase import flush_database_caches
-    >>> flush_database_caches()
-    >>> transaction.commit()
-
-    >>> bugtaskset.get(upstream_task_id).bugtargetdisplayname
-    u'Mozilla Thunderbird NG'
-
-With sourcepackage bugtasks that have accepted nominations to a
-series, additional sourcepackage bugtasks are automatically nominated
-to the same series. The nominations are implicitly accepted and have
-targetnamecache updated.
-
-    >>> new_bug, new_bug_event = bugset.createBugWithoutTarget(
-    ...     CreateBugParams(mark, 'New Bug', comment='New Bug'))
-
-    The first message of a new bug has index 0.
-    >>> new_bug.bug_messages[0].index
-    0
-
-    >>> bugtaskset.createTask(
-    ...     new_bug, mark, ubuntu.getSourcePackage('mozilla-firefox'))
-    <BugTask ...>
-
-    >>> new_bug.addNomination(mark, ubuntu.currentseries).approve(mark)
-
-The first task has been created and successfully nominated to Hoary.
-
-    >>> for task in new_bug.bugtasks:
-    ...     print task.bugtargetdisplayname
-    mozilla-firefox (Ubuntu)
-    mozilla-firefox (Ubuntu Hoary)
-
-    >>> bugtaskset.createTask(
-    ...     new_bug, mark, ubuntu.getSourcePackage('alsa-utils'))
-    <BugTask ...>
-
-The second task has been created and has also been successfully
-nominated to Hoary.
-
-    >>> for task in new_bug.bugtasks:
-    ...     print task.bugtargetdisplayname
-    alsa-utils (Ubuntu)
-    mozilla-firefox (Ubuntu)
-    alsa-utils (Ubuntu Hoary)
-    mozilla-firefox (Ubuntu Hoary)
-
-The updating of targetnamecaches is usually done by the cronjob, however
-it can also be invoked directly.
-
-    >>> thunderbird.name = 'thunderbird'
-    >>> thunderbird.displayname = 'Mozilla Thunderbird'
-    >>> transaction.commit()
-
-    >>> upstream_task.bugtargetdisplayname
-    u'Mozilla Thunderbird NG'
-
-    >>> from lp.bugs.scripts.bugtasktargetnamecaches import (
-    ...     BugTaskTargetNameCacheUpdater)
-    >>> from lp.services.log.logger import FakeLogger
-    >>> logger = FakeLogger()
-    >>> updater = BugTaskTargetNameCacheUpdater(transaction, logger)
-    >>> updater.run()
-    INFO Updating targetname cache of bugtasks.
-    INFO Calculating targets.
-    ...
-    INFO Updating (u'Mozilla Thunderbird NG',) to 'Mozilla Thunderbird'.
-    ...
-    INFO Updated 1 target names.
-    INFO Finished updating targetname cache of bugtasks.
-
-    >>> flush_database_caches()
-    >>> transaction.commit()
-    >>> upstream_task.bugtargetdisplayname
-    u'Mozilla Thunderbird'
-
-
-Target Uses Malone
-------------------
-
-Bug tasks have a flag, target_uses_malone, that says whether the bugtask
-target uses Malone as its official bugtracker.
-
-    >>> for bugtask in bug_one.bugtasks:
-    ...     print "%-30s %s" % (
-    ...         bugtask.bugtargetdisplayname, bugtask.target_uses_malone)
-    Evolution                      True
-    Mozilla Firefox                True
-    Mozilla Firefox 1.0            True
-    Mozilla Thunderbird            False
-    mozilla-firefox (Ubuntu)       True
-    Ubuntu Warty                   True
-    mozilla-firefox (Debian)       False
-    Tubuntu                        False
-
-
-BugTask badges
---------------
-
-A bug can have certain properties, which results in a badge being
-displayed in bug listings. BugTaskSet has a method,
-getBugTaskBadgeProperties(), which calculates these properties for
-multiple bug tasks in one go.
-
-    >>> from operator import attrgetter
-    >>> def print_badge_properties(badge_properties):
-    ...     bugtasks = sorted(badge_properties.keys(), key=attrgetter('id'))
-    ...     for bugtask in bugtasks:
-    ...         print "Properties for bug %s:" % (bugtask.bug.id)
-    ...         for key, value in sorted(badge_properties[bugtask].items()):
-    ...             print " %s: %s" % (key, value)
-
-    >>> bug_two = getUtility(IBugSet).get(2)
-    >>> bug_three = getUtility(IBugSet).get(3)
-    >>> some_bugtask = bug_two.bugtasks[0]
-    >>> another_bugtask = bug_three.bugtasks[0]
-    >>> badge_properties = getUtility(IBugTaskSet).getBugTaskBadgeProperties(
-    ...     [some_bugtask, another_bugtask])
-    >>> print_badge_properties(badge_properties)
-    Properties for bug 2:
-     has_branch: False
-     has_patch: False
-     has_specification: False
-    Properties for bug 3:
-     has_branch: False
-     has_patch: False
-     has_specification: False
-
-..., a specification gets linked...
-
-    >>> from lp.blueprints.interfaces.specification import ISpecificationSet
-    >>> spec = getUtility(ISpecificationSet).all_specifications[0]
-    >>> spec.linkBug(bug_two)
-    <SpecificationBug at ...>
-
-... or a branch gets linked to the bug...
-
-    >>> branch = factory.makeAnyBranch()
-    >>> bug_three.linkBranch(branch, no_priv)
-    <BugBranch at ...>
-
-... the properties for the bugtasks reflect this.
-
-    >>> badge_properties = getUtility(IBugTaskSet).getBugTaskBadgeProperties(
-    ...     [some_bugtask, another_bugtask])
-    >>> print_badge_properties(badge_properties)
-    Properties for bug 2:
-     has_branch: False
-     has_patch: False
-     has_specification: True
-    Properties for bug 3:
-     has_branch: True
-     has_patch: False
-     has_specification: False
-
-
-Bugtask Tags
-------------
-
-List of bugtasks often need to display related tags.  Since tags are
-related to bugtasks via bugs, BugTaskSet has a method getBugTaskTags
-that can calculate the tags in one query.
-
-    >>> some_bugtask.bug.tags = [u'foo', u'bar']
-    >>> another_bugtask.bug.tags = [u'baz', u'bop']
-    >>> tags_by_task = getUtility(IBugTaskSet).getBugTaskTags([
-    ...     some_bugtask, another_bugtask])
-    >>> print tags_by_task
-    {3: [u'bar', u'foo'], 6: [u'baz', u'bop']}
-
-
-Similar bugs
-------------
-
-It's possible to get a list of bugs similar to the current bug by
-accessing the similar_bugs property of its bug tasks.
-
-    >>> new_ff_bug = factory.makeBug(product=firefox, title="Firefox")
-    >>> ff_bugtask = new_ff_bug.bugtasks[0]
-
-    >>> similar_bugs = ff_bugtask.findSimilarBugs(user=sample_person)
-    >>> for similar_bug in sorted(similar_bugs, key=attrgetter('id')):
-    ...     print "%s: %s" % (similar_bug.id, similar_bug.title)
-    1: Firefox does not support SVG
-    5: Firefox install instructions should be complete
-
-This also works for distributions...
-
-    >>> ubuntu_bugtask = factory.makeBugTask(bug=new_ff_bug, target=ubuntu)
-    >>> similar_bugs = ubuntu_bugtask.findSimilarBugs(user=sample_person)
-    >>> for similar_bug in sorted(similar_bugs, key=attrgetter('id')):
-    ...     print "%s: %s" % (similar_bug.id, similar_bug.title)
-    1: Firefox does not support SVG
-
-... and for SourcePackages.
-
-    >>> a_ff_bug = factory.makeBug(product=firefox, title="a Firefox")
-    >>> firefox_package = ubuntu.getSourcePackage('mozilla-firefox')
-    >>> firefox_package_bugtask = factory.makeBugTask(
-    ...     bug=a_ff_bug, target=firefox_package)
-
-    >>> similar_bugs = firefox_package_bugtask.findSimilarBugs(
-    ...     user=sample_person)
-    >>> for similar_bug in sorted(similar_bugs, key=attrgetter('id')):
-    ...     print "%s: %s" % (similar_bug.id, similar_bug.title)
-    1: Firefox does not support SVG
-
-Private bugs won't show up in the list of similar bugs unless the user
-is a direct subscriber. We'll demonstrate this by creating a new bug
-against Firefox.
-
-    >>> second_ff_bug = factory.makeBug(
-    ...     product=firefox, title="Yet another Firefox bug")
-    >>> similar_bugs = ff_bugtask.findSimilarBugs(user=no_priv)
-    >>> for similar_bug in sorted(similar_bugs, key=attrgetter('id')):
-    ...     print "%s: %s" % (similar_bug.id, similar_bug.title)
-    1: Firefox does not support SVG
-    5: Firefox install instructions should be complete
-    ...Yet another Firefox bug
-
-If we mark the new bug as private, it won't appear in the similar bugs
-list for no_priv any more, since they're not a direct subscriber.
-
-    >>> second_ff_bug.setPrivate(True, foobar)
-    True
-
-    >>> similar_bugs = ff_bugtask.findSimilarBugs(user=no_priv)
-    >>> for similar_bug in sorted(similar_bugs, key=attrgetter('id')):
-    ...     print "%s: %s" % (similar_bug.id, similar_bug.title)
-    1: Firefox does not support SVG
-    5: Firefox install instructions should be complete
-    ...: a Firefox

=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py	2012-04-27 05:56:48 +0000
+++ lib/lp/bugs/interfaces/bug.py	2012-05-04 18:05:24 +0000
@@ -779,6 +779,9 @@
     def removeWatch(bug_watch, owner):
         """Remove a bug watch from the bug."""
 
+    def addManyTasks(owners, targets):
+        """Create multiple bug tasks on this bug."""
+
     @call_with(owner=REQUEST_USER)
     @operation_parameters(target=copy_field(IBugTask['target']))
     @export_factory_operation(IBugTask, [])

=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py	2012-04-17 08:04:20 +0000
+++ lib/lp/bugs/interfaces/bugtask.py	2012-05-04 18:05:24 +0000
@@ -1570,6 +1570,10 @@
         :return: A list of tuples containing (status_id, count).
         """
 
+    def createManyTasks(bug, owner, targets, status=None, importance=None,
+                   assignee=None, milestone=None):
+        """Create a series of bug tasks and return them."""
+
     def createTask(bug, owner, target, status=None, importance=None,
                    assignee=None, milestone=None):
         """Create a bug task on a bug and return it.

=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py	2012-05-02 23:15:38 +0000
+++ lib/lp/bugs/model/bug.py	2012-05-04 18:05:24 +0000
@@ -1237,6 +1237,11 @@
         """See `IBug`."""
         return getUtility(IBugTaskSet).createTask(self, owner, target)
 
+    def addManyTasks(self, owner, targets):
+        """See `IBug`."""
+        new_tasks = getUtility(IBugTaskSet).createManyTasks(self, owner, targets)
+        return new_tasks
+
     def addWatch(self, bugtracker, remotebug, owner):
         """See `IBug`."""
         # We shouldn't add duplicate bug watches.

=== modified file 'lib/lp/bugs/model/bugnomination.py'
--- lib/lp/bugs/model/bugnomination.py	2011-12-30 06:14:56 +0000
+++ lib/lp/bugs/model/bugnomination.py	2012-05-04 18:05:24 +0000
@@ -91,8 +91,8 @@
                     targets.append(distroseries)
         else:
             targets.append(self.productseries)
-        for target in targets:
-            bug_task = self.bug.addTask(approver, target)
+        bugtasks = self.bug.addManyTasks(approver, targets)
+        for bug_task in bugtasks:
             self.bug.addChange(BugTaskAdded(UTC_NOW, approver, bug_task))
 
     def decline(self, decliner):

=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py	2012-04-26 07:58:38 +0000
+++ lib/lp/bugs/model/bugtask.py	2012-05-04 18:05:24 +0000
@@ -112,7 +112,10 @@
 from lp.registry.model.pillar import pillar_sort_key
 from lp.registry.model.sourcepackagename import SourcePackageName
 from lp.services import features
-from lp.services.database.bulk import load_related
+from lp.services.database.bulk import (
+    create,
+    load_related,
+    )
 from lp.services.database.constants import UTC_NOW
 from lp.services.database.datetimecol import UtcDateTimeCol
 from lp.services.database.enumcol import EnumCol
@@ -143,31 +146,27 @@
           - distro tasks, followed by their distroseries tasks
           - ubuntu first among the distros
     """
+    product_name = None
+    productseries_name = None
+    distribution_name = None
+    distroseries_name = None
+    sourcepackage_name = None
+
     if bugtask.product:
         product_name = bugtask.product.name
-        productseries_name = None
     elif bugtask.productseries:
         productseries_name = bugtask.productseries.name
         product_name = bugtask.productseries.product.name
-    else:
-        product_name = None
-        productseries_name = None
 
     if bugtask.distribution:
         distribution_name = bugtask.distribution.name
-    else:
-        distribution_name = None
 
     if bugtask.distroseries:
         distroseries_name = bugtask.distroseries.version
         distribution_name = bugtask.distroseries.distribution.name
-    else:
-        distroseries_name = None
 
     if bugtask.sourcepackagename:
         sourcepackage_name = bugtask.sourcepackagename.name
-    else:
-        sourcepackage_name = None
 
     # Move ubuntu to the top.
     if distribution_name == 'ubuntu':
@@ -1597,11 +1596,8 @@
         params = BugTaskSearchParams(user, **kwargs)
         return self.search(params)
 
-    def createTask(self, bug, owner, target,
-                   status=IBugTask['status'].default,
-                   importance=IBugTask['importance'].default,
-                   assignee=None, milestone=None):
-        """See `IBugTaskSet`."""
+    def _init_new_task(self, bug, owner, target, status, importance, assignee,
+                       milestone):
         if not status:
             status = IBugTask['status'].default
         if not importance:
@@ -1633,18 +1629,63 @@
             milestone=milestone)
         create_params = non_target_create_params.copy()
         create_params.update(target_key)
+        return create_params, non_target_create_params
+
+    def createManyTasks(self, bug, owner, targets,
+                        status=IBugTask['status'].default,
+                        importance=IBugTask['importance'].default,
+                        assignee=None, milestone=None):
+        """See `IBugTaskSet`."""
+        params = [self._init_new_task(bug, owner, target, status, importance,
+            assignee, milestone) for target in targets]
+
+        fieldnames = (
+            'assignee',
+            'bug',
+            'distribution',
+            'distroseries',
+            'importance',
+            'milestone',
+            'owner',
+            'product',
+            'productseries',
+            'sourcepackagename',
+            '_status',
+        )
+
+        fields = [getattr(BugTask, field) for field in fieldnames]
+        # Values need to be a list of tuples in the order we're declaring our
+        # fields.
+        values = [[p[0].get(field) for field in fieldnames] for p in params]
+
+        taskset = create(fields, values, get_objects=True)
+        del get_property_cache(bug).bugtasks
+        for bugtask in taskset:
+            bugtask.updateTargetNameCache()
+            if bugtask.conjoined_slave:
+                bugtask._syncFromConjoinedSlave()
+        return taskset
+
+    def createTask(self, bug, owner, target,
+                   status=IBugTask['status'].default,
+                   importance=IBugTask['importance'].default,
+                   assignee=None, milestone=None):
+        """See `IBugTaskSet`."""
+        create_params, non_target_create_params = self._init_new_task(bug,
+            owner, target, status, importance, assignee, milestone)
         bugtask = BugTask(**create_params)
-        if target_key['distribution']:
+
+        if create_params['distribution']:
             # Create tasks for accepted nominations if this is a source
             # package addition.
             accepted_nominations = [
                 nomination for nomination in
-                bug.getNominations(target_key['distribution'])
+                bug.getNominations(create_params['distribution'])
                 if nomination.isApproved()]
             for nomination in accepted_nominations:
                 accepted_series_task = BugTask(
                     distroseries=nomination.distroseries,
-                    sourcepackagename=target_key['sourcepackagename'],
+                    sourcepackagename=create_params['sourcepackagename'],
                     **non_target_create_params)
                 accepted_series_task.updateTargetNameCache()
 

=== modified file 'lib/lp/bugs/tests/test_bugtaskset.py'
--- lib/lp/bugs/tests/test_bugtaskset.py	2012-05-02 05:25:11 +0000
+++ lib/lp/bugs/tests/test_bugtaskset.py	2012-05-04 18:05:24 +0000
@@ -5,22 +5,1271 @@
 
 __metaclass__ = type
 
+import datetime
+import subprocess
+import transaction
+from collections import namedtuple
+from lazr.lifecycle.event import ObjectModifiedEvent
+from lazr.lifecycle.snapshot import Snapshot
+from operator import attrgetter
+from storm.store import Store
+from testtools.testcase import ExpectedException
 from zope.component import getUtility
-
+from zope.event import notify
+from zope.security.interfaces import Unauthorized
+from lp.blueprints.interfaces.specification import ISpecificationSet
 from lp.bugs.interfaces.bugtask import (
+    BugTaskImportance,
+    BugTaskSearchParams,
     BugTaskStatus,
     BugTaskStatusSearch,
     IBugTaskSet,
-    )
+    UserCannotEditBugTaskImportance,
+    UserCannotEditBugTaskMilestone,
+    )
+from lp.bugs.interfaces.bug import (
+    CreateBugParams,
+    IBug,
+    IBugSet,
+    )
+from lp.bugs.scripts.bugtasktargetnamecaches import (
+    BugTaskTargetNameCacheUpdater)
 from lp.registry.enums import InformationType
-from lp.services.database.sqlbase import flush_database_updates
+from lp.registry.interfaces.accesspolicy import (
+    IAccessPolicyGrantSource,
+    IAccessPolicySource,
+    )
+from lp.registry.interfaces.distributionsourcepackage \
+    import IDistributionSourcePackage
+from lp.registry.interfaces.distribution import IDistributionSet
+from lp.registry.interfaces.distroseries import IDistroSeriesSet
+from lp.registry.interfaces.person import IPersonSet
+from lp.registry.interfaces.product import IProductSet
+from lp.registry.interfaces.sourcepackage import ISourcePackage
+from lp.registry.model.sourcepackage import SourcePackage
+from lp.services.database.sqlbase import (
+    flush_database_caches,
+    flush_database_updates,
+    )
+from lp.services.log.logger import FakeLogger
+from lp.services.searchbuilder import any
+from lp.services.webapp.interfaces import ILaunchBag
 from lp.testing import (
+    ANONYMOUS,
+    login,
     login_person,
+    TestCase,
     TestCaseWithFactory,
     )
 from lp.testing.layers import DatabaseFunctionalLayer
 
 
+def login_foobar():
+    """Helper to get the foobar logged in user"""
+    launchbag = getUtility(ILaunchBag)
+    login('foo.bar@xxxxxxxxxxxxx')
+    return launchbag.user
+
+
+def login_nopriv():
+    launchbag = getUtility(ILaunchBag)
+    login("no-priv@xxxxxxxxxxxxx")
+    return launchbag.user
+
+
+class TestBugTaskCreation(TestCaseWithFactory):
+    """Test BugTaskSet creation methods."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_upstream_task(self):
+        """A bug that has to be fixed in an upstream product."""
+        bugtaskset = getUtility(IBugTaskSet)
+        bug_one = getUtility(IBugSet).get(1)
+        mark = getUtility(IPersonSet).getByEmail('mark@xxxxxxxxxxx')
+        evolution = getUtility(IProductSet).get(5)
+
+        upstream_task = bugtaskset.createTask(
+            bug_one, mark, evolution,
+            status=BugTaskStatus.NEW,
+            importance=BugTaskImportance.MEDIUM)
+
+        self.assertEqual(upstream_task.product, evolution)
+        self.assertEqual(upstream_task.target, evolution)
+        # getPackageComponent only applies to tasks that specify package info.
+        self.assertEqual(upstream_task.getPackageComponent(), None)
+
+    def test_distro_specific_bug(self):
+        """A bug that needs to be fixed in a specific distro."""
+        bugtaskset = getUtility(IBugTaskSet)
+        bug_one = getUtility(IBugSet).get(1)
+        mark = getUtility(IPersonSet).getByEmail('mark@xxxxxxxxxxx')
+
+        a_distro = self.factory.makeDistribution(name='tubuntu')
+        distro_task = bugtaskset.createTask(
+            bug_one, mark, a_distro,
+            status=BugTaskStatus.NEW,
+            importance=BugTaskImportance.MEDIUM)
+
+        self.assertEqual(distro_task.distribution, a_distro)
+        self.assertEqual(distro_task.target, a_distro)
+        # getPackageComponent only applies to tasks that specify package info.
+        self.assertEqual(distro_task.getPackageComponent(), None)
+
+    def test_distroseries_specific_bug(self):
+        """A bug that needs to be fixed in a specific distro series
+
+        These tasks are usef for release management andbackporting
+        """
+        bugtaskset = getUtility(IBugTaskSet)
+        bug_one = getUtility(IBugSet).get(1)
+        mark = getUtility(IPersonSet).getByEmail('mark@xxxxxxxxxxx')
+        warty = getUtility(IDistroSeriesSet).get(1)
+
+        distro_series_task = bugtaskset.createTask(
+            bug_one, mark, warty,
+            status=BugTaskStatus.NEW, importance=BugTaskImportance.MEDIUM)
+
+        self.assertEqual(distro_series_task.distroseries, warty)
+        self.assertEqual(distro_series_task.target, warty)
+        # getPackageComponent only applies to tasks that specify package info.
+        self.assertEqual(distro_series_task.getPackageComponent(), None)
+
+    def test_createmany_bugtasks(self):
+        """We can create a set of bugtasks around different targets"""
+        bugtaskset = getUtility(IBugTaskSet)
+        mark = getUtility(IPersonSet).getByEmail('mark@xxxxxxxxxxx')
+        evolution = getUtility(IProductSet).get(5)
+        warty = getUtility(IDistroSeriesSet).get(1)
+        bug_many = getUtility(IBugSet).get(4)
+
+        a_distro = self.factory.makeDistribution(name='tubuntu')
+        taskset = bugtaskset.createManyTasks(bug_many, mark,
+            [evolution, a_distro, warty], status=BugTaskStatus.FIXRELEASED)
+        tasks = [(t.product, t.distribution, t.distroseries) for t in taskset]
+        tasks.sort()
+
+        self.assertEqual(tasks[0][2], warty)
+        self.assertEqual(tasks[1][1], a_distro)
+        self.assertEqual(tasks[2][0], evolution)
+
+
+class TestBugTaskTargets(TestCase):
+    """Verify we handle various bugtask targets correctly"""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_bugtask_target_productseries(self):
+        """The 'target' of a task can be a product series"""
+        login('foo.bar@xxxxxxxxxxxxx')
+        bugtaskset = getUtility(IBugTaskSet)
+        productset = getUtility(IProductSet)
+        bug_one = getUtility(IBugSet).get(1)
+        mark = getUtility(IPersonSet).getByEmail('mark@xxxxxxxxxxx')
+
+        firefox = productset['firefox']
+        firefox_1_0 = firefox.getSeries("1.0")
+        productseries_task = bugtaskset.createTask(bug_one, mark, firefox_1_0)
+
+        self.assertEqual(productseries_task.target, firefox_1_0)
+        # getPackageComponent only applies to tasks that specify package info.
+        self.assertEqual(productseries_task.getPackageComponent(), None)
+
+    def test_bugtask_target_distro_sourcepackage(self):
+        """The 'target' of a task can be a distro sourcepackage"""
+        login('foo.bar@xxxxxxxxxxxxx')
+        bugtaskset = getUtility(IBugTaskSet)
+
+        debian_ff_task = bugtaskset.get(4)
+        self.assertTrue(
+            IDistributionSourcePackage.providedBy(debian_ff_task.target))
+        self.assertEqual(debian_ff_task.getPackageComponent(), None)
+
+        target = debian_ff_task.target
+        self.assertEqual(target.distribution.name, u'debian')
+        self.assertEqual(target.sourcepackagename.name, u'mozilla-firefox')
+
+        ubuntu_linux_task = bugtaskset.get(25)
+        self.assertTrue(
+            IDistributionSourcePackage.providedBy(ubuntu_linux_task.target))
+        self.assertEqual(ubuntu_linux_task.getPackageComponent().name, 'main')
+
+        target = ubuntu_linux_task.target
+        self.assertEqual(target.distribution.name, u'ubuntu')
+        self.assertEqual(target.sourcepackagename.name, u'linux-source-2.6.15')
+
+    def test_bugtask_target_distroseries_sourcepackage(self):
+        """The 'target' of a task can be a distroseries sourcepackage"""
+        login('foo.bar@xxxxxxxxxxxxx')
+        bugtaskset = getUtility(IBugTaskSet)
+        distro_series_sp_task = bugtaskset.get(16)
+
+        self.assertEqual(distro_series_sp_task.getPackageComponent().name,
+            'main')
+
+        expected_target = SourcePackage(
+            distroseries=distro_series_sp_task.distroseries,
+            sourcepackagename=distro_series_sp_task.sourcepackagename)
+        got_target = distro_series_sp_task.target
+        self.assertTrue(
+            ISourcePackage.providedBy(distro_series_sp_task.target))
+        self.assertEqual(got_target.distroseries,
+            expected_target.distroseries)
+        self.assertEqual(got_target.sourcepackagename,
+            expected_target.sourcepackagename)
+
+
+class TestBugTaskTargetName(TestCase):
+    """Verify our targetdisplayname and targetname are correct."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_targetname_distribution(self):
+        """The distribution name will be concat'd"""
+        login('foo.bar@xxxxxxxxxxxxx')
+        bugtaskset = getUtility(IBugTaskSet)
+        bugtask = bugtaskset.get(17)
+
+        self.assertEqual(bugtask.bugtargetdisplayname,
+            u'mozilla-firefox (Ubuntu)')
+        self.assertEqual(bugtask.bugtargetname,
+            u'mozilla-firefox (Ubuntu)')
+
+    def test_targetname_series_product(self):
+        """The targetname for distro series/product versions will be name of
+        source package or binary package. """
+        login('foo.bar@xxxxxxxxxxxxx')
+        bugtaskset = getUtility(IBugTaskSet)
+        bugtask = bugtaskset.get(2)
+
+        self.assertEqual(bugtask.bugtargetdisplayname, u'Mozilla Firefox')
+        self.assertEqual(bugtask.bugtargetname, u'firefox')
+
+
+class TestEditingBugTask(TestCase):
+    """Verify out editing functionality of bugtasks."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_edit_upstream(self):
+        """You cannot edit upstream tasks as ANONYMOUS"""
+        login('foo.bar@xxxxxxxxxxxxx')
+        bugtaskset = getUtility(IBugTaskSet)
+        bug_one = getUtility(IBugSet).get(1)
+        mark = getUtility(IPersonSet).getByEmail('mark@xxxxxxxxxxx')
+
+        evolution = getUtility(IProductSet).get(5)
+        upstream_task = bugtaskset.createTask(
+            bug_one, mark, evolution,
+            status=BugTaskStatus.NEW,
+            importance=BugTaskImportance.MEDIUM)
+
+        # An anonymous user cannot edit the bugtask.
+        login(ANONYMOUS)
+        with ExpectedException(Unauthorized, ''):
+            upstream_task.transitionToStatus(
+                BugTaskStatus.CONFIRMED, getUtility(ILaunchBag.user))
+
+        # A logged in user can edit the upstream bugtask.
+        login('jeff.waugh@xxxxxxxxxxxxxxx')
+        upstream_task.transitionToStatus(
+            BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
+
+    def test_edit_distro_bugtasks(self):
+        """Any logged-in user can edit tasks filed on distros
+
+        However not if the bug is not marked private.
+        So, as an anonymous user, we cannot edit anything:
+        """
+        login(ANONYMOUS)
+
+        bugtaskset = getUtility(IBugTaskSet)
+        distro_task = bugtaskset.get(25)
+
+        # Anonymous cannot change the status.
+        with ExpectedException(Unauthorized, ''):
+            distro_task.transitionToStatus(
+            BugTaskStatus.FIXRELEASED,
+            getUtility(ILaunchBag).user)
+
+        # Anonymous cannot change the assignee.
+        sample_person = getUtility(IPersonSet).getByEmail('test@xxxxxxxxxxxxx')
+        with ExpectedException(Unauthorized, ''):
+            distro_task.transitionToAssignee(sample_person)
+
+        login('test@xxxxxxxxxxxxx')
+
+        distro_task.transitionToStatus(
+            BugTaskStatus.FIXRELEASED,
+            getUtility(ILaunchBag).user)
+        distro_task.transitionToAssignee(sample_person)
+
+
+class TestConjoinedBugTasks(TestCase):
+    """Current distro dev series bugtasks are kept in sync.
+
+    They represent the same piece of work. The same is true for product and
+    productseries tasks, when the productseries task is targeted to the
+    IProduct.developmentfocus. The following attributes are synced:
+
+    * status
+    * assignee
+    * importance
+    * milestone
+    * sourcepackagename
+    * date_confirmed
+    * date_inprogress
+    * date_assigned
+    * date_closed
+    * date_left_new
+    * date_triaged
+    * date_fix_committed
+    * date_fix_released
+
+    """
+
+    layer = DatabaseFunctionalLayer
+
+    def _build_ubuntu_netapplet_bug(self):
+        """Helper to build buuntu_netapplet_bug"""
+        login('test@xxxxxxxxxxxxx')
+        launchbag = getUtility(ILaunchBag)
+
+        BugHelper = namedtuple('BugHelper', ['distro', 'sourcepackage', 'bug'])
+        ubuntu = getUtility(IDistributionSet).get(1)
+        ubuntu_netapplet = ubuntu.getSourcePackage("netapplet")
+        params = CreateBugParams(
+            owner=launchbag.user,
+            title="a test bug",
+            comment="test bug description")
+        ubuntu_netapplet_bug = ubuntu_netapplet.createBug(params)
+        return BugHelper(ubuntu, ubuntu_netapplet, ubuntu_netapplet_bug)
+
+    def test_conjoined_tasks_sync(self):
+        """"""
+        login_foobar()
+        launchbag = getUtility(ILaunchBag)
+
+        sample_person = getUtility(IPersonSet).getByEmail('test@xxxxxxxxxxxxx')
+
+        ubuntu = getUtility(IDistributionSet).get(1)
+        params = CreateBugParams(
+            owner=launchbag.user,
+            title="a test bug",
+            comment="test bug description")
+        ubuntu_bug = ubuntu.createBug(params)
+
+        ubuntu_netapplet = ubuntu.getSourcePackage("netapplet")
+        ubuntu_netapplet_bug = ubuntu_netapplet.createBug(params)
+        generic_netapplet_task = ubuntu_netapplet_bug.bugtasks[0]
+
+        # First, we'll target the bug for the current Ubuntu series, Hoary.
+        # Note that the synced attributes are copied when the series-specific
+        # tasks are created. We'll set non-default attribute values for each
+        # generic task to demonstrate.
+        self.assertEqual('hoary', ubuntu.currentseries.name)
+
+        # Only owners, experts, or admins can create a milestone.
+        ubuntu_edgy_milestone = ubuntu.currentseries.newMilestone("knot1")
+
+        login('test@xxxxxxxxxxxxx')
+        generic_netapplet_task.transitionToStatus(
+            BugTaskStatus.INPROGRESS, getUtility(ILaunchBag).user)
+        generic_netapplet_task.transitionToAssignee(sample_person)
+        generic_netapplet_task.milestone = ubuntu_edgy_milestone
+        generic_netapplet_task.transitionToImportance(
+            BugTaskImportance.CRITICAL, ubuntu.owner)
+
+        getUtility(IBugTaskSet).createTask(ubuntu_bug, launchbag.user,
+            ubuntu.currentseries)
+        current_series_netapplet_task = getUtility(IBugTaskSet).createTask(
+            ubuntu_netapplet_bug, launchbag.user,
+            ubuntu_netapplet.development_version)
+
+        # The attributes were synced with the generic task.
+        self.assertEqual('In Progress',
+            current_series_netapplet_task.status.title)
+        self.assertEqual('Sample Person',
+            current_series_netapplet_task.assignee.displayname)
+        self.assertEqual('knot1',
+            current_series_netapplet_task.milestone.name)
+        self.assertEqual('Critical',
+            current_series_netapplet_task.importance.title)
+
+        self.assertEqual(current_series_netapplet_task.date_assigned,
+            generic_netapplet_task.date_assigned)
+        self.assertEqual(current_series_netapplet_task.date_confirmed,
+           generic_netapplet_task.date_confirmed)
+        self.assertEqual(current_series_netapplet_task.date_inprogress,
+            generic_netapplet_task.date_inprogress)
+        self.assertEqual(current_series_netapplet_task.date_closed,
+           generic_netapplet_task.date_closed)
+
+        # We'll also add some product and productseries tasks.
+        alsa_utils = getUtility(IProductSet)['alsa-utils']
+        self.assertEqual('trunk', alsa_utils.development_focus.name)
+
+        # The status attribute is synced.
+        self.assertEqual('In Progress', generic_netapplet_task.status.title)
+        self.assertEqual('In Progress',
+            current_series_netapplet_task.status.title)
+        self.assertIsNone(generic_netapplet_task.date_closed)
+        self.assertIsNone(current_series_netapplet_task.date_closed)
+
+        current_series_netapplet_task.transitionToStatus(
+            BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
+
+        self.assertIsInstance(generic_netapplet_task.date_left_new,
+            datetime.datetime,)
+        self.assertEqual(generic_netapplet_task.date_left_new,
+            current_series_netapplet_task.date_left_new)
+
+        self.assertIsInstance(generic_netapplet_task.date_triaged,
+            datetime.datetime)
+        self.assertEqual(generic_netapplet_task.date_triaged,
+            current_series_netapplet_task.date_triaged)
+
+        self.assertIsInstance(generic_netapplet_task.date_fix_committed,
+            datetime.datetime)
+        self.assertEqual(generic_netapplet_task.date_fix_committed,
+            current_series_netapplet_task.date_fix_committed)
+
+        self.assertEqual('Fix Released', generic_netapplet_task.status.title)
+        self.assertEqual('Fix Released',
+            current_series_netapplet_task.status.title)
+
+        self.assertIsInstance(generic_netapplet_task.date_closed,
+            datetime.datetime)
+        self.assertEqual(generic_netapplet_task.date_closed,
+            current_series_netapplet_task.date_closed)
+        self.assertIsInstance(generic_netapplet_task.date_fix_released,
+            datetime.datetime)
+        self.assertEqual(generic_netapplet_task.date_fix_released,
+            current_series_netapplet_task.date_fix_released)
+
+    def test_conjoined_assignee_sync(self):
+        """The assignee is synced across conjoined tasks"""
+        login('test@xxxxxxxxxxxxx')
+        launchbag = getUtility(ILaunchBag)
+        no_priv = getUtility(IPersonSet).getByEmail('no-priv@xxxxxxxxxxxxx')
+
+        data = self._build_ubuntu_netapplet_bug()
+
+        alsa_utils = getUtility(IProductSet)['alsa-utils']
+        generic_alsa_utils_task = getUtility(IBugTaskSet).createTask(
+            data.bug, launchbag.user, alsa_utils)
+        devel_focus_alsa_utils_task = getUtility(IBugTaskSet).createTask(
+            data.bug, launchbag.user,
+            alsa_utils.getSeries("trunk"))
+
+        self.assertIsNone(generic_alsa_utils_task.assignee)
+        self.assertIsNone(devel_focus_alsa_utils_task.assignee)
+        self.assertIsNone(generic_alsa_utils_task.date_assigned)
+        self.assertIsNone(devel_focus_alsa_utils_task.date_assigned)
+
+        devel_focus_alsa_utils_task.transitionToAssignee(no_priv)
+
+        self.assertEqual('No Privileges Person',
+            generic_alsa_utils_task.assignee.displayname)
+        self.assertEqual('No Privileges Person',
+            devel_focus_alsa_utils_task.assignee.displayname)
+
+        self.assertIsInstance(generic_alsa_utils_task.date_assigned,
+            datetime.datetime)
+        self.assertEqual(generic_alsa_utils_task.date_assigned,
+            devel_focus_alsa_utils_task.date_assigned)
+
+    def test_conjoined_importance_synced(self):
+        """The importance is synced across conjoined tasks."""
+        login('test@xxxxxxxxxxxxx')
+        launchbag = getUtility(ILaunchBag)
+        no_priv = getUtility(IPersonSet).getByEmail('no-priv@xxxxxxxxxxxxx')
+        data = self._build_ubuntu_netapplet_bug()
+
+        current_series_netapplet_task = getUtility(IBugTaskSet).createTask(
+            data.bug, launchbag.user,
+            data.sourcepackage.development_version)
+        generic_netapplet_task = data.bug.bugtasks[0]
+        generic_netapplet_task.transitionToImportance(
+            BugTaskImportance.CRITICAL, data.distro.owner)
+        self.assertEqual(generic_netapplet_task.importance.title,
+            'Critical')
+        self.assertEqual(current_series_netapplet_task.importance.title,
+            'Critical')
+
+        current_series_netapplet_task.transitionToImportance(
+            BugTaskImportance.MEDIUM, data.distro.owner)
+
+        self.assertEqual(generic_netapplet_task.importance.title,
+            'Medium')
+        self.assertEqual(current_series_netapplet_task.importance.title,
+            'Medium')
+
+        # Not everyone can edit the importance, though. If an unauthorised
+        # user is passed to transitionToImportance an exception is raised.
+        with ExpectedException(UserCannotEditBugTaskImportance, ''):
+            current_series_netapplet_task.transitionToImportance(
+                BugTaskImportance.LOW, no_priv)
+        self.assertEqual(generic_netapplet_task.importance.title, 'Medium')
+
+    def test_conjoined_milestone(self):
+        """Milestone attribute will sync across conjoined tasks."""
+        data = self._build_ubuntu_netapplet_bug()
+
+        login('foo.bar@xxxxxxxxxxxxx')
+        launchbag = getUtility(ILaunchBag)
+
+        alsa_utils = getUtility(IProductSet)['alsa-utils']
+        generic_alsa_utils_task = getUtility(IBugTaskSet).createTask(
+            data.bug, launchbag.user, alsa_utils)
+        devel_focus_alsa_utils_task = getUtility(IBugTaskSet).createTask(
+            data.bug, launchbag.user,
+            alsa_utils.getSeries("trunk"))
+
+        test_milestone = alsa_utils.development_focus.newMilestone("test")
+        noway_milestone = alsa_utils.development_focus.newMilestone("noway")
+        Store.of(test_milestone).flush()
+
+        self.assertIsNone(generic_alsa_utils_task.milestone)
+        self.assertIsNone(devel_focus_alsa_utils_task.milestone)
+
+        devel_focus_alsa_utils_task.transitionToMilestone(
+            test_milestone, alsa_utils.owner)
+
+        self.assertEqual(generic_alsa_utils_task.milestone.name,
+            'test')
+        self.assertEqual(devel_focus_alsa_utils_task.milestone.name,
+            'test')
+
+        # But a normal unprivileged user can't set the milestone.
+        no_priv = getUtility(IPersonSet).getByEmail('no-priv@xxxxxxxxxxxxx')
+        with ExpectedException(UserCannotEditBugTaskMilestone, ''):
+            devel_focus_alsa_utils_task.transitionToMilestone(
+                noway_milestone, no_priv)
+        self.assertEqual(devel_focus_alsa_utils_task.milestone.name,
+            'test')
+
+        devel_focus_alsa_utils_task.transitionToMilestone(
+            test_milestone, alsa_utils.owner)
+
+        self.assertEqual(generic_alsa_utils_task.milestone.name,
+            'test')
+        self.assertEqual(devel_focus_alsa_utils_task.milestone.name,
+            'test')
+
+    def test_conjoined_syncs_sourcepackage_name(self):
+        """Conjoined tasks will sync source package names."""
+        data = self._build_ubuntu_netapplet_bug()
+        generic_netapplet_task = data.bug.bugtasks[0]
+
+        login('foo.bar@xxxxxxxxxxxxx')
+        launchbag = getUtility(ILaunchBag)
+
+        ubuntu_pmount = data.distro.getSourcePackage("pmount")
+        current_series_netapplet_task = getUtility(IBugTaskSet).createTask(
+            data.bug, launchbag.user,
+            data.sourcepackage.development_version)
+
+        self.assertEqual(generic_netapplet_task.sourcepackagename.name,
+            'netapplet')
+        self.assertEqual(current_series_netapplet_task.sourcepackagename.name,
+            'netapplet')
+
+        current_series_netapplet_task.transitionToTarget(
+            ubuntu_pmount.development_version)
+
+        self.assertEqual(generic_netapplet_task.sourcepackagename.name,
+            'pmount')
+        self.assertEqual(current_series_netapplet_task.sourcepackagename.name,
+            'pmount')
+
+    def test_non_current_dev_lacks_conjoined(self):
+        """Tasks not the current dev focus lacks conjoined masters or slaves.
+        """
+        # Only owners, experts, or admins can create a series.
+        login('foo.bar@xxxxxxxxxxxxx')
+        launchbag = getUtility(ILaunchBag)
+        ubuntu = getUtility(IDistributionSet).get(1)
+        alsa_utils = getUtility(IProductSet)['alsa-utils']
+        ubuntu_netapplet = ubuntu.getSourcePackage("netapplet")
+
+        params = CreateBugParams(
+            owner=launchbag.user,
+            title="a test bug",
+            comment="test bug description")
+        ubuntu_netapplet_bug = ubuntu_netapplet.createBug(params)
+
+        alsa_utils_stable = alsa_utils.newSeries(
+            launchbag.user, 'stable', 'The stable series.')
+
+        login('test@xxxxxxxxxxxxx')
+        Store.of(alsa_utils_stable).flush()
+        self.assertNotEqual(alsa_utils.development_focus, alsa_utils_stable)
+
+        stable_netapplet_task = getUtility(IBugTaskSet).createTask(
+            ubuntu_netapplet_bug, launchbag.user, alsa_utils_stable)
+        self.assertIsNone(stable_netapplet_task.conjoined_master)
+        self.assertIsNone(stable_netapplet_task.conjoined_slave)
+
+        warty = ubuntu.getSeries('warty')
+        self.assertNotEqual(warty, ubuntu.currentseries)
+
+        warty_netapplet_task = getUtility(IBugTaskSet).createTask(
+            ubuntu_netapplet_bug, launchbag.user,
+            warty.getSourcePackage(ubuntu_netapplet.sourcepackagename))
+
+        self.assertIsNone(warty_netapplet_task.conjoined_master)
+        self.assertIsNone(warty_netapplet_task.conjoined_slave)
+
+    def test_no_conjoined_without_current_series(self):
+        """Distributions without current series lack a conjoined master/slave.
+        """
+        login('foo.bar@xxxxxxxxxxxxx')
+        launchbag = getUtility(ILaunchBag)
+        ubuntu = getUtility(IDistributionSet).get(1)
+        ubuntu_netapplet = ubuntu.getSourcePackage("netapplet")
+        params = CreateBugParams(
+            owner=launchbag.user,
+            title="a test bug",
+            comment="test bug description")
+        ubuntu_netapplet_bug = ubuntu_netapplet.createBug(params)
+
+        gentoo = getUtility(IDistributionSet).getByName('gentoo')
+        self.assertIsNone(gentoo.currentseries)
+
+        gentoo_netapplet_task = getUtility(IBugTaskSet).createTask(
+            ubuntu_netapplet_bug, launchbag.user,
+            gentoo.getSourcePackage(ubuntu_netapplet.sourcepackagename))
+        self.assertIsNone(gentoo_netapplet_task.conjoined_master)
+        self.assertIsNone(gentoo_netapplet_task.conjoined_slave)
+
+    def test_conjoined_broken_relationship(self):
+        """A conjoined relationship can be broken, though.
+
+        If the development task (i.e the conjoined master) is Won't Fix, it
+        means that the bug is deferred to the next series. In this case the
+        development task should be Won't Fix, while the generic task keeps the
+        value it had before, allowing it to stay open.
+        """
+        login('foo.bar@xxxxxxxxxxxxx')
+        launchbag = getUtility(ILaunchBag)
+        ubuntu = getUtility(IDistributionSet).get(1)
+        params = CreateBugParams(
+            owner=launchbag.user,
+            title="a test bug",
+            comment="test bug description")
+        ubuntu_netapplet = ubuntu.getSourcePackage("netapplet")
+        ubuntu_netapplet_bug = ubuntu_netapplet.createBug(params)
+        generic_netapplet_task = ubuntu_netapplet_bug.bugtasks[0]
+        current_series_netapplet_task = getUtility(IBugTaskSet).createTask(
+            ubuntu_netapplet_bug, launchbag.user,
+            ubuntu_netapplet.development_version)
+
+        # First let's change the status from Fix Released, since it doesn't
+        # make sense to reject such a task.
+        current_series_netapplet_task.transitionToStatus(
+            BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
+        self.assertEqual(generic_netapplet_task.status.title,
+            'Confirmed')
+        self.assertEqual(current_series_netapplet_task.status.title,
+            'Confirmed')
+        self.assertIsNone(generic_netapplet_task.date_closed)
+        self.assertIsNone(current_series_netapplet_task.date_closed)
+
+        # Now, if we set the current series task to Won't Fix, the generic task
+        # will still be confirmed.
+        netapplet_owner = current_series_netapplet_task.pillar.owner
+        current_series_netapplet_task.transitionToStatus(
+            BugTaskStatus.WONTFIX, netapplet_owner)
+
+        self.assertEqual(generic_netapplet_task.status.title,
+            'Confirmed')
+        self.assertEqual(current_series_netapplet_task.status.title,
+            "Won't Fix")
+
+        self.assertIsNone(generic_netapplet_task.date_closed)
+        self.assertIsNotNone(current_series_netapplet_task.date_closed)
+
+        # And the bugtasks are no longer conjoined:
+        self.assertIsNone(generic_netapplet_task.conjoined_master)
+        self.assertIsNone(current_series_netapplet_task.conjoined_slave)
+
+        # If the current development release is marked as Invalid, then the
+        # bug is invalid for all future series too, and so the general bugtask
+        # is therefore Invalid also. In other words, conjoined again.
+
+        current_series_netapplet_task.transitionToStatus(
+            BugTaskStatus.NEW, getUtility(ILaunchBag).user)
+
+        # XXX Gavin Panella 2007-06-06 bug=112746:
+        # We must make two transitions.
+        current_series_netapplet_task.transitionToStatus(
+            BugTaskStatus.INVALID, getUtility(ILaunchBag).user)
+
+        self.assertEqual(generic_netapplet_task.status.title,
+            'Invalid')
+        self.assertEqual(current_series_netapplet_task.status.title,
+            'Invalid')
+
+        self.assertIsNotNone(generic_netapplet_task.date_closed)
+        self.assertIsNotNone(current_series_netapplet_task.date_closed)
+
+
+class TestBugTaskPrivacy(TestCase):
+    """Verify that the bug is either private or public."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_bugtask_privacy(self):
+        # Let's log in as the user Foo Bar (to be allowed to edit bugs):
+        foobar = login_foobar()
+
+        # Mark one of the Firefox bugs private. While we do this, we're also
+        # going to subscribe the Ubuntu team to the bug report to help
+        # demonstrate later on the interaction between privacy and teams (see
+        # the section entitled _Privacy and Team Awareness_):
+        bug_upstream_firefox_crashes = getUtility(IBugTaskSet).get(15)
+
+        ubuntu_team = getUtility(IPersonSet).getByEmail('support@xxxxxxxxxx')
+        bug_upstream_firefox_crashes.bug.subscribe(ubuntu_team, ubuntu_team)
+
+        old_state = Snapshot(bug_upstream_firefox_crashes.bug,
+            providing=IBug)
+        self.assertTrue(bug_upstream_firefox_crashes.bug.setPrivate(True,
+            foobar))
+
+        bug_set_private = ObjectModifiedEvent(
+            bug_upstream_firefox_crashes.bug, old_state,
+            ["id", "title", "private"])
+        notify(bug_set_private)
+        flush_database_updates()
+
+        # If we now login as someone who was neither implicitly nor explicitly
+        # subscribed to this bug, e.g. No Privileges Person, they will not be
+        # able to access or set properties of the bugtask.
+        mr_no_privs = login_nopriv()
+
+        with ExpectedException(Unauthorized, ''):
+            bug_upstream_firefox_crashes.status
+
+        with ExpectedException(Unauthorized, ''):
+            bug_upstream_firefox_crashes.transitionToStatus(
+                BugTaskStatus.FIXCOMMITTED, getUtility(ILaunchBag).user)
+
+        # The private bugs will be invisible to No Privileges Person in the
+        # search results:
+        params = BugTaskSearchParams(
+            status=any(BugTaskStatus.NEW, BugTaskStatus.CONFIRMED),
+            orderby="id", user=mr_no_privs)
+        upstream_mozilla = getUtility(IProductSet).getByName('firefox')
+        bugtasks = upstream_mozilla.searchTasks(params)
+        self.assertEqual(bugtasks.count(), 3)
+
+        bug_ids = [bt.bug.id for bt in bugtasks]
+        self.assertEqual(sorted(bug_ids), [1, 4, 5])
+
+        # We can create an access policy grant on the pillar to which the bug
+        # is targeted and No Privileges Person will have access to the private
+        # bug
+        aps = getUtility(IAccessPolicySource)
+        [policy] = aps.find(
+            [(upstream_mozilla, InformationType.USERDATA)])
+        apgs = getUtility(IAccessPolicyGrantSource)
+        apgs.grant([(policy, mr_no_privs, ubuntu_team)])
+        bugtasks = upstream_mozilla.searchTasks(params)
+        self.assertEqual(bugtasks.count(), 3)
+
+        bug_ids = [bt.bug.id for bt in bugtasks]
+        self.assertEqual(sorted(bug_ids), [1, 4, 5, 6])
+        apgs.revoke([(policy, mr_no_privs)])
+
+        # Privacy and Priviledged Users
+        # Now, we'll log in as Mark Shuttleworth, who was assigned to this bug
+        # when it was marked private:
+        login("mark@xxxxxxxxxxx")
+
+        # And note that he can access and set the bugtask attributes:
+        self.assertEqual(bug_upstream_firefox_crashes.status.title,
+            'New')
+        bug_upstream_firefox_crashes.transitionToStatus(
+            BugTaskStatus.NEW, getUtility(ILaunchBag).user)
+
+        # Privacy and Team Awareness
+        # No Privileges Person can't see the private bug, because he's not a
+        # subscriber:
+        no_priv = login_nopriv()
+        params = BugTaskSearchParams(
+            status=any(BugTaskStatus.NEW, BugTaskStatus.CONFIRMED),
+                user=no_priv)
+
+        firefox = getUtility(IProductSet)['firefox']
+        firefox_bugtasks = firefox.searchTasks(params)
+        self.assertEqual([bugtask.bug.id for bugtask in firefox_bugtasks],
+            [1, 4, 5])
+
+        # But if we add No Privileges Person to the Ubuntu Team, and because
+        # the Ubuntu Team *is* subscribed to the bug, No Privileges Person
+        # will see the private bug.
+
+        login("mark@xxxxxxxxxxx")
+        ubuntu_team.addMember(no_priv, reviewer=ubuntu_team.teamowner)
+
+        login("no-priv@xxxxxxxxxxxxx")
+        params = BugTaskSearchParams(
+            status=any(BugTaskStatus.NEW, BugTaskStatus.CONFIRMED),
+                user=foobar)
+        firefox_bugtasks = firefox.searchTasks(params)
+        self.assertEqual([bugtask.bug.id for bugtask in firefox_bugtasks],
+            [1, 4, 5, 6])
+
+        # Privacy and Launchpad Admins
+        # ----------------------------
+        # Let's log in as Daniel Henrique Debonzi:
+        launchbag = getUtility(ILaunchBag)
+        login("daniel.debonzi@xxxxxxxxxxxxx")
+        debonzi = launchbag.user
+
+        # The same search as above yields the same result, because Daniel
+        # Debonzi is an administrator.
+        firefox = getUtility(IProductSet).get(4)
+        params = BugTaskSearchParams(status=any(BugTaskStatus.NEW,
+                                                BugTaskStatus.CONFIRMED),
+                                     user=debonzi)
+        firefox_bugtasks = firefox.searchTasks(params)
+        self.assertEqual([bugtask.bug.id for bugtask in firefox_bugtasks],
+            [1, 4, 5, 6])
+
+        # Trying to retrieve the bug directly will work fine:
+        bug_upstream_firefox_crashes = getUtility(IBugTaskSet).get(15)
+        # As will attribute access:
+        self.assertEqual(bug_upstream_firefox_crashes.status.title,
+            'New')
+
+        # And attribute setting:
+        bug_upstream_firefox_crashes.transitionToStatus(
+            BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
+        bug_upstream_firefox_crashes.transitionToStatus(
+            BugTaskStatus.NEW, getUtility(ILaunchBag).user)
+
+
+class TestCountsForProducts(TestCase):
+    """Test BugTaskSet.getOpenBugTasksPerProduct"""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_open_product_counts(self):
+        # IBugTaskSet.getOpenBugTasksPerProduct() will return a dictionary
+        # of product_id:count entries for bugs in an open status that
+        # the user given as a parameter is allowed to see. If a product,
+        # such as id=3 does not have any open bugs, it will not appear
+        # in the result.
+        foobar = login_foobar()
+
+        productset = getUtility(IProductSet)
+        products = [productset.get(id) for id in (3, 5, 20)]
+        sample_person = getUtility(IPersonSet).getByEmail('test@xxxxxxxxxxxxx')
+        bugtask_counts = getUtility(IBugTaskSet).getOpenBugTasksPerProduct(
+            sample_person, products)
+        res = sorted(bugtask_counts.items())
+        self.assertEqual(
+            'product_id=%d count=%d' % tuple(res[0]),
+            'product_id=5 count=1')
+        self.assertEqual(
+            'product_id=%d count=%d' % tuple(res[1]),
+            'product_id=20 count=2')
+
+        # A Launchpad admin will get a higher count for the product with id=20
+        # because he can see the private bug.
+        bugtask_counts = getUtility(IBugTaskSet).getOpenBugTasksPerProduct(
+            foobar, products)
+        res = sorted(bugtask_counts.items())
+        self.assertEqual(
+            'product_id=%d count=%d' % tuple(res[0]),
+            'product_id=5 count=1')
+        self.assertEqual(
+            'product_id=%d count=%d' % tuple(res[1]),
+            'product_id=20 count=3')
+
+        # Someone subscribed to the private bug on the product with id=20
+        # will also have it added to the count.
+        karl = getUtility(IPersonSet).getByName('karl')
+        bugtask_counts = getUtility(IBugTaskSet).getOpenBugTasksPerProduct(
+            karl, products)
+        res = sorted(bugtask_counts.items())
+        self.assertEqual(
+            'product_id=%d count=%d' % tuple(res[0]),
+            'product_id=5 count=1')
+        self.assertEqual(
+            'product_id=%d count=%d' % tuple(res[1]),
+            'product_id=20 count=3')
+
+
+class TestSortingBugTasks(TestCase):
+    """Bug tasks need to sort in a very particular order."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_sortingorder(self):
+        """We want product tasks, then ubuntu, then distro-related.
+
+        In the distro-related tasks we want a distribution-task first, then
+        distroseries-tasks for that same distribution. The distroseries tasks
+        should be sorted by distroseries version.
+        """
+        login('foo.bar@xxxxxxxxxxxxx')
+        bug_one = getUtility(IBugSet).get(1)
+        tasks = bug_one.bugtasks
+        task_names = [task.bugtargetdisplayname for task in tasks]
+        self.assertEqual(task_names, [
+            u'Mozilla Firefox',
+            'mozilla-firefox (Ubuntu)',
+            'mozilla-firefox (Debian)',
+        ])
+
+
+class TestBugTaskAdaptation(TestCase):
+    """Verify bugtask adaptation."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_bugtask_adaptation(self):
+        """An IBugTask can be adapted to an IBug"""
+        login('foo.bar@xxxxxxxxxxxxx')
+        bugtask_four = getUtility(IBugTaskSet).get(4)
+        bug = IBug(bugtask_four)
+        self.assertEqual(bug.title,
+            u'Firefox does not support SVG')
+
+
+class TestTargetNameCache(TestCase):
+    """BugTask table has a stored computed attribute.
+
+    This targetnamecache attribute which stores a computed value to allow us
+    to sort and search on that value without having to do lots of SQL joins.
+    This cached value gets updated daily by the
+    update-bugtask-targetnamecaches cronscript and whenever the bugtask is
+    changed.  Of course, it's also computed and set when a bugtask is
+    created.
+
+    `BugTask.bugtargetdisplayname` simply returns `targetnamecache`, and
+    the latter is not exposed in `IBugTask`, so the `bugtargetdisplayname`
+    is used here.
+    """
+
+    layer = DatabaseFunctionalLayer
+
+    def test_cron_updating_targetnamecache(self):
+        """Verify the initial target name cache."""
+        login('foo.bar@xxxxxxxxxxxxx')
+        bug_one = getUtility(IBugSet).get(1)
+        mark = getUtility(IPersonSet).getByEmail('mark@xxxxxxxxxxx')
+        netapplet = getUtility(IProductSet).get(11)
+
+        upstream_task = getUtility(IBugTaskSet).createTask(
+            bug_one, mark, netapplet,
+            status=BugTaskStatus.NEW, importance=BugTaskImportance.MEDIUM)
+        self.assertEqual(upstream_task.bugtargetdisplayname,
+            u'NetApplet')
+
+        thunderbird = getUtility(IProductSet).get(8)
+        upstream_task_id = upstream_task.id
+        upstream_task.transitionToTarget(thunderbird)
+        self.assertEqual(upstream_task.bugtargetdisplayname,
+            u'Mozilla Thunderbird')
+
+        thunderbird.name = 'thunderbird-ng'
+        thunderbird.displayname = 'Mozilla Thunderbird NG'
+
+        # XXX Guilherme Salgado 2005-11-07 bug=3989:
+        # This flush_database_updates() shouldn't be needed because we
+        # already have the transaction.commit() here, but without it
+        # (flush_database_updates), the cronscript won't see the thunderbird
+        # name change.
+        flush_database_updates()
+        transaction.commit()
+
+        process = subprocess.Popen(
+            'cronscripts/update-bugtask-targetnamecaches.py', shell=True,
+            stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE)
+        (out, err) = process.communicate()
+
+        self.assertTrue(err.startswith(("INFO    Creating lockfile: "
+            "/var/lock/launchpad-launchpad-targetnamecacheupdater.lock")))
+        self.assertTrue('INFO    Updating targetname cache of bugtasks' in err)
+        self.assertTrue('INFO    Calculating targets.' in err)
+        self.assertTrue('INFO    Will check ', err)
+        self.assertTrue("INFO    Updating (u'Mozilla Thunderbird',)" in err)
+        self.assertTrue('INFO    Updated 1 target names.' in err)
+        self.assertTrue('INFO    Finished updating targetname cache' in err)
+
+        self.assertEqual(process.returncode, 0)
+
+        # XXX Guilherme Salgado 2005-11-07:
+        # If we don't call flush_database_caches() here, we won't see the
+        # changes made by the cronscript in objects we already have cached.
+        flush_database_caches()
+        transaction.commit()
+
+        self.assertEqual(
+            getUtility(IBugTaskSet).get(upstream_task_id).bugtargetdisplayname,
+            u'Mozilla Thunderbird NG')
+
+        # With sourcepackage bugtasks that have accepted nominations to a
+        # series, additional sourcepackage bugtasks are automatically
+        # nominated to the same series. The nominations are implicitly
+        # accepted and have targetnamecache updated.
+        ubuntu = getUtility(IDistributionSet).get(1)
+
+        new_bug, new_bug_event = getUtility(IBugSet).createBugWithoutTarget(
+            CreateBugParams(mark, 'New Bug', comment='New Bug'))
+
+        # The first message of a new bug has index 0.
+        self.assertEqual(new_bug.bug_messages[0].index, 0)
+
+        getUtility(IBugTaskSet).createTask(
+            new_bug, mark, ubuntu.getSourcePackage('mozilla-firefox'))
+
+        # The first task has been created and successfully nominated to Hoary.
+        new_bug.addNomination(mark, ubuntu.currentseries).approve(mark)
+
+        task_set = [task.bugtargetdisplayname for task in new_bug.bugtasks]
+        self.assertEqual(task_set, [
+            'mozilla-firefox (Ubuntu)',
+            'mozilla-firefox (Ubuntu Hoary)',
+        ])
+
+        getUtility(IBugTaskSet).createTask(
+            new_bug, mark, ubuntu.getSourcePackage('alsa-utils'))
+
+        # The second task has been created and has also been successfully
+        # nominated to Hoary.
+
+        task_set = [task.bugtargetdisplayname for task in new_bug.bugtasks]
+        self.assertEqual(task_set, [
+            'alsa-utils (Ubuntu)',
+            'mozilla-firefox (Ubuntu)',
+            'alsa-utils (Ubuntu Hoary)',
+            'mozilla-firefox (Ubuntu Hoary)',
+        ])
+
+        # The updating of targetnamecaches is usually done by the cronjob,
+        # however it can also be invoked directly.
+        thunderbird.name = 'thunderbird'
+        thunderbird.displayname = 'Mozilla Thunderbird'
+        transaction.commit()
+
+        self.assertEqual(upstream_task.bugtargetdisplayname,
+            u'Mozilla Thunderbird NG')
+
+        logger = FakeLogger()
+        updater = BugTaskTargetNameCacheUpdater(transaction, logger)
+        updater.run()
+
+        flush_database_caches()
+        transaction.commit()
+        self.assertEqual(upstream_task.bugtargetdisplayname,
+            u'Mozilla Thunderbird')
+
+
+class TestTargetUsesMalone(TestCase):
+    """Verify bug task flag for using Malone is set."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_bugtask_users_malone(self):
+        """Verify the target uses Malone as its official bugtracker.
+        """
+        login('foo.bar@xxxxxxxxxxxxx')
+        bug_one = getUtility(IBugSet).get(1)
+        malone_info = [(task.bugtargetdisplayname, task.target_uses_malone)
+            for task in bug_one.bugtasks]
+
+        self.assertEqual(malone_info, [
+            ('Mozilla Firefox', True),
+            ('mozilla-firefox (Ubuntu)', True),
+            ('mozilla-firefox (Debian)', False),
+        ])
+
+
+class TestBugTaskBadges(TestCaseWithFactory):
+
+    """Verify getBugTaskBadgeProperties"""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_butask_badges_populated(self):
+        """getBugTaskBadgeProperties(), calcs properties for multiple tasks.
+
+        A bug can have certain properties, which results in a badge being
+        displayed in bug listings.
+        """
+        login('foo.bar@xxxxxxxxxxxxx')
+
+        def get_badge_properties(badge_properties):
+            bugtasks = sorted(badge_properties.keys(), key=attrgetter('id'))
+            res = []
+            for bugtask in bugtasks:
+                res.append("Properties for bug %s:" % (bugtask.bug.id))
+                for key, value in sorted(badge_properties[bugtask].items()):
+                    res.append(" %s: %s" % (key, value))
+            return res
+
+        bug_two = getUtility(IBugSet).get(2)
+        some_bugtask = bug_two.bugtasks[0]
+        bug_three = getUtility(IBugSet).get(3)
+        another_bugtask = bug_three.bugtasks[0]
+        badge_properties = getUtility(IBugTaskSet).getBugTaskBadgeProperties(
+            [some_bugtask, another_bugtask])
+
+        self.assertEqual(get_badge_properties(badge_properties),
+           ['Properties for bug 2:',
+           ' has_branch: False',
+           ' has_patch: False',
+           ' has_specification: False',
+           'Properties for bug 3:',
+           ' has_branch: False',
+           ' has_patch: False',
+           ' has_specification: False'])
+
+        # a specification gets linked...
+        spec = getUtility(ISpecificationSet).all_specifications[0]
+        spec.linkBug(bug_two)
+
+        # or a branch gets linked to the bug...
+        no_priv = getUtility(IPersonSet).getByEmail('no-priv@xxxxxxxxxxxxx')
+        branch = self.factory.makeAnyBranch()
+        bug_three.linkBranch(branch, no_priv)
+
+        # the properties for the bugtasks reflect this.
+        badge_properties = getUtility(IBugTaskSet).getBugTaskBadgeProperties(
+            [some_bugtask, another_bugtask])
+        self.assertEqual(get_badge_properties(badge_properties), [
+        'Properties for bug 2:',
+        ' has_branch: False',
+        ' has_patch: False',
+        ' has_specification: True',
+        'Properties for bug 3:',
+        ' has_branch: True',
+        ' has_patch: False',
+        ' has_specification: False',
+         ])
+
+
+class TestBugTaskTags(TestCase):
+    """List of bugtasks often need to display related tasks."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_getting_tags_from_bugs(self):
+        """Tags are related to bugtasks via bugs.
+
+        BugTaskSet has a method getBugTaskTags that can calculate the tags in
+        one query.
+        """
+        login('foo.bar@xxxxxxxxxxxxx')
+        bug_two = getUtility(IBugSet).get(2)
+        some_bugtask = bug_two.bugtasks[0]
+        bug_three = getUtility(IBugSet).get(3)
+        another_bugtask = bug_three.bugtasks[0]
+
+        some_bugtask.bug.tags = [u'foo', u'bar']
+        another_bugtask.bug.tags = [u'baz', u'bop']
+        tags_by_task = getUtility(IBugTaskSet).getBugTaskTags([
+            some_bugtask, another_bugtask])
+
+        self.assertEqual(tags_by_task,
+            {3: [u'bar', u'foo'], 6: [u'baz', u'bop']})
+
+
+class TestSimilarBugs(TestCaseWithFactory):
+    """It's possible to get a list of bugs similar to the current one."""
+
+    layer = DatabaseFunctionalLayer
+
+    def _verifySimilarResults(self, similar, expected):
+        """Helper to test the similar results with expected set."""
+        bug_info = [(bug.id, bug.title)
+            for bug in sorted(similar, key=attrgetter('id'))]
+        self.assertEqual(bug_info, expected)
+
+    def test_similar_bugs_property(self):
+
+        """Find similar via the similar_bugs property of its bug tasks."""
+        firefox = getUtility(IProductSet)['firefox']
+        new_ff_bug = self.factory.makeBug(product=firefox, title="Firefox")
+        ff_bugtask = new_ff_bug.bugtasks[0]
+        sample_person = getUtility(IPersonSet).getByEmail('test@xxxxxxxxxxxxx')
+
+        similar_bugs = ff_bugtask.findSimilarBugs(user=sample_person)
+        self._verifySimilarResults(similar_bugs, [
+            (1, u'Firefox does not support SVG'),
+            (5, u'Firefox install instructions should be complete'),
+        ])
+
+    def test_similar_bugs_distributions(self):
+        """This also works for distributions."""
+        firefox = getUtility(IProductSet)['firefox']
+        new_ff_bug = self.factory.makeBug(product=firefox, title="Firefox")
+        sample_person = getUtility(IPersonSet).getByEmail('test@xxxxxxxxxxxxx')
+        ubuntu = getUtility(IDistributionSet).get(1)
+        ubuntu_bugtask = self.factory.makeBugTask(bug=new_ff_bug,
+            target=ubuntu)
+        self.factory.makeBugTask(bug=new_ff_bug, target=ubuntu)
+        similar_bugs = ubuntu_bugtask.findSimilarBugs(user=sample_person)
+        self._verifySimilarResults(similar_bugs, [
+            (1, u'Firefox does not support SVG'),
+        ])
+
+    def test_similar_bugs_sourcepackages(self):
+        """Similar bugs should also be found through source packages"""
+        firefox = getUtility(IProductSet)['firefox']
+        sample_person = getUtility(IPersonSet).getByEmail('test@xxxxxxxxxxxxx')
+        ubuntu = getUtility(IDistributionSet).get(1)
+
+        a_ff_bug = self.factory.makeBug(product=firefox, title="a Firefox")
+        firefox_package = ubuntu.getSourcePackage('mozilla-firefox')
+        firefox_package_bugtask = self.factory.makeBugTask(
+            bug=a_ff_bug, target=firefox_package)
+
+        similar_bugs = firefox_package_bugtask.findSimilarBugs(
+             user=sample_person)
+        self._verifySimilarResults(similar_bugs, [
+            (1, u'Firefox does not support SVG'),
+        ])
+
+    def test_similar_bugs_privacy(self):
+        """Private bugs won't show up unless the user is a direct subscriber.
+
+        We'll demonstrate this by creating a new bug against Firefox.
+        """
+        firefox = getUtility(IProductSet)['firefox']
+        new_ff_bug = self.factory.makeBug(product=firefox, title="Firefox")
+        ff_bugtask = new_ff_bug.bugtasks[0]
+        no_priv = getUtility(IPersonSet).getByEmail('no-priv@xxxxxxxxxxxxx')
+
+        second_ff_bug = self.factory.makeBug(
+            product=firefox, title="Yet another Firefox bug")
+        similar_bugs = ff_bugtask.findSimilarBugs(user=no_priv)
+        self._verifySimilarResults(similar_bugs, [
+            (1, u'Firefox does not support SVG'),
+            (5, u'Firefox install instructions should be complete'),
+            (17, u'Yet another Firefox bug'),
+        ])
+
+        # If we mark the new bug as private, it won't appear in the similar
+        # bugs list for no_priv any more, since they're not a direct
+        # subscriber.
+        foobar = login_foobar()
+        self.assertEqual(second_ff_bug.setPrivate(True, foobar), True)
+
+        similar_bugs = ff_bugtask.findSimilarBugs(user=no_priv)
+        self._verifySimilarResults(similar_bugs, [
+            (1, u'Firefox does not support SVG'),
+            (5, u'Firefox install instructions should be complete'),
+        ])
+
+
 class TestStatusCountsForProductSeries(TestCaseWithFactory):
     """Test BugTaskSet.getStatusCountsForProductSeries()."""
 


Follow ups