Merge lp:~bac/launchpad/bug-524302 into lp:launchpad/db-devel
- bug-524302
- Merge into db-devel
Status: | Merged |
---|---|
Approved by: | Curtis Hovey |
Approved revision: | no longer in the source branch. |
Merged at revision: | not available |
Proposed branch: | lp:~bac/launchpad/bug-524302 |
Merge into: | lp:launchpad/db-devel |
Prerequisite: | lp:~bac/launchpad/productseries-js |
Diff against target: |
1830 lines (+1077/-379) 23 files modified
lib/lp/app/templates/base-layout-macros.pt (+5/-2) lib/lp/code/browser/bazaar.py (+3/-1) lib/lp/code/browser/branch.py (+3/-2) lib/lp/code/browser/configure.zcml (+0/-7) lib/lp/code/interfaces/codeimport.py (+0/-10) lib/lp/code/javascript/tests/test_productseries_setbranch.js (+3/-3) lib/lp/code/model/codeimport.py (+1/-49) lib/lp/code/model/tests/test_codeimport.py (+0/-194) lib/lp/code/stories/branches/xx-bazaar-home.txt (+1/-1) lib/lp/code/stories/branches/xx-branchmergeproposals.txt (+4/-1) lib/lp/code/stories/branches/xx-propose-for-merging.txt (+2/-0) lib/lp/code/stories/codeimport/xx-codeimport-list.txt (+0/-72) lib/lp/code/stories/codeimport/xx-codeimport-view.txt (+3/-3) lib/lp/code/templates/bazaar-index.pt (+1/-1) lib/lp/code/templates/branchmergeproposal-pagelet-summary.pt (+4/-0) lib/lp/registry/browser/configure.zcml (+7/-0) lib/lp/registry/browser/productseries.py (+380/-24) lib/lp/registry/browser/tests/productseries-setbranch-view.txt (+339/-0) lib/lp/registry/stories/productseries/xx-productseries-set-branch.txt (+147/-0) lib/lp/registry/templates/productseries-codesummary.pt (+3/-3) lib/lp/registry/templates/productseries-linkbranch.pt (+38/-2) lib/lp/registry/templates/productseries-setbranch.pt (+129/-0) lib/lp/testing/factory.py (+4/-4) |
To merge this branch: | bzr merge lp:~bac/launchpad/bug-524302 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Curtis Hovey (community) | code, ui | Approve | |
Edwin Grubbs (community) | code ui* | Approve | |
Review via email: mp+22180@code.launchpad.net |
Commit message
Create a productseries/
Description of the change
Add productseries/
The view is not currently navigable from anywhere. Eventually it will replace +linkbranch.
A new view test has been created:
bin/test -vvt productseries-
It may be preferred to roll that test into the productseries-
There are some known issues listed in the BRANCH.TODO file.
To demo, create a new project and go to https:/
Brad Crittenden (bac) wrote : | # |
Thanks for the excellent review Edwin. I've incorporated all of your suggestions. Having the view create the rendered items was a little more difficult due to the two different types of vocabularies.
I also took care of the items in my BRANCH.TODO list including not masking widget validation errors (which you also noted) and catching an error condition when an import URL has been requested before to avoid a db IntegrityError.
Finally I added a story test to show the high-level working of the new page.
Brad Crittenden (bac) wrote : | # |
Incremental diff at http://
Edwin Grubbs (edwin-grubbs) wrote : | # |
Hi Brad,
Thanks for making all the changes. This branch is definitely a lot harder than
I thought.
Since this branch is not linked anywhere yet, I would be ok with you deferring
items 2 and 3 below to a followup branch so you can land this before it gets
any bigger. I also have some comments inline, but they shouldn't be too hard
to fix in this branch.
merge-conditional
1. jslint: Lint found in '/home/
Line 65 character 15: The body of a for in should be wrapped in an if statement to filter unwanted properties from the prototype.
for (var sub in subscribers) {
The easiest way to avoid this potential Javascript problem is:
2. If the Branch name already exists, it appears as if the form does nothing.
3. If there is an existing imported SVN branch on a given URL, the form will
give you the correct error message, but if there is an existing imported BZR
branch on a given URL, it will give you this exception.
Traceback (most recent call last):
File "/home/
result = publication.
File "/home/
return mapply(ob, request.
File "/home/
return debug_call(obj, args)
File "/home/
return obj(*args)
File "/home/
self.
File "/home/
self.
File "/home/
return self.success_
File "/home/
data[
File "/home/
url=repo_url)
File "/home/
implicit_
File "/home/
rule = self.product.
File "/home/
item = self._selectOne
File "/home/...
Brad Crittenden (bac) wrote : | # |
Edwin,
Thanks for the review and the ideas about cleaning up the render() method.
I am going to defer the other two items for another, quick follow-on branch.
The incremental is at:
http://
Brad Crittenden (bac) wrote : | # |
Curtis a screenshot is available at http://
Curtis Hovey (sinzui) wrote : | # |
Hi Brad.
I think Branch name and owner are confusing. I think they subordinate to importing or creating a new branch but they appear to be enabled for setting the series to an existing branch. I know I cannot change the owner or name for the first option. I expect these two options to be, and appear to be, disbaled when I choose Link to a bazaar branch in Launchpad.
I do not see any test to verify the script is loaded on the page. Either we need to show that the
script is in the page or use windmill to verify it executed.
Brad Crittenden (bac) wrote : | # |
As we discussed, the branch name and owner are conditionally disabled correctly, though the screen shot does not show it well.
The extra windmill test will be added to the follow up branch.
Curtis Hovey (sinzui) : | # |
Preview Diff
1 | === modified file 'lib/lp/app/templates/base-layout-macros.pt' | |||
2 | --- lib/lp/app/templates/base-layout-macros.pt 2010-03-17 23:17:46 +0000 | |||
3 | +++ lib/lp/app/templates/base-layout-macros.pt 2010-04-07 13:24:38 +0000 | |||
4 | @@ -202,8 +202,11 @@ | |||
5 | 202 | tal:attributes="src string:${lp_js}/code/branchmergeproposal.status.js"> | 202 | tal:attributes="src string:${lp_js}/code/branchmergeproposal.status.js"> |
6 | 203 | </script> | 203 | </script> |
7 | 204 | <script type="text/javascript" | 204 | <script type="text/javascript" |
10 | 205 | tal:attributes="src string:${lp_js}/code/branchmergeproposal.reviewcomment.js"></script> | 205 | tal:attributes="src string:${lp_js}/code/branchmergeproposal.reviewcomment.js"> |
11 | 206 | 206 | </script> | |
12 | 207 | <script type="text/javascript" | ||
13 | 208 | tal:attributes="src string:${lp_js}/code/productseries-setbranch.js"> | ||
14 | 209 | </script> | ||
15 | 207 | <script type="text/javascript" | 210 | <script type="text/javascript" |
16 | 208 | tal:attributes="src string:${lp_js}/lp/comment.js"></script> | 211 | tal:attributes="src string:${lp_js}/lp/comment.js"></script> |
17 | 209 | <script type="text/javascript" | 212 | <script type="text/javascript" |
18 | 210 | 213 | ||
19 | === modified file 'lib/lp/code/browser/bazaar.py' | |||
20 | --- lib/lp/code/browser/bazaar.py 2009-08-28 01:31:51 +0000 | |||
21 | +++ lib/lp/code/browser/bazaar.py 2010-04-07 13:24:38 +0000 | |||
22 | @@ -21,6 +21,7 @@ | |||
23 | 21 | from canonical.launchpad.webapp.authorization import ( | 21 | from canonical.launchpad.webapp.authorization import ( |
24 | 22 | precache_permission_for_objects) | 22 | precache_permission_for_objects) |
25 | 23 | 23 | ||
26 | 24 | from lp.code.enums import CodeImportReviewStatus | ||
27 | 24 | from lp.code.interfaces.branch import IBranchCloud, IBranchSet | 25 | from lp.code.interfaces.branch import IBranchCloud, IBranchSet |
28 | 25 | from lp.code.interfaces.branchcollection import IAllBranches | 26 | from lp.code.interfaces.branchcollection import IAllBranches |
29 | 26 | from lp.code.interfaces.codeimport import ICodeImportSet | 27 | from lp.code.interfaces.codeimport import ICodeImportSet |
30 | @@ -61,7 +62,8 @@ | |||
31 | 61 | 62 | ||
32 | 62 | @property | 63 | @property |
33 | 63 | def import_count(self): | 64 | def import_count(self): |
35 | 64 | return getUtility(ICodeImportSet).getActiveImports().count() | 65 | return getUtility(ICodeImportSet).search( |
36 | 66 | review_status=CodeImportReviewStatus.REVIEWED).count() | ||
37 | 65 | 67 | ||
38 | 66 | @property | 68 | @property |
39 | 67 | def bzr_version(self): | 69 | def bzr_version(self): |
40 | 68 | 70 | ||
41 | === modified file 'lib/lp/code/browser/branch.py' | |||
42 | --- lib/lp/code/browser/branch.py 2010-03-16 19:04:48 +0000 | |||
43 | +++ lib/lp/code/browser/branch.py 2010-04-07 13:24:38 +0000 | |||
44 | @@ -16,6 +16,7 @@ | |||
45 | 16 | 'BranchReviewerEditView', | 16 | 'BranchReviewerEditView', |
46 | 17 | 'BranchMergeQueueView', | 17 | 'BranchMergeQueueView', |
47 | 18 | 'BranchMirrorStatusView', | 18 | 'BranchMirrorStatusView', |
48 | 19 | 'BranchNameValidationMixin', | ||
49 | 19 | 'BranchNavigation', | 20 | 'BranchNavigation', |
50 | 20 | 'BranchEditMenu', | 21 | 'BranchEditMenu', |
51 | 21 | 'BranchInProductView', | 22 | 'BranchInProductView', |
52 | @@ -612,7 +613,7 @@ | |||
53 | 612 | class BranchNameValidationMixin: | 613 | class BranchNameValidationMixin: |
54 | 613 | """Provide name validation logic used by several branch view classes.""" | 614 | """Provide name validation logic used by several branch view classes.""" |
55 | 614 | 615 | ||
57 | 615 | def _setBranchExists(self, existing_branch): | 616 | def _setBranchExists(self, existing_branch, field_name='name'): |
58 | 616 | owner = existing_branch.owner | 617 | owner = existing_branch.owner |
59 | 617 | if owner == self.user: | 618 | if owner == self.user: |
60 | 618 | prefix = "You already have" | 619 | prefix = "You already have" |
61 | @@ -622,7 +623,7 @@ | |||
62 | 622 | "%s a branch for <em>%s</em> called <em>%s</em>." | 623 | "%s a branch for <em>%s</em> called <em>%s</em>." |
63 | 623 | % (prefix, existing_branch.target.displayname, | 624 | % (prefix, existing_branch.target.displayname, |
64 | 624 | existing_branch.name)) | 625 | existing_branch.name)) |
66 | 625 | self.setFieldError('name', structured(message)) | 626 | self.setFieldError(field_name, structured(message)) |
67 | 626 | 627 | ||
68 | 627 | 628 | ||
69 | 628 | class BranchEditSchema(Interface): | 629 | class BranchEditSchema(Interface): |
70 | 629 | 630 | ||
71 | === modified file 'lib/lp/code/browser/configure.zcml' | |||
72 | --- lib/lp/code/browser/configure.zcml 2010-03-18 17:30:14 +0000 | |||
73 | +++ lib/lp/code/browser/configure.zcml 2010-04-07 13:24:38 +0000 | |||
74 | @@ -57,13 +57,6 @@ | |||
75 | 57 | permission="zope.Public" | 57 | permission="zope.Public" |
76 | 58 | /> | 58 | /> |
77 | 59 | <browser:page | 59 | <browser:page |
78 | 60 | for="canonical.launchpad.webapp.interfaces.ILaunchpadApplication" | ||
79 | 61 | name="+code-import-list" | ||
80 | 62 | class="lp.registry.browser.productseries.ProductSeriesSourceListView" | ||
81 | 63 | template="../templates/sources-list.pt" | ||
82 | 64 | permission="zope.Public" | ||
83 | 65 | /> | ||
84 | 66 | <browser:page | ||
85 | 67 | for="zope.interface.Interface" | 60 | for="zope.interface.Interface" |
86 | 68 | name="+test-webservice-js" | 61 | name="+test-webservice-js" |
87 | 69 | template="../../../canonical/launchpad/templates/test-webservice-js.pt" | 62 | template="../../../canonical/launchpad/templates/test-webservice-js.pt" |
88 | 70 | 63 | ||
89 | === modified file 'lib/lp/code/interfaces/codeimport.py' | |||
90 | --- lib/lp/code/interfaces/codeimport.py 2010-03-26 01:05:26 +0000 | |||
91 | +++ lib/lp/code/interfaces/codeimport.py 2010-04-07 13:24:38 +0000 | |||
92 | @@ -188,16 +188,6 @@ | |||
93 | 188 | :param target: An `IBranchTarget` that the code is associated with. | 188 | :param target: An `IBranchTarget` that the code is associated with. |
94 | 189 | """ | 189 | """ |
95 | 190 | 190 | ||
96 | 191 | def getActiveImports(text=None): | ||
97 | 192 | """Return an iterable of all 'active' CodeImport objects. | ||
98 | 193 | |||
99 | 194 | Active is defined, somewhat arbitrarily, as having | ||
100 | 195 | review_status==REVIEWED and having completed at least once. | ||
101 | 196 | |||
102 | 197 | :param text: If specifed, limit to the results to those that contain | ||
103 | 198 | ``text`` in the product or project titles and descriptions. | ||
104 | 199 | """ | ||
105 | 200 | |||
106 | 201 | def get(id): | 191 | def get(id): |
107 | 202 | """Get a CodeImport by its id. | 192 | """Get a CodeImport by its id. |
108 | 203 | 193 | ||
109 | 204 | 194 | ||
110 | === modified file 'lib/lp/code/javascript/tests/test_productseries_setbranch.js' | |||
111 | --- lib/lp/code/javascript/tests/test_productseries_setbranch.js 2010-04-05 18:58:07 +0000 | |||
112 | +++ lib/lp/code/javascript/tests/test_productseries_setbranch.js 2010-04-07 13:24:38 +0000 | |||
113 | @@ -61,10 +61,10 @@ | |||
114 | 61 | var custom_events = Y.Event.getListeners(field, 'click'); | 61 | var custom_events = Y.Event.getListeners(field, 'click'); |
115 | 62 | var click_event = custom_events[0]; | 62 | var click_event = custom_events[0]; |
116 | 63 | var subscribers = click_event.subscribers; | 63 | var subscribers = click_event.subscribers; |
119 | 64 | for (var sub in subscribers) { | 64 | Y.each(subscribers, function(sub) { |
120 | 65 | Y.Assert.isTrue(subscribers[sub].contains(expected), | 65 | Y.Assert.isTrue(sub.contains(expected), |
121 | 66 | 'branch_type_onclick handler setup'); | 66 | 'branch_type_onclick handler setup'); |
123 | 67 | }; | 67 | }); |
124 | 68 | }; | 68 | }; |
125 | 69 | 69 | ||
126 | 70 | check_handler(this.link_lp_bzr, module.onclick_branch_type); | 70 | check_handler(this.link_lp_bzr, module.onclick_branch_type); |
127 | 71 | 71 | ||
128 | === modified file 'lib/lp/code/model/codeimport.py' | |||
129 | --- lib/lp/code/model/codeimport.py 2010-03-18 15:39:58 +0000 | |||
130 | +++ lib/lp/code/model/codeimport.py 2010-04-07 13:24:38 +0000 | |||
131 | @@ -30,10 +30,9 @@ | |||
132 | 30 | from canonical.database.constants import DEFAULT | 30 | from canonical.database.constants import DEFAULT |
133 | 31 | from canonical.database.datetimecol import UtcDateTimeCol | 31 | from canonical.database.datetimecol import UtcDateTimeCol |
134 | 32 | from canonical.database.enumcol import EnumCol | 32 | from canonical.database.enumcol import EnumCol |
136 | 33 | from canonical.database.sqlbase import SQLBase, quote, sqlvalues | 33 | from canonical.database.sqlbase import SQLBase |
137 | 34 | from canonical.launchpad.interfaces import IStore | 34 | from canonical.launchpad.interfaces import IStore |
138 | 35 | from lp.code.model.codeimportjob import CodeImportJobWorkflow | 35 | from lp.code.model.codeimportjob import CodeImportJobWorkflow |
139 | 36 | from lp.registry.model.productseries import ProductSeries | ||
140 | 37 | from canonical.launchpad.webapp.interfaces import NotFoundError | 36 | from canonical.launchpad.webapp.interfaces import NotFoundError |
141 | 38 | from lp.code.enums import ( | 37 | from lp.code.enums import ( |
142 | 39 | BranchType, CodeImportJobState, CodeImportResultStatus, | 38 | BranchType, CodeImportJobState, CodeImportResultStatus, |
143 | @@ -41,8 +40,6 @@ | |||
144 | 41 | from lp.code.interfaces.codeimport import ICodeImport, ICodeImportSet | 40 | from lp.code.interfaces.codeimport import ICodeImport, ICodeImportSet |
145 | 42 | from lp.code.interfaces.codeimportevent import ICodeImportEventSet | 41 | from lp.code.interfaces.codeimportevent import ICodeImportEventSet |
146 | 43 | from lp.code.interfaces.codeimportjob import ICodeImportJobWorkflow | 42 | from lp.code.interfaces.codeimportjob import ICodeImportJobWorkflow |
147 | 44 | from lp.code.interfaces.branchnamespace import ( | ||
148 | 45 | get_branch_namespace) | ||
149 | 46 | from lp.code.model.codeimportresult import CodeImportResult | 43 | from lp.code.model.codeimportresult import CodeImportResult |
150 | 47 | from lp.code.mail.codeimport import code_import_updated | 44 | from lp.code.mail.codeimport import code_import_updated |
151 | 48 | from lp.registry.interfaces.person import validate_public_person | 45 | from lp.registry.interfaces.person import validate_public_person |
152 | @@ -256,51 +253,6 @@ | |||
153 | 256 | CodeImportJob.delete(code_import.import_job.id) | 253 | CodeImportJob.delete(code_import.import_job.id) |
154 | 257 | CodeImport.delete(code_import.id) | 254 | CodeImport.delete(code_import.id) |
155 | 258 | 255 | ||
156 | 259 | def getActiveImports(self, text=None): | ||
157 | 260 | """See `ICodeImportSet`.""" | ||
158 | 261 | query = self.composeQueryString(text) | ||
159 | 262 | return CodeImport.select( | ||
160 | 263 | query, orderBy=['product.name', 'branch.name'], | ||
161 | 264 | clauseTables=['Product', 'Branch']) | ||
162 | 265 | |||
163 | 266 | def composeQueryString(self, text=None): | ||
164 | 267 | """Build SQL "where" clause for `CodeImport` search. | ||
165 | 268 | |||
166 | 269 | :param text: Text to search for in the product and project titles and | ||
167 | 270 | descriptions. | ||
168 | 271 | """ | ||
169 | 272 | conditions = [ | ||
170 | 273 | "date_last_successful IS NOT NULL", | ||
171 | 274 | "review_status=%s" % sqlvalues(CodeImportReviewStatus.REVIEWED), | ||
172 | 275 | "CodeImport.branch = Branch.id", | ||
173 | 276 | "Branch.product = Product.id", | ||
174 | 277 | ] | ||
175 | 278 | if text == u'': | ||
176 | 279 | text = None | ||
177 | 280 | |||
178 | 281 | # First filter on text, if supplied. | ||
179 | 282 | if text is not None: | ||
180 | 283 | conditions.append(""" | ||
181 | 284 | ((Project.fti @@ ftq(%s) AND Product.project IS NOT NULL) OR | ||
182 | 285 | Product.fti @@ ftq(%s))""" % (quote(text), quote(text))) | ||
183 | 286 | |||
184 | 287 | # Exclude deactivated products. | ||
185 | 288 | conditions.append('Product.active IS TRUE') | ||
186 | 289 | |||
187 | 290 | # Exclude deactivated projects, too. | ||
188 | 291 | conditions.append( | ||
189 | 292 | "((Product.project = Project.id AND Project.active) OR" | ||
190 | 293 | " Product.project IS NULL)") | ||
191 | 294 | |||
192 | 295 | # And build the query. | ||
193 | 296 | query = " AND ".join(conditions) | ||
194 | 297 | return """ | ||
195 | 298 | codeimport.id IN | ||
196 | 299 | (SELECT codeimport.id FROM codeimport, branch, product, project | ||
197 | 300 | WHERE %s) | ||
198 | 301 | AND codeimport.branch = branch.id | ||
199 | 302 | AND branch.product = product.id""" % query | ||
200 | 303 | |||
201 | 304 | def get(self, id): | 256 | def get(self, id): |
202 | 305 | """See `ICodeImportSet`.""" | 257 | """See `ICodeImportSet`.""" |
203 | 306 | try: | 258 | try: |
204 | 307 | 259 | ||
205 | === modified file 'lib/lp/code/model/tests/test_codeimport.py' | |||
206 | --- lib/lp/code/model/tests/test_codeimport.py 2010-03-18 17:49:21 +0000 | |||
207 | +++ lib/lp/code/model/tests/test_codeimport.py 2010-04-07 13:24:38 +0000 | |||
208 | @@ -11,14 +11,11 @@ | |||
209 | 11 | from storm.store import Store | 11 | from storm.store import Store |
210 | 12 | from zope.component import getUtility | 12 | from zope.component import getUtility |
211 | 13 | 13 | ||
212 | 14 | from lp.codehosting.codeimport.tests.test_workermonitor import ( | ||
213 | 15 | nuke_codeimport_sample_data) | ||
214 | 16 | from lp.code.model.codeimport import CodeImportSet | 14 | from lp.code.model.codeimport import CodeImportSet |
215 | 17 | from lp.code.model.codeimportevent import CodeImportEvent | 15 | from lp.code.model.codeimportevent import CodeImportEvent |
216 | 18 | from lp.code.model.codeimportjob import CodeImportJob, CodeImportJobSet | 16 | from lp.code.model.codeimportjob import CodeImportJob, CodeImportJobSet |
217 | 19 | from lp.code.model.codeimportresult import CodeImportResult | 17 | from lp.code.model.codeimportresult import CodeImportResult |
218 | 20 | from lp.code.interfaces.branchtarget import IBranchTarget | 18 | from lp.code.interfaces.branchtarget import IBranchTarget |
219 | 21 | from lp.code.interfaces.codeimport import ICodeImportSet | ||
220 | 22 | from lp.registry.interfaces.person import IPersonSet | 19 | from lp.registry.interfaces.person import IPersonSet |
221 | 23 | from lp.code.enums import ( | 20 | from lp.code.enums import ( |
222 | 24 | CodeImportResultStatus, CodeImportReviewStatus, RevisionControlSystems) | 21 | CodeImportResultStatus, CodeImportReviewStatus, RevisionControlSystems) |
223 | @@ -551,196 +548,5 @@ | |||
224 | 551 | requester, code_import.import_job.requesting_user) | 548 | requester, code_import.import_job.requesting_user) |
225 | 552 | 549 | ||
226 | 553 | 550 | ||
227 | 554 | def make_active_import(factory, project_name=None, product_name=None, | ||
228 | 555 | branch_name=None, svn_branch_url=None, | ||
229 | 556 | cvs_root=None, cvs_module=None, git_repo_url=None, | ||
230 | 557 | hg_repo_url=None, last_update=None, rcs_type=None): | ||
231 | 558 | """Make a new CodeImport for a new Product, maybe in a new Project. | ||
232 | 559 | |||
233 | 560 | The import will be 'active' in the sense used by | ||
234 | 561 | `ICodeImportSet.getActiveImports`. | ||
235 | 562 | """ | ||
236 | 563 | if project_name is not None: | ||
237 | 564 | project = factory.makeProject(name=project_name) | ||
238 | 565 | else: | ||
239 | 566 | project = None | ||
240 | 567 | product = factory.makeProduct( | ||
241 | 568 | name=product_name, displayname=product_name, project=project) | ||
242 | 569 | code_import = factory.makeProductCodeImport( | ||
243 | 570 | product=product, branch_name=branch_name, | ||
244 | 571 | svn_branch_url=svn_branch_url, cvs_root=cvs_root, | ||
245 | 572 | cvs_module=cvs_module, git_repo_url=git_repo_url, | ||
246 | 573 | hg_repo_url=hg_repo_url, rcs_type=None) | ||
247 | 574 | make_import_active(factory, code_import, last_update) | ||
248 | 575 | return code_import | ||
249 | 576 | |||
250 | 577 | |||
251 | 578 | def make_import_active(factory, code_import, last_update=None): | ||
252 | 579 | """Make `code_import` active as per `ICodeImportSet.getActiveImports`.""" | ||
253 | 580 | from zope.security.proxy import removeSecurityProxy | ||
254 | 581 | naked_import = removeSecurityProxy(code_import) | ||
255 | 582 | if naked_import.review_status != CodeImportReviewStatus.REVIEWED: | ||
256 | 583 | naked_import.updateFromData( | ||
257 | 584 | {'review_status': CodeImportReviewStatus.REVIEWED}, | ||
258 | 585 | factory.makePerson()) | ||
259 | 586 | if last_update is None: | ||
260 | 587 | # If last_update is not specfied, presumably we don't care what it is | ||
261 | 588 | # so we just use some made up value. | ||
262 | 589 | last_update = datetime(2008, 1, 1, tzinfo=pytz.UTC) | ||
263 | 590 | naked_import.date_last_successful = last_update | ||
264 | 591 | |||
265 | 592 | |||
266 | 593 | def deactivate(project_or_product): | ||
267 | 594 | """Mark `project_or_product` as not active.""" | ||
268 | 595 | from zope.security.proxy import removeSecurityProxy | ||
269 | 596 | removeSecurityProxy(project_or_product).active = False | ||
270 | 597 | |||
271 | 598 | |||
272 | 599 | class TestGetActiveImports(TestCaseWithFactory): | ||
273 | 600 | """Tests for CodeImportSet.getActiveImports().""" | ||
274 | 601 | |||
275 | 602 | layer = DatabaseFunctionalLayer | ||
276 | 603 | |||
277 | 604 | def setUp(self): | ||
278 | 605 | """Prepare by deleting all the import data in the sample data. | ||
279 | 606 | |||
280 | 607 | This means that the tests only have to care about the import | ||
281 | 608 | data they create. | ||
282 | 609 | """ | ||
283 | 610 | super(TestGetActiveImports, self).setUp() | ||
284 | 611 | nuke_codeimport_sample_data() | ||
285 | 612 | login('no-priv@canonical.com') | ||
286 | 613 | |||
287 | 614 | def tearDown(self): | ||
288 | 615 | super(TestGetActiveImports, self).tearDown() | ||
289 | 616 | logout() | ||
290 | 617 | |||
291 | 618 | def testEmpty(self): | ||
292 | 619 | # We start out with no code imports, so getActiveImports() returns no | ||
293 | 620 | # results. | ||
294 | 621 | results = getUtility(ICodeImportSet).getActiveImports() | ||
295 | 622 | self.assertEquals(list(results), []) | ||
296 | 623 | |||
297 | 624 | def testOneSeries(self): | ||
298 | 625 | # When there is one active import, it is returned. | ||
299 | 626 | code_import = make_active_import(self.factory) | ||
300 | 627 | results = getUtility(ICodeImportSet).getActiveImports() | ||
301 | 628 | self.assertEquals(list(results), [code_import]) | ||
302 | 629 | |||
303 | 630 | def testOneSeriesWithProject(self): | ||
304 | 631 | # Code imports for products with a project should be returned too. | ||
305 | 632 | code_import = make_active_import( | ||
306 | 633 | self.factory, project_name="whatever") | ||
307 | 634 | results = getUtility(ICodeImportSet).getActiveImports() | ||
308 | 635 | self.assertEquals(list(results), [code_import]) | ||
309 | 636 | |||
310 | 637 | def testExcludeDeactivatedProducts(self): | ||
311 | 638 | # Deactivating a product means that code imports associated to it are | ||
312 | 639 | # no longer returned. | ||
313 | 640 | code_import = make_active_import(self.factory) | ||
314 | 641 | self.failUnless(code_import.branch.product.active) | ||
315 | 642 | results = getUtility(ICodeImportSet).getActiveImports() | ||
316 | 643 | self.assertEquals(list(results), [code_import]) | ||
317 | 644 | deactivate(code_import.branch.product) | ||
318 | 645 | results = getUtility(ICodeImportSet).getActiveImports() | ||
319 | 646 | self.assertEquals(list(results), []) | ||
320 | 647 | |||
321 | 648 | def testExcludeDeactivatedProjects(self): | ||
322 | 649 | # Deactivating a project means that code imports associated to | ||
323 | 650 | # products in it are no longer returned. | ||
324 | 651 | code_import = make_active_import( | ||
325 | 652 | self.factory, project_name="whatever") | ||
326 | 653 | self.failUnless(code_import.branch.product.project.active) | ||
327 | 654 | results = getUtility(ICodeImportSet).getActiveImports() | ||
328 | 655 | self.assertEquals(list(results), [code_import]) | ||
329 | 656 | deactivate(code_import.branch.product.project) | ||
330 | 657 | results = getUtility(ICodeImportSet).getActiveImports() | ||
331 | 658 | self.assertEquals(list(results), []) | ||
332 | 659 | |||
333 | 660 | def testSorting(self): | ||
334 | 661 | # Returned code imports are sorted by product name, then branch name. | ||
335 | 662 | prod1_a = make_active_import( | ||
336 | 663 | self.factory, product_name='prod1', branch_name='a') | ||
337 | 664 | prod2_a = make_active_import( | ||
338 | 665 | self.factory, product_name='prod2', branch_name='a') | ||
339 | 666 | prod1_b = self.factory.makeProductCodeImport( | ||
340 | 667 | product=prod1_a.branch.product, branch_name='b') | ||
341 | 668 | make_import_active(self.factory, prod1_b) | ||
342 | 669 | results = getUtility(ICodeImportSet).getActiveImports() | ||
343 | 670 | self.assertEquals( | ||
344 | 671 | list(results), [prod1_a, prod1_b, prod2_a]) | ||
345 | 672 | |||
346 | 673 | def testSearchByProduct(self): | ||
347 | 674 | # Searching can filter by product name and other texts. | ||
348 | 675 | code_import = make_active_import( | ||
349 | 676 | self.factory, product_name='product') | ||
350 | 677 | results = getUtility(ICodeImportSet).getActiveImports( | ||
351 | 678 | text='product') | ||
352 | 679 | self.assertEquals( | ||
353 | 680 | list(results), [code_import]) | ||
354 | 681 | |||
355 | 682 | def testSearchByProductWithProject(self): | ||
356 | 683 | # Searching can filter by product name and other texts, and returns | ||
357 | 684 | # matching imports even if the associated product is in a project | ||
358 | 685 | # which does not match. | ||
359 | 686 | code_import = make_active_import( | ||
360 | 687 | self.factory, project_name='whatever', product_name='product') | ||
361 | 688 | results = getUtility(ICodeImportSet).getActiveImports( | ||
362 | 689 | text='product') | ||
363 | 690 | self.assertEquals( | ||
364 | 691 | list(results), [code_import]) | ||
365 | 692 | |||
366 | 693 | def testSearchByProject(self): | ||
367 | 694 | # Searching can filter by project name and other texts. | ||
368 | 695 | code_import = make_active_import( | ||
369 | 696 | self.factory, project_name='project', product_name='product') | ||
370 | 697 | results = getUtility(ICodeImportSet).getActiveImports( | ||
371 | 698 | text='project') | ||
372 | 699 | self.assertEquals( | ||
373 | 700 | list(results), [code_import]) | ||
374 | 701 | |||
375 | 702 | def testSearchByProjectWithNonMatchingProduct(self): | ||
376 | 703 | # If a project matches the text, it's an easy mistake to make to | ||
377 | 704 | # consider all the products with no project as matching too. | ||
378 | 705 | code_import_1 = make_active_import( | ||
379 | 706 | self.factory, product_name='product1') | ||
380 | 707 | code_import_2 = make_active_import( | ||
381 | 708 | self.factory, project_name='thisone', product_name='product2') | ||
382 | 709 | results = getUtility(ICodeImportSet).getActiveImports( | ||
383 | 710 | text='thisone') | ||
384 | 711 | self.assertEquals( | ||
385 | 712 | list(results), [code_import_2]) | ||
386 | 713 | |||
387 | 714 | def testJoining(self): | ||
388 | 715 | # Test that the query composed by CodeImportSet.composeQueryString | ||
389 | 716 | # gets the joins right. We create code imports for each of the | ||
390 | 717 | # possibilities of active or inactive product and active or inactive | ||
391 | 718 | # or absent project. | ||
392 | 719 | expected = set() | ||
393 | 720 | source = {} | ||
394 | 721 | for project_active in [True, False, None]: | ||
395 | 722 | for product_active in [True, False]: | ||
396 | 723 | if project_active is not None: | ||
397 | 724 | project_name = self.factory.getUniqueString() | ||
398 | 725 | else: | ||
399 | 726 | project_name = None | ||
400 | 727 | code_import = make_active_import( | ||
401 | 728 | self.factory, project_name=project_name) | ||
402 | 729 | if code_import.branch.product.project and not project_active: | ||
403 | 730 | deactivate(code_import.branch.product.project) | ||
404 | 731 | if not product_active: | ||
405 | 732 | deactivate(code_import.branch.product) | ||
406 | 733 | if project_active != False and product_active: | ||
407 | 734 | expected.add(code_import) | ||
408 | 735 | source[code_import] = (product_active, project_active) | ||
409 | 736 | results = set(getUtility(ICodeImportSet).getActiveImports()) | ||
410 | 737 | errors = [] | ||
411 | 738 | for extra in results - expected: | ||
412 | 739 | errors.append(('extra', source[extra])) | ||
413 | 740 | for missing in expected - results: | ||
414 | 741 | errors.append(('extra', source[missing])) | ||
415 | 742 | self.assertEquals(errors, []) | ||
416 | 743 | |||
417 | 744 | |||
418 | 745 | def test_suite(): | 551 | def test_suite(): |
419 | 746 | return unittest.TestLoader().loadTestsFromName(__name__) | 552 | return unittest.TestLoader().loadTestsFromName(__name__) |
420 | 747 | 553 | ||
421 | === modified file 'lib/lp/code/stories/branches/xx-bazaar-home.txt' | |||
422 | --- lib/lp/code/stories/branches/xx-bazaar-home.txt 2009-08-28 05:57:37 +0000 | |||
423 | +++ lib/lp/code/stories/branches/xx-bazaar-home.txt 2010-04-07 13:24:38 +0000 | |||
424 | @@ -16,7 +16,7 @@ | |||
425 | 16 | >>> print extract_text(footer) | 16 | >>> print extract_text(footer) |
426 | 17 | 30 branches registered in | 17 | 30 branches registered in |
427 | 18 | 6 projects | 18 | 6 projects |
429 | 19 | 0 imported branches | 19 | 1 imported branches |
430 | 20 | 2 branches associated with bug reports | 20 | 2 branches associated with bug reports |
431 | 21 | Launchpad uses Bazaar 0.92.0. | 21 | Launchpad uses Bazaar 0.92.0. |
432 | 22 | 22 | ||
433 | 23 | 23 | ||
434 | === modified file 'lib/lp/code/stories/branches/xx-branchmergeproposals.txt' | |||
435 | --- lib/lp/code/stories/branches/xx-branchmergeproposals.txt 2010-02-23 21:48:53 +0000 | |||
436 | +++ lib/lp/code/stories/branches/xx-branchmergeproposals.txt 2010-04-07 13:24:38 +0000 | |||
437 | @@ -80,13 +80,16 @@ | |||
438 | 80 | ... print extract_text(find_tag_by_id( | 80 | ... print extract_text(find_tag_by_id( |
439 | 81 | ... browser.contents, 'proposal-summary')) | 81 | ... browser.contents, 'proposal-summary')) |
440 | 82 | >>> print_summary(nopriv_browser) | 82 | >>> print_summary(nopriv_browser) |
442 | 83 | Status:... | 83 | Status: |
443 | 84 | ... | ||
444 | 84 | Proposed branch: | 85 | Proposed branch: |
445 | 85 | lp://dev/~name12/gnome-terminal/klingon | 86 | lp://dev/~name12/gnome-terminal/klingon |
446 | 86 | Merge into: | 87 | Merge into: |
447 | 87 | lp://dev/~name12/gnome-terminal/main | 88 | lp://dev/~name12/gnome-terminal/main |
448 | 88 | Prerequisite: | 89 | Prerequisite: |
449 | 89 | lp://dev/~name12/gnome-terminal/pushed | 90 | lp://dev/~name12/gnome-terminal/pushed |
450 | 91 | To merge this branch: | ||
451 | 92 | bzr merge lp://dev/~name12/gnome-terminal/klingon | ||
452 | 90 | 93 | ||
453 | 91 | 94 | ||
454 | 92 | Editing a commit message | 95 | Editing a commit message |
455 | 93 | 96 | ||
456 | === modified file 'lib/lp/code/stories/branches/xx-propose-for-merging.txt' | |||
457 | --- lib/lp/code/stories/branches/xx-propose-for-merging.txt 2010-01-14 04:32:38 +0000 | |||
458 | +++ lib/lp/code/stories/branches/xx-propose-for-merging.txt 2010-04-07 13:24:38 +0000 | |||
459 | @@ -35,6 +35,7 @@ | |||
460 | 35 | Status: Needs review | 35 | Status: Needs review |
461 | 36 | Proposed branch: ... | 36 | Proposed branch: ... |
462 | 37 | Merge into: lp://dev/fooix | 37 | Merge into: lp://dev/fooix |
463 | 38 | To merge this branch: bzr merge ... | ||
464 | 38 | 39 | ||
465 | 39 | 40 | ||
466 | 40 | Work in progress | 41 | Work in progress |
467 | @@ -57,3 +58,4 @@ | |||
468 | 57 | Status: Work in progress | 58 | Status: Work in progress |
469 | 58 | Proposed branch: ... | 59 | Proposed branch: ... |
470 | 59 | Merge into: lp://dev/fooix | 60 | Merge into: lp://dev/fooix |
471 | 61 | To merge this branch: bzr merge ... | ||
472 | 60 | 62 | ||
473 | === removed file 'lib/lp/code/stories/codeimport/xx-codeimport-list.txt' | |||
474 | --- lib/lp/code/stories/codeimport/xx-codeimport-list.txt 2010-01-12 22:09:23 +0000 | |||
475 | +++ lib/lp/code/stories/codeimport/xx-codeimport-list.txt 1970-01-01 00:00:00 +0000 | |||
476 | @@ -1,72 +0,0 @@ | |||
477 | 1 | There is a page listing all the active code imports on the code | ||
478 | 2 | homepage. | ||
479 | 3 | |||
480 | 4 | We start by deleting all the code import sample data and creating a | ||
481 | 5 | few imports that will be displayed in the listing. | ||
482 | 6 | |||
483 | 7 | >>> import datetime | ||
484 | 8 | >>> import pytz | ||
485 | 9 | >>> from lp.codehosting.codeimport.tests.test_workermonitor import ( | ||
486 | 10 | ... nuke_codeimport_sample_data) | ||
487 | 11 | >>> from lp.code.enums import RevisionControlSystems | ||
488 | 12 | >>> from lp.code.model.tests.test_codeimport import ( | ||
489 | 13 | ... make_active_import) | ||
490 | 14 | >>> from lp.code.interfaces.codeimport import ICodeImportSet | ||
491 | 15 | >>> from lp.testing import login, logout | ||
492 | 16 | >>> from zope.component import getUtility | ||
493 | 17 | >>> login('david.allouche@canonical.com') | ||
494 | 18 | >>> nuke_codeimport_sample_data() | ||
495 | 19 | >>> code_import_set = getUtility(ICodeImportSet) | ||
496 | 20 | >>> active_svn_import = make_active_import( | ||
497 | 21 | ... factory, product_name="myproject", branch_name="trunk", | ||
498 | 22 | ... svn_branch_url="http://example.com/svn/myproject/trunk", | ||
499 | 23 | ... last_update=datetime.datetime(2007, 1, 1, tzinfo=pytz.UTC)) | ||
500 | 24 | >>> active_bzr_svn_import = make_active_import( | ||
501 | 25 | ... factory, product_name="ourproject", branch_name="trunk", | ||
502 | 26 | ... svn_branch_url="http://example.com/bzr-svn/myproject/trunk", | ||
503 | 27 | ... rcs_type=RevisionControlSystems.BZR_SVN, | ||
504 | 28 | ... last_update=datetime.datetime(2007, 1, 2, tzinfo=pytz.UTC)) | ||
505 | 29 | >>> active_cvs_import = make_active_import( | ||
506 | 30 | ... factory, product_name="hisproj", branch_name="main", | ||
507 | 31 | ... cvs_root=":pserver:anon@example.com:/cvs", cvs_module="hisproj", | ||
508 | 32 | ... last_update=datetime.datetime(2007, 1, 3, tzinfo=pytz.UTC)) | ||
509 | 33 | >>> active_git_import = make_active_import( | ||
510 | 34 | ... factory, product_name="herproj", branch_name="master", | ||
511 | 35 | ... git_repo_url="git://git.example.org/herproj", | ||
512 | 36 | ... last_update=datetime.datetime(2007, 1, 4, tzinfo=pytz.UTC)) | ||
513 | 37 | >>> active_hg_import = make_active_import( | ||
514 | 38 | ... factory, product_name="hg-proj", branch_name="tip", | ||
515 | 39 | ... hg_repo_url="http://hg.example.org/proj", | ||
516 | 40 | ... last_update=datetime.datetime(2007, 1, 5, tzinfo=pytz.UTC)) | ||
517 | 41 | >>> len(list(code_import_set.getActiveImports())) | ||
518 | 42 | 5 | ||
519 | 43 | >>> logout() | ||
520 | 44 | |||
521 | 45 | The page is linked to from the "$N imported branches" text. | ||
522 | 46 | |||
523 | 47 | >>> browser.open('http://code.launchpad.dev') | ||
524 | 48 | >>> browser.getLink('5 imported branches').click() | ||
525 | 49 | >>> print browser.title | ||
526 | 50 | Available code imports | ||
527 | 51 | |||
528 | 52 | It lists the active imports, sorted by product then branch name: | ||
529 | 53 | |||
530 | 54 | >>> def print_import_table(): | ||
531 | 55 | ... table = first_tag_by_class(browser.contents, 'listing') | ||
532 | 56 | ... print extract_text(table) | ||
533 | 57 | |||
534 | 58 | >>> print_import_table() | ||
535 | 59 | Project Branch Name Source Details Last Updated | ||
536 | 60 | herproj master git://git.example.org/herproj 2007-01-04 | ||
537 | 61 | hg-proj tip http://hg.example.org/proj 2007-01-05 | ||
538 | 62 | hisproj main :pserver:anon@example.com:/cvs hisproj 2007-01-03 | ||
539 | 63 | myproject trunk http://example.com/svn/myproject/trunk 2007-01-01 | ||
540 | 64 | ourproject trunk http://example.com/bzr-svn/myproject/trunk 2007-01-02 | ||
541 | 65 | |||
542 | 66 | You can filter the list by product: | ||
543 | 67 | |||
544 | 68 | >>> browser.getControl(name='text').value = 'hisproj' | ||
545 | 69 | >>> browser.getControl('Search', index=0).click() | ||
546 | 70 | >>> print_import_table() | ||
547 | 71 | Project Branch Name Source Details Last Updated | ||
548 | 72 | hisproj main :pserver:anon@example.com:/cvs hisproj 2007-01-03 | ||
549 | 73 | 0 | ||
550 | === modified file 'lib/lp/code/stories/codeimport/xx-codeimport-view.txt' | |||
551 | --- lib/lp/code/stories/codeimport/xx-codeimport-view.txt 2010-03-18 17:49:21 +0000 | |||
552 | +++ lib/lp/code/stories/codeimport/xx-codeimport-view.txt 2010-04-07 13:24:38 +0000 | |||
553 | @@ -1,10 +1,10 @@ | |||
554 | 1 | Code imports | 1 | Code imports |
555 | 2 | ============ | 2 | ============ |
556 | 3 | 3 | ||
559 | 4 | For now, there is no link to the page that lists all code imports, so | 4 | The code imports overview page is linked of the main code page. |
558 | 5 | we browse there directly: | ||
560 | 6 | 5 | ||
562 | 7 | >>> browser.open('http://code.launchpad.dev/+code-imports') | 6 | >>> browser.open('http://code.launchpad.dev') |
563 | 7 | >>> browser.getLink('1 imported branches').click() | ||
564 | 8 | >>> print browser.title | 8 | >>> print browser.title |
565 | 9 | Code Imports | 9 | Code Imports |
566 | 10 | 10 | ||
567 | 11 | 11 | ||
568 | === modified file 'lib/lp/code/templates/bazaar-index.pt' | |||
569 | --- lib/lp/code/templates/bazaar-index.pt 2010-03-11 06:43:00 +0000 | |||
570 | +++ lib/lp/code/templates/bazaar-index.pt 2010-04-07 13:24:38 +0000 | |||
571 | @@ -121,7 +121,7 @@ | |||
572 | 121 | </a> | 121 | </a> |
573 | 122 | </div> | 122 | </div> |
574 | 123 | <div> | 123 | <div> |
576 | 124 | <a href="/+code-import-list"> | 124 | <a href="/+code-imports"> |
577 | 125 | <strong tal:content="view/import_count">123</strong> | 125 | <strong tal:content="view/import_count">123</strong> |
578 | 126 | imported branches | 126 | imported branches |
579 | 127 | </a> | 127 | </a> |
580 | 128 | 128 | ||
581 | === modified file 'lib/lp/code/templates/branchmergeproposal-pagelet-summary.pt' | |||
582 | --- lib/lp/code/templates/branchmergeproposal-pagelet-summary.pt 2010-02-24 08:05:27 +0000 | |||
583 | +++ lib/lp/code/templates/branchmergeproposal-pagelet-summary.pt 2010-04-07 13:24:38 +0000 | |||
584 | @@ -131,5 +131,9 @@ | |||
585 | 131 | tal:content="context/preview_diff/conflicts"/> | 131 | tal:content="context/preview_diff/conflicts"/> |
586 | 132 | </td> | 132 | </td> |
587 | 133 | </tr> | 133 | </tr> |
588 | 134 | <tr id="summary-row-merge-instruction"> | ||
589 | 135 | <th>To merge this branch:</th> | ||
590 | 136 | <td>bzr merge <span class="branch-url" tal:content="context/source_branch/bzr_identity" /></td> | ||
591 | 137 | </tr> | ||
592 | 134 | </tbody> | 138 | </tbody> |
593 | 135 | </table> | 139 | </table> |
594 | 136 | 140 | ||
595 | === modified file 'lib/lp/registry/browser/configure.zcml' | |||
596 | --- lib/lp/registry/browser/configure.zcml 2010-04-01 18:48:04 +0000 | |||
597 | +++ lib/lp/registry/browser/configure.zcml 2010-04-07 13:24:38 +0000 | |||
598 | @@ -1664,6 +1664,13 @@ | |||
599 | 1664 | facet="overview" | 1664 | facet="overview" |
600 | 1665 | permission="launchpad.Edit"/> | 1665 | permission="launchpad.Edit"/> |
601 | 1666 | <browser:page | 1666 | <browser:page |
602 | 1667 | for="lp.registry.interfaces.productseries.IProductSeries" | ||
603 | 1668 | name="+setbranch" | ||
604 | 1669 | class="lp.registry.browser.productseries.ProductSeriesSetBranchView" | ||
605 | 1670 | template="../templates/productseries-setbranch.pt" | ||
606 | 1671 | facet="overview" | ||
607 | 1672 | permission="launchpad.Edit"/> | ||
608 | 1673 | <browser:page | ||
609 | 1667 | name="+review" | 1674 | name="+review" |
610 | 1668 | for="lp.registry.interfaces.productseries.IProductSeries" | 1675 | for="lp.registry.interfaces.productseries.IProductSeries" |
611 | 1669 | class="lp.registry.browser.productseries.ProductSeriesReviewView" | 1676 | class="lp.registry.browser.productseries.ProductSeriesReviewView" |
612 | 1670 | 1677 | ||
613 | === modified file 'lib/lp/registry/browser/productseries.py' | |||
614 | --- lib/lp/registry/browser/productseries.py 2010-03-23 00:39:45 +0000 | |||
615 | +++ lib/lp/registry/browser/productseries.py 2010-04-07 13:24:38 +0000 | |||
616 | @@ -1,4 +1,4 @@ | |||
618 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
619 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
620 | 3 | 3 | ||
621 | 4 | """View classes for `IProductSeries`.""" | 4 | """View classes for `IProductSeries`.""" |
622 | @@ -20,7 +20,7 @@ | |||
623 | 20 | 'ProductSeriesOverviewNavigationMenu', | 20 | 'ProductSeriesOverviewNavigationMenu', |
624 | 21 | 'ProductSeriesRdfView', | 21 | 'ProductSeriesRdfView', |
625 | 22 | 'ProductSeriesReviewView', | 22 | 'ProductSeriesReviewView', |
627 | 23 | 'ProductSeriesSourceListView', | 23 | 'ProductSeriesSetBranchView', |
628 | 24 | 'ProductSeriesSpecificationsMenu', | 24 | 'ProductSeriesSpecificationsMenu', |
629 | 25 | 'ProductSeriesUbuntuPackagingView', | 25 | 'ProductSeriesUbuntuPackagingView', |
630 | 26 | 'ProductSeriesView', | 26 | 'ProductSeriesView', |
631 | @@ -34,6 +34,7 @@ | |||
632 | 34 | from zope.component import getUtility | 34 | from zope.component import getUtility |
633 | 35 | from zope.app.form.browser import TextAreaWidget, TextWidget | 35 | from zope.app.form.browser import TextAreaWidget, TextWidget |
634 | 36 | from zope.formlib import form | 36 | from zope.formlib import form |
635 | 37 | from zope.interface import Interface | ||
636 | 37 | from zope.schema import Choice | 38 | from zope.schema import Choice |
637 | 38 | from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary | 39 | from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary |
638 | 39 | 40 | ||
639 | @@ -41,7 +42,7 @@ | |||
640 | 41 | 42 | ||
641 | 42 | from canonical.cachedproperty import cachedproperty | 43 | from canonical.cachedproperty import cachedproperty |
642 | 43 | from canonical.launchpad import _ | 44 | from canonical.launchpad import _ |
644 | 44 | from lp.code.browser.branchref import BranchRef | 45 | from canonical.launchpad.fields import URIField |
645 | 45 | from lp.blueprints.browser.specificationtarget import ( | 46 | from lp.blueprints.browser.specificationtarget import ( |
646 | 46 | HasSpecificationsMenuMixin) | 47 | HasSpecificationsMenuMixin) |
647 | 47 | from lp.blueprints.interfaces.specification import ( | 48 | from lp.blueprints.interfaces.specification import ( |
648 | @@ -49,9 +50,15 @@ | |||
649 | 49 | from lp.bugs.interfaces.bugtask import BugTaskStatus | 50 | from lp.bugs.interfaces.bugtask import BugTaskStatus |
650 | 50 | from lp.bugs.browser.bugtask import BugTargetTraversalMixin | 51 | from lp.bugs.browser.bugtask import BugTargetTraversalMixin |
651 | 51 | from canonical.launchpad.helpers import browserLanguages | 52 | from canonical.launchpad.helpers import browserLanguages |
652 | 53 | from lp.code.browser.branch import BranchNameValidationMixin | ||
653 | 54 | from lp.code.browser.branchref import BranchRef | ||
654 | 55 | from lp.code.enums import BranchType, RevisionControlSystems | ||
655 | 56 | from lp.code.interfaces.branch import ( | ||
656 | 57 | BranchCreationForbidden, BranchExists, IBranch) | ||
657 | 52 | from lp.code.interfaces.branchjob import IRosettaUploadJobSource | 58 | from lp.code.interfaces.branchjob import IRosettaUploadJobSource |
658 | 59 | from lp.code.interfaces.branchtarget import IBranchTarget | ||
659 | 53 | from lp.code.interfaces.codeimport import ( | 60 | from lp.code.interfaces.codeimport import ( |
661 | 54 | ICodeImportSet) | 61 | ICodeImport, ICodeImportSet) |
662 | 55 | from lp.services.worlddata.interfaces.country import ICountry | 62 | from lp.services.worlddata.interfaces.country import ICountry |
663 | 56 | from lp.bugs.interfaces.bugtask import IBugTaskSet | 63 | from lp.bugs.interfaces.bugtask import IBugTaskSet |
664 | 57 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities | 64 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
665 | @@ -70,12 +77,13 @@ | |||
666 | 70 | Link, Navigation, NavigationMenu, StandardLaunchpadFacets, stepthrough, | 77 | Link, Navigation, NavigationMenu, StandardLaunchpadFacets, stepthrough, |
667 | 71 | stepto) | 78 | stepto) |
668 | 72 | from canonical.launchpad.webapp.authorization import check_permission | 79 | from canonical.launchpad.webapp.authorization import check_permission |
669 | 73 | from canonical.launchpad.webapp.batching import BatchNavigator | ||
670 | 74 | from canonical.launchpad.webapp.breadcrumb import Breadcrumb | 80 | from canonical.launchpad.webapp.breadcrumb import Breadcrumb |
672 | 75 | from canonical.launchpad.webapp.interfaces import NotFoundError | 81 | from canonical.launchpad.webapp.interfaces import ( |
673 | 82 | NotFoundError, UnexpectedFormData) | ||
674 | 76 | from canonical.launchpad.webapp.launchpadform import ( | 83 | from canonical.launchpad.webapp.launchpadform import ( |
675 | 77 | action, custom_widget, LaunchpadEditFormView, LaunchpadFormView) | 84 | action, custom_widget, LaunchpadEditFormView, LaunchpadFormView) |
676 | 78 | from canonical.launchpad.webapp.menu import structured | 85 | from canonical.launchpad.webapp.menu import structured |
677 | 86 | from canonical.widgets.itemswidgets import LaunchpadRadioWidget | ||
678 | 79 | from canonical.widgets.textwidgets import StrippedTextWidget | 87 | from canonical.widgets.textwidgets import StrippedTextWidget |
679 | 80 | 88 | ||
680 | 81 | from lp.registry.browser import ( | 89 | from lp.registry.browser import ( |
681 | @@ -83,6 +91,9 @@ | |||
682 | 83 | from lp.registry.interfaces.series import SeriesStatus | 91 | from lp.registry.interfaces.series import SeriesStatus |
683 | 84 | from lp.registry.interfaces.productseries import IProductSeries | 92 | from lp.registry.interfaces.productseries import IProductSeries |
684 | 85 | 93 | ||
685 | 94 | from lazr.enum import DBItem | ||
686 | 95 | from lazr.restful.interface import copy_field, use_template | ||
687 | 96 | |||
688 | 86 | 97 | ||
689 | 87 | def quote(text): | 98 | def quote(text): |
690 | 88 | """Escape and quote text.""" | 99 | """Escape and quote text.""" |
691 | @@ -644,7 +655,369 @@ | |||
692 | 644 | self.next_url = canonical_url(product) | 655 | self.next_url = canonical_url(product) |
693 | 645 | 656 | ||
694 | 646 | 657 | ||
696 | 647 | class ProductSeriesLinkBranchView(LaunchpadEditFormView): | 658 | LINK_LP_BZR = 'link-lp-bzr' |
697 | 659 | CREATE_NEW = 'create-new' | ||
698 | 660 | IMPORT_EXTERNAL = 'import-external' | ||
699 | 661 | |||
700 | 662 | |||
701 | 663 | BRANCH_TYPE_VOCABULARY = SimpleVocabulary(( | ||
702 | 664 | SimpleTerm(LINK_LP_BZR, LINK_LP_BZR, | ||
703 | 665 | _("Link to a Bazaar branch already on Launchpad")), | ||
704 | 666 | SimpleTerm(CREATE_NEW, CREATE_NEW, | ||
705 | 667 | _("Create a new, empty branch in Launchpad and " | ||
706 | 668 | "link to this series")), | ||
707 | 669 | SimpleTerm(IMPORT_EXTERNAL, IMPORT_EXTERNAL, | ||
708 | 670 | _("Import a branch hosted somewhere else")), | ||
709 | 671 | )) | ||
710 | 672 | |||
711 | 673 | |||
712 | 674 | class RevisionControlSystemsExtended(RevisionControlSystems): | ||
713 | 675 | """External RCS plus Bazaar.""" | ||
714 | 676 | BZR = DBItem(99, """ | ||
715 | 677 | Bazaar | ||
716 | 678 | |||
717 | 679 | External Bazaar branch. | ||
718 | 680 | """) | ||
719 | 681 | |||
720 | 682 | |||
721 | 683 | class SetBranchForm(Interface): | ||
722 | 684 | """The fields presented on the form for setting a branch.""" | ||
723 | 685 | |||
724 | 686 | use_template( | ||
725 | 687 | ICodeImport, | ||
726 | 688 | ['cvs_module']) | ||
727 | 689 | |||
728 | 690 | rcs_type = Choice(title=_("Type of RCS"), | ||
729 | 691 | required=False, vocabulary=RevisionControlSystemsExtended, | ||
730 | 692 | description=_( | ||
731 | 693 | "The version control system to import from. ")) | ||
732 | 694 | |||
733 | 695 | repo_url = URIField( | ||
734 | 696 | title=_("Branch URL"), required=True, | ||
735 | 697 | description=_("The URL of the branch."), | ||
736 | 698 | allowed_schemes=["http", "https"], | ||
737 | 699 | allow_userinfo=False, | ||
738 | 700 | allow_port=True, | ||
739 | 701 | allow_query=False, | ||
740 | 702 | allow_fragment=False, | ||
741 | 703 | trailing_slash=False) | ||
742 | 704 | |||
743 | 705 | branch_location = copy_field( | ||
744 | 706 | IProductSeries['branch'], | ||
745 | 707 | __name__='branch_location', | ||
746 | 708 | title=_('Branch'), | ||
747 | 709 | description=_( | ||
748 | 710 | "The Bazaar branch for this series in Launchpad, " | ||
749 | 711 | "if one exists."), | ||
750 | 712 | ) | ||
751 | 713 | |||
752 | 714 | branch_type = Choice( | ||
753 | 715 | title=_('Import type'), | ||
754 | 716 | vocabulary=BRANCH_TYPE_VOCABULARY, | ||
755 | 717 | description=_("The type of import"), | ||
756 | 718 | required=True) | ||
757 | 719 | |||
758 | 720 | branch_name = copy_field( | ||
759 | 721 | IBranch['name'], | ||
760 | 722 | __name__='branch_name', | ||
761 | 723 | title=_('Branch name'), | ||
762 | 724 | description=_(''), | ||
763 | 725 | required=True, | ||
764 | 726 | ) | ||
765 | 727 | |||
766 | 728 | branch_owner = copy_field( | ||
767 | 729 | IBranch['owner'], | ||
768 | 730 | __name__='branch_owner', | ||
769 | 731 | title=_('Branch owner'), | ||
770 | 732 | description=_(''), | ||
771 | 733 | required=True, | ||
772 | 734 | ) | ||
773 | 735 | |||
774 | 736 | |||
775 | 737 | class ProductSeriesSetBranchView(LaunchpadFormView, ProductSeriesView, | ||
776 | 738 | BranchNameValidationMixin): | ||
777 | 739 | """The view to set a branch for the ProductSeries.""" | ||
778 | 740 | |||
779 | 741 | schema = SetBranchForm | ||
780 | 742 | # Set for_input to True to ensure fields marked read-only will be editable | ||
781 | 743 | # upon creation. | ||
782 | 744 | for_input = True | ||
783 | 745 | |||
784 | 746 | custom_widget('rcs_type', LaunchpadRadioWidget) | ||
785 | 747 | custom_widget('branch_type', LaunchpadRadioWidget) | ||
786 | 748 | initial_values = { | ||
787 | 749 | 'rcs_type': RevisionControlSystemsExtended.BZR, | ||
788 | 750 | 'branch_type': LINK_LP_BZR, | ||
789 | 751 | } | ||
790 | 752 | |||
791 | 753 | def setUpWidgets(self): | ||
792 | 754 | """See `LaunchpadFormView`.""" | ||
793 | 755 | super(ProductSeriesSetBranchView, self).setUpWidgets() | ||
794 | 756 | |||
795 | 757 | def render(widget, term_value, current_value, label=None): | ||
796 | 758 | term = widget.vocabulary.getTerm(term_value) | ||
797 | 759 | if term.value == current_value: | ||
798 | 760 | render = widget.renderSelectedItem | ||
799 | 761 | else: | ||
800 | 762 | render = widget.renderItem | ||
801 | 763 | if label is None: | ||
802 | 764 | label = term.title | ||
803 | 765 | value = term.token | ||
804 | 766 | return render(index=term.value, | ||
805 | 767 | text=label, | ||
806 | 768 | value=value, | ||
807 | 769 | name=widget.name, | ||
808 | 770 | cssClass='') | ||
809 | 771 | |||
810 | 772 | widget = self.widgets['rcs_type'] | ||
811 | 773 | vocab = widget.vocabulary | ||
812 | 774 | current_value = widget._getFormValue() | ||
813 | 775 | self.rcs_type_cvs = render(widget, vocab.CVS, current_value, 'CVS') | ||
814 | 776 | self.rcs_type_svn = render(widget, vocab.BZR_SVN, current_value, | ||
815 | 777 | 'SVN') | ||
816 | 778 | self.rcs_type_git = render(widget, vocab.GIT, current_value) | ||
817 | 779 | self.rcs_type_hg = render(widget, vocab.HG, current_value) | ||
818 | 780 | self.rcs_type_bzr = render(widget, vocab.BZR, current_value) | ||
819 | 781 | self.rcs_type_emptymarker = widget._emptyMarker() | ||
820 | 782 | |||
821 | 783 | widget = self.widgets['branch_type'] | ||
822 | 784 | current_value = widget._getFormValue() | ||
823 | 785 | vocab = widget.vocabulary | ||
824 | 786 | |||
825 | 787 | (self.branch_type_link, | ||
826 | 788 | self.branch_type_create, | ||
827 | 789 | self.branch_type_import) = [ | ||
828 | 790 | render(widget, value, current_value) | ||
829 | 791 | for value in (LINK_LP_BZR, CREATE_NEW, IMPORT_EXTERNAL)] | ||
830 | 792 | |||
831 | 793 | def _validateLinkLpBzr(self, data): | ||
832 | 794 | """Validate data for link-lp-bzr case.""" | ||
833 | 795 | if 'branch_location' not in data: | ||
834 | 796 | self.setFieldError( | ||
835 | 797 | 'branch_location', | ||
836 | 798 | 'The branch location must be set.') | ||
837 | 799 | |||
838 | 800 | def _validateCreateNew(self, data): | ||
839 | 801 | """Validate data for create new case.""" | ||
840 | 802 | self._validateBranch(data) | ||
841 | 803 | |||
842 | 804 | def _validateImportExternal(self, data): | ||
843 | 805 | """Validate data for import external case.""" | ||
844 | 806 | rcs_type = data.get('rcs_type') | ||
845 | 807 | repo_url = data.get('repo_url') | ||
846 | 808 | |||
847 | 809 | if repo_url is None: | ||
848 | 810 | self.setFieldError('repo_url', | ||
849 | 811 | 'You must set the external repository URL.') | ||
850 | 812 | else: | ||
851 | 813 | # Ensure this URL has not been imported before. | ||
852 | 814 | code_import = getUtility(ICodeImportSet).getByURL(repo_url) | ||
853 | 815 | if code_import is not None: | ||
854 | 816 | self.setFieldError( | ||
855 | 817 | 'repo_url', | ||
856 | 818 | structured(""" | ||
857 | 819 | This foreign branch URL is already specified for | ||
858 | 820 | the imported branch <a href="%s">%s</a>.""", | ||
859 | 821 | canonical_url(code_import.branch), | ||
860 | 822 | code_import.branch.unique_name)) | ||
861 | 823 | |||
862 | 824 | # RCS type is mandatory. | ||
863 | 825 | # This condition should never happen since an initial value is set. | ||
864 | 826 | if rcs_type is None: | ||
865 | 827 | # The error shows but does not identify the widget. | ||
866 | 828 | self.setFieldError( | ||
867 | 829 | 'rcs_type', | ||
868 | 830 | 'You must specify the type of RCS for the remote host.') | ||
869 | 831 | elif rcs_type == RevisionControlSystemsExtended.CVS: | ||
870 | 832 | if 'cvs_module' not in data: | ||
871 | 833 | self.setFieldError( | ||
872 | 834 | 'cvs_module', | ||
873 | 835 | 'The CVS module must be set.') | ||
874 | 836 | self._validateBranch(data) | ||
875 | 837 | |||
876 | 838 | def _validateBranch(self, data): | ||
877 | 839 | """Validate that branch name and owner are set.""" | ||
878 | 840 | if 'branch_name' not in data: | ||
879 | 841 | self.setFieldError( | ||
880 | 842 | 'branch_name', | ||
881 | 843 | 'The branch name must be set.') | ||
882 | 844 | if 'branch_owner' not in data: | ||
883 | 845 | self.setFieldError( | ||
884 | 846 | 'branch_owner', | ||
885 | 847 | 'The branch owner must be set.') | ||
886 | 848 | |||
887 | 849 | def _setRequired(self, names, value): | ||
888 | 850 | """Mark the widget field as optional.""" | ||
889 | 851 | for name in names: | ||
890 | 852 | widget = self.widgets[name] | ||
891 | 853 | # The 'required' property on the widget context is set to False. | ||
892 | 854 | # The widget also has a 'required' property but it isn't used | ||
893 | 855 | # during validation. | ||
894 | 856 | widget.context.required = value | ||
895 | 857 | |||
896 | 858 | def _validSchemes(self, rcs_type): | ||
897 | 859 | """Return the valid schemes for the repository URL.""" | ||
898 | 860 | schemes = set(['http', 'https']) | ||
899 | 861 | # Extend the allowed schemes for the repository URL based on | ||
900 | 862 | # rcs_type. | ||
901 | 863 | extra_schemes = { | ||
902 | 864 | RevisionControlSystemsExtended.BZR_SVN:['svn'], | ||
903 | 865 | RevisionControlSystemsExtended.GIT:['git'], | ||
904 | 866 | } | ||
905 | 867 | schemes.update(extra_schemes.get(rcs_type, [])) | ||
906 | 868 | return schemes | ||
907 | 869 | |||
908 | 870 | def validate_widgets(self, data, names=None): | ||
909 | 871 | """See `LaunchpadFormView`.""" | ||
910 | 872 | names = ['branch_type', 'rcs_type'] | ||
911 | 873 | super(ProductSeriesSetBranchView, self).validate_widgets(data, names) | ||
912 | 874 | branch_type = data.get('branch_type') | ||
913 | 875 | if branch_type == LINK_LP_BZR: | ||
914 | 876 | # Mark other widgets as non-required. | ||
915 | 877 | self._setRequired(['rcs_type', 'repo_url', 'cvs_module', | ||
916 | 878 | 'branch_name', 'branch_owner'], False) | ||
917 | 879 | elif branch_type == CREATE_NEW: | ||
918 | 880 | self._setRequired( | ||
919 | 881 | ['branch_location', 'repo_url', 'rcs_type', 'cvs_module'], | ||
920 | 882 | False) | ||
921 | 883 | elif branch_type == IMPORT_EXTERNAL: | ||
922 | 884 | rcs_type = data.get('rcs_type') | ||
923 | 885 | |||
924 | 886 | # Set the valid schemes based on rcs_type. | ||
925 | 887 | self.widgets['repo_url'].field.allowed_schemes = ( | ||
926 | 888 | self._validSchemes(rcs_type)) | ||
927 | 889 | # The branch location is not required for validation. | ||
928 | 890 | self._setRequired(['branch_location'], False) | ||
929 | 891 | # The cvs_module is required if it is a CVS import. | ||
930 | 892 | if rcs_type == RevisionControlSystemsExtended.CVS: | ||
931 | 893 | self._setRequired(['cvs_module'], True) | ||
932 | 894 | else: | ||
933 | 895 | raise AssertionError("Unknown branch type %s" % branch_type) | ||
934 | 896 | # Perform full validation now. | ||
935 | 897 | super(ProductSeriesSetBranchView, self).validate_widgets(data) | ||
936 | 898 | |||
937 | 899 | def validate(self, data): | ||
938 | 900 | """See `LaunchpadFormView`.""" | ||
939 | 901 | # If widget validation returned errors then there is no need to | ||
940 | 902 | # continue as we'd likely just override the errors reported there. | ||
941 | 903 | if len(self.errors) > 0: | ||
942 | 904 | return | ||
943 | 905 | branch_type = data['branch_type'] | ||
944 | 906 | if branch_type == IMPORT_EXTERNAL: | ||
945 | 907 | self._validateImportExternal(data) | ||
946 | 908 | elif branch_type == LINK_LP_BZR: | ||
947 | 909 | self._validateLinkLpBzr(data) | ||
948 | 910 | elif branch_type == CREATE_NEW: | ||
949 | 911 | self._validateCreateNew(data) | ||
950 | 912 | else: | ||
951 | 913 | raise AssertionError("Unknown branch type %s" % branch_type) | ||
952 | 914 | |||
953 | 915 | @property | ||
954 | 916 | def target(self): | ||
955 | 917 | """The branch target for the context.""" | ||
956 | 918 | return IBranchTarget(self.context) | ||
957 | 919 | |||
958 | 920 | @action(_('Update'), name='update') | ||
959 | 921 | def update_action(self, action, data): | ||
960 | 922 | self.next_url = canonical_url(self.context) | ||
961 | 923 | branch_type = data.get('branch_type') | ||
962 | 924 | if branch_type == LINK_LP_BZR: | ||
963 | 925 | branch_location = data.get('branch_location') | ||
964 | 926 | if branch_location != self.context.branch: | ||
965 | 927 | self.context.branch = branch_location | ||
966 | 928 | # Request an initial upload of translation files. | ||
967 | 929 | getUtility(IRosettaUploadJobSource).create( | ||
968 | 930 | self.context.branch, NULL_REVISION) | ||
969 | 931 | else: | ||
970 | 932 | self.context.branch = branch_location | ||
971 | 933 | self.request.response.addInfoNotification( | ||
972 | 934 | 'Series code location updated.') | ||
973 | 935 | else: | ||
974 | 936 | branch_name = data.get('branch_name') | ||
975 | 937 | branch_owner = data.get('branch_owner') | ||
976 | 938 | |||
977 | 939 | # Create a new branch. | ||
978 | 940 | if branch_type == CREATE_NEW: | ||
979 | 941 | branch = self._createBzrBranch( | ||
980 | 942 | BranchType.HOSTED, branch_name, branch_owner) | ||
981 | 943 | if branch is not None: | ||
982 | 944 | self.context.branch = branch | ||
983 | 945 | self.request.response.addInfoNotification( | ||
984 | 946 | 'New branch created and linked to the series.') | ||
985 | 947 | |||
986 | 948 | # Import or mirror an external branch. | ||
987 | 949 | elif branch_type == IMPORT_EXTERNAL: | ||
988 | 950 | # Either create an externally hosted bzr branch | ||
989 | 951 | # (a.k.a. 'mirrored') or create a new code import. | ||
990 | 952 | rcs_type = data.get('rcs_type') | ||
991 | 953 | if rcs_type == RevisionControlSystemsExtended.BZR: | ||
992 | 954 | branch = self._createBzrBranch( | ||
993 | 955 | BranchType.MIRRORED, branch_name, branch_owner, | ||
994 | 956 | data['repo_url']) | ||
995 | 957 | |||
996 | 958 | if branch is not None: | ||
997 | 959 | self.context.branch = branch | ||
998 | 960 | self.request.response.addInfoNotification( | ||
999 | 961 | 'Mirrored branch created and linked to ' | ||
1000 | 962 | 'the series.') | ||
1001 | 963 | else: | ||
1002 | 964 | # We need to create an import request. | ||
1003 | 965 | |||
1004 | 966 | # Ensure the URL has not already been imported. | ||
1005 | 967 | if rcs_type == RevisionControlSystemsExtended.CVS: | ||
1006 | 968 | cvs_root = data.get('repo_url') | ||
1007 | 969 | cvs_module = data.get('cvs_module') | ||
1008 | 970 | url = None | ||
1009 | 971 | else: | ||
1010 | 972 | cvs_root = None | ||
1011 | 973 | cvs_module = None | ||
1012 | 974 | url = data.get('repo_url') | ||
1013 | 975 | rcs_item = RevisionControlSystems.items[rcs_type.name] | ||
1014 | 976 | code_import = getUtility(ICodeImportSet).new( | ||
1015 | 977 | registrant=branch_owner, | ||
1016 | 978 | target=self.target, | ||
1017 | 979 | branch_name=branch_name, | ||
1018 | 980 | rcs_type=rcs_item, | ||
1019 | 981 | url=url, | ||
1020 | 982 | cvs_root=cvs_root, | ||
1021 | 983 | cvs_module=cvs_module) | ||
1022 | 984 | self.context.branch = code_import.branch | ||
1023 | 985 | self.request.response.addInfoNotification( | ||
1024 | 986 | 'Code import created and branch linked to the ' | ||
1025 | 987 | 'series.') | ||
1026 | 988 | else: | ||
1027 | 989 | raise UnexpectedFormData(branch_type) | ||
1028 | 990 | |||
1029 | 991 | def _createBzrBranch(self, branch_type, branch_name, | ||
1030 | 992 | branch_owner, repo_url=None): | ||
1031 | 993 | """Create a new Bazaar branch. It may be hosted or mirrored. | ||
1032 | 994 | |||
1033 | 995 | Return the branch on success or None. | ||
1034 | 996 | """ | ||
1035 | 997 | branch = None | ||
1036 | 998 | try: | ||
1037 | 999 | namespace = self.target.getNamespace(branch_owner) | ||
1038 | 1000 | branch = namespace.createBranch(branch_type=branch_type, | ||
1039 | 1001 | name=branch_name, | ||
1040 | 1002 | registrant=self.user, | ||
1041 | 1003 | url=repo_url) | ||
1042 | 1004 | if branch_type == BranchType.MIRRORED: | ||
1043 | 1005 | branch.requestMirror() | ||
1044 | 1006 | except BranchCreationForbidden: | ||
1045 | 1007 | self.addError( | ||
1046 | 1008 | "You are not allowed to create branches in %s." % | ||
1047 | 1009 | self.context.displayname) | ||
1048 | 1010 | except BranchExists, e: | ||
1049 | 1011 | self._setBranchExists(e.existing_branch, 'branch_name') | ||
1050 | 1012 | return branch | ||
1051 | 1013 | |||
1052 | 1014 | @property | ||
1053 | 1015 | def cancel_url(self): | ||
1054 | 1016 | """See `LaunchpadFormView`.""" | ||
1055 | 1017 | return canonical_url(self.context) | ||
1056 | 1018 | |||
1057 | 1019 | |||
1058 | 1020 | class ProductSeriesLinkBranchView(LaunchpadEditFormView, ProductSeriesView): | ||
1059 | 648 | """View to set the bazaar branch for a product series.""" | 1021 | """View to set the bazaar branch for a product series.""" |
1060 | 649 | 1022 | ||
1061 | 650 | schema = IProductSeries | 1023 | schema = IProductSeries |
1062 | @@ -753,23 +1126,6 @@ | |||
1063 | 753 | return encodeddata | 1126 | return encodeddata |
1064 | 754 | 1127 | ||
1065 | 755 | 1128 | ||
1066 | 756 | class ProductSeriesSourceListView(LaunchpadView): | ||
1067 | 757 | """A listing of all the running imports. | ||
1068 | 758 | |||
1069 | 759 | See `ICodeImportSet.getActiveImports` for our definition of running. | ||
1070 | 760 | """ | ||
1071 | 761 | |||
1072 | 762 | page_title = 'Available code imports' | ||
1073 | 763 | label = page_title | ||
1074 | 764 | |||
1075 | 765 | def initialize(self): | ||
1076 | 766 | """See `LaunchpadFormView`.""" | ||
1077 | 767 | self.text = self.request.get('text') | ||
1078 | 768 | results = getUtility(ICodeImportSet).getActiveImports(text=self.text) | ||
1079 | 769 | |||
1080 | 770 | self.batchnav = BatchNavigator(results, self.request) | ||
1081 | 771 | |||
1082 | 772 | |||
1083 | 773 | class ProductSeriesFileBugRedirect(LaunchpadView): | 1129 | class ProductSeriesFileBugRedirect(LaunchpadView): |
1084 | 774 | """Redirect to the product's +filebug page.""" | 1130 | """Redirect to the product's +filebug page.""" |
1085 | 775 | 1131 | ||
1086 | 776 | 1132 | ||
1087 | === added file 'lib/lp/registry/browser/tests/productseries-setbranch-view.txt' | |||
1088 | --- lib/lp/registry/browser/tests/productseries-setbranch-view.txt 1970-01-01 00:00:00 +0000 | |||
1089 | +++ lib/lp/registry/browser/tests/productseries-setbranch-view.txt 2010-04-07 13:24:38 +0000 | |||
1090 | @@ -0,0 +1,339 @@ | |||
1091 | 1 | Set branch | ||
1092 | 2 | ---------- | ||
1093 | 3 | |||
1094 | 4 | The productseries +setbranch view allows the user to set a branch for | ||
1095 | 5 | this series. The branch can be one that already exists in Launchpad, | ||
1096 | 6 | or a new branch in Launchpad can be defined, or it can be a repository | ||
1097 | 7 | that exists externally in a variety of version control systems. | ||
1098 | 8 | |||
1099 | 9 | >>> from canonical.launchpad.testing.pages import find_tag_by_id | ||
1100 | 10 | >>> product = factory.makeProduct(name="chevy") | ||
1101 | 11 | >>> series = factory.makeProductSeries(name="impala", product=product) | ||
1102 | 12 | >>> transaction.commit() | ||
1103 | 13 | >>> login_person(product.owner) | ||
1104 | 14 | >>> view = create_initialized_view(series, name='+setbranch', | ||
1105 | 15 | ... principal=product.owner) | ||
1106 | 16 | >>> print find_tag_by_id(view.render(), 'maincontent') | ||
1107 | 17 | <div... | ||
1108 | 18 | ...Link to a Bazaar branch already on Launchpad... | ||
1109 | 19 | ...Create a new, empty branch in Launchpad and link to this series... | ||
1110 | 20 | ...Import a branch hosted somewhere else... | ||
1111 | 21 | ...Branch name:... | ||
1112 | 22 | ...Branch owner:... | ||
1113 | 23 | |||
1114 | 24 | |||
1115 | 25 | Linking to an existing branch | ||
1116 | 26 | ----------------------------- | ||
1117 | 27 | |||
1118 | 28 | If linking to an existing branch is selected then the branch location | ||
1119 | 29 | must be provided. | ||
1120 | 30 | |||
1121 | 31 | >>> form = { | ||
1122 | 32 | ... 'field.branch_type': 'link-lp-bzr', | ||
1123 | 33 | ... 'field.actions.update': 'Update', | ||
1124 | 34 | ... } | ||
1125 | 35 | >>> view = create_initialized_view(series, name='+setbranch', | ||
1126 | 36 | ... principal=product.owner, form=form) | ||
1127 | 37 | >>> for error in view.errors: | ||
1128 | 38 | ... print error | ||
1129 | 39 | The branch location must be set. | ||
1130 | 40 | |||
1131 | 41 | Setting the branch location to an invalid branch results in another | ||
1132 | 42 | validation error. | ||
1133 | 43 | |||
1134 | 44 | >>> form = { | ||
1135 | 45 | ... 'field.branch_type': 'link-lp-bzr', | ||
1136 | 46 | ... 'field.branch_location': 'foo', | ||
1137 | 47 | ... 'field.actions.update': 'Update', | ||
1138 | 48 | ... } | ||
1139 | 49 | >>> view = create_initialized_view(series, name='+setbranch', | ||
1140 | 50 | ... principal=product.owner, form=form) | ||
1141 | 51 | >>> for error in view.errors: | ||
1142 | 52 | ... print error | ||
1143 | 53 | ('Invalid value', InvalidValue("token 'foo' not found in vocabulary")) | ||
1144 | 54 | |||
1145 | 55 | Providing a valid branch results in a successful linking. | ||
1146 | 56 | |||
1147 | 57 | >>> series.branch is None | ||
1148 | 58 | True | ||
1149 | 59 | >>> branch = factory.makeBranch(name='impala-branch', | ||
1150 | 60 | ... owner=product.owner, product=product) | ||
1151 | 61 | >>> form = { | ||
1152 | 62 | ... 'field.branch_type': 'link-lp-bzr', | ||
1153 | 63 | ... 'field.branch_location': branch.unique_name, | ||
1154 | 64 | ... 'field.actions.update': 'Update', | ||
1155 | 65 | ... } | ||
1156 | 66 | >>> view = create_initialized_view(series, name='+setbranch', | ||
1157 | 67 | ... principal=product.owner, form=form) | ||
1158 | 68 | >>> for error in view.errors: | ||
1159 | 69 | ... print error | ||
1160 | 70 | >>> for notification in view.request.response.notifications: | ||
1161 | 71 | ... print notification.message | ||
1162 | 72 | Series code location updated. | ||
1163 | 73 | |||
1164 | 74 | >>> print series.branch.name | ||
1165 | 75 | impala-branch | ||
1166 | 76 | |||
1167 | 77 | |||
1168 | 78 | Creating a new branch | ||
1169 | 79 | --------------------- | ||
1170 | 80 | |||
1171 | 81 | When creating a new branch the branch name and owner must be specified. | ||
1172 | 82 | |||
1173 | 83 | >>> series = factory.makeProductSeries(name="camaro", product=product) | ||
1174 | 84 | >>> transaction.commit() | ||
1175 | 85 | |||
1176 | 86 | >>> form = { | ||
1177 | 87 | ... 'field.branch_type': 'create-new', | ||
1178 | 88 | ... 'field.actions.update': 'Update', | ||
1179 | 89 | ... } | ||
1180 | 90 | >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form) | ||
1181 | 91 | >>> for notification in view.request.response.notifications: | ||
1182 | 92 | ... print notification.message | ||
1183 | 93 | >>> for error in view.errors: | ||
1184 | 94 | ... print error | ||
1185 | 95 | The branch name must be set. | ||
1186 | 96 | The branch owner must be set. | ||
1187 | 97 | |||
1188 | 98 | >>> from lp.registry.interfaces.person import IPersonSet | ||
1189 | 99 | >>> mark = getUtility(IPersonSet).getByEmail('mark@example.com') | ||
1190 | 100 | >>> form = { | ||
1191 | 101 | ... 'field.branch_type': 'create-new', | ||
1192 | 102 | ... 'field.branch_name': 'camaro-branch', | ||
1193 | 103 | ... 'field.branch_owner': product.owner.name, | ||
1194 | 104 | ... 'field.actions.update': 'Update', | ||
1195 | 105 | ... } | ||
1196 | 106 | |||
1197 | 107 | >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form) | ||
1198 | 108 | >>> for error in view.errors: | ||
1199 | 109 | ... print error | ||
1200 | 110 | >>> for notification in view.request.response.notifications: | ||
1201 | 111 | ... print notification.message | ||
1202 | 112 | New branch created and linked to the series. | ||
1203 | 113 | >>> print series.branch.name | ||
1204 | 114 | camaro-branch | ||
1205 | 115 | |||
1206 | 116 | |||
1207 | 117 | Import a branch hosted elsewhere | ||
1208 | 118 | -------------------------------- | ||
1209 | 119 | |||
1210 | 120 | Importing an externally hosted branch can either be a mirror, if a | ||
1211 | 121 | Bazaar branch, or an import, if a git, hg, cvs, or svn branch. | ||
1212 | 122 | |||
1213 | 123 | Lots of data are required to create an import. | ||
1214 | 124 | |||
1215 | 125 | >>> series = factory.makeProductSeries(name="blazer", product=product) | ||
1216 | 126 | >>> transaction.commit() | ||
1217 | 127 | |||
1218 | 128 | >>> form = { | ||
1219 | 129 | ... 'field.branch_type': 'import-external', | ||
1220 | 130 | ... 'field.actions.update': 'Update', | ||
1221 | 131 | ... } | ||
1222 | 132 | >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form) | ||
1223 | 133 | >>> for notification in view.request.response.notifications: | ||
1224 | 134 | ... print notification.message | ||
1225 | 135 | >>> for error in view.errors: | ||
1226 | 136 | ... print error | ||
1227 | 137 | You must set the external repository URL. | ||
1228 | 138 | You must specify the type of RCS for the remote host. | ||
1229 | 139 | The branch name must be set. | ||
1230 | 140 | The branch owner must be set. | ||
1231 | 141 | |||
1232 | 142 | For Bazaar branches the scheme may only be http or https. | ||
1233 | 143 | |||
1234 | 144 | >>> form = { | ||
1235 | 145 | ... 'field.branch_type': 'import-external', | ||
1236 | 146 | ... 'field.rcs_type': 'BZR', | ||
1237 | 147 | ... 'field.branch_name': 'blazer-branch', | ||
1238 | 148 | ... 'field.branch_owner': product.owner.name, | ||
1239 | 149 | ... 'field.repo_url': 'bzr://bzr.com/foo', | ||
1240 | 150 | ... 'field.actions.update': 'Update', | ||
1241 | 151 | ... } | ||
1242 | 152 | >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form) | ||
1243 | 153 | >>> for notification in view.request.response.notifications: | ||
1244 | 154 | ... print notification.message | ||
1245 | 155 | >>> for error in view.errors: | ||
1246 | 156 | ... print error | ||
1247 | 157 | ('repo_url'...The URI scheme "bzr" is not allowed. Only URIs with the following schemes may be | ||
1248 | 158 | used: http, https')) | ||
1249 | 159 | |||
1250 | 160 | A correct URL is accepted. | ||
1251 | 161 | |||
1252 | 162 | >>> form = { | ||
1253 | 163 | ... 'field.branch_type': 'import-external', | ||
1254 | 164 | ... 'field.rcs_type': 'BZR', | ||
1255 | 165 | ... 'field.branch_name': 'blazer-branch', | ||
1256 | 166 | ... 'field.branch_owner': product.owner.name, | ||
1257 | 167 | ... 'field.repo_url': 'http://bzr.com/foo', | ||
1258 | 168 | ... 'field.actions.update': 'Update', | ||
1259 | 169 | ... } | ||
1260 | 170 | >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form) | ||
1261 | 171 | >>> for error in view.errors: | ||
1262 | 172 | ... print error | ||
1263 | 173 | >>> for notification in view.request.response.notifications: | ||
1264 | 174 | ... print notification.message | ||
1265 | 175 | Mirrored branch created and linked to the series. | ||
1266 | 176 | >>> print series.branch.name | ||
1267 | 177 | blazer-branch | ||
1268 | 178 | |||
1269 | 179 | Git branches cannnot use svn. | ||
1270 | 180 | |||
1271 | 181 | >>> form = { | ||
1272 | 182 | ... 'field.branch_type': 'import-external', | ||
1273 | 183 | ... 'field.rcs_type': 'GIT', | ||
1274 | 184 | ... 'field.branch_name': 'chevette-branch', | ||
1275 | 185 | ... 'field.branch_owner': product.owner.name, | ||
1276 | 186 | ... 'field.repo_url': 'svn://svn.com/chevette', | ||
1277 | 187 | ... 'field.actions.update': 'Update', | ||
1278 | 188 | ... } | ||
1279 | 189 | >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form) | ||
1280 | 190 | >>> for notification in view.request.response.notifications: | ||
1281 | 191 | ... print notification.message | ||
1282 | 192 | >>> for error in view.errors: | ||
1283 | 193 | ... print error | ||
1284 | 194 | ('repo_url'...'The URI scheme "svn" is not allowed. Only | ||
1285 | 195 | URIs with the following schemes may be used: git, http, https')) | ||
1286 | 196 | |||
1287 | 197 | But Git branches may use git. | ||
1288 | 198 | |||
1289 | 199 | >>> series = factory.makeProductSeries(name="chevette", product=product) | ||
1290 | 200 | >>> transaction.commit() | ||
1291 | 201 | >>> form = { | ||
1292 | 202 | ... 'field.branch_type': 'import-external', | ||
1293 | 203 | ... 'field.rcs_type': 'GIT', | ||
1294 | 204 | ... 'field.branch_name': 'chevette-branch', | ||
1295 | 205 | ... 'field.branch_owner': product.owner.name, | ||
1296 | 206 | ... 'field.repo_url': 'git://github.com/chevette', | ||
1297 | 207 | ... 'field.actions.update': 'Update', | ||
1298 | 208 | ... } | ||
1299 | 209 | >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form) | ||
1300 | 210 | >>> transaction.commit() | ||
1301 | 211 | >>> for error in view.errors: | ||
1302 | 212 | ... print error | ||
1303 | 213 | >>> for notification in view.request.response.notifications: | ||
1304 | 214 | ... print notification.message | ||
1305 | 215 | Code import created and branch linked to the series. | ||
1306 | 216 | >>> print series.branch.name | ||
1307 | 217 | chevette-branch | ||
1308 | 218 | |||
1309 | 219 | But Subversion branches cannnot use git. | ||
1310 | 220 | |||
1311 | 221 | >>> form = { | ||
1312 | 222 | ... 'field.branch_type': 'import-external', | ||
1313 | 223 | ... 'field.rcs_type': 'BZR_SVN', | ||
1314 | 224 | ... 'field.branch_name': 'suburban-branch', | ||
1315 | 225 | ... 'field.branch_owner': product.owner.name, | ||
1316 | 226 | ... 'field.repo_url': 'git://github.com/suburban', | ||
1317 | 227 | ... 'field.actions.update': 'Update', | ||
1318 | 228 | ... } | ||
1319 | 229 | >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form) | ||
1320 | 230 | >>> for notification in view.request.response.notifications: | ||
1321 | 231 | ... print notification.message | ||
1322 | 232 | >>> for error in view.errors: | ||
1323 | 233 | ... print error | ||
1324 | 234 | ('repo_url'...'The URI scheme "git" is not allowed. Only | ||
1325 | 235 | URIs with the following schemes may be used: http, https, svn')) | ||
1326 | 236 | |||
1327 | 237 | But Subversion branches may use svn as the scheme. | ||
1328 | 238 | |||
1329 | 239 | >>> series = factory.makeProductSeries(name="suburban", product=product) | ||
1330 | 240 | >>> transaction.commit() | ||
1331 | 241 | >>> form = { | ||
1332 | 242 | ... 'field.branch_type': 'import-external', | ||
1333 | 243 | ... 'field.rcs_type': 'BZR_SVN', | ||
1334 | 244 | ... 'field.branch_name': 'suburban-branch', | ||
1335 | 245 | ... 'field.branch_owner': product.owner.name, | ||
1336 | 246 | ... 'field.repo_url': 'svn://svn.com/suburban', | ||
1337 | 247 | ... 'field.actions.update': 'Update', | ||
1338 | 248 | ... } | ||
1339 | 249 | >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form) | ||
1340 | 250 | >>> for error in view.errors: | ||
1341 | 251 | ... print error | ||
1342 | 252 | >>> for notification in view.request.response.notifications: | ||
1343 | 253 | ... print notification.message | ||
1344 | 254 | Code import created and branch linked to the series. | ||
1345 | 255 | >>> print series.branch.name | ||
1346 | 256 | suburban-branch | ||
1347 | 257 | |||
1348 | 258 | Mercurial branches must use http or https as the scheme. | ||
1349 | 259 | |||
1350 | 260 | >>> series = factory.makeProductSeries(name="malibu", product=product) | ||
1351 | 261 | >>> transaction.commit() | ||
1352 | 262 | >>> form = { | ||
1353 | 263 | ... 'field.branch_type': 'import-external', | ||
1354 | 264 | ... 'field.rcs_type': 'HG', | ||
1355 | 265 | ... 'field.branch_name': 'malibu-branch', | ||
1356 | 266 | ... 'field.branch_owner': product.owner.name, | ||
1357 | 267 | ... 'field.repo_url': 'https://mercurial.com/branch', | ||
1358 | 268 | ... 'field.actions.update': 'Update', | ||
1359 | 269 | ... } | ||
1360 | 270 | >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form) | ||
1361 | 271 | >>> for error in view.errors: | ||
1362 | 272 | ... print error | ||
1363 | 273 | >>> for notification in view.request.response.notifications: | ||
1364 | 274 | ... print notification.message | ||
1365 | 275 | Code import created and branch linked to the series. | ||
1366 | 276 | >>> print series.branch.name | ||
1367 | 277 | malibu-branch | ||
1368 | 278 | |||
1369 | 279 | CVS branches must use http or https as the scheme and must have the | ||
1370 | 280 | CVS module field specified. | ||
1371 | 281 | |||
1372 | 282 | >>> series = factory.makeProductSeries(name="corvair", product=product) | ||
1373 | 283 | >>> transaction.commit() | ||
1374 | 284 | >>> form = { | ||
1375 | 285 | ... 'field.branch_type': 'import-external', | ||
1376 | 286 | ... 'field.rcs_type': 'CVS', | ||
1377 | 287 | ... 'field.branch_name': 'corvair-branch', | ||
1378 | 288 | ... 'field.branch_owner': product.owner.name, | ||
1379 | 289 | ... 'field.repo_url': 'https://cvs.com/branch', | ||
1380 | 290 | ... 'field.actions.update': 'Update', | ||
1381 | 291 | ... } | ||
1382 | 292 | >>> view = create_initialized_view(series, name='+setbranch', | ||
1383 | 293 | ... principal=product.owner, form=form) | ||
1384 | 294 | >>> for notification in view.request.response.notifications: | ||
1385 | 295 | ... print notification.message | ||
1386 | 296 | >>> for error in view.errors: | ||
1387 | 297 | ... print error | ||
1388 | 298 | The CVS module must be set. | ||
1389 | 299 | |||
1390 | 300 | >>> form = { | ||
1391 | 301 | ... 'field.branch_type': 'import-external', | ||
1392 | 302 | ... 'field.rcs_type': 'CVS', | ||
1393 | 303 | ... 'field.branch_name': 'corvair-branch', | ||
1394 | 304 | ... 'field.branch_owner': product.owner.name, | ||
1395 | 305 | ... 'field.repo_url': 'https://cvs.com/branch', | ||
1396 | 306 | ... 'field.cvs_module': 'root', | ||
1397 | 307 | ... 'field.actions.update': 'Update', | ||
1398 | 308 | ... } | ||
1399 | 309 | >>> view = create_initialized_view(series, name='+setbranch', | ||
1400 | 310 | ... principal=product.owner, form=form) | ||
1401 | 311 | >>> for error in view.errors: | ||
1402 | 312 | ... print error | ||
1403 | 313 | >>> for notification in view.request.response.notifications: | ||
1404 | 314 | ... print notification.message | ||
1405 | 315 | Code import created and branch linked to the series. | ||
1406 | 316 | >>> print series.branch.name | ||
1407 | 317 | corvair-branch | ||
1408 | 318 | |||
1409 | 319 | Attempting to import a location that has already been imported results | ||
1410 | 320 | in an error. | ||
1411 | 321 | |||
1412 | 322 | >>> form = { | ||
1413 | 323 | ... 'field.branch_type': 'import-external', | ||
1414 | 324 | ... 'field.rcs_type': 'GIT', | ||
1415 | 325 | ... 'field.branch_name': 'chevette-branch-dup', | ||
1416 | 326 | ... 'field.branch_owner': product.owner.name, | ||
1417 | 327 | ... 'field.repo_url': 'git://github.com/chevette', | ||
1418 | 328 | ... 'field.actions.update': 'Update', | ||
1419 | 329 | ... } | ||
1420 | 330 | >>> view = create_initialized_view(series, name='+setbranch', | ||
1421 | 331 | ... principal=product.owner, form=form) | ||
1422 | 332 | >>> for error in view.errors: | ||
1423 | 333 | ... print error | ||
1424 | 334 | <BLANKLINE> | ||
1425 | 335 | This foreign branch URL is already specified for | ||
1426 | 336 | the imported branch <a href="http://code.launchpad.dev/~.../chevy/chevette-branch">~.../chevy/chevette-branch</a>. | ||
1427 | 337 | |||
1428 | 338 | >>> for notification in view.request.response.notifications: | ||
1429 | 339 | ... print notification.message | ||
1430 | 0 | 340 | ||
1431 | === added file 'lib/lp/registry/stories/productseries/xx-productseries-set-branch.txt' | |||
1432 | --- lib/lp/registry/stories/productseries/xx-productseries-set-branch.txt 1970-01-01 00:00:00 +0000 | |||
1433 | +++ lib/lp/registry/stories/productseries/xx-productseries-set-branch.txt 2010-04-07 13:24:38 +0000 | |||
1434 | @@ -0,0 +1,147 @@ | |||
1435 | 1 | Setting the branch for a product series | ||
1436 | 2 | ======================================= | ||
1437 | 3 | |||
1438 | 4 | A product series should have a branch set for it. The branch can be | ||
1439 | 5 | hosted on Launchpad or somewhere else. Foreign branches can be in | ||
1440 | 6 | Bazaar, Git, Mercurial, Subversion, or CVS. Though internally | ||
1441 | 7 | Launchpad treats those scenarios differently we provide a single page | ||
1442 | 8 | to the user to set up the branch. | ||
1443 | 9 | |||
1444 | 10 | At present, the unified page for setting up the branch is not linked | ||
1445 | 11 | from anywhere, so it must be navigated to directly. | ||
1446 | 12 | |||
1447 | 13 | >>> browser = setupBrowser(auth="Basic test@canonical.com:test") | ||
1448 | 14 | >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch') | ||
1449 | 15 | |||
1450 | 16 | The default choice for the type of branch to set is one that | ||
1451 | 17 | already exists on Launchpad. | ||
1452 | 18 | |||
1453 | 19 | >>> print_radio_button_field(browser.contents, 'branch_type') | ||
1454 | 20 | (*) Link to a Bazaar branch already on Launchpad | ||
1455 | 21 | ( ) Create a new, empty branch in Launchpad and link to this series | ||
1456 | 22 | ( ) Import a branch hosted somewhere else | ||
1457 | 23 | |||
1458 | 24 | |||
1459 | 25 | Linking to an existing branch | ||
1460 | 26 | ----------------------------- | ||
1461 | 27 | |||
1462 | 28 | A user can choose to link to an existing branch on Launchpad. | ||
1463 | 29 | |||
1464 | 30 | >>> login('test@canonical.com') | ||
1465 | 31 | >>> from zope.component import getUtility | ||
1466 | 32 | >>> from lp.registry.interfaces.product import IProductSet | ||
1467 | 33 | >>> productset = getUtility(IProductSet) | ||
1468 | 34 | >>> firefox = productset.getByName('firefox') | ||
1469 | 35 | >>> branch = factory.makeBranch(name="firefox-hosted-branch", product=firefox) | ||
1470 | 36 | >>> branch_name = branch.unique_name | ||
1471 | 37 | >>> logout() | ||
1472 | 38 | |||
1473 | 39 | >>> browser.getControl(name='field.branch_location').value = branch_name | ||
1474 | 40 | >>> browser.getControl('Update').click() | ||
1475 | 41 | >>> for message in get_feedback_messages(browser.contents): | ||
1476 | 42 | ... print extract_text(message) | ||
1477 | 43 | Series code location updated. | ||
1478 | 44 | >>> print browser.url | ||
1479 | 45 | http://launchpad.dev/firefox/trunk | ||
1480 | 46 | |||
1481 | 47 | |||
1482 | 48 | Creating a new branch | ||
1483 | 49 | --------------------- | ||
1484 | 50 | |||
1485 | 51 | A brand new, empty branch on Launchpad can be created and set as the | ||
1486 | 52 | series branch. | ||
1487 | 53 | |||
1488 | 54 | >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch') | ||
1489 | 55 | >>> browser.getControl('Create a new, empty branch in Launchpad').click() | ||
1490 | 56 | >>> browser.getControl('Update').click() | ||
1491 | 57 | >>> for message in get_feedback_messages(browser.contents): | ||
1492 | 58 | ... print extract_text(message) | ||
1493 | 59 | There is 1 error. | ||
1494 | 60 | Required input is missing. | ||
1495 | 61 | |||
1496 | 62 | However in order to create the branch the name and owner must be | ||
1497 | 63 | specified. The owner is a pre-populated dropdown list so the default | ||
1498 | 64 | can be used. | ||
1499 | 65 | |||
1500 | 66 | >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch') | ||
1501 | 67 | >>> browser.getControl('Create a new, empty branch in Launchpad').click() | ||
1502 | 68 | >>> browser.getControl(name='field.branch_name').value = 'new-firefox-branch' | ||
1503 | 69 | >>> browser.getControl('Update').click() | ||
1504 | 70 | >>> for message in get_feedback_messages(browser.contents): | ||
1505 | 71 | ... print extract_text(message) | ||
1506 | 72 | New branch created and linked to the series. | ||
1507 | 73 | >>> print browser.url | ||
1508 | 74 | http://launchpad.dev/firefox/trunk | ||
1509 | 75 | |||
1510 | 76 | |||
1511 | 77 | Linking to an external branch | ||
1512 | 78 | ----------------------------- | ||
1513 | 79 | |||
1514 | 80 | An external branch can be linked. The branch can be a Bazaar branch | ||
1515 | 81 | or be a Git, Mercurial, Subversion, or CVS branch. | ||
1516 | 82 | |||
1517 | 83 | Each of these types must provide the URL of the external repository, | ||
1518 | 84 | the branch name to use in Launchpad, and the branch owner. | ||
1519 | 85 | |||
1520 | 86 | >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch') | ||
1521 | 87 | >>> browser.getControl('Import a branch hosted somewhere else').click() | ||
1522 | 88 | >>> browser.getControl('Branch name').value = 'bzr-firefox-branch' | ||
1523 | 89 | >>> browser.getControl('Bazaar', index=0).click() | ||
1524 | 90 | >>> browser.getControl('Branch URL').value = 'https://bzr.example.com/branch' | ||
1525 | 91 | >>> browser.getControl('Update').click() | ||
1526 | 92 | >>> for message in get_feedback_messages(browser.contents): | ||
1527 | 93 | ... print extract_text(message) | ||
1528 | 94 | Series code location updated. | ||
1529 | 95 | >>> print browser.url | ||
1530 | 96 | http://launchpad.dev/firefox/trunk | ||
1531 | 97 | |||
1532 | 98 | The process is the same for a Git external branch, though the novel | ||
1533 | 99 | "git://" scheme can also be used. | ||
1534 | 100 | |||
1535 | 101 | >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch') | ||
1536 | 102 | >>> browser.getControl('Import a branch hosted somewhere else').click() | ||
1537 | 103 | >>> browser.getControl('Branch name').value = 'git-firefox-branch' | ||
1538 | 104 | >>> browser.getControl('Git').click() | ||
1539 | 105 | >>> browser.getControl('Branch URL').value = 'git://git.example.com/branch' | ||
1540 | 106 | >>> browser.getControl('Update').click() | ||
1541 | 107 | >>> for message in get_feedback_messages(browser.contents): | ||
1542 | 108 | ... print extract_text(message) | ||
1543 | 109 | Code import created and branch linked to the series. | ||
1544 | 110 | >>> print browser.url | ||
1545 | 111 | http://launchpad.dev/firefox/trunk | ||
1546 | 112 | |||
1547 | 113 | Likewise Subversion can use the "svn://" scheme. | ||
1548 | 114 | |||
1549 | 115 | >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch') | ||
1550 | 116 | >>> browser.getControl('Import a branch hosted somewhere else').click() | ||
1551 | 117 | >>> browser.getControl('Branch name').value = 'svn-firefox-branch' | ||
1552 | 118 | >>> browser.getControl('SVN').click() | ||
1553 | 119 | >>> browser.getControl('Branch URL').value = 'svn://svn.example.com/branch' | ||
1554 | 120 | >>> browser.getControl('Update').click() | ||
1555 | 121 | >>> for message in get_feedback_messages(browser.contents): | ||
1556 | 122 | ... print extract_text(message) | ||
1557 | 123 | Code import created and branch linked to the series. | ||
1558 | 124 | >>> print browser.url | ||
1559 | 125 | http://launchpad.dev/firefox/trunk | ||
1560 | 126 | |||
1561 | 127 | The branch owner can be the logged in user or one of her teams. | ||
1562 | 128 | |||
1563 | 129 | >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch') | ||
1564 | 130 | >>> browser.getControl('Import a branch hosted somewhere else').click() | ||
1565 | 131 | >>> browser.getControl('Branch name').value = 'hg-firefox-branch' | ||
1566 | 132 | >>> browser.getControl('Mercurial').click() | ||
1567 | 133 | >>> browser.getControl('Branch URL').value = 'http://hg.example.com/branch' | ||
1568 | 134 | >>> browser.getControl('Branch owner').value = ['hwdb-team'] | ||
1569 | 135 | >>> browser.getControl('Update').click() | ||
1570 | 136 | >>> for message in get_feedback_messages(browser.contents): | ||
1571 | 137 | ... print extract_text(message) | ||
1572 | 138 | Code import created and branch linked to the series. | ||
1573 | 139 | >>> print browser.url | ||
1574 | 140 | http://launchpad.dev/firefox/trunk | ||
1575 | 141 | >>> login('test@canonical.com') | ||
1576 | 142 | >>> firefox_trunk = firefox.getSeries('trunk') | ||
1577 | 143 | >>> print firefox_trunk.branch.unique_name | ||
1578 | 144 | ~hwdb-team/firefox/hg-firefox-branch | ||
1579 | 145 | >>> print firefox_trunk.branch.owner.name | ||
1580 | 146 | hwdb-team | ||
1581 | 147 | >>> logout() | ||
1582 | 0 | 148 | ||
1583 | === modified file 'lib/lp/registry/templates/productseries-codesummary.pt' | |||
1584 | --- lib/lp/registry/templates/productseries-codesummary.pt 2010-03-09 22:06:12 +0000 | |||
1585 | +++ lib/lp/registry/templates/productseries-codesummary.pt 2010-04-07 13:24:38 +0000 | |||
1586 | @@ -29,7 +29,7 @@ | |||
1587 | 29 | <li> | 29 | <li> |
1588 | 30 | <p> | 30 | <p> |
1589 | 31 | If the code is in a Bazaar branch not yet on Launchpad | 31 | If the code is in a Bazaar branch not yet on Launchpad |
1591 | 32 | you can either | 32 | you can either: |
1592 | 33 | </p> | 33 | </p> |
1593 | 34 | 34 | ||
1594 | 35 | <ul class="bulleted" style="margin-bottom: 0;"> | 35 | <ul class="bulleted" style="margin-bottom: 0;"> |
1595 | @@ -39,7 +39,7 @@ | |||
1596 | 39 | registering a mirrored branch</a> | 39 | registering a mirrored branch</a> |
1597 | 40 | </li> | 40 | </li> |
1598 | 41 | <li id="ssh-key-directions"> | 41 | <li id="ssh-key-directions"> |
1600 | 42 | Push the branch directly to Launchpad. eg. with <br /> | 42 | Push the branch directly to Launchpad, e.g. with:<br /> |
1601 | 43 | <tt><strong> | 43 | <tt><strong> |
1602 | 44 | bzr push lp:~<tal:user replace="view/user/name"/>/<tal:project replace="context/product/name"/>/trunk | 44 | bzr push lp:~<tal:user replace="view/user/name"/>/<tal:project replace="context/product/name"/>/trunk |
1603 | 45 | </strong></tt> | 45 | </strong></tt> |
1604 | @@ -58,7 +58,7 @@ | |||
1605 | 58 | <a tal:attributes="href view/request_import_link">request that the branch be imported to Bazaar</a>. | 58 | <a tal:attributes="href view/request_import_link">request that the branch be imported to Bazaar</a>. |
1606 | 59 | </li> | 59 | </li> |
1607 | 60 | </ul> | 60 | </ul> |
1609 | 61 | 61 | ||
1610 | 62 | <ul class="horizontal"> | 62 | <ul class="horizontal"> |
1611 | 63 | <li> | 63 | <li> |
1612 | 64 | <a tal:replace="structure context/menu:overview/link_branch/fmt:link" /> | 64 | <a tal:replace="structure context/menu:overview/link_branch/fmt:link" /> |
1613 | 65 | 65 | ||
1614 | === modified file 'lib/lp/registry/templates/productseries-linkbranch.pt' | |||
1615 | --- lib/lp/registry/templates/productseries-linkbranch.pt 2009-08-11 21:26:30 +0000 | |||
1616 | +++ lib/lp/registry/templates/productseries-linkbranch.pt 2010-04-07 13:24:38 +0000 | |||
1617 | @@ -7,8 +7,44 @@ | |||
1618 | 7 | i18n:domain="launchpad"> | 7 | i18n:domain="launchpad"> |
1619 | 8 | <body> | 8 | <body> |
1620 | 9 | <div metal:fill-slot="main"> | 9 | <div metal:fill-slot="main"> |
1622 | 10 | <div metal:use-macro="context/@@launchpad_form/form" /> | 10 | <ul> |
1623 | 11 | <li>If the code is already in a Bazaar branch registered with Launchpad, | ||
1624 | 12 | specify it here: | ||
1625 | 13 | <div metal:use-macro="context/@@launchpad_form/form" /> | ||
1626 | 14 | </li> | ||
1627 | 15 | |||
1628 | 16 | <li> | ||
1629 | 17 | <p> | ||
1630 | 18 | Otherwise, if the code is in a Bazaar branch not yet on Launchpad | ||
1631 | 19 | you can either: | ||
1632 | 20 | </p> | ||
1633 | 21 | |||
1634 | 22 | <ul class="bulleted" style="margin-bottom: 0;"> | ||
1635 | 23 | <li> | ||
1636 | 24 | Have the branch mirrored from a remote location by | ||
1637 | 25 | <a tal:attributes="href context/menu:overview/branch_add/fmt:url"> | ||
1638 | 26 | registering a mirrored branch</a> | ||
1639 | 27 | </li> | ||
1640 | 28 | <li id="ssh-key-directions"> | ||
1641 | 29 | Push the branch directly to Launchpad, e.g. with:<br /> | ||
1642 | 30 | <tt><strong> | ||
1643 | 31 | bzr push lp:~<tal:user replace="view/user/name"/>/<tal:project replace="context/product/name"/>/trunk | ||
1644 | 32 | </strong></tt> | ||
1645 | 33 | <tal:no-keys condition="not:view/user/sshkeys"> | ||
1646 | 34 | <br/>To authenticate with the Launchpad branch upload service, | ||
1647 | 35 | you need to | ||
1648 | 36 | <a tal:attributes="href string:${view/user/fmt:url}/+editsshkeys"> | ||
1649 | 37 | register a SSH key</a>. | ||
1650 | 38 | </tal:no-keys> | ||
1651 | 39 | </li> | ||
1652 | 40 | </ul> | ||
1653 | 41 | </li> | ||
1654 | 42 | |||
1655 | 43 | <li> | ||
1656 | 44 | If the code is in git, CVS or Subversion you can | ||
1657 | 45 | <a tal:attributes="href view/request_import_link">request that the branch be imported to Bazaar</a>. | ||
1658 | 46 | </li> | ||
1659 | 47 | </ul> | ||
1660 | 11 | </div> | 48 | </div> |
1661 | 12 | </body> | 49 | </body> |
1662 | 13 | </html> | 50 | </html> |
1663 | 14 | |||
1664 | 15 | 51 | ||
1665 | === added file 'lib/lp/registry/templates/productseries-setbranch.pt' | |||
1666 | --- lib/lp/registry/templates/productseries-setbranch.pt 1970-01-01 00:00:00 +0000 | |||
1667 | +++ lib/lp/registry/templates/productseries-setbranch.pt 2010-04-07 13:24:38 +0000 | |||
1668 | @@ -0,0 +1,129 @@ | |||
1669 | 1 | <html | ||
1670 | 2 | xmlns="http://www.w3.org/1999/xhtml" | ||
1671 | 3 | xmlns:tal="http://xml.zope.org/namespaces/tal" | ||
1672 | 4 | xmlns:metal="http://xml.zope.org/namespaces/metal" | ||
1673 | 5 | xmlns:i18n="http://xml.zope.org/namespaces/i18n" | ||
1674 | 6 | metal:use-macro="view/macro:page/main_only" | ||
1675 | 7 | i18n:domain="launchpad"> | ||
1676 | 8 | |||
1677 | 9 | <body> | ||
1678 | 10 | |||
1679 | 11 | <metal:block fill-slot="head_epilogue"> | ||
1680 | 12 | <style type="text/css"> | ||
1681 | 13 | .subordinate { | ||
1682 | 14 | margin: 0.5em 0 0.5em 4em; | ||
1683 | 15 | } | ||
1684 | 16 | </style> | ||
1685 | 17 | </metal:block> | ||
1686 | 18 | |||
1687 | 19 | <div metal:fill-slot="main"> | ||
1688 | 20 | |||
1689 | 21 | <div metal:use-macro="context/@@launchpad_form/form"> | ||
1690 | 22 | |||
1691 | 23 | <metal:formbody fill-slot="widgets"> | ||
1692 | 24 | |||
1693 | 25 | <table class="form"> | ||
1694 | 26 | |||
1695 | 27 | <tr> | ||
1696 | 28 | <td> | ||
1697 | 29 | <label tal:replace="structure view/branch_type_link"> | ||
1698 | 30 | Link to a Bazaar branch already in Launchpad | ||
1699 | 31 | </label> | ||
1700 | 32 | <table class="subordinate"> | ||
1701 | 33 | <tal:widget define="widget nocall:view/widgets/branch_location"> | ||
1702 | 34 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
1703 | 35 | </tal:widget> | ||
1704 | 36 | </table> | ||
1705 | 37 | </td> | ||
1706 | 38 | </tr> | ||
1707 | 39 | |||
1708 | 40 | <tr> | ||
1709 | 41 | <td> | ||
1710 | 42 | <label tal:replace="structure view/branch_type_create"> | ||
1711 | 43 | Create a new, empty branch in Launchpad and link | ||
1712 | 44 | to this series | ||
1713 | 45 | </label> | ||
1714 | 46 | </td> | ||
1715 | 47 | </tr> | ||
1716 | 48 | |||
1717 | 49 | <tr> | ||
1718 | 50 | <td> | ||
1719 | 51 | <label tal:replace="structure view/branch_type_import"> | ||
1720 | 52 | Import a branch hosted somewhere else | ||
1721 | 53 | </label> | ||
1722 | 54 | <table class="subordinate"> | ||
1723 | 55 | <tal:widget define="widget nocall:view/widgets/repo_url"> | ||
1724 | 56 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
1725 | 57 | </tal:widget> | ||
1726 | 58 | |||
1727 | 59 | <tr> | ||
1728 | 60 | <td> | ||
1729 | 61 | <label tal:replace="structure view/rcs_type_bzr"> | ||
1730 | 62 | Bazaar, hosted externally | ||
1731 | 63 | </label> | ||
1732 | 64 | </td> | ||
1733 | 65 | </tr> | ||
1734 | 66 | |||
1735 | 67 | <tr> | ||
1736 | 68 | <td> | ||
1737 | 69 | <label tal:replace="structure view/rcs_type_git"> | ||
1738 | 70 | Git | ||
1739 | 71 | </label> | ||
1740 | 72 | </td> | ||
1741 | 73 | </tr> | ||
1742 | 74 | |||
1743 | 75 | <tr> | ||
1744 | 76 | <td> | ||
1745 | 77 | <label tal:replace="structure view/rcs_type_svn"> | ||
1746 | 78 | SVN | ||
1747 | 79 | </label> | ||
1748 | 80 | </td> | ||
1749 | 81 | </tr> | ||
1750 | 82 | |||
1751 | 83 | <tr> | ||
1752 | 84 | <td> | ||
1753 | 85 | <label tal:replace="structure view/rcs_type_hg"> | ||
1754 | 86 | Mercurial | ||
1755 | 87 | </label> | ||
1756 | 88 | </td> | ||
1757 | 89 | </tr> | ||
1758 | 90 | |||
1759 | 91 | <tr> | ||
1760 | 92 | <td> | ||
1761 | 93 | <label tal:replace="structure view/rcs_type_cvs"> | ||
1762 | 94 | CVS | ||
1763 | 95 | </label> | ||
1764 | 96 | <table class="subordinate"> | ||
1765 | 97 | <tal:widget define="widget nocall:view/widgets/cvs_module"> | ||
1766 | 98 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
1767 | 99 | </tal:widget> | ||
1768 | 100 | </table> | ||
1769 | 101 | </td> | ||
1770 | 102 | </tr> | ||
1771 | 103 | |||
1772 | 104 | </table> | ||
1773 | 105 | </td> | ||
1774 | 106 | </tr> | ||
1775 | 107 | |||
1776 | 108 | <tal:widget define="widget nocall:view/widgets/branch_name"> | ||
1777 | 109 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
1778 | 110 | </tal:widget> | ||
1779 | 111 | <tal:widget define="widget nocall:view/widgets/branch_owner"> | ||
1780 | 112 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
1781 | 113 | </tal:widget> | ||
1782 | 114 | |||
1783 | 115 | </table> | ||
1784 | 116 | <input tal:replace="structure view/rcs_type_emptymarker" /> | ||
1785 | 117 | |||
1786 | 118 | </metal:formbody> | ||
1787 | 119 | </div> | ||
1788 | 120 | |||
1789 | 121 | <script type="text/javascript"> | ||
1790 | 122 | YUI().use('lp.code.productseries_setbranch', function(Y) { | ||
1791 | 123 | Y.on('domready', Y.lp.code.productseries_setbranch.setup); | ||
1792 | 124 | }); | ||
1793 | 125 | </script> | ||
1794 | 126 | |||
1795 | 127 | </div> | ||
1796 | 128 | </body> | ||
1797 | 129 | </html> | ||
1798 | 0 | 130 | ||
1799 | === modified file 'lib/lp/testing/factory.py' | |||
1800 | --- lib/lp/testing/factory.py 2010-04-05 17:40:35 +0000 | |||
1801 | +++ lib/lp/testing/factory.py 2010-04-07 13:24:38 +0000 | |||
1802 | @@ -149,8 +149,8 @@ | |||
1803 | 149 | 149 | ||
1804 | 150 | DIFF = """\ | 150 | DIFF = """\ |
1805 | 151 | === zbqvsvrq svyr 'yvo/yc/pbqr/vagresnprf/qvss.cl' | 151 | === zbqvsvrq svyr 'yvo/yc/pbqr/vagresnprf/qvss.cl' |
1808 | 152 | --- yvo/yc/pbqr/vagresnprf/qvss.cl 2009-10-01 13:25:12 +0000 | 152 | --- yvo/yc/pbqr/vagresnprf/qvss.cl 2009-10-01 13:25:12 +0000 |
1809 | 153 | +++ yvo/yc/pbqr/vagresnprf/qvss.cl 2010-02-02 15:48:56 +0000 | 153 | +++ yvo/yc/pbqr/vagresnprf/qvss.cl 2010-02-02 15:48:56 +0000 |
1810 | 154 | @@ -121,6 +121,10 @@ | 154 | @@ -121,6 +121,10 @@ |
1811 | 155 | 'Gur pbasyvpgf grkg qrfpevovat nal cngu be grkg pbasyvpgf.'), | 155 | 'Gur pbasyvpgf grkg qrfpevovat nal cngu be grkg pbasyvpgf.'), |
1812 | 156 | ernqbayl=Gehr)) | 156 | ernqbayl=Gehr)) |
1813 | @@ -635,7 +635,7 @@ | |||
1814 | 635 | def makeProcessorFamily(self, name, title=None, description=None, | 635 | def makeProcessorFamily(self, name, title=None, description=None, |
1815 | 636 | restricted=False): | 636 | restricted=False): |
1816 | 637 | """Create a new processor family. | 637 | """Create a new processor family. |
1818 | 638 | 638 | ||
1819 | 639 | :param name: Name of the family (e.g. x86) | 639 | :param name: Name of the family (e.g. x86) |
1820 | 640 | :param title: Optional title of the family | 640 | :param title: Optional title of the family |
1821 | 641 | :param description: Optional extended description | 641 | :param description: Optional extended description |
1822 | @@ -1568,7 +1568,7 @@ | |||
1823 | 1568 | 1568 | ||
1824 | 1569 | :param branch: If supplied, the branch to set as | 1569 | :param branch: If supplied, the branch to set as |
1825 | 1570 | ProductSeries.branch. | 1570 | ProductSeries.branch. |
1827 | 1571 | :param product: If supplied, the name of the series. | 1571 | :param name: If supplied, the name of the series. |
1828 | 1572 | :param product: If supplied, the series is created for this product. | 1572 | :param product: If supplied, the series is created for this product. |
1829 | 1573 | Otherwise, a new product is created. | 1573 | Otherwise, a new product is created. |
1830 | 1574 | """ | 1574 | """ |
Hi Brad,
This interface is a nice improvement. I've only set the series for a
branch once before, and I totally did it wrong because I was on the wrong
form.
Since I don't know if you are planning a followup branch for your
BRANCH.TODO items, I'm marking this:
needs-fixing
It seems odd that productseries- setbranch. pt is in lp.registry but setbranch. js is in lp.code. There are some more
productseries-
comments below.
-Edwin
>=== modified file 'BRANCH.TODO' dev/proj/ series displays the overview page but it should registry/ browser/ productseries. py' registry/ browser/ productseries. py 2010-03-23 00:39:45 +0000 registry/ browser/ productseries. py 2010-03-26 15:28:25 +0000 url(product) nkBranchView( LaunchpadEditFo rmView) : ocabulary( ): y(terms)
>--- BRANCH.TODO 2010-03-19 07:13:15 +0000
>+++ BRANCH.TODO 2010-03-26 15:28:25 +0000
>@@ -2,3 +2,10 @@
> # landing. There is a test to ensure it is empty in trunk. If there is
> # stuff still here when you are ready to land, the items should probably
> # be converted to bugs so they can be scheduled.
>+
>+TODO:
>+
>+* validation errors give misleading messages
>+* uncaught constraint error on duplicate of code import URL
>+* code.lp.
>+ direct away from the code vhost
>=== modified file 'lib/lp/
>--- lib/lp/
>+++ lib/lp/
>@@ -644,7 +658,340 @@
> self.next_url = canonical_
>
>
>-class ProductSeriesLi
>+LINK_LP_BZR = 'link-lp-bzr'
>+CREATE_NEW = 'create-new'
>+IMPORT_EXTERNAL = 'import-external'
>+
>+
>+def _getBranchTypeV
>+ items = (
>+ (LINK_LP_BZR,
>+ _("Link to a Bazaar branch already on Launchpad")),
>+ (CREATE_NEW,
>+ _("Create a new, empty branch in Launchpad and "
>+ "link to this series")),
>+ (IMPORT_EXTERNAL,
>+ _("Import a branch hosted somewhere else")),
>+ )
>+ terms = [
>+ SimpleTerm(name, name, label) for name, label in items]
>+ return SimpleVocabular
Why is this a function instead of a constant? If you are TYPE_VOCABULARY = SimpleVocabulary(( m(LINK_ LP_BZR, LINK_LP_BZR, 'foo'),
trying to avoid extra variables defined in the module, you could
just do:
BRANCH_
SimpleTer
...
>+class RevisionControl SystemsExtended (RevisionContro lSystems) : Interface) : title=_ ("Type of RCS"), RevisionControl SystemsExtended , schemes= ["http" , "https"], False, False, slash=False) 'branch' ], _='branch_ location' ,
>+ """External RCS plus Bazaar."""
>+ BZR = DBItem(99, """
>+ Bazaar
>+
>+ External Bazaar branch.
>+ """)
>+
>+
>+class SetBranchForm(
>+ """The fields presented on the form for setting a branch."""
>+
>+ use_template(
>+ ICodeImport,
>+ ['cvs_module'])
>+
>+ rcs_type = Choice(
>+ required=False, vocabulary=
>+ description=_(
>+ "The version control system to import from. "))
>+
>+ repo_url = URIField(
>+ title=_("Branch URL"), required=True,
>+ description=_("The URL of the branch."),
>+ allowed_
>+ allow_userinfo=
>+ allow_port=True,
>+ allow_query=False,
>+ allow_fragment=
>+ trailing_
>+
>+ branch_location = copy_field(
>+ IProductSeries[
>+ __name_
>+ titl...