← Back to team overview

launchpad-reviewers team mailing list archive

lp:~wallyworld/launchpad/disallow-private-for-multipillar-bugs-879138 into lp:launchpad

 

Ian Booth has proposed merging lp:~wallyworld/launchpad/disallow-private-for-multipillar-bugs-879138 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #879138 in Launchpad itself: "Do not permit bugs to be made private if there is more than one pillar"
  https://bugs.launchpad.net/launchpad/+bug/879138

For more details, see:
https://code.launchpad.net/~wallyworld/launchpad/disallow-private-for-multipillar-bugs-879138/+merge/83366

== Implementation ==

We need to stop people from making public multi-pillar bugs private (unless they are permitted by the feature flag "disclosure.allow_multipillar_private_bugs.enable").
Two things are done:
1. Bug privacy form (ajax and HTML)
- remove the Private checkbox for multi-pillar bugs
2. API
- raise a BugCannotBePrivate exception if someone calls setPrivate(True) on a multi-pillar bug

The privacy form looks a little sparse when the privacy checkbox is hidden, but it only ever had 2 checkboxes on it anyway. We could look to improve the ui in a subsequent branch.

== Tests ==

A bit of a nightmare here. The new tests were easy to write. However, there's a bunch of existing doc tests which call setPrivate(True) on multi-pillar bugs from the sample data.

New tests:

TestBugSecrecyViews
- test_hide_private_option_for_multipillar_bugs

TestBugPrivacy
- test_multipillar_private_bugs_disallowed

lp/bugs/tests/test_errors.py
- tests that the bug exceptions are correctly mapped to http errors

Existing broken tests:

Sadly, these were all doc tests. I did one of two things:
1. Change the doc test to use a single-pillar bug from sample data
2. Where 1) was too hard, used the feature flag to make the test pass.

For 2), when the flag is removed, the tests will need to be fixed.

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/canonical/launchpad/doc/canonical_url_examples.txt
  lib/canonical/launchpad/doc/object-privacy.txt
  lib/canonical/launchpad/doc/webapp-authorization.txt
  lib/lp/bugs/errors.py
  lib/lp/bugs/browser/bug.py
  lib/lp/bugs/browser/tests/bugs-views.txt
  lib/lp/bugs/browser/tests/test_bug_views.py
  lib/lp/bugs/doc/bug-export.txt
  lib/lp/bugs/doc/bug-tags.txt
  lib/lp/bugs/doc/bug.txt
  lib/lp/bugs/doc/bugattachments.txt
  lib/lp/bugs/doc/bugnotification-email.txt
  lib/lp/bugs/doc/bugsubscription.txt
  lib/lp/bugs/doc/bugtask-expiration.txt
  lib/lp/bugs/doc/bugtask-find-similar.txt
  lib/lp/bugs/doc/bugtask.txt
  lib/lp/bugs/doc/initial-bug-contacts.txt
  lib/lp/bugs/model/bug.py
  lib/lp/bugs/model/tests/test_bug.py
  lib/lp/bugs/stories/bug-privacy/xx-bug-privacy.txt
  lib/lp/bugs/tests/test_errors.py

Clean apart from some "narrative uses a moin header" noise and other doc test crap.
-- 
https://code.launchpad.net/~wallyworld/launchpad/disallow-private-for-multipillar-bugs-879138/+merge/83366
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wallyworld/launchpad/disallow-private-for-multipillar-bugs-879138 into lp:launchpad.
=== modified file 'lib/canonical/launchpad/doc/canonical_url_examples.txt'
--- lib/canonical/launchpad/doc/canonical_url_examples.txt	2011-07-27 08:04:46 +0000
+++ lib/canonical/launchpad/doc/canonical_url_examples.txt	2011-11-25 04:57:29 +0000
@@ -222,9 +222,9 @@
 
 An IBugTask on a distribution series source package.
 
-    >>> distro_series_task = getUtility(IBugTaskSet).get(16)
+    >>> distro_series_task = getUtility(IBugTaskSet).get(19)
     >>> canonical_url(distro_series_task)
-    u'http://bugs.launchpad.dev/ubuntu/warty/+source/mozilla-firefox/+bug/5'
+    u'http://bugs.launchpad.dev/debian/sarge/+source/mozilla-firefox/+bug/3'
 
 An IBugTask on a distribution series without a sourcepackage.
 
@@ -232,7 +232,7 @@
     >>> distro_series_task.transitionToTarget(
     ...     distro_series_task.target.distroseries)
     >>> canonical_url(distro_series_task)
-    u'http://bugs.launchpad.dev/ubuntu/warty/+bug/5'
+    u'http://bugs.launchpad.dev/debian/sarge/+bug/3'
     >>> distro_series_task.transitionToTarget(temp_target)
 
 A private bug, as an anonymous user! (We'll temporarily subscribe to the bug,
@@ -250,12 +250,12 @@
     >>> login(ANONYMOUS)
 
     >>> canonical_url(distro_series_task.bug)
-    u'http://bugs.launchpad.dev/bugs/5'
+    u'http://bugs.launchpad.dev/bugs/3'
 
 A private bugtask, as an anonymous user.
 
     >>> canonical_url(distro_series_task)
-    u'http://bugs.launchpad.dev/ubuntu/warty/+source/mozilla-firefox/+bug/5'
+    u'http://bugs.launchpad.dev/debian/sarge/+source/mozilla-firefox/+bug/3'
 
     >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> distro_series_task.bug.setPrivate(False, getUtility(ILaunchBag).user)

=== modified file 'lib/canonical/launchpad/doc/object-privacy.txt'
--- lib/canonical/launchpad/doc/object-privacy.txt	2010-10-18 22:24:59 +0000
+++ lib/canonical/launchpad/doc/object-privacy.txt	2011-11-25 04:57:29 +0000
@@ -11,7 +11,7 @@
     >>> from lp.answers.interfaces.questioncollection import IQuestionSet
     >>> from lp.bugs.interfaces.bug import IBugSet
     >>> from lp.registry.interfaces.person import IPersonSet
-    >>> bug = getUtility(IBugSet).get(1)
+    >>> bug = getUtility(IBugSet).get(4)
     >>> bug.private
     False
     >>> IObjectPrivacy(bug).is_private

=== modified file 'lib/canonical/launchpad/doc/webapp-authorization.txt'
--- lib/canonical/launchpad/doc/webapp-authorization.txt	2011-06-28 15:04:29 +0000
+++ lib/canonical/launchpad/doc/webapp-authorization.txt	2011-11-25 04:57:29 +0000
@@ -62,10 +62,10 @@
 
     >>> from lp.bugs.interfaces.bug import IBugSet
     >>> login('test@xxxxxxxxxxxxx')
-    >>> bug_1 = getUtility(IBugSet).get(1)
-    >>> bug_1.setPrivate(True, sample_person)
+    >>> bug_4 = getUtility(IBugSet).get(4)
+    >>> bug_4.setPrivate(True, sample_person)
     True
-    >>> check_permission('launchpad.View', bug_1)
+    >>> check_permission('launchpad.View', bug_4)
     True
 
 A principal with permission to read only non-private objects won't have
@@ -74,7 +74,7 @@
     >>> logout()
     >>> principal.access_level = AccessLevel.READ_PUBLIC
     >>> setupInteraction(principal)
-    >>> check_permission('launchpad.View', bug_1)
+    >>> check_permission('launchpad.View', bug_4)
     False
 
 A token used for desktop integration has a level of permission
@@ -82,7 +82,7 @@
 
     >>> principal.access_level = AccessLevel.DESKTOP_INTEGRATION
     >>> setupInteraction(principal)
-    >>> check_permission('launchpad.View', bug_1)
+    >>> check_permission('launchpad.View', bug_4)
     True
 
     >>> check_permission('launchpad.Edit', sample_person)

=== modified file 'lib/lp/bugs/browser/bug.py'
--- lib/lp/bugs/browser/bug.py	2011-11-20 06:30:18 +0000
+++ lib/lp/bugs/browser/bug.py	2011-11-25 04:57:29 +0000
@@ -117,6 +117,7 @@
 from lp.bugs.model.structuralsubscription import (
     get_structural_subscriptions_for_bug,
     )
+from lp.services import features
 from lp.services.fields import DuplicateBug
 from lp.services.propertycache import cachedproperty
 
@@ -837,6 +838,14 @@
         security_related_field = copy_field(
             IBug['security_related'], readonly=False)
 
+    def setUpFields(self):
+        """See `LaunchpadFormView`."""
+        LaunchpadFormView.setUpFields(self)
+        if (not bool(features.getFeatureFlag(
+                'disclosure.allow_multipillar_private_bugs.enabled'))
+                and len(self.context.bug.affected_pillars) > 1):
+            self.form_fields = self.form_fields.omit('private')
+
     @property
     def next_url(self):
         """Return the next URL to call when this call completes."""

=== modified file 'lib/lp/bugs/browser/tests/bugs-views.txt'
--- lib/lp/bugs/browser/tests/bugs-views.txt	2011-10-05 18:51:37 +0000
+++ lib/lp/bugs/browser/tests/bugs-views.txt	2011-11-25 04:57:29 +0000
@@ -73,11 +73,11 @@
     8: Printing doesn't work
 
 Only the bugs that the user has permission to view are shown in the
-list, so if we mark bug #2 as private, No Privileges won't see it, since
+list, so if we mark bug #4 as private, No Privileges won't see it, since
 he's not subscribed to it.
 
-    >>> bug_two = getUtility(IBugSet).get(2)
-    >>> bug_two.setPrivate(True, getUtility(ILaunchBag).user)
+    >>> bug_4 = getUtility(IBugSet).get(4)
+    >>> bug_4.setPrivate(True, getUtility(ILaunchBag).user)
     True
 
     >>> login('no-priv@xxxxxxxxxxxxx')
@@ -86,19 +86,19 @@
     >>> for bug in bugs_view.getMostRecentlyFixedBugs():
     ...     print "%s: %s" % (bug.id, bug.title)
     1: Firefox does not support SVG
-    4: Reflow problems with complex page layouts
+    2: Blackhole Trash folder
     8: Printing doesn't work
 
-If Sample Person get subscribed to bug #2, he can see it in the list.
+If Person David gets subscribed to bug #4, he can see it in the list.
 
     >>> from lp.registry.interfaces.person import IPersonSet
     >>> person_set = getUtility(IPersonSet)
     >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> bug_two.subscribe(
-    ...     person_set.getByEmail('test@xxxxxxxxxxxxx'),
+    >>> bug_4.subscribe(
+    ...     person_set.getByEmail('david@xxxxxxxxxxxxx'),
     ...     person_set.getByEmail('foo.bar@xxxxxxxxxxxxx'))
     <lp.bugs.model.bugsubscription.BugSubscription ...>
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login('david@xxxxxxxxxxxxx')
     >>> bugs_view = MaloneView(MaloneApplication(), LaunchpadTestRequest())
     >>> bugs_view.initialize()
     >>> for bug in bugs_view.getMostRecentlyFixedBugs():

=== modified file 'lib/lp/bugs/browser/tests/test_bug_views.py'
--- lib/lp/bugs/browser/tests/test_bug_views.py	2011-11-22 06:54:05 +0000
+++ lib/lp/bugs/browser/tests/test_bug_views.py	2011-11-25 04:57:29 +0000
@@ -396,6 +396,23 @@
         with person_logged_in(owner):
             self.assertTrue(bug.security_related)
 
+    def test_hide_private_option_for_multipillar_bugs(self):
+        # A multi-pillar bug cannot be made private, so hide the form field.
+        bug = self.factory.makeBug()
+        product = self.factory.makeProduct()
+        self.factory.makeBugTask(bug=bug, target=product)
+        view = create_initialized_view(bug.default_bugtask, '+secrecy')
+        self.assertIsNone(find_tag_by_id(view.render(), 'field.private'))
+
+        # Some teams though need to use a foot gun.
+        feature_flag = {
+            'disclosure.allow_multipillar_private_bugs.enabled': 'on'
+            }
+        with FeatureFixture(feature_flag):
+            view = create_initialized_view(bug.default_bugtask, '+secrecy')
+            self.assertIsNotNone(
+                find_tag_by_id(view.render(), 'field.private'))
+
 
 class TestBugTextViewPrivateTeams(TestCaseWithFactory):
     """ Test for rendering BugTextView with private team artifacts.

=== modified file 'lib/lp/bugs/doc/bug-export.txt'
--- lib/lp/bugs/doc/bug-export.txt	2011-09-26 06:30:07 +0000
+++ lib/lp/bugs/doc/bug-export.txt	2011-11-25 04:57:29 +0000
@@ -132,9 +132,9 @@
 the file when we later serialise the bug:
 
     >>> login('test@xxxxxxxxxxxxx')
-    >>> bug1 = getUtility(IBugSet).get(1)
+    >>> bug4 = getUtility(IBugSet).get(4)
     >>> sampleperson = getUtility(IPersonSet).getByEmail('test@xxxxxxxxxxxxx')
-    >>> bug1.addAttachment(
+    >>> bug4.addAttachment(
     ...     sampleperson, StringIO('Hello World'), 'Added attachment',
     ...     'hello.txt', description='"Hello World" attachment')
     <BugAttachment ...>
@@ -144,16 +144,16 @@
 A reference to the attachment is included with the new comment with the
 attachment contents encoded using base-64:
 
-    >>> node = serialise_bugtask(bug1.bugtasks[0])
+    >>> node = serialise_bugtask(bug4.bugtasks[0])
     >>> tree = ET.ElementTree(node)
     >>> tree.write(sys.stdout)
-    <bug id="1">
+    <bug id="4">
     ...
     <comment>
     <sender name="name12">Sample Person</sender>
     <date>...</date>
     <text>Added attachment</text>
-    <attachment href="http://bugs.launchpad.dev/bugs/1/.../+files/hello.txt";>
+    <attachment href="http://bugs.launchpad.dev/bugs/4/.../+files/hello.txt";>
     <type>UNSPECIFIED</type>
     <filename>hello.txt</filename>
     <title>"Hello World" attachment</title>
@@ -172,7 +172,7 @@
 they can be included by passing the --include-private flag to the import
 script.  To test this, we'll make a bug private:
 
-    >>> bug1.setPrivate(True, getUtility(ILaunchBag).user)
+    >>> bug4.setPrivate(True, getUtility(ILaunchBag).user)
     True
 
     >>> transaction.commit()
@@ -181,12 +181,12 @@
 
     >>> output = StringIO()
     >>> export_bugtasks(transaction, firefox, output)
-    >>> '<bug id="1">' in output.getvalue()
+    >>> '<bug id="4">' in output.getvalue()
     False
 
-However, bug #1 will appear in the export if we include private bugs:
+However, bug #4 will appear in the export if we include private bugs:
 
     >>> output = StringIO()
     >>> export_bugtasks(transaction, firefox, output, include_private=True)
-    >>> '<bug id="1">' in output.getvalue()
+    >>> '<bug id="4">' in output.getvalue()
     True

=== modified file 'lib/lp/bugs/doc/bug-tags.txt'
--- lib/lp/bugs/doc/bug-tags.txt	2011-06-07 03:30:25 +0000
+++ lib/lp/bugs/doc/bug-tags.txt	2011-11-25 04:57:29 +0000
@@ -363,6 +363,13 @@
     [(u'dataloss', 1L), (u'layout-test', 1L), (u'pebcak', 1L)]
 
 Only bugs that the supplied user has access to will be counted:
+We need a feature flag so that multi-pillar bugs can be made private.
+
+    >>> from lp.services.features.testing import FeatureFixture
+    >>> feature_flag = {
+    ...     'disclosure.allow_multipillar_private_bugs.enabled': 'on'}
+    >>> flags = FeatureFixture(feature_flag)
+    >>> flags.setUp()
 
     >>> bug_nine = getUtility(IBugSet).get(9)
     >>> bug_nine.setPrivate(True, getUtility(ILaunchBag).user)
@@ -378,6 +385,10 @@
     >>> ubuntu_thunderbird.getUsedBugTagsWithOpenCounts(sample_person)
     {u'crash': 1L}
 
+Clean up the feature flag.
+    >>> flags.cleanUp()
+
+
 When context doesn't have any tags getUsedBugTags() returns a empty list.
 
     >>> gimp = getUtility(IProjectGroupSet).getByName('gimp')

=== modified file 'lib/lp/bugs/doc/bug.txt'
--- lib/lp/bugs/doc/bug.txt	2011-11-14 08:30:52 +0000
+++ lib/lp/bugs/doc/bug.txt	2011-11-25 04:57:29 +0000
@@ -360,15 +360,15 @@
 As one would expect, the permissions are team aware. So, let's retrieve a bug
 and set it private (as Foo Bar again who, of course, is an admin.)
 
-    >>> blackhole_trash_folder = bugset.get(2)
+    >>> reflow_problems_bug = bugset.get(4)
 
 And again, let's fake setting the bug private:
 
-    >>> old_state = Snapshot(blackhole_trash_folder, providing=IBug)
-    >>> blackhole_trash_folder.setPrivate(True, current_user())
+    >>> old_state = Snapshot(reflow_problems_bug, providing=IBug)
+    >>> reflow_problems_bug.setPrivate(True, current_user())
     True
     >>> bug_set_private = ObjectModifiedEvent(
-    ...     blackhole_trash_folder, old_state,
+    ...     reflow_problems_bug, old_state,
     ...     ["id", "title", "private"])
 
     >>> notify(bug_set_private)
@@ -381,22 +381,22 @@
     >>> personset = getUtility(IPersonSet)
 
     >>> ubuntu_team = personset.get(17)
-    >>> subscription = blackhole_trash_folder.subscribe(
+    >>> subscription = reflow_problems_bug.subscribe(
     ...     ubuntu_team, ubuntu_team)
 
 Jeff Waugh, a member of the Ubuntu Team, is able to access this bug:
 
     >>> login("jeff.waugh@xxxxxxxxxxxxxxx")
 
-    >>> old_title = blackhole_trash_folder.title
-    >>> blackhole_trash_folder.title = "new title"
-    >>> blackhole_trash_folder.title
+    >>> old_title = reflow_problems_bug.title
+    >>> reflow_problems_bug.title = "new title"
+    >>> reflow_problems_bug.title
     u'new title'
-    >>> blackhole_trash_folder.title = old_title
-    >>> blackhole_trash_folder.title
-    u'Blackhole Trash folder'
+    >>> reflow_problems_bug.title = old_title
+    >>> reflow_problems_bug.title
+    u'Reflow problems with complex page layouts'
 
-Bug #2 is visible to him in searches. Note that bugs #6 and #14 are
+Bug #4 is visible to him in searches. Note that bugs #6 and #14 are
 hidden from him.
 
     >>> hidden_bugs()
@@ -411,16 +411,17 @@
 Trying to access a property of this bug will again raise an
 Unauthorized:
 
-    >>> blackhole_trash_folder.title
+    >>> reflow_problems_bug.title
     Traceback (most recent call last):
       ...
     Unauthorized: (..., 'title', 'launchpad.View')
 
-And, as you might have guessed, bug #2 is invisible in searches, in
+And, as you might have guessed, bug #4 is invisible in searches, in
 addition to bugs #6 and #14:
 
     >>> hidden_bugs()
-    [2, 6, 14]
+    [4, 6, 14]
+
 
 
 Filing Public vs. Private Bugs
@@ -821,6 +822,11 @@
 
     >>> bug_before_modification = Snapshot(firefox_bug, providing=IBug)
 
+We need a feature flag for this test since multi-pillar bugs shouldn't be
+private by default.
+    >>> flags = FeatureFixture(feature_flag)
+    >>> flags.setUp()
+
     >>> firefox_bug.private
     False
     >>> firefox_bug.setPrivate(True, current_user())
@@ -836,6 +842,9 @@
     >>> firefox_bug.date_last_updated > current_date_last_updated
     True
 
+Clean up the feature flag.
+    >>> flags.cleanUp()
+
 Changing bug security.
 
     >>> bug_before_modification = Snapshot(firefox_bug, providing=IBug)

=== modified file 'lib/lp/bugs/doc/bugattachments.txt'
--- lib/lp/bugs/doc/bugattachments.txt	2011-05-13 16:50:50 +0000
+++ lib/lp/bugs/doc/bugattachments.txt	2011-11-25 04:57:29 +0000
@@ -15,8 +15,8 @@
     >>> from lp.bugs.interfaces.bugattachment import IBugAttachment
     >>> from lp.registry.interfaces.person import IPersonSet
     >>> bugset = getUtility(IBugSet)
-    >>> bug_one = bugset.get(1)
-    >>> bug_one.attachments.count()
+    >>> bug_four = bugset.get(4)
+    >>> bug_four.attachments.count()
     0
 
 
@@ -45,7 +45,7 @@
     ...    content="a comment for the attachment",
     ...    owner=foobar)
 
-    >>> bug_one.addAttachment(
+    >>> bug_four.addAttachment(
     ...     owner=foobar,
     ...     data=data,
     ...     filename="foo.bar",
@@ -58,9 +58,9 @@
     >>> import transaction
     >>> transaction.commit()
 
-    >>> bug_one.attachments.count()
+    >>> bug_four.attachments.count()
     1
-    >>> attachment = bug_one.attachments[0]
+    >>> attachment = bug_four.attachments[0]
     >>> attachment.type.title
     'Unspecified'
 
@@ -68,7 +68,7 @@
 passed in is often a file-like object, but can be a string too.
 
     >>> data = filecontent
-    >>> attachment_from_strings = bug_one.addAttachment(
+    >>> attachment_from_strings = bug_four.addAttachment(
     ...     owner=foobar,
     ...     data=data,
     ...     filename="foo.baz",
@@ -83,7 +83,7 @@
 If no description is given, the title is set to the filename.
 
     >>> data = StringIO(filecontent)
-    >>> screenshot = bug_one.addAttachment(
+    >>> screenshot = bug_four.addAttachment(
     ...     owner=foobar,
     ...     data=data,
     ...     filename="screenshot.jpg",
@@ -99,7 +99,7 @@
     u'image/jpeg'
 
     >>> data = StringIO('</something-htmlish>')
-    >>> debdiff = bug_one.addAttachment(
+    >>> debdiff = bug_four.addAttachment(
     ...     owner=foobar,
     ...     data=data,
     ...     filename="something.debdiff",
@@ -131,10 +131,10 @@
     ...           'field.actions.save': 'Save Changes'})
 
 Note that the +addcomment-form view is actually registered on a "bug in
-context", i.e. an IBugTask, so let's grab the first bugtask on bug_one
+context", i.e. an IBugTask, so let's grab the first bugtask on bug_four
 and work with that:
 
-    >>> bugtask = bug_one.bugtasks[0]
+    >>> bugtask = bug_four.bugtasks[0]
 
     >>> add_comment_view = getMultiAdapter(
     ...     (bugtask, add_request), name='+addcomment-form')
@@ -229,7 +229,7 @@
     Attachment added: u'RA.txt'
     >>> len(add_comment_view.errors)
     0
-    >>> bug_one.attachments[bug_one.attachments.count()-1].title
+    >>> bug_four.attachments[bug_four.attachments.count()-1].title
     u'RA.txt'
 
 Since the ObjectCreatedEvent was generated, a notification about the
@@ -261,10 +261,10 @@
     Attachment added: u'fo\xf6 bar'
     >>> len(add_comment_view.errors)
     0
-    >>> attachments = bug_one.attachments
-    >>> attachments[bug_one.attachments.count()-1].libraryfile.filename
+    >>> attachments = bug_four.attachments
+    >>> attachments[bug_four.attachments.count()-1].libraryfile.filename
     u'fo\xf6 bar'
-    >>> attachments[bug_one.attachments.count()-1].libraryfile.http_url
+    >>> attachments[bug_four.attachments.count()-1].libraryfile.http_url
     'http://.../fo%C3%B6%20bar'
 
 If a filename contains a slash, it will be converted to a dash instead.
@@ -287,9 +287,9 @@
     Attachment added: u'foo-bar-baz'
     >>> len(add_comment_view.errors)
     0
-    >>> attachments[bug_one.attachments.count()-1].libraryfile.filename
+    >>> attachments[bug_four.attachments.count()-1].libraryfile.filename
     u'foo-bar-baz'
-    >>> attachments[bug_one.attachments.count()-1].libraryfile.http_url
+    >>> attachments[bug_four.attachments.count()-1].libraryfile.http_url
     'http://.../foo-bar-baz'
 
     >>> config_data = config.pop('max_attachment_size')
@@ -300,7 +300,7 @@
 --------
 
 If a user can view/edit the bug the attachment is attached to, he can
-also view/edit the attachment. At the moment the bug_one is public, so
+also view/edit the attachment. At the moment the bug_four is public, so
 anonymous can read the attachment's attributes, but he can't set them:
 
     >>> login(ANONYMOUS)
@@ -328,7 +328,7 @@
 
 Now let's make the bug private instead:
 
-    >>> bug_one.setPrivate(True, getUtility(ILaunchBag).user)
+    >>> bug_four.setPrivate(True, getUtility(ILaunchBag).user)
     True
 
 Foo Bar isn't explicitly subscribed to the bug, BUT he is an admin, so he can
@@ -338,7 +338,7 @@
     u'Even Better Title'
     >>> attachment.title = 'Even Better Title'
 
-Mr. No Privs, who is not subscribed to bug_one, cannot access or set the
+Mr. No Privs, who is not subscribed to bug_four, cannot access or set the
 attachments attributes:
 
     >>> login("no-priv@xxxxxxxxxxxxx")
@@ -375,7 +375,7 @@
 
 Let's make the bug public again:
 
-    >>> bug_one.setPrivate(False, getUtility(ILaunchBag).user)
+    >>> bug_four.setPrivate(False, getUtility(ILaunchBag).user)
     True
 
 
@@ -398,7 +398,7 @@
     >>> len(bugs)
     1
     >>> bugs[0].id
-    1
+    4
 
     >>> from canonical.launchpad.searchbuilder import any
     >>> attachmenttype = any(*BugAttachmentType.items)
@@ -409,7 +409,7 @@
     >>> len(bugs)
     1
     >>> bugs[0].id
-    1
+    4
 
 There are no patches attached to any bugs:
 
@@ -435,7 +435,7 @@
     >>> len(bugs)
     1
     >>> bugs[0].id
-    1
+    4
 
 An easy way to determine whether an attachment is a patch is to read its
 `is_patch` attribute.

=== modified file 'lib/lp/bugs/doc/bugnotification-email.txt'
--- lib/lp/bugs/doc/bugnotification-email.txt	2011-09-26 06:30:07 +0000
+++ lib/lp/bugs/doc/bugnotification-email.txt	2011-11-25 04:57:29 +0000
@@ -218,18 +218,19 @@
 (Note that there's a blank line in the email that contains whitespace.  You
 may see a lint warning for that.)
 
-Let's make the bug security-related, and private (we need to switch
+Let's make a bug security-related, and private (we need to switch
 logins to a user that is explicitly subscribed to this bug):
 
     >>> login("steve.alexander@xxxxxxxxxxxxxxx")
 
+    >>> edited_bug = getUtility(IBugSet).get(6)
     >>> edited_bug.setPrivate(True, getUtility(ILaunchBag).user)
     True
     >>> changed = edited_bug.setSecurityRelated(
     ...     True, getUtility(ILaunchBag).user)
     >>> bug_delta = BugDelta(
     ...     bug=edited_bug,
-    ...     bugurl="http://www.example.com/bugs/2";,
+    ...     bugurl="http://www.example.com/bugs/6";,
     ...     user=sample_person,
     ...     private={'old': False, 'new': edited_bug.private},
     ...     security_related={
@@ -256,7 +257,7 @@
     ...     False, getUtility(ILaunchBag).user)
     >>> bug_delta = BugDelta(
     ...     bug=edited_bug,
-    ...     bugurl="http://www.example.com/bugs/2";,
+    ...     bugurl="http://www.example.com/bugs/6";,
     ...     user=sample_person,
     ...     private={'old': True, 'new': edited_bug.private},
     ...     security_related={
@@ -278,7 +279,7 @@
     >>> edited_bug.tags = [u'foo', u'bar']
     >>> bug_delta = BugDelta(
     ...        bug=edited_bug,
-    ...        bugurl="http://www.example.com/bugs/2";,
+    ...        bugurl="http://www.example.com/bugs/6";,
     ...        user=sample_person,
     ...        tags={'old': old_tags, 'new': edited_bug.tags})
     >>> for change in get_bug_changes(bug_delta):
@@ -326,7 +327,7 @@
     >>> verifyObject(IBugTaskDelta, example_delta)
     True
 
-    >>> edited_bugtask = getUtility(IBugTaskSet).get(3)
+    >>> edited_bugtask = getUtility(IBugTaskSet).get(15)
     >>> edited_bugtask.transitionToStatus(
     ...        BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
     >>> edited_bugtask.transitionToAssignee(sample_person)
@@ -336,17 +337,17 @@
     ...     assignee={'old' : None, 'new' : edited_bugtask.assignee})
     >>> bug_delta = BugDelta(
     ...     bug=edited_bug,
-    ...     bugurl="http://www.example.com/bugs/2";,
+    ...     bugurl="http://www.example.com/bugs/6";,
     ...     user=sample_person,
     ...     bugtask_deltas=bugtask_delta)
     >>> for change in get_bug_changes(bug_delta):
     ...     notification = change.getBugNotification()
     ...     print notification['text'] #doctest: -NORMALIZE_WHITESPACE
     ...     print "-----------------------------"
-    ** Changed in: tomcat
+    ** Changed in: firefox
            Status: New => Confirmed
     -----------------------------
-    ** Changed in: tomcat
+    ** Changed in: firefox
          Assignee: (unassigned) => Sample Person (name12)
     -----------------------------
 
@@ -363,7 +364,7 @@
     ...     assignee={'old' : sample_person, 'new' : None})
     >>> bug_delta = BugDelta(
     ...     bug=edited_bug,
-    ...     bugurl="http://www.example.com/bugs/2";,
+    ...     bugurl="http://www.example.com/bugs/6";,
     ...     user=sample_person,
     ...     bugtask_deltas=bugtask_delta)
     >>> for change in get_bug_changes(bug_delta):
@@ -385,7 +386,7 @@
     ...     filename='screenshot.png')
     >>> bug_delta = BugDelta(
     ...     bug=edited_bug,
-    ...     bugurl="http://www.example.com/bugs/2";,
+    ...     bugurl="http://www.example.com/bugs/6";,
     ...     user=sample_person,
     ...     attachment={'new' : attachment, 'old': None})
     >>> for change in get_bug_changes(bug_delta):
@@ -400,7 +401,7 @@
 
     >>> bug_delta = BugDelta(
     ...     bug=edited_bug,
-    ...     bugurl="http://www.example.com/bugs/2";,
+    ...     bugurl="http://www.example.com/bugs/6";,
     ...     user=sample_person,
     ...     attachment={'old' : attachment, 'new': None})
     >>> for change in get_bug_changes(bug_delta):
@@ -419,7 +420,7 @@
     ...     filename='new-icon.png', is_patch=True)
     >>> bug_delta = BugDelta(
     ...     bug=edited_bug,
-    ...     bugurl="http://www.example.com/bugs/2";,
+    ...     bugurl="http://www.example.com/bugs/6";,
     ...     user=sample_person,
     ...     attachment={'new' : attachment, 'old': None})
     >>> for change in get_bug_changes(bug_delta):
@@ -434,7 +435,7 @@
 
     >>> bug_delta = BugDelta(
     ...     bug=edited_bug,
-    ...     bugurl="http://www.example.com/bugs/2";,
+    ...     bugurl="http://www.example.com/bugs/6";,
     ...     user=sample_person,
     ...     attachment={'old' : attachment, 'new': None})
     >>> for change in get_bug_changes(bug_delta):

=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
--- lib/lp/bugs/doc/bugsubscription.txt	2011-09-30 16:05:20 +0000
+++ lib/lp/bugs/doc/bugsubscription.txt	2011-11-25 04:57:29 +0000
@@ -384,10 +384,11 @@
 direct subscribers (eg bugtask pillar maintainers) will remain subscribed.
 
 We currently use a feature flag to control who is subscribed when a bug is
-made private.
+made private and to allow multi-pillar bugs to be private.
 
     >>> from lp.services.features.testing import FeatureFixture
     >>> feature_flag = {
+    ...     'disclosure.allow_multipillar_private_bugs.enabled': 'on',
     ...     'disclosure.enhanced_private_bug_subscriptions.enabled': 'on'}
     >>> flags = FeatureFixture(feature_flag)
     >>> flags.setUp()

=== modified file 'lib/lp/bugs/doc/bugtask-expiration.txt'
--- lib/lp/bugs/doc/bugtask-expiration.txt	2011-09-26 06:30:07 +0000
+++ lib/lp/bugs/doc/bugtask-expiration.txt	2011-11-25 04:57:29 +0000
@@ -396,6 +396,13 @@
 
 If one of the bugs is set to private, anonymous users can no longer see
 it as being marked for expiration.
+We need a feature flag so that multi-pillar bugs can be made private.
+
+    >>> from lp.services.features.testing import FeatureFixture
+    >>> feature_flag = {
+    ...     'disclosure.allow_multipillar_private_bugs.enabled': 'on'}
+    >>> flags = FeatureFixture(feature_flag)
+    >>> flags.setUp()
 
     >>> private_bug = ubuntu_bugtask.bug
     >>> private_bug.title
@@ -450,6 +457,10 @@
     True
     >>> reset_bug_modified_date(private_bug, 351)
 
+Clean up the feature flag.
+    >>> flags.cleanUp()
+
+
 The default expiration age
 --------------------------
 

=== modified file 'lib/lp/bugs/doc/bugtask-find-similar.txt'
--- lib/lp/bugs/doc/bugtask-find-similar.txt	2011-06-28 15:04:29 +0000
+++ lib/lp/bugs/doc/bugtask-find-similar.txt	2011-11-25 04:57:29 +0000
@@ -82,6 +82,14 @@
 the Firefox bug to private, and repeat the search as a user who isn't
 allowed to view it, only the Thunderbird bug will be returned this time.
 
+We need a feature flag so that multi-pillar bugs can be made private.
+
+    >>> from lp.services.features.testing import FeatureFixture
+    >>> feature_flag = {
+    ...     'disclosure.allow_multipillar_private_bugs.enabled': 'on'}
+    >>> flags = FeatureFixture(feature_flag)
+    >>> flags.setUp()
+
     >>> from lp.bugs.interfaces.bug import IBugSet
     >>> login('test@xxxxxxxxxxxxx')
     >>> firefox_svg_bug = getUtility(IBugSet).get(1)
@@ -104,6 +112,9 @@
     >>> firefox_svg_bug.setPrivate(False, getUtility(ILaunchBag).user)
     True
 
+Clean up the feature flag.
+    >>> flags.cleanUp()
+
 
 == Ordering of search results ==
 

=== modified file 'lib/lp/bugs/doc/bugtask.txt'
--- lib/lp/bugs/doc/bugtask.txt	2011-10-27 13:03:04 +0000
+++ lib/lp/bugs/doc/bugtask.txt	2011-11-25 04:57:29 +0000
@@ -721,19 +721,19 @@
     >>> from lazr.lifecycle.snapshot import Snapshot
     >>> from lp.bugs.interfaces.bug import IBug
 
-    >>> bug_upstream_firefox_no_svg_support = bugtaskset.get(2)
+    >>> bug_upstream_firefox_crashes = bugtaskset.get(15)
 
     >>> ubuntu_team = personset.getByEmail('support@xxxxxxxxxx')
-    >>> subscription = bug_upstream_firefox_no_svg_support.bug.subscribe(
+    >>> subscription = bug_upstream_firefox_crashes.bug.subscribe(
     ...     ubuntu_team, ubuntu_team)
 
     >>> old_state = Snapshot(
-    ...     bug_upstream_firefox_no_svg_support.bug, providing=IBug)
-    >>> bug_upstream_firefox_no_svg_support.bug.setPrivate(
+    ...     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_no_svg_support.bug, old_state,
+    ...     bug_upstream_firefox_crashes.bug, old_state,
     ...     ["id", "title", "private"])
     >>> notify(bug_set_private)
 
@@ -747,12 +747,12 @@
     >>> login("no-priv@xxxxxxxxxxxxx")
     >>> mr_no_privs = launchbag.user
 
-    >>> bug_upstream_firefox_no_svg_support.status
+    >>> bug_upstream_firefox_crashes.status
     Traceback (most recent call last):
       ...
     Unauthorized: (..., 'status', 'launchpad.View')
 
-    >>> bug_upstream_firefox_no_svg_support.transitionToStatus(
+    >>> bug_upstream_firefox_crashes.transitionToStatus(
     ...     BugTaskStatus.FIXCOMMITTED, getUtility(ILaunchBag).user)
     Traceback (most recent call last):
       ...
@@ -772,7 +772,7 @@
     3
     >>> bug_ids = [bt.bug.id for bt in bugtasks]
     >>> print sorted(bug_ids)
-    [4, 5, 6]
+    [1, 4, 5]
 
 Likewise when the No Privileges Person tries to do a search on tasks
 maintained by Foo Bar.
@@ -832,10 +832,10 @@
 
 And note that he can access and set the bugtask attributes:
 
-    >>> bug_upstream_firefox_no_svg_support.status.title
+    >>> bug_upstream_firefox_crashes.status.title
     'New'
 
-    >>> bug_upstream_firefox_no_svg_support.transitionToStatus(
+    >>> bug_upstream_firefox_crashes.transitionToStatus(
     ...     BugTaskStatus.NEW, getUtility(ILaunchBag).user)
 
 
@@ -849,7 +849,7 @@
     ...     status=any(STATUS_NEW, STATUS_CONFIRMED), user=no_priv)
     >>> firefox_bugtasks = firefox.searchTasks(params)
     >>> [bugtask.bug.id for bugtask in firefox_bugtasks]
-    [4, 5, 6]
+    [1, 4, 5]
 
 
 But if we add No Privileges Person to the Ubuntu Team, and because the
@@ -908,18 +908,18 @@
 
 Trying to retrieve the bug directly will work fine:
 
-    >>> bug_upstream_firefox_no_svg_support = bugtaskset.get(2)
+    >>> bug_upstream_firefox_crashes = bugtaskset.get(15)
 
 As will attribute access:
 
-    >>> bug_upstream_firefox_no_svg_support.status.title
+    >>> bug_upstream_firefox_crashes.status.title
     'New'
 
 And attribute setting:
 
-    >>> bug_upstream_firefox_no_svg_support.transitionToStatus(
+    >>> bug_upstream_firefox_crashes.transitionToStatus(
     ...     BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
-    >>> bug_upstream_firefox_no_svg_support.transitionToStatus(
+    >>> bug_upstream_firefox_crashes.transitionToStatus(
     ...     BugTaskStatus.NEW, getUtility(ILaunchBag).user)
 
 
@@ -976,11 +976,6 @@
 the latter is not exposed in `IBugTask`, so the `bugtargetdisplayname`
 is used here.
 
-    # We do not allow private bugs to affect multiple projects so we need to
-    # first make the bug public.
-    >>> bug_one.setPrivate(False, mark)
-    True
-
     >>> netapplet = productset.get(11)
     >>> upstream_task = bugtaskset.createTask(
     ...     bug_one, mark, netapplet,

=== modified file 'lib/lp/bugs/doc/initial-bug-contacts.txt'
--- lib/lp/bugs/doc/initial-bug-contacts.txt	2011-09-30 16:05:20 +0000
+++ lib/lp/bugs/doc/initial-bug-contacts.txt	2011-11-25 04:57:29 +0000
@@ -290,10 +290,11 @@
 
 
 We currently use a feature flag to control who is subscribed when a bug is
-made private.
+made private and to allow multi-pillar bugs to be private.
 
     >>> from lp.services.features.testing import FeatureFixture
     >>> feature_flag = {
+    ...     'disclosure.allow_multipillar_private_bugs.enabled': 'on',
     ...     'disclosure.enhanced_private_bug_subscriptions.enabled': 'on'}
     >>> flags = FeatureFixture(feature_flag)
     >>> flags.setUp()

=== modified file 'lib/lp/bugs/errors.py'
--- lib/lp/bugs/errors.py	2011-11-14 08:30:52 +0000
+++ lib/lp/bugs/errors.py	2011-11-25 04:57:29 +0000
@@ -5,6 +5,7 @@
 
 __metaclass__ = type
 __all__ = [
+    'BugCannotBePrivate',
     'InvalidBugTargetType',
     'InvalidDuplicateValue',
     'SubscriptionPrivacyViolation',
@@ -30,3 +31,8 @@
 @error_status(httplib.BAD_REQUEST)
 class SubscriptionPrivacyViolation(Exception):
     """The subscription would violate privacy policies."""
+
+
+@error_status(httplib.BAD_REQUEST)
+class BugCannotBePrivate(Exception):
+    """The bug is not allowed to be private."""

=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py	2011-11-17 22:54:10 +0000
+++ lib/lp/bugs/model/bug.py	2011-11-25 04:57:29 +0000
@@ -132,6 +132,7 @@
     )
 from lp.bugs.enum import BugNotificationLevel
 from lp.bugs.errors import (
+    BugCannotBePrivate,
     InvalidDuplicateValue,
     SubscriptionPrivacyViolation,
     )
@@ -1701,6 +1702,14 @@
                 self.reconcileSubscribers(private, security_related, who)
 
         if self.private != private:
+            # We do not allow multi-pillar private bugs except for those teams
+            # who want to shoot themselves in the foot.
+            if (not bool(getFeatureFlag(
+                    'disclosure.allow_multipillar_private_bugs.enabled'))
+                    and len(self.affected_pillars) > 1
+                    and private):
+                raise BugCannotBePrivate(
+                    "Multi-pillar bugs cannot be private.")
             private_changed = True
             self.private = private
 

=== modified file 'lib/lp/bugs/model/tests/test_bug.py'
--- lib/lp/bugs/model/tests/test_bug.py	2011-10-28 17:09:49 +0000
+++ lib/lp/bugs/model/tests/test_bug.py	2011-11-25 04:57:29 +0000
@@ -21,6 +21,7 @@
     BugNotificationLevel,
     BugNotificationStatus,
     )
+from lp.bugs.errors import BugCannotBePrivate
 from lp.bugs.interfaces.bugnotification import IBugNotificationSet
 from lp.bugs.interfaces.bugtask import BugTaskStatus
 from lp.bugs.model.bug import (
@@ -817,6 +818,31 @@
                 expected_reason_body, True, False, 'Security Contact')
 
 
+class TestBugPrivacy(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_multipillar_private_bugs_disallowed(self):
+        # A multi-pillar bug cannot be made private.
+        bug = self.factory.makeBug()
+        product = self.factory.makeProduct()
+        self.factory.makeBugTask(bug=bug, target=product)
+        login_person(bug.owner)
+        self.assertRaises(
+            BugCannotBePrivate, bug.setPrivacyAndSecurityRelated, True, False,
+            bug.owner)
+        self.assertRaises(
+            BugCannotBePrivate, bug.setPrivate, True, bug.owner)
+
+        # Some teams though need to use a foot gun.
+        feature_flag = {
+            'disclosure.allow_multipillar_private_bugs.enabled': 'on'
+            }
+        with FeatureFixture(feature_flag):
+            bug.setPrivacyAndSecurityRelated(True, False, bug.owner)
+            self.assertTrue(bug.private)
+
+
 class TestBugPrivateAndSecurityRelatedUpdatesPrivateProject(
         TestBugPrivateAndSecurityRelatedUpdatesMixin, TestCaseWithFactory):
 

=== modified file 'lib/lp/bugs/stories/bug-privacy/xx-bug-privacy.txt'
--- lib/lp/bugs/stories/bug-privacy/xx-bug-privacy.txt	2011-09-27 14:59:19 +0000
+++ lib/lp/bugs/stories/bug-privacy/xx-bug-privacy.txt	2011-11-25 04:57:29 +0000
@@ -22,11 +22,12 @@
     Ubuntu Team
 
 We current use a feature flag to control who is subscribed when a bug is made
-private.
+private and also to allow multi-pillar private bugs.
 
     >>> from lp.services.features.testing import FeatureFixture
     >>> feature_flag = {
-    ...     'disclosure.enhanced_private_bug_subscriptions.enabled': 'on'}
+    ...     'disclosure.enhanced_private_bug_subscriptions.enabled': 'on',
+    ...     'disclosure.allow_multipillar_private_bugs.enabled': 'on'}
     >>> flags = FeatureFixture(feature_flag)
     >>> flags.setUp()
 
@@ -56,10 +57,6 @@
     >>> print_also_notified(browser.contents)
     Also notified:
 
-Clean up the feature flag.
-
-    >>> flags.cleanUp()
-
 When we go back to the secrecy form, the previously set value is pre-selected.
 
     >>> browser.open(
@@ -68,6 +65,11 @@
     >>> browser.getControl("This bug report should be private").selected
     True
 
+Clean up the feature flags.
+
+    >>> flags.cleanUp()
+
+
 Foo Bar files a security (private) bug on Ubuntu Linux. He gets
 redirected to the bug page.
 

=== added file 'lib/lp/bugs/tests/test_errors.py'
--- lib/lp/bugs/tests/test_errors.py	1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/tests/test_errors.py	2011-11-25 04:57:29 +0000
@@ -0,0 +1,42 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for bugs errors."""
+
+
+__metaclass__ = type
+
+
+from httplib import (
+    BAD_REQUEST,
+    EXPECTATION_FAILED,
+    )
+
+from canonical.testing.layers import FunctionalLayer
+from lp.bugs.errors import (
+    InvalidBugTargetType,
+    InvalidDuplicateValue,
+    SubscriptionPrivacyViolation,
+    )
+from lp.testing import TestCase
+from lp.testing.views import create_webservice_error_view
+
+
+class TestWebServiceErrors(TestCase):
+    """ Test that errors are correctly mapped to HTTP status codes."""
+
+    layer = FunctionalLayer
+
+    def test_InvalidBugTargetType_bad_rquest(self):
+        error_view = create_webservice_error_view(InvalidBugTargetType())
+        self.assertEqual(BAD_REQUEST, error_view.status)
+
+    def test_InvalidDuplicateValue_expectation_failed(self):
+        error_view = create_webservice_error_view(
+            InvalidDuplicateValue("Dup"))
+        self.assertEqual(EXPECTATION_FAILED, error_view.status)
+
+    def test_SubscriptionPrivacyViolation_bad_request(self):
+        error_view = create_webservice_error_view(
+            SubscriptionPrivacyViolation())
+        self.assertEqual(BAD_REQUEST, error_view.status)