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

Proposed by James Westby
Status: Merged
Approved by: Michael Hudson-Doyle
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~james-w/launchpad/code-import-request
Merge into: lp:launchpad
Prerequisite: lp:~james-w/launchpad/register-code-import
Diff against target: 325 lines (+183/-17)
8 files modified
lib/lp/code/browser/branch.py (+13/-14)
lib/lp/code/configure.zcml (+2/-1)
lib/lp/code/errors.py (+23/-0)
lib/lp/code/interfaces/codeimport.py (+29/-1)
lib/lp/code/interfaces/webservice.py (+3/-1)
lib/lp/code/model/codeimport.py (+24/-0)
lib/lp/code/model/tests/test_codeimport.py (+67/-0)
lib/lp/code/stories/webservice/xx-code-import.txt (+22/-0)
To merge this branch: bzr merge lp:~james-w/launchpad/code-import-request
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle Approve
Review via email: mp+22727@code.launchpad.net

Commit message

ICodeImport.requestImport() is now available over the API.

Description of the change

Hi,

This branch adds ICodeImport.requestImport() over the API.

It first adds the method on the interface and model, with associated tests, then exports it over the
webservice with a test that calls the method using that. Lastly it ports the browser code to use
the method to reduce duplication.

The most interesting thing here is the exception handling.

To make a nice API for scripting it doesn't error if it is already queued, as the outcome is the same,
but the browser code needs to know this, so it passes a parameter to find out.

Catching the exceptions in the browser code isn't too pretty, but seemed like the best way to balance
the needs of the two APIs.

The exceptions that can be thrown during webservice access are both marked to trigger a 400 response.
This isn't ideal, as scripts will have a tough time distinguishing them, but choosing arbitrary status codes
to do that is equally ugly. The docstring at least explains what can happen and tells you how to check
for it.

Thanks,

James

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

Hi,

This adds requestImport() to ICodeImport to allow you to request
that an import happens ASAP.

Thanks,

James

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Subject to a few test cleanups mentioned on skype, this is fine.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/branch.py'
2--- lib/lp/code/browser/branch.py 2010-03-25 15:28:49 +0000
3+++ lib/lp/code/browser/branch.py 2010-04-14 11:34:34 +0000
4@@ -81,10 +81,12 @@
5 from lp.code.browser.branchmergeproposal import (
6 latest_proposals_for_each_branch)
7 from lp.code.enums import (
8- BranchLifecycleStatus, BranchType, CodeImportJobState,
9+ BranchLifecycleStatus, BranchType,
10 CodeImportResultStatus, CodeImportReviewStatus, RevisionControlSystems,
11 UICreatableBranchType)
12-from lp.code.errors import InvalidBranchMergeProposal
13+from lp.code.errors import (
14+ CodeImportAlreadyRequested, CodeImportAlreadyRunning,
15+ CodeImportNotInReviewedState, InvalidBranchMergeProposal)
16 from lp.code.interfaces.branch import (
17 BranchCreationForbidden, BranchExists, IBranch,
18 user_has_special_branch_access)
19@@ -1317,26 +1319,23 @@
20
21 @action('Import Now', name='request')
22 def request_import_action(self, action, data):
23- if self.context.code_import.import_job is None:
24+ try:
25+ self.context.code_import.requestImport(
26+ self.user, error_if_already_requested=True)
27+ self.request.response.addNotification(
28+ "Import will run as soon as possible.")
29+ except CodeImportNotInReviewedState:
30 self.request.response.addNotification(
31 "This import is no longer being updated automatically.")
32- elif (self.context.code_import.import_job.state !=
33- CodeImportJobState.PENDING):
34- assert (self.context.code_import.import_job.state ==
35- CodeImportJobState.RUNNING)
36+ except CodeImportAlreadyRunning:
37 self.request.response.addNotification(
38 "The import is already running.")
39- elif self.context.code_import.import_job.requesting_user is not None:
40- user = self.context.code_import.import_job.requesting_user
41+ except CodeImportAlreadyRequested, e:
42+ user = e.requesting_user
43 adapter = queryAdapter(user, IPathAdapter, 'fmt')
44 self.request.response.addNotification(
45 structured("The import has already been requested by %s." %
46 adapter.link(None)))
47- else:
48- getUtility(ICodeImportJobWorkflow).requestJob(
49- self.context.code_import.import_job, self.user)
50- self.request.response.addNotification(
51- "Import will run as soon as possible.")
52
53 @property
54 def prefix(self):
55
56=== modified file 'lib/lp/code/configure.zcml'
57--- lib/lp/code/configure.zcml 2010-04-12 14:51:11 +0000
58+++ lib/lp/code/configure.zcml 2010-04-14 11:34:34 +0000
59@@ -789,7 +789,8 @@
60 getImportDetailsForDisplay"/>
61 <require
62 permission="launchpad.AnyPerson"
63- attributes="tryFailingImportAgain"/>
64+ attributes="tryFailingImportAgain
65+ requestImport"/>
66 <require
67 permission="launchpad.Edit"
68 attributes="updateFromData"/>
69
70=== modified file 'lib/lp/code/errors.py'
71--- lib/lp/code/errors.py 2010-04-05 21:38:40 +0000
72+++ lib/lp/code/errors.py 2010-04-14 11:34:34 +0000
73@@ -8,6 +8,9 @@
74 'BadBranchMergeProposalSearchContext',
75 'BadStateTransition',
76 'BranchMergeProposalExists',
77+ 'CodeImportAlreadyRequested',
78+ 'CodeImportAlreadyRunning',
79+ 'CodeImportNotInReviewedState',
80 'ClaimReviewFailed',
81 'InvalidBranchMergeProposal',
82 'ReviewNotPending',
83@@ -68,3 +71,23 @@
84
85 class UnknownBranchTypeError(Exception):
86 """Raised when the user specifies an unrecognized branch type."""
87+
88+
89+class CodeImportNotInReviewedState(Exception):
90+ """Raised when the user requests an import of a non-automatic import."""
91+
92+ webservice_error(400)
93+
94+
95+class CodeImportAlreadyRequested(Exception):
96+ """Raised when the user requests an import that is already requested."""
97+
98+ def __init__(self, msg, requesting_user):
99+ super(CodeImportAlreadyRequested, self).__init__(msg)
100+ self.requesting_user = requesting_user
101+
102+
103+class CodeImportAlreadyRunning(Exception):
104+ """Raised when the user requests an import that is already running."""
105+
106+ webservice_error(400)
107
108=== modified file 'lib/lp/code/interfaces/codeimport.py'
109--- lib/lp/code/interfaces/codeimport.py 2010-04-01 20:58:42 +0000
110+++ lib/lp/code/interfaces/codeimport.py 2010-04-14 11:34:34 +0000
111@@ -25,7 +25,8 @@
112 from lp.code.interfaces.branch import IBranch
113
114 from lazr.restful.declarations import (
115- export_as_webservice_entry, exported)
116+ call_with, export_as_webservice_entry, exported, export_write_operation,
117+ REQUEST_USER)
118 from lazr.restful.fields import ReferenceChoice
119
120
121@@ -177,6 +178,33 @@
122 :param user: the user who is requesting the import be tried again.
123 """
124
125+ @call_with(requester=REQUEST_USER)
126+ @export_write_operation()
127+ def requestImport(requester, error_if_already_requested=False):
128+ """Request that an import be tried soon.
129+
130+ This method will schedule an import to happen soon for this branch.
131+
132+ The import must be in the Reviewed state, if not then a
133+ CodeImportNotInReviewedState error will be thrown. If using the
134+ API then a status code of 400 will result.
135+
136+ If the import is already running then a CodeImportAlreadyRunning
137+ error will be thrown. If using the API then a status code of
138+ 400 will result.
139+
140+ The two cases can be distinguished over the API by seeing if the
141+ exception names appear in the body of the response.
142+
143+ If used over the API and the request has already been made then this
144+ method will silently do nothing.
145+ If called internally then the error_if_already_requested parameter
146+ controls whether a CodeImportAlreadyRequested exception will be
147+ thrown in that situation.
148+
149+ :return: None
150+ """
151+
152
153 class ICodeImportSet(Interface):
154 """Interface representing the set of code imports."""
155
156=== modified file 'lib/lp/code/interfaces/webservice.py'
157--- lib/lp/code/interfaces/webservice.py 2010-04-01 23:04:10 +0000
158+++ lib/lp/code/interfaces/webservice.py 2010-04-14 11:34:34 +0000
159@@ -5,7 +5,9 @@
160
161 # The exceptions are imported so that they can produce the special
162 # status code defined by webservice_error when they are raised.
163-from lp.code.errors import BranchMergeProposalExists
164+from lp.code.errors import (
165+ BranchMergeProposalExists, CodeImportAlreadyRunning,
166+ CodeImportNotInReviewedState)
167 from lp.code.interfaces.branch import (
168 IBranch, IBranchSet, BranchCreatorNotMemberOfOwnerTeam,
169 BranchCreatorNotOwner, BranchExists)
170
171=== modified file 'lib/lp/code/model/codeimport.py'
172--- lib/lp/code/model/codeimport.py 2010-04-01 20:58:42 +0000
173+++ lib/lp/code/model/codeimport.py 2010-04-14 11:34:34 +0000
174@@ -37,6 +37,9 @@
175 from lp.code.enums import (
176 BranchType, CodeImportJobState, CodeImportResultStatus,
177 CodeImportReviewStatus, RevisionControlSystems)
178+from lp.code.errors import (
179+ CodeImportAlreadyRequested, CodeImportAlreadyRunning,
180+ CodeImportNotInReviewedState)
181 from lp.code.interfaces.codeimport import ICodeImport, ICodeImportSet
182 from lp.code.interfaces.codeimportevent import ICodeImportEventSet
183 from lp.code.interfaces.codeimportjob import ICodeImportJobWorkflow
184@@ -196,6 +199,27 @@
185 {'review_status': CodeImportReviewStatus.REVIEWED}, user)
186 getUtility(ICodeImportJobWorkflow).requestJob(self.import_job, user)
187
188+ def requestImport(self, requester, error_if_already_requested=False):
189+ """See `ICodeImport`."""
190+ if self.import_job is None: # not in automatic mode
191+ raise CodeImportNotInReviewedState("This code import is %s, and "
192+ "must be Reviewed for you to call requestImport."
193+ % self.review_status.name)
194+ if (self.import_job.state != CodeImportJobState.PENDING):
195+ assert (self.import_job.state == CodeImportJobState.RUNNING)
196+ # Already running
197+ raise CodeImportAlreadyRunning("This code import is already "
198+ "running.")
199+ elif self.import_job.requesting_user is not None:
200+ if error_if_already_requested:
201+ raise CodeImportAlreadyRequested("This code import has "
202+ "already been requested to run.",
203+ self.import_job.requesting_user)
204+ else:
205+ getUtility(ICodeImportJobWorkflow).requestJob(
206+ self.import_job, requester)
207+ return None
208+
209
210 class CodeImportSet:
211 """See `ICodeImportSet`."""
212
213=== modified file 'lib/lp/code/model/tests/test_codeimport.py'
214--- lib/lp/code/model/tests/test_codeimport.py 2010-04-01 20:58:42 +0000
215+++ lib/lp/code/model/tests/test_codeimport.py 2010-04-14 11:34:34 +0000
216@@ -12,6 +12,11 @@
217 from zope.component import getUtility
218 from zope.security.proxy import removeSecurityProxy
219
220+from canonical.launchpad.testing.codeimporthelpers import (
221+ make_running_import)
222+from lp.code.errors import (
223+ CodeImportAlreadyRequested, CodeImportAlreadyRunning,
224+ CodeImportNotInReviewedState)
225 from lp.code.model.codeimport import CodeImportSet
226 from lp.code.model.codeimportevent import CodeImportEvent
227 from lp.code.model.codeimportjob import CodeImportJob, CodeImportJobSet
228@@ -620,5 +625,67 @@
229 requester, code_import.import_job.requesting_user)
230
231
232+class TestRequestImport(TestCaseWithFactory):
233+ """Tests for `ICodeImport.requestImport`."""
234+
235+ layer = DatabaseFunctionalLayer
236+
237+ def setUp(self):
238+ # We have to be logged in to request imports
239+ TestCaseWithFactory.setUp(self, user='no-priv@canonical.com')
240+
241+ def test_requestsJob(self):
242+ code_import = self.factory.makeCodeImport(
243+ git_repo_url=self.factory.getUniqueURL())
244+ requester = self.factory.makePerson()
245+ old_date = code_import.import_job.date_due
246+ code_import.requestImport(requester)
247+ self.assertEqual(requester, code_import.import_job.requesting_user)
248+ self.assertTrue(code_import.import_job.date_due <= old_date)
249+
250+ def test_noop_if_already_requested(self):
251+ code_import = self.factory.makeCodeImport(
252+ git_repo_url=self.factory.getUniqueURL())
253+ requester = self.factory.makePerson()
254+ code_import.requestImport(requester)
255+ old_date = code_import.import_job.date_due
256+ code_import.requestImport(requester)
257+ # The checks don't matter so much, it's more that we don't get
258+ # an exception.
259+ self.assertEqual(requester, code_import.import_job.requesting_user)
260+ self.assertEqual(old_date, code_import.import_job.date_due)
261+
262+ def test_optional_error_if_already_requested(self):
263+ code_import = self.factory.makeCodeImport(
264+ git_repo_url=self.factory.getUniqueURL())
265+ requester = self.factory.makePerson()
266+ code_import.requestImport(requester)
267+ old_date = code_import.import_job.date_due
268+ e = self.assertRaises(
269+ CodeImportAlreadyRequested, code_import.requestImport, requester,
270+ error_if_already_requested=True)
271+ self.assertEqual(requester, e.requesting_user)
272+
273+ def test_exception_on_disabled(self):
274+ # get an SVN request, which isn't reviewed by default
275+ code_import = self.factory.makeCodeImport(
276+ svn_branch_url=self.factory.getUniqueURL())
277+ requester = self.factory.makePerson()
278+ # which leads to an exception if we try and ask for an import
279+ self.assertRaises(
280+ CodeImportNotInReviewedState, code_import.requestImport,
281+ requester)
282+
283+ def test_exception_if_already_running(self):
284+ code_import = self.factory.makeCodeImport(
285+ git_repo_url=self.factory.getUniqueURL())
286+ code_import = make_running_import(factory=self.factory,
287+ code_import=code_import)
288+ requester = self.factory.makePerson()
289+ self.assertRaises(
290+ CodeImportAlreadyRunning, code_import.requestImport,
291+ requester)
292+
293+
294 def test_suite():
295 return unittest.TestLoader().loadTestsFromName(__name__)
296
297=== modified file 'lib/lp/code/stories/webservice/xx-code-import.txt'
298--- lib/lp/code/stories/webservice/xx-code-import.txt 2010-04-13 14:56:00 +0000
299+++ lib/lp/code/stories/webservice/xx-code-import.txt 2010-04-14 11:34:34 +0000
300@@ -228,3 +228,25 @@
301 None
302 >>> print representation['date_last_successful']
303 None
304+
305+
306+== Requesting an Import ==
307+
308+You can request that an approved, working import happen soon over the
309+API using the requestImport() method.
310+
311+ >>> login(ANONYMOUS)
312+ >>> git_import = factory.makeProductCodeImport(
313+ ... registrant=person, product=product, branch_name='git-import',
314+ ... git_repo_url=factory.getUniqueURL())
315+ >>> git_import_url = (
316+ ... '/' + git_import.branch.unique_name + '/+code-import')
317+ >>> logout()
318+ >>> import_webservice = webservice_for_person(
319+ ... person, permission=OAuthPermission.WRITE_PUBLIC)
320+ >>> response = import_webservice.named_post(
321+ ... git_import_url, 'requestImport')
322+ >>> print response.status
323+ 200
324+ >>> print response.jsonBody()
325+ None