← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wallyworld/launchpad/private-by-default-ui-885503 into lp:launchpad

 

Ian Booth has proposed merging lp:~wallyworld/launchpad/private-by-default-ui-885503 into lp:launchpad with lp:~wallyworld/launchpad/drop-private-bugs-need-contact-constraint as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #885503 in Launchpad itself: "Its not obvious on the UI how to choose to have all bugs reported against your project marked private by default."
  https://bugs.launchpad.net/launchpad/+bug/885503

For more details, see:
https://code.launchpad.net/~wallyworld/launchpad/private-by-default-ui-885503/+merge/92273

== Implementation ==

The bug report mentions that the private_bugs field is on the Configure Bug Tracker page but it is on the Product +admin page. All the fields on the +admin page are protected by the launchpad.Moderate permission. But we want to allow project maintainers with commercial subscriptions to edit the field. So...

I copied the field to the Configure Bug Tracker page (the same page where Bug Supervisor etc is set). The field needs to be set by both ~registry and ~admin (launchpad.Moderate) and commercial subscribers (these have launchpad.Edit). There are 2 permissions involved so the best approach is to use a mutator and put the permission checks in a new method on Product privateBugsAllowed(). NB I had to update the recently added hasCurrentCommercialSubscription() method to take an optional product parameter.

A new exception CommercialSubscribersOnly is raised in the mutator so that unauthorised people cannot turn on private bugs using the API.

Summary:
Product+admin and Product+review-license pages can still be used as before by ~registry and ~admin to set private_bugs = on
Product+configure-bugtracker can be used by commercial subscribers to set private_bugs = on
Any user who can edit fields on the +configure-bugtracker page can turn private_bugs = off

I removed the unnecessary validation check which complained if the bug supervisor is not set when turning on the private_bugs field. I discovered there is also a db constraint on the product table so a pre-requisite branch needs to remove this constraint during FDT.

== Tests ==

Add new test to TestPerson for the new hasCurrentCommercialSubscription behaviour:
- test_has_current_commercial_subscription_for_product

Add new tests to TestProduct for the privateBugsAllowed() method. Also add tests for setPrivateBug()

Add new tests to TestProductBugConfigurationView for setting private_bugs
- test_commercial_subscriber_can_turn_on_private_bugs
- test_unauthorised_cannot_turn_on_private_bugs
- test_anyone_can_turn_off_private_bugs

== Lint ==
Linting changed files:
  lib/lp/bugs/browser/bugtarget.py
  lib/lp/bugs/browser/tests/test_bugtarget_configure.py
  lib/lp/registry/configure.zcml
  lib/lp/registry/errors.py
  lib/lp/registry/browser/product.py
  lib/lp/registry/browser/tests/product-views.txt
  lib/lp/registry/interfaces/person.py
  lib/lp/registry/interfaces/product.py
  lib/lp/registry/model/person.py
  lib/lp/registry/model/product.py
  lib/lp/registry/stories/product/xx-product-with-private-defaults.txt
  lib/lp/registry/tests/test_errors.py
  lib/lp/registry/tests/test_person.py
  lib/lp/registry/tests/test_product.py

-- 
https://code.launchpad.net/~wallyworld/launchpad/private-by-default-ui-885503/+merge/92273
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wallyworld/launchpad/private-by-default-ui-885503 into lp:launchpad.
=== modified file 'lib/lp/bugs/browser/bugtarget.py'
--- lib/lp/bugs/browser/bugtarget.py	2012-01-09 11:36:12 +0000
+++ lib/lp/bugs/browser/bugtarget.py	2012-02-09 13:45:41 +0000
@@ -113,7 +113,10 @@
 from lp.bugs.publisher import BugsLayer
 from lp.bugs.utilities.filebugdataparser import FileBugData
 from lp.hardwaredb.interfaces.hwdb import IHWSubmissionSet
-from lp.registry.browser.product import ProductConfigureBase
+from lp.registry.browser.product import (
+    ProductConfigureBase,
+    ProductPrivateBugsMixin,
+    )
 from lp.registry.interfaces.distribution import IDistribution
 from lp.registry.interfaces.distributionsourcepackage import (
     IDistributionSourcePackage,
@@ -151,6 +154,8 @@
     bug_supervisor = copy_field(
         IHasBugSupervisor['bug_supervisor'], readonly=False)
     security_contact = copy_field(IHasSecurityContact['security_contact'])
+    private_bugs = copy_field(
+        IProduct['private_bugs'], readonly=False)
     official_malone = copy_field(ILaunchpadUsage['official_malone'])
     enable_bug_expiration = copy_field(
         ILaunchpadUsage['enable_bug_expiration'])
@@ -171,7 +176,9 @@
     return product
 
 
-class ProductConfigureBugTrackerView(BugRoleMixin, ProductConfigureBase):
+class ProductConfigureBugTrackerView(BugRoleMixin,
+                                     ProductPrivateBugsMixin,
+                                     ProductConfigureBase):
     """View class to configure the bug tracker for a project."""
 
     label = "Configure bug tracker"
@@ -192,6 +199,7 @@
             "bug_reporting_guidelines",
             "bug_reported_acknowledgement",
             "enable_bugfiling_duplicate_search",
+            "private_bugs"
             ]
         if check_permission("launchpad.Edit", self.context):
             field_names.extend(["bug_supervisor", "security_contact"])
@@ -200,6 +208,7 @@
 
     def validate(self, data):
         """Constrain bug expiration to Launchpad Bugs tracker."""
+        super(ProductConfigureBugTrackerView, self).validate(data)
         if check_permission("launchpad.Edit", self.context):
             self.validateBugSupervisor(data)
             self.validateSecurityContact(data)

=== modified file 'lib/lp/bugs/browser/tests/test_bugtarget_configure.py'
--- lib/lp/bugs/browser/tests/test_bugtarget_configure.py	2012-01-01 02:58:52 +0000
+++ lib/lp/bugs/browser/tests/test_bugtarget_configure.py	2012-02-09 13:45:41 +0000
@@ -39,6 +39,7 @@
             'field.bug_reporting_guidelines': 'guidelines',
             'field.bug_reported_acknowledgement': 'acknowledgement message',
             'field.enable_bugfiling_duplicate_search': False,
+            'field.private_bugs': 'off',
             'field.actions.change': 'Change',
             }
 
@@ -50,8 +51,8 @@
         fields = [
             'bugtracker', 'enable_bug_expiration', 'remote_product',
             'bug_reporting_guidelines', 'bug_reported_acknowledgement',
-            'enable_bugfiling_duplicate_search', 'bug_supervisor',
-            'security_contact']
+            'enable_bugfiling_duplicate_search', 'private_bugs',
+            'bug_supervisor', 'security_contact']
         self.assertEqual(fields, view.field_names)
         self.assertEqual('http://launchpad.dev/boing', view.next_url)
         self.assertEqual('http://launchpad.dev/boing', view.cancel_url)
@@ -65,7 +66,7 @@
         fields = [
             'bugtracker', 'enable_bug_expiration', 'remote_product',
             'bug_reporting_guidelines', 'bug_reported_acknowledgement',
-            'enable_bugfiling_duplicate_search']
+            'enable_bugfiling_duplicate_search', 'private_bugs']
         self.assertEqual(fields, view.field_names)
         self.assertEqual('http://launchpad.dev/boing', view.next_url)
         self.assertEqual('http://launchpad.dev/boing', view.cancel_url)
@@ -186,3 +187,35 @@
         self.assertEqual([], view.errors)
         self.assertEqual(
             'new guidelines', self.product.bug_reporting_guidelines)
+
+    def test_commercial_subscriber_can_turn_on_private_bugs(self):
+        # Verify commercial subscribers can set private_bugs to on.
+        form = self._makeForm()
+        self.factory.makeCommercialSubscription(self.product)
+        form['field.private_bugs'] = 'on'
+        login_person(self.product.owner)
+        view = create_initialized_view(
+            self.product, name='+configure-bugtracker', form=form)
+        self.assertEqual([], view.errors)
+        self.assertTrue(self.product.private_bugs)
+
+    def test_unauthorised_cannot_turn_on_private_bugs(self):
+        # Verify unauthorised users cannot set private_bugs to on.
+        form = self._makeForm()
+        form['field.private_bugs'] = 'on'
+        view = create_initialized_view(
+            self.product, name='+configure-bugtracker', form=form)
+        self.assertEqual(
+            [u'A valid commercial subscription is required to turn on '
+             u'default private bugs.'],
+            view.errors)
+
+    def test_anyone_can_turn_off_private_bugs(self):
+        # Verify any user who can edit the product can set private_bugs off.
+        registry_expert = self.factory.makeRegistryExpert()
+        self.product.setPrivateBugs(registry_expert, True)
+        form = self._makeForm()
+        view = create_initialized_view(
+            self.product, name='+configure-bugtracker', form=form)
+        self.assertEqual([], view.errors)
+        self.assertFalse(self.product.private_bugs)

=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py	2012-02-01 15:26:32 +0000
+++ lib/lp/registry/browser/product.py	2012-02-09 13:45:41 +0000
@@ -26,6 +26,7 @@
     'ProductOverviewMenu',
     'ProductPackagesView',
     'ProductPackagesPortletView',
+    'ProductPrivateBugsMixin',
     'ProductPurchaseSubscriptionView',
     'ProductRdfView',
     'ProductReviewLicenseView',
@@ -1524,6 +1525,34 @@
     usage_fieldname = 'answers_usage'
 
 
+class ProductPrivateBugsMixin():
+    """A mixin for setting the product private_bugs field."""
+    def setUpFields(self):
+        # private_bugs is readonly since we are using a mutator but we need
+        # to edit it on the form.
+        super(ProductPrivateBugsMixin, self).setUpFields()
+        self.form_fields['private_bugs'].field.readonly = False
+
+    def validate(self, data):
+        super(ProductPrivateBugsMixin, self).validate(data)
+        if (data.get('private_bugs', False) and
+            not self.context.privateBugsAllowed(self.user)):
+            self.setFieldError(
+                'private_bugs',
+                'A valid commercial subscription is required to turn on '
+                'default private bugs.')
+
+    def updateContextFromData(self, data, context=None, notify_modified=True):
+        # private_bugs uses a mutator to check permissions, so it needs to
+        # be handled separately.
+        if (data.get('private_bugs') is not None
+                and data['private_bugs'] != self.context.private_bugs):
+            self.context.setPrivateBugs(self.user, data['private_bugs'])
+            del data['private_bugs']
+        parent = super(ProductPrivateBugsMixin, self)
+        return parent.updateContextFromData(data, context, notify_modified)
+
+
 class ProductEditView(ProductLicenseMixin, LaunchpadEditFormView):
     """View class that lets you edit a Product object."""
 
@@ -1602,15 +1631,6 @@
 
 class ProductValidationMixin:
 
-    def validate_private_bugs(self, data):
-        """Perform validation for the private bugs setting."""
-        if data.get('private_bugs') and self.context.bug_supervisor is None:
-            self.setFieldError('private_bugs',
-                structured(
-                    'Set a <a href="%s/+bugsupervisor">bug supervisor</a> '
-                    'for this project first.',
-                    canonical_url(self.context, rootsite="bugs")))
-
     def validate_deactivation(self, data):
         """Verify whether a product can be safely deactivated."""
         if data['active'] == False and self.context.active == True:
@@ -1623,7 +1643,8 @@
                         canonical_url(self.context, view_name='+packages')))
 
 
-class ProductAdminView(ProductEditView, ProductValidationMixin):
+class ProductAdminView(ProductEditView, ProductValidationMixin,
+                       ProductPrivateBugsMixin):
     """View for $project/+admin"""
     label = "Administer project details"
     default_field_names = [
@@ -1691,7 +1712,7 @@
 
     def validate(self, data):
         """See `LaunchpadFormView`."""
-        self.validate_private_bugs(data)
+        super(ProductAdminView, self).validate(data)
         self.validate_deactivation(data)
 
     @property
@@ -1701,7 +1722,8 @@
 
 
 class ProductReviewLicenseView(ReturnToReferrerMixin,
-                               ProductEditView, ProductValidationMixin):
+                               ProductEditView, ProductValidationMixin,
+                               ProductPrivateBugsMixin):
     """A view to review a project and change project privileges."""
     label = "Review project"
     field_names = [
@@ -1720,6 +1742,7 @@
     def validate(self, data):
         """See `LaunchpadFormView`."""
 
+        super(ProductReviewLicenseView, self).validate(data)
         # A project can only be approved if it has OTHER_OPEN_SOURCE as one of
         # its licenses and not OTHER_PROPRIETARY.
         licenses = self.context.licenses
@@ -1737,9 +1760,6 @@
                 # approved.
                 pass
 
-        # Private bugs can only be enabled if the product has a bug
-        # supervisor.
-        self.validate_private_bugs(data)
         self.validate_deactivation(data)
 
 

=== modified file 'lib/lp/registry/browser/tests/product-views.txt'
--- lib/lp/registry/browser/tests/product-views.txt	2011-12-24 17:49:30 +0000
+++ lib/lp/registry/browser/tests/product-views.txt	2012-02-09 13:45:41 +0000
@@ -208,34 +208,6 @@
     >>> view = create_initialized_view(
     ...     firefox, name='+review-license', form=form)
 
-He encounters an error, though, as the project does not have a bug
-supervisor set, which is required for turning on private bugs.
-
-    >>> from lp.testing.pages import extract_text
-    >>> for error in view.errors:
-    ...     print extract_text(error)
-    Set a bug supervisor for this project first.
-
-    >>> firefox.bug_supervisor is None
-    True
-
-After setting the bug supervisor the project can have private bugs enabled.
-
-    >>> login('mark@xxxxxxxxxxx')
-    >>> firefox.setBugSupervisor(cprov, mark)
-    >>> login('commercial-member@xxxxxxxxxxxxx')
-
-    >>> firefox.private_bugs
-    False
-
-    >>> form = {
-    ...     'field.active': 'on',
-    ...     'field.private_bugs': 'on',
-    ...     'field.reviewer_whiteboard': 'Reinstated old project',
-    ...     'field.actions.change': 'Change',
-    ...     }
-    >>> view = create_initialized_view(
-    ...     firefox, name='+review-license', form=form)
     >>> view.errors
     []
     >>> firefox.active
@@ -250,7 +222,7 @@
 
     >>> from lp.registry.interfaces.product import License
 
-    >>> firefox.private_bugs = False
+    >>> firefox.setPrivateBugs(commercial_member, False)
     >>> login('test@xxxxxxxxxxxxx')
     >>> firefox.licenses = [License.OTHER_PROPRIETARY]
 

=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml	2012-02-08 06:00:46 +0000
+++ lib/lp/registry/configure.zcml	2012-02-09 13:45:41 +0000
@@ -1273,7 +1273,7 @@
                 project_reviewed
                 license_approved"
             set_schema="lp.registry.interfaces.product.IProductModerateRestricted"
-            set_attributes="active private_bugs "/>
+            set_attributes="active"/>
 
         <!-- IHasAliases -->
 

=== modified file 'lib/lp/registry/errors.py'
--- lib/lp/registry/errors.py	2011-11-09 01:02:03 +0000
+++ lib/lp/registry/errors.py	2012-02-09 13:45:41 +0000
@@ -6,6 +6,7 @@
     'DistroSeriesDifferenceError',
     'NotADerivedSeriesError',
     'CannotTransitionToCountryMirror',
+    'CommercialSubscribersOnly',
     'CountryMirrorAlreadySet',
     'DeleteSubscriptionError',
     'InvalidName',
@@ -67,6 +68,15 @@
     """
 
 
+@error_status(httplib.UNAUTHORIZED)
+class CommercialSubscribersOnly(Unauthorized):
+    """Feature is only available to current commercial subscribers.
+
+    Raised when a user tries to invoke an operation that is only available to
+    current commercial subscribers and they don't have an active subscription.
+    """
+
+
 class NoSuchSourcePackageName(NameLookupFailed):
     """Raised when we can't find a particular sourcepackagename."""
     _message_prefix = "No such source package"

=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py	2012-02-03 11:23:30 +0000
+++ lib/lp/registry/interfaces/person.py	2012-02-09 13:45:41 +0000
@@ -1243,7 +1243,7 @@
         :return: list
         """
 
-    def hasCurrentCommercialSubscription():
+    def hasCurrentCommercialSubscription(product=None):
         """Return if the user has a current commercial subscription."""
 
     def assignKarma(action_name, product=None, distribution=None,

=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py	2012-01-23 20:12:30 +0000
+++ lib/lp/registry/interfaces/product.py	2012-02-09 13:45:41 +0000
@@ -39,7 +39,10 @@
     export_factory_operation,
     export_operation_as,
     export_read_operation,
+    export_write_operation,
     exported,
+    mutator_for,
+    operation_for_version,
     operation_parameters,
     operation_returns_collection_of,
     operation_returns_entry,
@@ -51,6 +54,7 @@
     Reference,
     ReferenceChoice,
     )
+from lazr.restful.interface import copy_field
 from zope.interface import (
     Attribute,
     Interface,
@@ -613,7 +617,7 @@
         description=_("Whether or not this project's attributes are "
                       "updated automatically."))
 
-    private_bugs = exported(Bool(title=_('Private bugs'),
+    private_bugs = exported(Bool(title=_('Private bugs'), readonly=True,
                         description=_(
                             "Whether or not bugs reported into this project "
                             "are private by default.")))
@@ -740,6 +744,21 @@
 
     packagings = Attribute(_("All the packagings for the project."))
 
+    def privateBugsAllowed(user):
+        """Is the user allowed to turn on private bugs.
+
+        Generally, this is restricted to ~registry or ~admin or product
+        maintainers with active commercial subscriptions.
+        """
+
+    @mutator_for(private_bugs)
+    @call_with(user=REQUEST_USER)
+    @operation_parameters(private_bugs=copy_field(private_bugs))
+    @export_write_operation()
+    @operation_for_version("devel")
+    def setPrivateBugs(user, private_bugs):
+        """Mutator for private_bugs that checks entitlement."""
+
     def getVersionSortedSeries(statuses=None, filter_statuses=None):
         """Return all the series sorted by the name field as a version.
 

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py	2012-02-03 11:23:30 +0000
+++ lib/lp/registry/model/person.py	2012-02-09 13:45:41 +0000
@@ -1230,7 +1230,7 @@
                 (voucher.voucher_id, voucher.status))
         return vouchers
 
-    def hasCurrentCommercialSubscription(self):
+    def hasCurrentCommercialSubscription(self, product=None):
         """See `IPerson`."""
         # Circular imports.
         from lp.registry.model.commercialsubscription import (
@@ -1239,6 +1239,10 @@
         from lp.registry.model.person import Person
         from lp.registry.model.product import Product
         from lp.registry.model.teammembership import TeamParticipation
+
+        filter = [Person.id == self.id]
+        if product:
+            filter.append(Product.id == product.id)
         person = Store.of(self).using(
             Person,
             Join(
@@ -1253,7 +1257,7 @@
                 Person,
                 CommercialSubscription.date_expires > datetime.now(
                     pytz.UTC),
-                Person.id == self.id)
+                *filter)
         return not person.is_empty()
 
     def iterTopProjectsContributedTo(self, limit=10):

=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py	2012-02-08 06:00:46 +0000
+++ lib/lp/registry/model/product.py	2012-02-09 13:45:41 +0000
@@ -3,6 +3,8 @@
 # pylint: disable-msg=E0611,W0212
 
 """Database classes including and related to Product."""
+from lp.registry.errors import CommercialSubscribersOnly
+from lp.services.webapp.authorization import check_permission
 
 __metaclass__ = type
 __all__ = [
@@ -508,6 +510,22 @@
                                notNull=True, default=False,
                                storm_validator=_validate_license_approved)
 
+    def privateBugsAllowed(self, user):
+        """See `IProductPublic`."""
+        if user is None:
+            return False
+        return (
+            check_permission('launchpad.Moderate', self) or
+            user.hasCurrentCommercialSubscription(self))
+
+    def setPrivateBugs(self, user, private_bugs):
+        """ See `IProductEditRestricted`."""
+        if private_bugs and not self.privateBugsAllowed(user):
+            raise CommercialSubscribersOnly(
+                'A valid commercial subscription is required to turn on '
+                'default private bugs.')
+        self.private_bugs = private_bugs
+
     @cachedproperty
     def commercial_subscription(self):
         return CommercialSubscription.selectOneBy(product=self)

=== modified file 'lib/lp/registry/stories/product/xx-product-with-private-defaults.txt'
--- lib/lp/registry/stories/product/xx-product-with-private-defaults.txt	2011-12-24 15:18:32 +0000
+++ lib/lp/registry/stories/product/xx-product-with-private-defaults.txt	2012-02-09 13:45:41 +0000
@@ -19,19 +19,6 @@
     >>> admin_browser.getControl(name="field.private_bugs").value = True
     >>> admin_browser.getControl("Change").click()
 
-However, the product's bug supervisor must be set, and if it is not, as is
-the case with Firefox, the user will get an error:
-
-    >>> admin_browser.open("http://launchpad.dev/firefox/+admin";)
-    >>> admin_browser.getControl(name="field.private_bugs").value = True
-    >>> admin_browser.getControl("Change").click()
-    >>> admin_browser.url
-    'http://launchpad.dev/firefox/+admin'
-    >>> for tag in find_tags_by_class(admin_browser.contents, "message"):
-    ...     print tag.renderContents()
-    There is 1 error.
-    <BLANKLINE>
-    Set a <a href="...+bugsupervisor">bug supervisor</a> for this project first.
 
 Filing a new bug
 ----------------

=== modified file 'lib/lp/registry/tests/test_errors.py'
--- lib/lp/registry/tests/test_errors.py	2012-01-01 02:58:52 +0000
+++ lib/lp/registry/tests/test_errors.py	2012-02-09 13:45:41 +0000
@@ -16,6 +16,7 @@
 
 from lp.registry.errors import (
     CannotTransitionToCountryMirror,
+    CommercialSubscribersOnly,
     DeleteSubscriptionError,
     DistroSeriesDifferenceError,
     JoinNotAllowed,
@@ -90,3 +91,7 @@
     def test_NameAlreadyTaken_bad_request(self):
         error_view = create_webservice_error_view(NameAlreadyTaken())
         self.assertEqual(CONFLICT, error_view.status)
+
+    def test_CommercialSubscribersOnly_bad_request(self):
+        error_view = create_webservice_error_view(CommercialSubscribersOnly())
+        self.assertEqual(UNAUTHORIZED, error_view.status)

=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py	2012-02-03 11:23:30 +0000
+++ lib/lp/registry/tests/test_person.py	2012-02-09 13:45:41 +0000
@@ -538,13 +538,23 @@
         self.assertFalse(person.isAnySecurityContact())
 
     def test_has_current_commercial_subscription(self):
-        # IPerson.hasCurrentCommercialSubscription() checks for one. 
+        # IPerson.hasCurrentCommercialSubscription() checks for one.
         team = self.factory.makeTeam(
             subscription_policy=TeamSubscriptionPolicy.MODERATED)
         product = self.factory.makeProduct(owner=team)
         self.factory.makeCommercialSubscription(product)
         self.assertTrue(team.teamowner.hasCurrentCommercialSubscription())
 
+    def test_has_current_commercial_subscription_for_product(self):
+        # IPerson.hasCurrentCommercialSubscription() checks for a commercial
+        # subscription for a particular product.
+        team = self.factory.makeTeam(
+            subscription_policy=TeamSubscriptionPolicy.MODERATED)
+        product = self.factory.makeProduct(owner=team)
+        self.factory.makeCommercialSubscription(product)
+        self.assertTrue(
+            team.teamowner.hasCurrentCommercialSubscription(product))
+
     def test_does_not_have_current_commercial_subscription(self):
         # IPerson.hasCurrentCommercialSubscription() is false if it has
         # expired.

=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py	2012-02-08 06:00:46 +0000
+++ lib/lp/registry/tests/test_product.py	2012-02-09 13:45:41 +0000
@@ -22,7 +22,15 @@
     )
 from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
 from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
+<<<<<<< TREE
 from lp.registry.errors import OpenTeamLinkageError
+=======
+from lp.bugs.interfaces.bugtarget import IHasBugHeat
+from lp.registry.errors import (
+    CommercialSubscribersOnly,
+    OpenTeamLinkageError,
+    )
+>>>>>>> MERGE-SOURCE
 from lp.registry.interfaces.oopsreferences import IHasOOPSReferences
 from lp.registry.interfaces.person import (
     CLOSED_TEAM_POLICY,
@@ -258,6 +266,39 @@
             closed_team = self.factory.makeTeam(subscription_policy=policy)
             self.factory.makeProduct(security_contact=closed_team)
 
+    def test_private_bugs_not_allowed_for_anonymous(self):
+        product = self.factory.makeProduct()
+        self.assertFalse(product.privateBugsAllowed(None))
+
+    def test_private_bugs_not_allowed_for_unauthorised(self):
+        product = self.factory.makeProduct()
+        someone = self.factory.makePerson()
+        self.assertFalse(product.privateBugsAllowed(someone))
+
+    def test_private_bugs_allowed_for_moderators(self):
+        product = self.factory.makeProduct()
+        registry_expert = self.factory.makeRegistryExpert()
+        self.assertTrue(product.privateBugsAllowed(registry_expert))
+
+    def test_private_bugs_allowed_for_commercial_subscribers(self):
+        product = self.factory.makeProduct()
+        self.factory.makeCommercialSubscription(product)
+        self.assertTrue(product.privateBugsAllowed(product.owner))
+
+    def test_unauthorised_set_private_bugs_raises(self):
+        # Test Product.setPrivateBugs raises an error if user unauthorised.
+        product = self.factory.makeProduct()
+        self.assertRaises(
+            CommercialSubscribersOnly,
+            product.setPrivateBugs, product.owner, True)
+
+    def test_set_private_bugs(self):
+        # Test Product.setPrivateBugs()
+        product = self.factory.makeProduct()
+        self.factory.makeCommercialSubscription(product)
+        product.setPrivateBugs(product.owner, True)
+        self.assertTrue(product.private_bugs)
+
 
 class TestProductFiles(TestCase):
     """Tests for downloadable product files."""