launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #17658
[Merge] lp:~wgrant/launchpad/blueprint-moderation-api into lp:launchpad
William Grant has proposed merging lp:~wgrant/launchpad/blueprint-moderation-api into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~wgrant/launchpad/blueprint-moderation-api/+merge/245626
Let ~registry edit all specifications, and allow target and information_type to be set through the API.
Specs were the last major type of object that can be anonymously created but not moderated by staff without resorting to sysadmins.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wgrant/launchpad/blueprint-moderation-api into lp:launchpad.
=== modified file 'lib/lp/blueprints/browser/specification.py'
--- lib/lp/blueprints/browser/specification.py 2014-06-17 06:13:08 +0000
+++ lib/lp/blueprints/browser/specification.py 2015-01-06 07:28:24 +0000
@@ -978,9 +978,14 @@
cancel_url = next_url
+class ISpecificationRetargetingSchema(Interface):
+
+ target = copy_field(ISpecification['target'], readonly=False)
+
+
class SpecificationRetargetingView(LaunchpadFormView):
- schema = ISpecification
+ schema = ISpecificationRetargetingSchema
field_names = ['target']
label = _('Move this blueprint to a different project')
=== modified file 'lib/lp/blueprints/errors.py'
--- lib/lp/blueprints/errors.py 2010-10-19 00:35:05 +0000
+++ lib/lp/blueprints/errors.py 2015-01-06 07:28:24 +0000
@@ -9,11 +9,16 @@
'TargetAlreadyHasSpecification',
]
-
+import httplib
+
+from lazr.restful.declarations import error_status
+
+
+@error_status(httplib.BAD_REQUEST)
class TargetAlreadyHasSpecification(Exception):
"""The ISpecificationTarget already has a specification of that name."""
def __init__(self, target, name):
- msg = "The target %s already has a specification named %s" % (
- target, name)
+ msg = "There is already a blueprint named %s for %s." % (
+ name, target.displayname)
super(TargetAlreadyHasSpecification, self).__init__(msg)
=== modified file 'lib/lp/blueprints/interfaces/specification.py'
--- lib/lp/blueprints/interfaces/specification.py 2014-06-19 02:12:50 +0000
+++ lib/lp/blueprints/interfaces/specification.py 2015-01-06 07:28:24 +0000
@@ -276,13 +276,12 @@
# using setTarget() as the mutator.
target = exported(
ReferenceChoice(
- title=_('For'), required=True, vocabulary='DistributionOrProduct',
+ title=_('For'), required=True, readonly=True,
+ vocabulary='DistributionOrProduct',
description=_(
"The project for which this proposal is being made."),
schema=ISpecificationTarget),
- as_of="devel",
- readonly=True,
- )
+ as_of="devel")
productseries = Choice(
title=_('Series Goal'), required=False,
@@ -635,12 +634,23 @@
:param target: an IProduct or IDistribution.
"""
+ @mutator_for(ISpecificationView['target'])
+ @operation_parameters(
+ target=copy_field(ISpecificationView['target']))
+ @export_write_operation()
+ @operation_for_version('devel')
def retarget(target):
"""Move the spec to the given target.
The new target must be an IProduct or IDistribution.
"""
+ @mutator_for(ISpecificationPublic['information_type'])
+ @call_with(who=REQUEST_USER)
+ @operation_parameters(
+ information_type=copy_field(ISpecificationPublic['information_type']))
+ @export_write_operation()
+ @operation_for_version('devel')
def transitionToInformationType(information_type, who):
"""Change the information type of the Specification."""
=== modified file 'lib/lp/blueprints/tests/test_specification.py'
--- lib/lp/blueprints/tests/test_specification.py 2014-06-19 10:04:55 +0000
+++ lib/lp/blueprints/tests/test_specification.py 2015-01-06 07:28:24 +0000
@@ -53,6 +53,7 @@
IAccessArtifactGrantSource,
IAccessPolicySource,
)
+from lp.registry.interfaces.person import IPersonSet
from lp.security import (
AdminSpecification,
EditSpecificationByRelatedPeople,
@@ -414,6 +415,27 @@
specification.target.owner, specification,
error_expected=False, attribute='name', value='foo')
+ def test_registry_write_access(self):
+ # Users with special privileges can change the attributes
+ # of public and private specifcations.
+ specification = self.factory.makeSpecification()
+ removeSecurityProxy(specification.target)._ensurePolicies(
+ PRIVATE_INFORMATION_TYPES)
+ all_types = specification.getAllowedInformationTypes(
+ specification.owner)
+ expert = self.factory.makePerson(
+ member_of=[getUtility(IPersonSet).getByName('registry')])
+ for information_type in all_types:
+ with person_logged_in(specification.target.owner):
+ specification.transitionToInformationType(
+ information_type, specification.owner)
+ self.write_access_to_ISpecificationView(
+ expert, specification, error_expected=False,
+ attribute='whiteboard', value='foo')
+ self.write_access_to_ISpecificationView(
+ expert, specification, error_expected=False,
+ attribute='name', value='foo')
+
def _fetch_specs_visible_for_user(self, user):
return Store.of(self.product).find(
Specification,
=== modified file 'lib/lp/blueprints/tests/test_webservice.py'
--- lib/lp/blueprints/tests/test_webservice.py 2014-06-19 01:21:06 +0000
+++ lib/lp/blueprints/tests/test_webservice.py 2015-01-06 07:28:24 +0000
@@ -5,15 +5,20 @@
__metaclass__ = type
+import json
+
+from testtools.matchers import MatchesStructure
import transaction
from zope.security.management import endInteraction
from zope.security.proxy import removeSecurityProxy
+from lp.app.enums import InformationType
from lp.blueprints.enums import SpecificationDefinitionStatus
+from lp.registry.enums import SpecificationSharingPolicy
from lp.services.webapp.interaction import ANONYMOUS
from lp.services.webapp.interfaces import OAuthPermission
-from lp.registry.enums import SpecificationSharingPolicy
from lp.testing import (
+ admin_logged_in,
api_url,
launchpadlib_for,
person_logged_in,
@@ -144,11 +149,23 @@
self.assertEqual(response.status, 200)
self.assertContentEqual(expected_keys, response.jsonBody().keys())
- def test_representation_contains_name(self):
+ def test_representation_basics(self):
spec = self.factory.makeSpecification()
- spec_name = spec.name
spec_webservice = self.getSpecOnWebservice(spec)
- self.assertEqual(spec_name, spec_webservice.name)
+ with person_logged_in(ANONYMOUS):
+ self.assertThat(
+ spec_webservice,
+ MatchesStructure.byEquality(
+ name=spec.name,
+ title=spec.title,
+ specification_url=spec.specurl,
+ summary=spec.summary,
+ implementation_status=spec.implementation_status.title,
+ definition_status=spec.definition_status.title,
+ priority=spec.priority.title,
+ date_created=spec.datecreated,
+ whiteboard=spec.whiteboard,
+ workitems_text=spec.workitems_text))
def test_representation_contains_target(self):
spec = self.factory.makeSpecification(
@@ -157,39 +174,6 @@
spec_webservice = self.getSpecOnWebservice(spec)
self.assertEqual(spec_target_name, spec_webservice.target.name)
- def test_representation_contains_title(self):
- spec = self.factory.makeSpecification(title='Foo')
- spec_title = spec.title
- spec_webservice = self.getSpecOnWebservice(spec)
- self.assertEqual(spec_title, spec_webservice.title)
-
- def test_representation_contains_specification_url(self):
- spec = self.factory.makeSpecification(specurl='http://example.com')
- spec_specurl = spec.specurl
- spec_webservice = self.getSpecOnWebservice(spec)
- self.assertEqual(spec_specurl, spec_webservice.specification_url)
-
- def test_representation_contains_summary(self):
- spec = self.factory.makeSpecification(summary='Foo')
- spec_summary = spec.summary
- spec_webservice = self.getSpecOnWebservice(spec)
- self.assertEqual(spec_summary, spec_webservice.summary)
-
- def test_representation_contains_implementation_status(self):
- spec = self.factory.makeSpecification()
- spec_implementation_status = spec.implementation_status
- spec_webservice = self.getSpecOnWebservice(spec)
- self.assertEqual(
- spec_implementation_status.title,
- spec_webservice.implementation_status)
-
- def test_representation_contains_definition_status(self):
- spec = self.factory.makeSpecification()
- spec_definition_status = spec.definition_status
- spec_webservice = self.getSpecOnWebservice(spec)
- self.assertEqual(
- spec_definition_status.title, spec_webservice.definition_status)
-
def test_representation_contains_assignee(self):
# Hard-code the person's name or else we'd need to set up a zope
# interaction as IPerson.name is protected.
@@ -216,30 +200,6 @@
spec_webservice = self.getSpecOnWebservice(spec)
self.assertEqual('test-person', spec_webservice.owner.name)
- def test_representation_contains_priority(self):
- spec = self.factory.makeSpecification()
- spec_priority = spec.priority
- spec_webservice = self.getSpecOnWebservice(spec)
- self.assertEqual(spec_priority.title, spec_webservice.priority)
-
- def test_representation_contains_date_created(self):
- spec = self.factory.makeSpecification()
- spec_datecreated = spec.datecreated
- spec_webservice = self.getSpecOnWebservice(spec)
- self.assertEqual(spec_datecreated, spec_webservice.date_created)
-
- def test_representation_contains_whiteboard(self):
- spec = self.factory.makeSpecification(whiteboard='Test')
- spec_whiteboard = spec.whiteboard
- spec_webservice = self.getSpecOnWebservice(spec)
- self.assertEqual(spec_whiteboard, spec_webservice.whiteboard)
-
- def test_representation_contains_workitems(self):
- work_item = self.factory.makeSpecificationWorkItem()
- spec_webservice = self.getSpecOnWebservice(work_item.specification)
- self.assertEqual(work_item.specification.workitems_text,
- spec_webservice.workitems_text)
-
def test_representation_contains_milestone(self):
product = self.factory.makeProduct()
productseries = self.factory.makeProductSeries(product=product)
@@ -278,6 +238,55 @@
self.assertEqual(bug.id, spec_webservice.bugs[0].id)
+class SpecificationMutationTests(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_set_information_type(self):
+ product = self.factory.makeProduct(
+ specification_sharing_policy=(
+ SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
+ spec = self.factory.makeSpecification(product=product)
+ self.assertEqual(InformationType.PUBLIC, spec.information_type)
+ spec_url = api_url(spec)
+ webservice = webservice_for_person(
+ product.owner, permission=OAuthPermission.WRITE_PRIVATE)
+ response = webservice.patch(
+ spec_url, "application/json",
+ json.dumps(dict(information_type='Proprietary')),
+ api_version='devel')
+ self.assertEqual(209, response.status)
+ with admin_logged_in():
+ self.assertEqual(
+ InformationType.PROPRIETARY, spec.information_type)
+
+ def test_set_target(self):
+ old_target = self.factory.makeProduct()
+ spec = self.factory.makeSpecification(product=old_target, name='foo')
+ new_target = self.factory.makeProduct(displayname='Fooix')
+ spec_url = api_url(spec)
+ new_target_url = api_url(new_target)
+ webservice = webservice_for_person(
+ old_target.owner, permission=OAuthPermission.WRITE_PRIVATE)
+ response = webservice.patch(
+ spec_url, "application/json",
+ json.dumps(dict(target_link=new_target_url)), api_version='devel')
+ self.assertEqual(301, response.status)
+ with admin_logged_in():
+ self.assertEqual(new_target, spec.target)
+
+ # Moving another spec with the same name fails.
+ other_spec = self.factory.makeSpecification(
+ product=old_target, name='foo')
+ other_spec_url = api_url(other_spec)
+ response = webservice.patch(
+ other_spec_url, "application/json",
+ json.dumps(dict(target_link=new_target_url)), api_version='devel')
+ self.assertEqual(400, response.status)
+ self.assertEqual(
+ "There is already a blueprint named foo for Fooix.", response.body)
+
+
class SpecificationTargetTests(SpecificationWebserviceTestCase):
"""Tests for accessing specifications via their targets."""
layer = AppServerLayer
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2014-07-07 03:43:30 +0000
+++ lib/lp/security.py 2015-01-06 07:28:24 +0000
@@ -593,6 +593,7 @@
if user.isOwner(goal) or user.isDriver(goal):
return True
return (user.in_admin or
+ user.in_registry_experts or
user.isOwner(self.obj.target) or
user.isDriver(self.obj.target) or
user.isOneOf(
@@ -605,9 +606,11 @@
def checkAuthenticated(self, user):
assert self.obj.target
- return (user.isOwner(self.obj.target) or
- user.isDriver(self.obj.target) or
- user.in_admin)
+ return (
+ user.in_admin or
+ user.in_registry_experts or
+ user.isOwner(self.obj.target) or
+ user.isDriver(self.obj.target))
class DriverSpecification(AuthorizationBase):
Follow ups