Merge lp:~james-w/launchpad/register-code-import into lp:launchpad

Proposed by James Westby
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~james-w/launchpad/register-code-import
Merge into: lp:launchpad
Prerequisite: lp:~james-w/launchpad/code-imports-for-source-packages
Diff against target: 732 lines (+302/-35)
18 files modified
lib/canonical/launchpad/interfaces/_schema_circular_imports.py (+5/-1)
lib/lp/code/errors.py (+1/-0)
lib/lp/code/interfaces/branch.py (+8/-1)
lib/lp/code/interfaces/branchtarget.py (+3/-1)
lib/lp/code/interfaces/codeimport.py (+5/-1)
lib/lp/code/interfaces/hasbranches.py (+49/-5)
lib/lp/code/interfaces/webservice.py (+6/-1)
lib/lp/code/mail/tests/test_codeimport.py (+1/-1)
lib/lp/code/model/branchtarget.py (+20/-2)
lib/lp/code/model/codeimport.py (+6/-3)
lib/lp/code/model/hasbranches.py (+15/-1)
lib/lp/code/model/tests/test_codeimport.py (+40/-0)
lib/lp/code/stories/webservice/xx-branchmergeproposal.txt (+1/-1)
lib/lp/code/stories/webservice/xx-code-import.txt (+129/-7)
lib/lp/registry/interfaces/product.py (+4/-2)
lib/lp/registry/interfaces/sourcepackage.py (+3/-2)
lib/lp/registry/model/product.py (+3/-3)
lib/lp/registry/model/sourcepackage.py (+3/-3)
To merge this branch: bzr merge lp:~james-w/launchpad/register-code-import
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+22668@code.launchpad.net

Commit message

There is now a newCodeImport method on Product and SourcePackage in the API.

Description of the change

Hi,

This exposes the creation of code imports over the API.

It first adds an "owner" argument, so that we can specify
this through the API, with associated tests.

It then exposes the IBranchTarget.newCodeImport() method
over the API using a small interface and mixin, due to it being
on an adapter.

It then adds a bunch of tests for this, including error cases
where the user may specify bad information.

For the last bit I used webservice_error so that they would get
400 not 500 responses, and there won't be OOPSes generated for them.

To do this I had to add the exception classes that have the decorator
to the interfaces/webservice.py file so that lazr.restful can register
views for them.

Doing this I discovered that the existing webservice_error in lp.code
wasn't working, and in fact had a test asserting it wasn't working,
so I fixed that along the way.

Thanks,

James

To post a comment you must log in.
Revision history for this message
James Westby (james-w) wrote :

Passes ec2, so requesting a review.

Thanks,

James

Revision history for this message
Aaron Bentley (abentley) wrote :

It's nice of you to defer requesting a review until all tests are passing, but I think a lot of us don't observe that.

Was there a preimplementation call?

Was there any lint?

AIUI PEP8 requires that items within a class definition be separated by a single blank line, so a bunch of your webservice_error(400) lines need a blank line in front of them.

Why are you using export_write_operation rather than export_factory_operation on branchtarget?

On IHasCodeImports, you claim that newCodeImport returns BranchMergeProposal. I think this is inaccurate.

What would you think about expressing xx-code-import.txt's "Exceptions" heading as a unit test? Failing that, it needs two blank lines separating it from the previous section.

review: Needs Information
Revision history for this message
James Westby (james-w) wrote :

> It's nice of you to defer requesting a review until all tests are passing, but
> I think a lot of us don't observe that.
>
> Was there a preimplementation call?

No, but I chatted with wgrant about the strategy.

> Was there any lint?

I don't know, running "make lint" runs it on lots of unexpected files.

> AIUI PEP8 requires that items within a class definition be separated by a
> single blank line, so a bunch of your webservice_error(400) lines need a blank
> line in front of them.

Done.

> Why are you using export_write_operation rather than export_factory_operation
> on branchtarget?

That's not supposed to be there any more, deleted.

> On IHasCodeImports, you claim that newCodeImport returns BranchMergeProposal.
> I think this is inaccurate.

Fixed.

> What would you think about expressing xx-code-import.txt's "Exceptions"
> heading as a unit test? Failing that, it needs two blank lines separating it
> from the previous section.

I don't know how to do webservice tests as unit tests, if you can point me to
examples I will convert them.

Blank lines issue fixed anyway, and I updated the text to make the intent of
the tests clearer as well.

Thanks,

James

Revision history for this message
Aaron Bentley (abentley) wrote :
Download full text (14.5 KiB)

On 04/05/2010 05:39 PM, James Westby wrote:

>> Was there any lint?
>
> I don't know, running "make lint" runs it on lots of unexpected files.

When there are no uncommitted changes, "make lint" compares your tree to
the parent branch to determine which files it runs against. This means
it will also lint files changed by your prerequisite branch. You parent
branch may also be out of date.

You can also run bin/lint.sh with the list of files you modified. If
you use pipelines and have the lp-review-body plugin installed, you can
use lp-propose (formerly lp-submit) and it will use the previous pipe to
determine which files to lint.

Anyhow, there was lots of lint:
$ make lint
= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
   lib/canonical/launchpad/interfaces/_schema_circular_imports.py
   lib/lp/code/errors.py
   lib/lp/code/interfaces/branch.py
   lib/lp/code/interfaces/branchtarget.py
   lib/lp/code/interfaces/codeimport.py
   lib/lp/code/interfaces/hasbranches.py
   lib/lp/code/interfaces/webservice.py
   lib/lp/code/model/branchtarget.py
   lib/lp/code/model/codeimport.py
   lib/lp/code/model/hasbranches.py
   lib/lp/code/model/tests/test_codeimport.py
   lib/lp/code/stories/webservice/xx-branchmergeproposal.txt
   lib/lp/code/stories/webservice/xx-code-import.txt
   lib/lp/registry/interfaces/product.py
   lib/lp/registry/interfaces/sourcepackage.py
   lib/lp/registry/model/product.py
   lib/lp/registry/model/sourcepackage.py

== Pyflakes notices ==

lib/canonical/launchpad/interfaces/_schema_circular_imports.py
     43: 'IBranchTarget' imported but unused

lib/lp/code/interfaces/branch.py
     11: undefined name 'UnknownBranchTypeError' in __all__

lib/lp/code/interfaces/webservice.py
     8: 'BranchMergeProposalExists' imported but unused
     9: 'BranchCreatorNotOwner' imported but unused
     9: 'IBranchSet' imported but unused
     9: 'IBranch' imported but unused
     9: 'BranchExists' imported but unused
     9: 'BranchCreatorNotMemberOfOwnerTeam' imported but unused
     12: 'IBranchMergeProposal' imported but unused
     13: 'IBranchSubscription' imported but unused
     14: 'ICodeImport' imported but unused
     15: 'ICodeReviewComment' imported but unused
     16: 'ICodeReviewVoteReference' imported but unused
     17: 'IStaticDiff' imported but unused
     17: 'IDiff' imported but unused
     17: 'IPreviewDiff' imported but unused

lib/lp/registry/model/sourcepackage.py
     684: local variable 'displayname' is assigned to but never used

== Pylint notices ==

lib/canonical/launchpad/interfaces/_schema_circular_imports.py
     18: [F0401] Unable to import 'lazr.restful.declarations'
     43: [W0611] Unused import IBranchTarget

lib/lp/code/errors.py
     20: [F0401] Unable to import 'lazr.restful.declarations'

lib/lp/code/interfaces/branch.py
     442: [C0322, IBranch.setOwner] Operator not preceded by a space
     title=_("The new owner of the branch."),
     ^
     schema=IPerson))
     @export_write_operation()
     def setOwner(new_owner, user):
     451: [C0322, IBranch.setTarget] Opera...

Revision history for this message
James Westby (james-w) wrote :
Download full text (9.6 KiB)

On Tue, 06 Apr 2010 15:33:31 -0000, Aaron Bentley <email address hidden> wrote:
> On 04/05/2010 05:39 PM, James Westby wrote:
>
> >> Was there any lint?
> >
> > I don't know, running "make lint" runs it on lots of unexpected files.
>
> When there are no uncommitted changes, "make lint" compares your tree to
> the parent branch to determine which files it runs against. This means
> it will also lint files changed by your prerequisite branch. You parent
> branch may also be out of date.

I am using pipelines, so I would have thought it would run against the
previous pipe?

> You can also run bin/lint.sh with the list of files you modified. If
> you use pipelines and have the lp-review-body plugin installed, you can
> use lp-propose (formerly lp-submit) and it will use the previous pipe to
> determine which files to lint.

That's what I used (at least for one of these proposals,) but I didn't
have the extra plugin installed.

> lib/canonical/launchpad/interfaces/_schema_circular_imports.py
> 43: 'IBranchTarget' imported but unused

Fixed.

> lib/lp/code/interfaces/branch.py
> 11: undefined name 'UnknownBranchTypeError' in __all__

Fixed.

> lib/lp/code/interfaces/webservice.py
> 8: 'BranchMergeProposalExists' imported but unused
> 9: 'BranchCreatorNotOwner' imported but unused
> 9: 'IBranchSet' imported but unused
> 9: 'IBranch' imported but unused
> 9: 'BranchExists' imported but unused
> 9: 'BranchCreatorNotMemberOfOwnerTeam' imported but unused
> 12: 'IBranchMergeProposal' imported but unused
> 13: 'IBranchSubscription' imported but unused
> 14: 'ICodeImport' imported but unused
> 15: 'ICodeReviewComment' imported but unused
> 16: 'ICodeReviewVoteReference' imported but unused
> 17: 'IStaticDiff' imported but unused
> 17: 'IDiff' imported but unused
> 17: 'IPreviewDiff' imported but unused

Noise. This file is purely for importing.

> lib/lp/registry/model/sourcepackage.py
> 684: local variable 'displayname' is assigned to but never used

Fixed.

> lib/lp/code/interfaces/branch.py
> 442: [C0322, IBranch.setOwner] Operator not preceded by a space
> title=_("The new owner of the branch."),
> ^
> schema=IPerson))
> @export_write_operation()
> def setOwner(new_owner, user):
> 451: [C0322, IBranch.setTarget] Operator not preceded by a space
> title=_("The project the branch belongs to."),
> ^
> schema=Interface, required=False), # Really IProduct
> source_package=Reference(
> title=_("The source package the branch belongs to."),
> schema=Interface, required=False)) # Really ISourcePackage
> @export_write_operation()
> def setTarget(user, project=None, source_package=None):
> 511: [C0322, IBranch.isPersonTrustedReviewer] Operator not preceded
> by a space
> title=_("A person for which the reviewer status is in question."),
> ^
> schema=IPerson))
> @export_read_operation()
> def isPersonTrustedReviewer(reviewer):
> 760: [C0322, IBranch._createMergeProposal] Operator not preceded by
> a space
> needs_review=Bool(title=_('Needs review'),
> ...

Read more...

Revision history for this message
Aaron Bentley (abentley) wrote :

On 04/07/2010 10:06 AM, James Westby wrote:
> On Tue, 06 Apr 2010 15:33:31 -0000, Aaron Bentley<email address hidden> wrote:
>> When there are no uncommitted changes, "make lint" compares your tree to
>> the parent branch to determine which files it runs against. This means
>> it will also lint files changed by your prerequisite branch. You parent
>> branch may also be out of date.
>
> I am using pipelines, so I would have thought it would run against the
> previous pipe?

No, I did not design it and it predates pipelines.

>> lib/lp/code/interfaces/webservice.py
>> 8: 'BranchMergeProposalExists' imported but unused
>> 9: 'BranchCreatorNotOwner' imported but unused
>> 9: 'IBranchSet' imported but unused
>> 9: 'IBranch' imported but unused
>> 9: 'BranchExists' imported but unused
>> 9: 'BranchCreatorNotMemberOfOwnerTeam' imported but unused
>> 12: 'IBranchMergeProposal' imported but unused
>> 13: 'IBranchSubscription' imported but unused
>> 14: 'ICodeImport' imported but unused
>> 15: 'ICodeReviewComment' imported but unused
>> 16: 'ICodeReviewVoteReference' imported but unused
>> 17: 'IStaticDiff' imported but unused
>> 17: 'IDiff' imported but unused
>> 17: 'IPreviewDiff' imported but unused
>
> Noise. This file is purely for importing.

It seems really weird to me. If it's there for external users, why isn't there an __all__? I'll chat with Leonard, but it doesn't need to block you.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'lib/canonical/launchpad/apidoc'
2=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
3--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-04-09 15:46:09 +0000
4+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-04-13 15:21:36 +0000
5@@ -45,7 +45,7 @@
6 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
7 from lp.code.interfaces.diff import IPreviewDiff
8 from lp.code.interfaces.hasbranches import (
9- IHasBranches, IHasMergeProposals, IHasRequestedReviews)
10+ IHasBranches, IHasCodeImports, IHasMergeProposals, IHasRequestedReviews)
11 from lp.code.interfaces.sourcepackagerecipebuild import (
12 ISourcePackageRecipeBuild)
13 from lp.registry.interfaces.distribution import IDistribution
14@@ -130,6 +130,10 @@
15 IHasMergeProposals, 'getMergeProposals', IBranchMergeProposal)
16 patch_collection_return_type(
17 IHasRequestedReviews, 'getRequestedReviews', IBranchMergeProposal)
18+patch_entry_return_type(
19+ IHasCodeImports, 'newCodeImport', ICodeImport)
20+patch_plain_parameter_type(
21+ IHasCodeImports, 'newCodeImport', 'owner', IPerson)
22
23 # IBugTask
24
25
26=== modified file 'lib/lp/code/errors.py'
27--- lib/lp/code/errors.py 2010-03-10 18:53:50 +0000
28+++ lib/lp/code/errors.py 2010-04-13 15:21:36 +0000
29@@ -41,6 +41,7 @@
30
31 class BranchMergeProposalExists(InvalidBranchMergeProposal):
32 """Raised if there is already a matching BranchMergeProposal."""
33+
34 webservice_error(400) #Bad request.
35
36
37
38=== modified file 'lib/lp/code/interfaces/branch.py'
39--- lib/lp/code/interfaces/branch.py 2010-04-09 02:08:39 +0000
40+++ lib/lp/code/interfaces/branch.py 2010-04-13 15:21:36 +0000
41@@ -49,7 +49,8 @@
42 export_as_webservice_collection, export_as_webservice_entry,
43 export_destructor_operation, export_factory_operation,
44 export_operation_as, export_read_operation, export_write_operation,
45- exported, mutator_for, operation_parameters, operation_returns_entry)
46+ exported, mutator_for, operation_parameters, operation_returns_entry,
47+ webservice_error)
48
49 from canonical.config import config
50
51@@ -93,6 +94,8 @@
52 class BranchExists(BranchCreationException):
53 """Raised when creating a branch that already exists."""
54
55+ webservice_error(400)
56+
57 def __init__(self, existing_branch):
58 # XXX: TimPenhey 2009-07-12 bug=405214: This error
59 # message logic is incorrect, but the exact text is being tested
60@@ -135,6 +138,8 @@
61 the branch to a team that they are not a member of.
62 """
63
64+ webservice_error(400)
65+
66
67 class BranchCreationNoTeamOwnedJunkBranches(BranchCreationException):
68 """We forbid the creation of team-owned +junk branches.
69@@ -158,6 +163,8 @@
70 the branch to another user.
71 """
72
73+ webservice_error(400)
74+
75
76 class BranchTypeError(Exception):
77 """An operation cannot be performed for a particular branch type.
78
79=== modified file 'lib/lp/code/interfaces/branchtarget.py'
80--- lib/lp/code/interfaces/branchtarget.py 2010-04-09 02:08:39 +0000
81+++ lib/lp/code/interfaces/branchtarget.py 2010-04-13 15:21:36 +0000
82@@ -121,7 +121,7 @@
83 """Get the BugTask for a given bug related to the branch target."""
84
85 def newCodeImport(registrant, branch_name, rcs_type, url=None,
86- cvs_root=None, cvs_module=None):
87+ cvs_root=None, cvs_module=None, owner=None):
88 """Create a new code import for this target.
89
90 :param registrant: the `IPerson` who should be recorded as creating
91@@ -131,6 +131,8 @@
92 :param url: the url to import from if the import isn't CVS.
93 :param cvs_root: if the import is from CVS the CVSROOT to import from.
94 :param cvs_module: if the import is from CVS the module to import.
95+ :param owner: the `IPerson` to own the resulting branch, or None to
96+ use registrant.
97 :returns: an `ICodeImport`.
98 :raises AssertionError: if supports_code_imports is False.
99 """
100
101=== modified file 'lib/lp/code/interfaces/codeimport.py'
102--- lib/lp/code/interfaces/codeimport.py 2010-03-26 02:24:29 +0000
103+++ lib/lp/code/interfaces/codeimport.py 2010-04-13 15:21:36 +0000
104@@ -182,10 +182,14 @@
105 """Interface representing the set of code imports."""
106
107 def new(registrant, target, branch_name, rcs_type, url=None,
108- cvs_root=None, cvs_module=None, review_status=None):
109+ cvs_root=None, cvs_module=None, review_status=None,
110+ owner=None):
111 """Create a new CodeImport.
112
113 :param target: An `IBranchTarget` that the code is associated with.
114+ :param owner: The `IPerson` to set as the owner of the branch, or
115+ None to use registrant. registrant must be a member of owner to
116+ do this.
117 """
118
119 def get(id):
120
121=== modified file 'lib/lp/code/interfaces/hasbranches.py'
122--- lib/lp/code/interfaces/hasbranches.py 2010-02-08 14:37:50 +0000
123+++ lib/lp/code/interfaces/hasbranches.py 2010-04-13 15:21:36 +0000
124@@ -1,25 +1,30 @@
125 # Copyright 2009 Canonical Ltd. This software is licensed under the
126 # GNU Affero General Public License version 3 (see the file LICENSE).
127
128+# pylint: disable-msg=E0213
129+
130 """Interface definitions for IHas<code related bits>."""
131
132 __metaclass__ = type
133 __all__ = [
134 'IHasBranches',
135+ 'IHasCodeImports',
136 'IHasMergeProposals',
137 'IHasRequestedReviews',
138 ]
139
140
141 from zope.interface import Interface
142-from zope.schema import Choice, Datetime, List
143+from zope.schema import Choice, Datetime, List, TextLine
144
145 from canonical.launchpad import _
146-from lp.code.enums import BranchLifecycleStatus, BranchMergeProposalStatus
147+from lp.code.enums import (
148+ BranchLifecycleStatus, BranchMergeProposalStatus, RevisionControlSystems)
149
150 from lazr.restful.declarations import (
151- REQUEST_USER, call_with, export_read_operation, operation_parameters,
152- operation_returns_collection_of)
153+ REQUEST_USER, call_with, export_factory_operation, export_read_operation,
154+ operation_parameters, operation_returns_collection_of)
155+from lazr.restful.fields import Reference
156
157
158 class IHasBranches(Interface):
159@@ -98,7 +103,7 @@
160 @operation_returns_collection_of(Interface) # Really IBranchMergeProposal.
161 @export_read_operation()
162 def getRequestedReviews(status=None, visible_by_user=None):
163- """Returns merge proposals that a review was requested from the person.
164+ """Returns merge proposals where a person was asked to review.
165
166 This does not include merge proposals that were requested from
167 teams that the person is part of. If status is not passed then
168@@ -108,3 +113,42 @@
169 :param visible_by_user: Normally the user who is asking.
170 :returns: A list of `IBranchMergeProposal`.
171 """
172+
173+
174+class IHasCodeImports(Interface):
175+ """Some things can have code imports that target them.
176+
177+ This interface defines the common methods that for working with them.
178+ """
179+
180+ # In order to minimise dependancies the returns_collection is defined as
181+ # Interface here and defined fully in the circular imports file.
182+
183+ @operation_parameters(
184+ branch_name=TextLine(
185+ title=_('Name of branch to create'), required=True),
186+ rcs_type=Choice(vocabulary=RevisionControlSystems, required=True),
187+ url=TextLine(title=_('Foreign VCS URL')),
188+ cvs_root=TextLine(title=_('CVS root URL')),
189+ cvs_module=TextLine(title=_('CVS module to import')),
190+ owner=Reference(title=_('Owner of the resulting branch'),
191+ schema=Interface)
192+ )
193+ @call_with(registrant=REQUEST_USER)
194+ @export_factory_operation(Interface, []) # Really ICodeImport.
195+ def newCodeImport(registrant=None, branch_name=None, rcs_type=None,
196+ url=None, cvs_root=None, cvs_module=None, owner=None):
197+ """Create a new code import.
198+
199+ :param registrant: The IPerson to record as the registrant of the
200+ import
201+ :param branch_name: The name of the branch to create.
202+ :param rcs_type: The type of the foreign VCS.
203+ :param url: The URL to import from if the VCS type uses a single URL
204+ (i.e. isn't CVS).
205+ :param cvs_root: The CVSROOT for a CVS import.
206+ :param cvs_module: The module to import for a CVS import.
207+ :param owner: Who should own the created branch, or None for it to
208+ be the same as the registrant, or the caller over the API.
209+ :returns: An instance of `ICodeImport`.
210+ """
211
212=== modified file 'lib/lp/code/interfaces/webservice.py'
213--- lib/lp/code/interfaces/webservice.py 2010-03-16 19:04:48 +0000
214+++ lib/lp/code/interfaces/webservice.py 2010-04-13 15:21:36 +0000
215@@ -3,7 +3,12 @@
216
217 """All the interfaces that are exposed through the webservice."""
218
219-from lp.code.interfaces.branch import IBranch, IBranchSet
220+# The exceptions are imported so that they can produce the special
221+# status code defined by webservice_error when they are raised.
222+from lp.code.errors import BranchMergeProposalExists
223+from lp.code.interfaces.branch import (
224+ IBranch, IBranchSet, BranchCreatorNotMemberOfOwnerTeam,
225+ BranchCreatorNotOwner, BranchExists)
226 from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal
227 from lp.code.interfaces.branchsubscription import IBranchSubscription
228 from lp.code.interfaces.codeimport import ICodeImport
229
230=== modified file 'lib/lp/code/mail/tests/test_codeimport.py'
231--- lib/lp/code/mail/tests/test_codeimport.py 2010-04-13 15:21:34 +0000
232+++ lib/lp/code/mail/tests/test_codeimport.py 2010-04-13 15:21:36 +0000
233@@ -118,7 +118,7 @@
234 series = self.factory.makeDistroSeries(
235 name='manic', distribution=distro)
236 fooix = self.factory.makeSourcePackage(
237- sourcename='fooix', distroseries=series)
238+ sourcepackagename='fooix', distroseries=series)
239 # Eric needs to be logged in for the mail to be sent.
240 login_person(eric)
241 code_import = self.factory.makePackageCodeImport(
242
243=== modified file 'lib/lp/code/model/branchtarget.py'
244--- lib/lp/code/model/branchtarget.py 2010-04-13 15:21:34 +0000
245+++ lib/lp/code/model/branchtarget.py 2010-04-13 15:21:36 +0000
246@@ -37,10 +37,11 @@
247 return self.context != other.context
248
249 def newCodeImport(self, registrant, branch_name, rcs_type, url=None,
250- cvs_root=None, cvs_module=None):
251+ cvs_root=None, cvs_module=None, owner=None):
252+ """See `IBranchTarget`."""
253 return getUtility(ICodeImportSet).new(
254 registrant, self, branch_name, rcs_type, url=url,
255- cvs_root=cvs_root, cvs_module=cvs_module)
256+ cvs_root=cvs_root, cvs_module=cvs_module, owner=owner)
257
258
259 class PackageBranchTarget(_BaseBranchTarget):
260@@ -336,6 +337,23 @@
261 branch.sourcepackagename = None
262
263
264+class ProductSeriesBranchTarget(ProductBranchTarget):
265+
266+ def __init__(self, productseries):
267+ ProductBranchTarget.__init__(self, productseries.product)
268+ self.productseries = productseries
269+
270+ @property
271+ def context(self):
272+ """See `IBranchTarget`."""
273+ return self.productseries
274+
275+ @property
276+ def supports_code_imports(self):
277+ """See `IBranchTarget`."""
278+ return False
279+
280+
281 def get_canonical_url_data_for_target(branch_target):
282 """Return the `ICanonicalUrlData` for an `IBranchTarget`."""
283 return ICanonicalUrlData(branch_target.context)
284
285=== modified file 'lib/lp/code/model/codeimport.py'
286--- lib/lp/code/model/codeimport.py 2010-03-26 01:41:16 +0000
287+++ lib/lp/code/model/codeimport.py 2010-04-13 15:21:36 +0000
288@@ -203,7 +203,8 @@
289 implements(ICodeImportSet)
290
291 def new(self, registrant, target, branch_name, rcs_type,
292- url=None, cvs_root=None, cvs_module=None, review_status=None):
293+ url=None, cvs_root=None, cvs_module=None, review_status=None,
294+ owner=None):
295 """See `ICodeImportSet`."""
296 if rcs_type == RevisionControlSystems.CVS:
297 assert cvs_root is not None and cvs_module is not None
298@@ -227,14 +228,16 @@
299 review_status = CodeImportReviewStatus.NEW
300 if not target.supports_code_imports:
301 raise AssertionError("%r doesn't support code imports" % target)
302+ if owner is None:
303+ owner = registrant
304 # Create the branch for the CodeImport.
305- namespace = target.getNamespace(registrant)
306+ namespace = target.getNamespace(owner)
307 import_branch = namespace.createBranch(
308 branch_type=BranchType.IMPORTED, name=branch_name,
309 registrant=registrant)
310
311 code_import = CodeImport(
312- registrant=registrant, owner=registrant, branch=import_branch,
313+ registrant=registrant, owner=owner, branch=import_branch,
314 rcs_type=rcs_type, url=url,
315 cvs_root=cvs_root, cvs_module=cvs_module,
316 review_status=review_status)
317
318=== modified file 'lib/lp/code/model/hasbranches.py'
319--- lib/lp/code/model/hasbranches.py 2010-03-09 17:30:18 +0000
320+++ lib/lp/code/model/hasbranches.py 2010-04-13 15:21:36 +0000
321@@ -6,6 +6,7 @@
322 __metaclass__ = type
323 __all__ = [
324 'HasBranchesMixin',
325+ 'HasCodeImportsMixin',
326 'HasMergeProposalsMixin',
327 'HasRequestedReviewsMixin',
328 ]
329@@ -16,6 +17,7 @@
330 from lp.code.interfaces.branch import DEFAULT_BRANCH_STATUS_IN_LISTING
331 from lp.code.interfaces.branchcollection import (
332 IAllBranches, IBranchCollection)
333+from lp.code.interfaces.branchtarget import IBranchTarget
334
335
336 class HasBranchesMixin:
337@@ -59,5 +61,17 @@
338
339 visible_branches = getUtility(IAllBranches).visibleByUser(
340 visible_by_user)
341- proposals = visible_branches.getMergeProposalsForReviewer(self, status)
342+ proposals = visible_branches.getMergeProposalsForReviewer(
343+ self, status)
344 return proposals
345+
346+
347+class HasCodeImportsMixin:
348+
349+ def newCodeImport(self, registrant=None, branch_name=None,
350+ rcs_type=None, url=None, cvs_root=None, cvs_module=None,
351+ owner=None):
352+ """See `IHasCodeImports`."""
353+ return IBranchTarget(self).newCodeImport(registrant, branch_name,
354+ rcs_type, url=url, cvs_root=cvs_root, cvs_module=cvs_module,
355+ owner=owner)
356
357=== modified file 'lib/lp/code/model/tests/test_codeimport.py'
358--- lib/lp/code/model/tests/test_codeimport.py 2010-04-13 15:21:34 +0000
359+++ lib/lp/code/model/tests/test_codeimport.py 2010-04-13 15:21:36 +0000
360@@ -16,6 +16,7 @@
361 from lp.code.model.codeimportevent import CodeImportEvent
362 from lp.code.model.codeimportjob import CodeImportJob, CodeImportJobSet
363 from lp.code.model.codeimportresult import CodeImportResult
364+from lp.code.interfaces.branch import BranchCreatorNotMemberOfOwnerTeam
365 from lp.code.interfaces.branchtarget import IBranchTarget
366 from lp.registry.interfaces.person import IPersonSet
367 from lp.code.enums import (
368@@ -155,6 +156,45 @@
369 # And a job is still created
370 self.assertIsNot(None, code_import.import_job)
371
372+ def test_set_owner(self):
373+ """Test that we can create an import owned by someone else."""
374+ registrant = self.factory.makePerson()
375+ owner = self.factory.makeTeam()
376+ removeSecurityProxy(registrant).join(owner)
377+ source_package = self.factory.makeSourcePackage()
378+ target = IBranchTarget(source_package)
379+ code_import = CodeImportSet().new(
380+ registrant=registrant,
381+ target=target,
382+ branch_name='imported',
383+ rcs_type=RevisionControlSystems.HG,
384+ url=self.factory.getUniqueURL(),
385+ review_status=None, owner=owner)
386+ code_import = removeSecurityProxy(code_import)
387+ self.assertEqual(registrant, code_import.registrant)
388+ self.assertEqual(owner, code_import.branch.owner)
389+ self.assertEqual(registrant, code_import.branch.registrant)
390+ self.assertEqual(target, code_import.branch.target)
391+ self.assertEqual(source_package, code_import.branch.sourcepackage)
392+ # And a job is still created
393+ self.assertIsNot(None, code_import.import_job)
394+
395+ def test_registrant_must_be_in_owner(self):
396+ """Test that we can't create an import for an arbitrary team."""
397+ registrant = self.factory.makePerson()
398+ owner = self.factory.makeTeam()
399+ source_package = self.factory.makeSourcePackage()
400+ target = IBranchTarget(source_package)
401+ self.assertRaises(
402+ BranchCreatorNotMemberOfOwnerTeam,
403+ CodeImportSet().new,
404+ registrant=registrant,
405+ target=target,
406+ branch_name='imported',
407+ rcs_type=RevisionControlSystems.HG,
408+ url=self.factory.getUniqueURL(),
409+ review_status=None, owner=owner)
410+
411
412 class TestCodeImportDeletion(TestCaseWithFactory):
413 """Test the deletion of CodeImports."""
414
415=== modified file 'lib/lp/code/stories/webservice/xx-branchmergeproposal.txt'
416--- lib/lp/code/stories/webservice/xx-branchmergeproposal.txt 2010-04-12 15:53:56 +0000
417+++ lib/lp/code/stories/webservice/xx-branchmergeproposal.txt 2010-04-13 15:21:36 +0000
418@@ -84,7 +84,7 @@
419 ... initial_comment='Merge\nit!', needs_review=True,
420 ... commit_message='It was merged!\n', reviewers=[reviewer_url],
421 ... review_types=['green'])
422- HTTP/1.1 500 Internal Server Error ...
423+ HTTP/1.1 400 Bad Request ...
424 BranchMergeProposalExists: There is already a branch merge proposal
425 registered for branch ... to land on ... that is still active.
426
427
428=== modified file 'lib/lp/code/stories/webservice/xx-code-import.txt'
429--- lib/lp/code/stories/webservice/xx-code-import.txt 2010-04-13 15:21:34 +0000
430+++ lib/lp/code/stories/webservice/xx-code-import.txt 2010-04-13 15:21:36 +0000
431@@ -5,24 +5,33 @@
432 if it is an import branch.
433
434 >>> from zope.security.proxy import removeSecurityProxy
435+ >>> from canonical.launchpad.testing.pages import webservice_for_person
436+ >>> from canonical.launchpad.webapp.interfaces import OAuthPermission
437
438 First we create some objects for use in the tests.
439
440 >>> login(ANONYMOUS)
441 >>> person = factory.makePerson(name='import-owner')
442+ >>> team = factory.makeTeam(name='import-owner-team')
443+ >>> other_team = factory.makeTeam(name='other-team')
444+ >>> other_person = factory.makePerson(name='other-person')
445+ >>> removeSecurityProxy(person).join(team)
446 >>> product = factory.makeProduct(name='scruff')
447+ >>> svn_branch_url = "http://svn.domain.com/source"
448 >>> code_import = removeSecurityProxy(factory.makeProductCodeImport(
449 ... registrant=person, product=product, branch_name='import',
450- ... svn_branch_url="http://svn.domain.com/source"))
451+ ... svn_branch_url=svn_branch_url))
452 >>> no_import_branch = removeSecurityProxy(factory.makeProductBranch(
453 ... owner=person, product=product, name='no-import'))
454 >>> logout()
455+ >>> import_webservice = webservice_for_person(
456+ ... person, permission=OAuthPermission.WRITE_PUBLIC)
457
458 If we query a branch with no import then we find that it tells us
459 it doesn't have one.
460
461 >>> branch_url = '/' + no_import_branch.unique_name
462- >>> response = webservice.get(branch_url)
463+ >>> response = import_webservice.get(branch_url)
464 >>> representation = response.jsonBody()
465 >>> print representation['code_import_link']
466 None
467@@ -31,7 +40,7 @@
468 representation.
469
470 >>> branch_url = '/' + code_import.branch.unique_name
471- >>> response = webservice.get(branch_url)
472+ >>> response = import_webservice.get(branch_url)
473 >>> representation = response.jsonBody()
474 >>> print representation['code_import_link']
475 http://.../~import-owner/scruff/import/+code-import
476@@ -39,7 +48,7 @@
477 We can get some information about the import using this URL.
478
479 >>> import_url = representation['code_import_link']
480- >>> response = webservice.get(import_url)
481+ >>> response = import_webservice.get(import_url)
482 >>> representation = response.jsonBody()
483 >>> print representation['self_link'] == import_url
484 True
485@@ -69,17 +78,19 @@
486 >>> distroseries = factory.makeDistroSeries(
487 ... name='manic', distribution=distribution)
488 >>> source_package = factory.makeSourcePackage(
489- ... sourcename='scruff', distroseries=distroseries)
490+ ... sourcepackagename='scruff', distroseries=distroseries)
491 >>> code_import = removeSecurityProxy(factory.makePackageCodeImport(
492 ... registrant=person, sourcepackage=source_package,
493 ... branch_name='import',
494 ... svn_branch_url="http://svn.domain.com/package_source"))
495 >>> logout()
496+ >>> import_webservice = webservice_for_person(
497+ ... person, permission=OAuthPermission.WRITE_PUBLIC)
498
499 There is a link on the branch object
500
501 >>> branch_url = '/' + code_import.branch.unique_name
502- >>> response = webservice.get(branch_url)
503+ >>> response = import_webservice.get(branch_url)
504 >>> representation = response.jsonBody()
505 >>> print representation['code_import_link']
506 http://.../~import-owner/scruffbuntu/manic/scruff/import/+code-import
507@@ -87,7 +98,7 @@
508 and there is information available about the import itsef.
509
510 >>> import_url = representation['code_import_link']
511- >>> response = webservice.get(import_url)
512+ >>> response = import_webservice.get(import_url)
513 >>> representation = response.jsonBody()
514 >>> print representation['self_link'] == import_url
515 True
516@@ -106,3 +117,114 @@
517 >>> print representation['date_last_successful']
518 None
519
520+== Creating Imports ==
521+
522+We can create an import using the API by calling a method on the project.
523+
524+ >>> product_url = '/' + product.name
525+ >>> new_remote_url = factory.getUniqueURL()
526+ >>> response = import_webservice.named_post(product_url, 'newCodeImport',
527+ ... branch_name='new-import', rcs_type='Git',
528+ ... url=new_remote_url)
529+ >>> print response.status
530+ 201
531+ >>> location = response.getHeader('Location')
532+ >>> response = import_webservice.get(location)
533+ >>> representation = response.jsonBody()
534+ >>> print representation['self_link']
535+ http://.../~import-owner/scruff/new-import/+code-import
536+ >>> print representation['branch_link']
537+ http://.../~import-owner/scruff/new-import
538+ >>> print representation['rcs_type']
539+ Git
540+ >>> print representation['url'] == new_remote_url
541+ True
542+ >>> print representation['cvs_root']
543+ None
544+ >>> print representation['cvs_module']
545+ None
546+ >>> print representation['date_last_successful']
547+ None
548+
549+If we must we can create a CVS import.
550+
551+ >>> product_url = '/' + product.name
552+ >>> new_remote_url = factory.getUniqueURL()
553+ >>> response = import_webservice.named_post(product_url, 'newCodeImport',
554+ ... branch_name='cvs-import', rcs_type='Concurrent Versions System',
555+ ... cvs_root=new_remote_url, cvs_module="foo")
556+ >>> print response.status
557+ 201
558+ >>> location = response.getHeader('Location')
559+ >>> response = import_webservice.get(location)
560+ >>> representation = response.jsonBody()
561+ >>> print representation['self_link']
562+ http://.../~import-owner/scruff/cvs-import/+code-import
563+ >>> print representation['branch_link']
564+ http://.../~import-owner/scruff/cvs-import
565+ >>> print representation['rcs_type']
566+ Concurrent Versions System
567+ >>> print representation['url']
568+ None
569+ >>> print representation['cvs_root'] == new_remote_url
570+ True
571+ >>> print representation['cvs_module'] == "foo"
572+ True
573+ >>> print representation['date_last_successful']
574+ None
575+
576+We can also create an import targetting a source package.
577+
578+ >>> source_package_url = (
579+ ... '/' + distribution.name + '/' + distroseries.name + '/+source/'
580+ ... + source_package.name)
581+ >>> new_remote_url = factory.getUniqueURL()
582+ >>> response = import_webservice.named_post(source_package_url,
583+ ... 'newCodeImport', branch_name='new-import', rcs_type='Mercurial',
584+ ... url=new_remote_url)
585+ >>> print response.status
586+ 201
587+ >>> location = response.getHeader('Location')
588+ >>> response = import_webservice.get(location)
589+ >>> representation = response.jsonBody()
590+ >>> print representation['self_link']
591+ http://.../~import-owner/scruffbuntu/manic/scruff/new-import/+code-import
592+ >>> print representation['branch_link']
593+ http://.../~import-owner/scruffbuntu/manic/scruff/new-import
594+ >>> print representation['rcs_type']
595+ Mercurial
596+ >>> print representation['url'] == new_remote_url
597+ True
598+ >>> print representation['cvs_root']
599+ None
600+ >>> print representation['cvs_module']
601+ None
602+ >>> print representation['date_last_successful']
603+ None
604+
605+If we wish to create a branch owned by a team we are part of then we can.
606+
607+ >>> team_url = import_webservice.getAbsoluteUrl('/~import-owner-team')
608+ >>> new_remote_url = factory.getUniqueURL()
609+ >>> response = import_webservice.named_post(product_url, 'newCodeImport',
610+ ... branch_name='team-import', rcs_type='Git',
611+ ... url=new_remote_url, owner=team_url)
612+ >>> print response.status
613+ 201
614+ >>> location = response.getHeader('Location')
615+ >>> response = import_webservice.get(location)
616+ >>> representation = response.jsonBody()
617+ >>> print representation['self_link']
618+ http://.../~import-owner-team/scruff/team-import/+code-import
619+ >>> print representation['branch_link']
620+ http://.../~import-owner-team/scruff/team-import
621+ >>> print representation['rcs_type']
622+ Git
623+ >>> print representation['url'] == new_remote_url
624+ True
625+ >>> print representation['cvs_root']
626+ None
627+ >>> print representation['cvs_module']
628+ None
629+ >>> print representation['date_last_successful']
630+ None
631
632=== modified file 'lib/lp/registry/interfaces/product.py'
633--- lib/lp/registry/interfaces/product.py 2010-04-07 19:23:28 +0000
634+++ lib/lp/registry/interfaces/product.py 2010-04-13 15:21:36 +0000
635@@ -43,7 +43,8 @@
636 from lp.app.interfaces.headings import IRootContext
637 from lp.code.interfaces.branchvisibilitypolicy import (
638 IHasBranchVisibilityPolicy)
639-from lp.code.interfaces.hasbranches import IHasBranches, IHasMergeProposals
640+from lp.code.interfaces.hasbranches import (
641+ IHasBranches, IHasCodeImports, IHasMergeProposals)
642 from lp.code.interfaces.hasrecipes import IHasRecipes
643 from lp.bugs.interfaces.bugtarget import (
644 IBugTarget, IOfficialBugTagTargetPublic, IOfficialBugTagTargetRestricted)
645@@ -339,7 +340,8 @@
646 IHasLogo, IHasMentoringOffers, IHasMergeProposals, IHasMilestones,
647 IHasMugshot, IHasOwner, IHasSecurityContact, IHasSprints,
648 ITranslationPolicy, IKarmaContext, ILaunchpadUsage, IMakesAnnouncements,
649- IOfficialBugTagTargetPublic, IPillar, ISpecificationTarget, IHasRecipes):
650+ IOfficialBugTagTargetPublic, IPillar, ISpecificationTarget, IHasRecipes,
651+ IHasCodeImports):
652 """Public IProduct properties."""
653
654 # XXX Mark Shuttleworth 2004-10-12: Let's get rid of ID's in interfaces
655
656=== modified file 'lib/lp/registry/interfaces/sourcepackage.py'
657--- lib/lp/registry/interfaces/sourcepackage.py 2010-03-02 15:30:18 +0000
658+++ lib/lp/registry/interfaces/sourcepackage.py 2010-04-13 15:21:36 +0000
659@@ -22,7 +22,8 @@
660
661 from canonical.launchpad import _
662 from lp.bugs.interfaces.bugtarget import IBugTarget, IHasOfficialBugTags
663-from lp.code.interfaces.hasbranches import IHasBranches, IHasMergeProposals
664+from lp.code.interfaces.hasbranches import (
665+ IHasBranches, IHasCodeImports, IHasMergeProposals)
666 from lp.soyuz.interfaces.component import IComponent
667 from lazr.restful.fields import Reference, ReferenceChoice
668 from lazr.restful.declarations import (
669@@ -32,7 +33,7 @@
670
671
672 class ISourcePackage(IBugTarget, IHasBranches, IHasMergeProposals,
673- IHasOfficialBugTags):
674+ IHasOfficialBugTags, IHasCodeImports):
675 """A SourcePackage. See the MagicSourcePackage specification. This
676 interface preserves as much as possible of the old SourcePackage
677 interface from the SourcePackage table, with the new table-less
678
679=== modified file 'lib/lp/registry/model/product.py'
680--- lib/lp/registry/model/product.py 2010-04-09 19:11:36 +0000
681+++ lib/lp/registry/model/product.py 2010-04-13 15:21:36 +0000
682@@ -33,7 +33,8 @@
683 from canonical.launchpad.interfaces.lpstorm import IStore
684 from lp.code.model.branchvisibilitypolicy import (
685 BranchVisibilityPolicyMixin)
686-from lp.code.model.hasbranches import HasBranchesMixin, HasMergeProposalsMixin
687+from lp.code.model.hasbranches import (
688+ HasBranchesMixin, HasCodeImportsMixin, HasMergeProposalsMixin)
689 from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
690 from lp.code.model.sourcepackagerecipedata import SourcePackageRecipeData
691 from lp.bugs.interfaces.bugtarget import IHasBugHeat
692@@ -173,8 +174,7 @@
693 HasAliasMixin, StructuralSubscriptionTargetMixin,
694 HasMilestonesMixin, OfficialBugTagTargetMixin, HasBranchesMixin,
695 HasCustomLanguageCodesMixin, HasMergeProposalsMixin,
696- HasBugHeatMixin):
697-
698+ HasBugHeatMixin, HasCodeImportsMixin):
699 """A Product."""
700
701 implements(
702
703=== modified file 'lib/lp/registry/model/sourcepackage.py'
704--- lib/lp/registry/model/sourcepackage.py 2010-04-12 08:29:02 +0000
705+++ lib/lp/registry/model/sourcepackage.py 2010-04-13 15:21:36 +0000
706@@ -23,7 +23,8 @@
707 from canonical.lazr.utils import smartquote
708 from lp.buildmaster.interfaces.buildbase import BuildStatus
709 from lp.code.model.branch import Branch
710-from lp.code.model.hasbranches import HasBranchesMixin, HasMergeProposalsMixin
711+from lp.code.model.hasbranches import (
712+ HasBranchesMixin, HasCodeImportsMixin, HasMergeProposalsMixin)
713 from lp.bugs.interfaces.bugtarget import IHasBugHeat
714 from lp.bugs.model.bug import get_bug_tags_open_count
715 from lp.bugs.model.bugtarget import BugTargetBase, HasBugHeatMixin
716@@ -160,7 +161,7 @@
717 class SourcePackage(BugTargetBase, SourcePackageQuestionTargetMixin,
718 HasTranslationImportsMixin, HasTranslationTemplatesMixin,
719 HasBranchesMixin, HasMergeProposalsMixin,
720- HasBugHeatMixin):
721+ HasBugHeatMixin, HasCodeImportsMixin):
722 """A source package, e.g. apache2, in a distroseries.
723
724 This object is not a true database object, but rather attempts to
725@@ -681,7 +682,6 @@
726 our_format = PackageUploadCustomFormat.ROSETTA_TRANSLATIONS
727
728 packagename = self.sourcepackagename.name
729- displayname = self.displayname
730 distro = self.distroseries.distribution
731
732 histories = distro.main_archive.getPublishedSources(