Hi Cody, Thanks for doing this! This is a nice branch. I have a few small comments, that's all. Approved, but please address those issues before landing it. Gavin. > === modified file 'lib/canonical/launchpad/interfaces/launchpad.py' > --- lib/canonical/launchpad/interfaces/launchpad.py 2010-04-19 14:47:49 +0000 > +++ lib/canonical/launchpad/interfaces/launchpad.py 2010-04-28 16:42:08 +0000 > @@ -14,6 +14,7 @@ > from zope.schema import Bool, Choice, Int, TextLine > from persistent import IPersistent > > +from lazr.restful.declarations import exported > from lazr.restful.interfaces import IServiceRootResource > from canonical.launchpad import _ > from canonical.launchpad.fields import PublicPersonChoice > @@ -439,11 +440,11 @@ > class IHasSecurityContact(Interface): > """An object that has a security contact.""" > > - security_contact = PublicPersonChoice( > + security_contact = exported(PublicPersonChoice( > title=_("Security Contact"), > description=_( > "The person or team who handles security-related bug reports"), > - required=False, vocabulary='ValidPersonOrTeam') > + required=False, vocabulary='ValidPersonOrTeam')) > > > class IHasIcon(Interface): > > === modified file 'lib/lp/bugs/browser/bugsupervisor.py' > --- lib/lp/bugs/browser/bugsupervisor.py 2009-11-12 15:33:27 +0000 > +++ lib/lp/bugs/browser/bugsupervisor.py 2010-04-28 16:42:08 +0000 > @@ -7,17 +7,30 @@ > > __all__ = ['BugSupervisorEditView'] > > - > -from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor > from canonical.launchpad.webapp import ( > action, canonical_url, LaunchpadEditFormView) > from canonical.launchpad.webapp.menu import structured > - > +from lazr.restful.interface import copy_field, use_template > +from zope.interface import Interface > + > +from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor > + 2 blank lines between classes. > +class BugSupervisorEditSchema(Interface): > + """Defines the fields for the edit form. > + > + This is necessary to make an editable field for bug supervisor as it is > + defined as read-only in the interface to prevent setting it directly. > + """ > + use_template(IHasBugSupervisor, include=[ > + 'bug_supervisor', > + ]) I don't think you need this use_template() call; its effects are immediately overwritten below. > + bug_supervisor = copy_field( > + IHasBugSupervisor['bug_supervisor'], readonly=False) > Again, 2 blank lines. > class BugSupervisorEditView(LaunchpadEditFormView): > """Browser view class for editing the bug supervisor.""" > > - schema = IHasBugSupervisor > + schema = BugSupervisorEditSchema > field_names = ['bug_supervisor'] > > @property > @@ -30,6 +43,11 @@ > """The page title.""" > return self.label > > + @property > + def adapters(self): > + """See `LaunchpadFormView`""" > + return {BugSupervisorEditSchema: self.context} Interesting, I've never seen this before. > + > @action('Change', name='change') > def change_action(self, action, data): > """Redirect to the target page with a success message.""" > > === modified file 'lib/lp/bugs/interfaces/bugsupervisor.py' > --- lib/lp/bugs/interfaces/bugsupervisor.py 2009-12-05 18:50:48 +0000 > +++ lib/lp/bugs/interfaces/bugsupervisor.py 2010-04-28 16:42:08 +0000 > @@ -14,17 +14,27 @@ > from canonical.launchpad import _ > from canonical.launchpad.fields import PublicPersonChoice > > -from lp.registry.interfaces.structuralsubscription import ( > - IStructuralSubscriptionTarget) > - > - > -class IHasBugSupervisor(IStructuralSubscriptionTarget): > - > - bug_supervisor = PublicPersonChoice( > +from zope.interface import Interface > + > +from lazr.restful.declarations import ( > + REQUEST_USER, call_with, exported, export_write_operation, > + mutator_for, operation_parameters) > +from lazr.restful.interface import copy_field > + > +from lp.registry.interfaces.person import IPerson This import is not needed. Also, 2 lines :) > + > +class IHasBugSupervisor(Interface): > + > + bug_supervisor = exported(PublicPersonChoice( > title=_("Bug Supervisor"), > description=_( > "The person or team responsible for bug management."), > - required=False, vocabulary='ValidPersonOrTeam') > + required=False, vocabulary='ValidPersonOrTeam', readonly=True)) > > - def setBugSupervisor(self, bug_supervisor, user): > + @mutator_for(bug_supervisor) > + @call_with(user=REQUEST_USER) > + @operation_parameters( > + bug_supervisor=copy_field(bug_supervisor)) I suspect you don't need copy_field() here. > + @export_write_operation() > + def setBugSupervisor(bug_supervisor, user): > """Set the bug contact and create a bug subscription.""" > > === modified file 'lib/lp/bugs/stories/webservice/xx-bug-target.txt' > --- lib/lp/bugs/stories/webservice/xx-bug-target.txt 2009-08-20 04:46:48 +0000 > +++ lib/lp/bugs/stories/webservice/xx-bug-target.txt 2010-04-28 16:42:08 +0000 > @@ -117,3 +117,86 @@ > ... '/testix', 'application/json', > ... dumps({'official_bug_tags': [u'foo', u'bar']})) > HTTP/1.1 209 Content Returned... > + > +== bug_supervisor == > + > +We can retrieve or set a person or team as the bug supervisor for projects. > + > + >>> firefox_project = webservice.get('/firefox').jsonBody() > + >>> print firefox_project['bug_supervisor_link'] > + None > + > + >>> print webservice.patch( > + ... '/firefox', 'application/json', > + ... dumps({'bug_supervisor_link': firefox_project['owner_link']})) > + HTTP/1.1 209 Content Returned... Recently a new helper, launchpadlib_for(), has been added to the tree. This is fine as it is, but if you plan to do more web API stuff it might make things easier. > + > + >>> firefox_project = webservice.get('/firefox').jsonBody() > + >>> print firefox_project['bug_supervisor_link'] > + http://api.launchpad.dev/beta/~name12 > + > +We can also do this for distributions. > + > + >>> ubuntutest_dist = webservice.get('/ubuntutest').jsonBody() > + >>> print ubuntutest_dist['bug_supervisor_link'] > + None > + > + >>> print webservice.patch( > + ... '/ubuntutest', 'application/json', > + ... dumps({'bug_supervisor_link': ubuntutest_dist['owner_link']})) > + HTTP/1.1 209 Content Returned... > + > + >>> ubuntutest_dist = webservice.get('/ubuntutest').jsonBody() > + >>> print ubuntutest_dist['bug_supervisor_link'] > + http://api.launchpad.dev/beta/~ubuntu-team > + > +Setting the bug supervisor is restricted to owners and launchpad admins. > + > + >>> print user_webservice.patch( > + ... '/ubuntutest', 'application/json', > + ... dumps({'bug_supervisor_link': None})) > + HTTP/1.1 401 Unauthorized > + ... > + > + > +== security_contact == > + > +We can retrieve or set a person or team as the security contact for projects. > + > + >>> firefox_project = webservice.get('/firefox').jsonBody() > + >>> print firefox_project['security_contact_link'] > + None > + > + >>> print webservice.patch( > + ... '/firefox', 'application/json', > + ... dumps({'security_contact_link': firefox_project['owner_link']})) > + HTTP/1.1 209 Content Returned... > + > + >>> firefox_project = webservice.get('/firefox').jsonBody() > + >>> print firefox_project['security_contact_link'] > + http://api.launchpad.dev/beta/~name12 > + > +We can also do this for distributions. > + > + >>> ubuntutest_dist = webservice.get('/ubuntutest').jsonBody() > + >>> print ubuntutest_dist['security_contact_link'] > + None > + > + >>> print webservice.patch( > + ... '/ubuntutest', 'application/json', > + ... dumps({'security_contact_link': ubuntutest_dist['owner_link']})) > + HTTP/1.1 209 Content Returned... > + > + >>> ubuntutest_dist = webservice.get('/ubuntutest').jsonBody() > + >>> print ubuntutest_dist['security_contact_link'] > + http://api.launchpad.dev/beta/~ubuntu-team > + > +Setting the security contact is restricted to owners and launchpad admins. > + > + >>> print user_webservice.patch( > + ... '/ubuntutest', 'application/json', > + ... dumps({'security_contact_link': None})) > + HTTP/1.1 401 Unauthorized > + ... > + > + > > === modified file 'lib/lp/registry/interfaces/distribution.py' > --- lib/lp/registry/interfaces/distribution.py 2010-04-09 00:21:24 +0000 > +++ lib/lp/registry/interfaces/distribution.py 2010-04-28 16:42:08 +0000 > @@ -40,6 +40,7 @@ > from lp.app.interfaces.headings import IRootContext > from lp.registry.interfaces.announcement import IMakesAnnouncements > from lp.registry.interfaces.distributionmirror import IDistributionMirror > +from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor > from lp.bugs.interfaces.bugtarget import ( > IBugTarget, IOfficialBugTagTargetPublic, IOfficialBugTagTargetRestricted) > from lp.soyuz.interfaces.buildrecords import IHasBuildRecords > @@ -548,8 +549,9 @@ > """Can the user edit this distribution?""" > > > -class IDistribution(IDistributionEditRestricted, IDistributionPublic, > - IRootContext, IStructuralSubscriptionTarget): > +class IDistribution( > + IDistributionEditRestricted, IDistributionPublic, IHasBugSupervisor, > + IRootContext, IStructuralSubscriptionTarget): > """An operating system distribution.""" > export_as_webservice_entry() > > > === modified file 'lib/lp/registry/interfaces/product.py' > --- lib/lp/registry/interfaces/product.py 2010-04-23 02:55:08 +0000 > +++ lib/lp/registry/interfaces/product.py 2010-04-28 16:42:08 +0000 > @@ -38,6 +38,7 @@ > Description, IconImageUpload, LogoImageUpload, MugshotImageUpload, > ParticipatingPersonChoice, ProductBugTracker, ProductNameField, > PublicPersonChoice, Summary, Title, URIField) > +from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor > from lp.registry.interfaces.structuralsubscription import ( > IStructuralSubscriptionTarget) > from lp.app.interfaces.headings import IRootContext > @@ -715,9 +716,9 @@ > """Return basic timeline data useful for creating a diagram.""" > > > -class IProduct(IProductEditRestricted, IProductProjectReviewRestricted, > - IProductDriverRestricted, IProductPublic, IRootContext, > - IStructuralSubscriptionTarget): > +class IProduct(IHasBugSupervisor, IProductEditRestricted, > + IProductProjectReviewRestricted, IProductDriverRestricted, > + IProductPublic, IRootContext, IStructuralSubscriptionTarget): This should be indented either like: class IProduct(IHasBugSupervisor, IProductEditRestricted, IProductProjectReviewRestricted, IProductDriverRestricted, IProductPublic, IRootContext, IStructuralSubscriptionTarget): or like: class IProduct( IHasBugSupervisor, IProductEditRestricted, IProductProjectReviewRestricted, IProductDriverRestricted, IProductPublic, IRootContext, IStructuralSubscriptionTarget): > """A Product. > > The Launchpad Registry describes the open source world as ProjectGroups >