Merge lp:~bac/launchpad/bug-524302 into lp:launchpad/db-devel

Proposed by Brad Crittenden
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
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/+setbranch page for setting/creating/importing a branch for the productseries.

Description of the change

Add productseries/+setbranch view to consolidate many other views dealing with creating/mirroring/importing branches. This view allows a user to do one of those things (though the terminology is hidden) and links the branch to the product series.

The view is not currently navigable from anywhere. Eventually it will replace +linkbranch.

A new view test has been created:

bin/test -vvt productseries-setbranch-views.txt

It may be preferred to roll that test into the productseries-views.txt test but it was expeditious to create it stand alone.

There are some known issues listed in the BRANCH.TODO file.

To demo, create a new project and go to https://launchpad.dev/<newproject>/trunk/+setbranch

To post a comment you must log in.
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (31.1 KiB)

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
productseries-setbranch.js is in lp.code. There are some more
comments below.

-Edwin

>=== modified file 'BRANCH.TODO'
>--- 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.dev/proj/series displays the overview page but it should
>+ direct away from the code vhost
>=== modified file 'lib/lp/registry/browser/productseries.py'
>--- lib/lp/registry/browser/productseries.py 2010-03-23 00:39:45 +0000
>+++ lib/lp/registry/browser/productseries.py 2010-03-26 15:28:25 +0000
>@@ -644,7 +658,340 @@
> self.next_url = canonical_url(product)
>
>
>-class ProductSeriesLinkBranchView(LaunchpadEditFormView):
>+LINK_LP_BZR = 'link-lp-bzr'
>+CREATE_NEW = 'create-new'
>+IMPORT_EXTERNAL = 'import-external'
>+
>+
>+def _getBranchTypeVocabulary():
>+ 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 SimpleVocabulary(terms)

Why is this a function instead of a constant? If you are
trying to avoid extra variables defined in the module, you could
just do:
  BRANCH_TYPE_VOCABULARY = SimpleVocabulary((
      SimpleTerm(LINK_LP_BZR, LINK_LP_BZR, 'foo'),
      ...

>+class RevisionControlSystemsExtended(RevisionControlSystems):
>+ """External RCS plus Bazaar."""
>+ BZR = DBItem(99, """
>+ Bazaar
>+
>+ External Bazaar branch.
>+ """)
>+
>+
>+class SetBranchForm(Interface):
>+ """The fields presented on the form for setting a branch."""
>+
>+ use_template(
>+ ICodeImport,
>+ ['cvs_module'])
>+
>+ rcs_type = Choice(title=_("Type of RCS"),
>+ required=False, vocabulary=RevisionControlSystemsExtended,
>+ description=_(
>+ "The version control system to import from. "))
>+
>+ repo_url = URIField(
>+ title=_("Branch URL"), required=True,
>+ description=_("The URL of the branch."),
>+ allowed_schemes=["http", "https"],
>+ allow_userinfo=False,
>+ allow_port=True,
>+ allow_query=False,
>+ allow_fragment=False,
>+ trailing_slash=False)
>+
>+ branch_location = copy_field(
>+ IProductSeries['branch'],
>+ __name__='branch_location',
>+ titl...

review: Needs Fixing
Revision history for this message
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.

Revision history for this message
Brad Crittenden (bac) wrote :
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (19.5 KiB)

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/egrubbs/canonical/lp-branches/review/lib/canonical/launchpad/javascript/code/tests/test_productseries_setbranch.js':
    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:
                    Y.each(subscribers, function(sub) {

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/egrubbs/canonical/lp-sourcedeps/eggs/zope.publisher-3.10.0-py2.5.egg/zope/publisher/publish.py", line 134, in publish
    result = publication.callObject(request, obj)
  File "/home/egrubbs/canonical/lp-branches/review/lib/canonical/launchpad/webapp/publication.py", line 422, in callObject
    return mapply(ob, request.getPositionalArguments(), request)
  File "/home/egrubbs/canonical/lp-sourcedeps/eggs/zope.publisher-3.10.0-py2.5.egg/zope/publisher/publish.py", line 109, in mapply
    return debug_call(obj, args)
  File "/home/egrubbs/canonical/lp-sourcedeps/eggs/zope.publisher-3.10.0-py2.5.egg/zope/publisher/publish.py", line 115, in debug_call
    return obj(*args)
  File "/home/egrubbs/canonical/lp-branches/review/lib/canonical/launchpad/webapp/publisher.py", line 274, in __call__
    self.initialize()
  File "/home/egrubbs/canonical/lp-branches/review/lib/canonical/launchpad/webapp/launchpadform.py", line 111, in initialize
    self.form_result = action.success(data)
  File "/home/egrubbs/canonical/lp-sourcedeps/eggs/zope.formlib-3.6.0-py2.5.egg/zope/formlib/form.py", line 606, in success
    return self.success_handler(self.form, self, data)
  File "/home/egrubbs/canonical/lp-branches/review/lib/lp/registry/browser/productseries.py", line 993, in update_action
    data['repo_url'])
  File "/home/egrubbs/canonical/lp-branches/review/lib/lp/registry/browser/productseries.py", line 1040, in _createBzrBranch
    url=repo_url)
  File "/home/egrubbs/canonical/lp-branches/review/lib/lp/code/model/branchnamespace.py", line 103, in createBranch
    implicit_subscription = self.getPrivacySubscriber()
  File "/home/egrubbs/canonical/lp-branches/review/lib/lp/code/model/branchnamespace.py", line 343, in getPrivacySubscriber
    rule = self.product.getBranchVisibilityRuleForTeam(self.owner)
  File "/home/egrubbs/canonical/lp-branches/review/lib/lp/code/model/branchvisibilitypolicy.py", line 108, in getBranchVisibilityRuleForTeam
    item = self._selectOneBranchVisibilityTeamPolicy(team)
  File "/home/...

review: Approve (code ui*)
Revision history for this message
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://pastebin.ubuntu.com/409614/

Revision history for this message
Brad Crittenden (bac) wrote :

Curtis a screenshot is available at http://people.canonical.com/~bac/setbranch.png

Revision history for this message
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.

review: Needs Information (code, ui)
Revision history for this message
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.

Revision history for this message
Curtis Hovey (sinzui) :
review: Approve (code, ui)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/app/templates/base-layout-macros.pt'
--- lib/lp/app/templates/base-layout-macros.pt 2010-03-17 23:17:46 +0000
+++ lib/lp/app/templates/base-layout-macros.pt 2010-04-07 13:24:38 +0000
@@ -202,8 +202,11 @@
202 tal:attributes="src string:${lp_js}/code/branchmergeproposal.status.js">202 tal:attributes="src string:${lp_js}/code/branchmergeproposal.status.js">
203 </script>203 </script>
204 <script type="text/javascript"204 <script type="text/javascript"
205 tal:attributes="src string:${lp_js}/code/branchmergeproposal.reviewcomment.js"></script>205 tal:attributes="src string:${lp_js}/code/branchmergeproposal.reviewcomment.js">
206206 </script>
207 <script type="text/javascript"
208 tal:attributes="src string:${lp_js}/code/productseries-setbranch.js">
209 </script>
207 <script type="text/javascript"210 <script type="text/javascript"
208 tal:attributes="src string:${lp_js}/lp/comment.js"></script>211 tal:attributes="src string:${lp_js}/lp/comment.js"></script>
209 <script type="text/javascript"212 <script type="text/javascript"
210213
=== modified file 'lib/lp/code/browser/bazaar.py'
--- lib/lp/code/browser/bazaar.py 2009-08-28 01:31:51 +0000
+++ lib/lp/code/browser/bazaar.py 2010-04-07 13:24:38 +0000
@@ -21,6 +21,7 @@
21from canonical.launchpad.webapp.authorization import (21from canonical.launchpad.webapp.authorization import (
22 precache_permission_for_objects)22 precache_permission_for_objects)
2323
24from lp.code.enums import CodeImportReviewStatus
24from lp.code.interfaces.branch import IBranchCloud, IBranchSet25from lp.code.interfaces.branch import IBranchCloud, IBranchSet
25from lp.code.interfaces.branchcollection import IAllBranches26from lp.code.interfaces.branchcollection import IAllBranches
26from lp.code.interfaces.codeimport import ICodeImportSet27from lp.code.interfaces.codeimport import ICodeImportSet
@@ -61,7 +62,8 @@
6162
62 @property63 @property
63 def import_count(self):64 def import_count(self):
64 return getUtility(ICodeImportSet).getActiveImports().count()65 return getUtility(ICodeImportSet).search(
66 review_status=CodeImportReviewStatus.REVIEWED).count()
6567
66 @property68 @property
67 def bzr_version(self):69 def bzr_version(self):
6870
=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py 2010-03-16 19:04:48 +0000
+++ lib/lp/code/browser/branch.py 2010-04-07 13:24:38 +0000
@@ -16,6 +16,7 @@
16 'BranchReviewerEditView',16 'BranchReviewerEditView',
17 'BranchMergeQueueView',17 'BranchMergeQueueView',
18 'BranchMirrorStatusView',18 'BranchMirrorStatusView',
19 'BranchNameValidationMixin',
19 'BranchNavigation',20 'BranchNavigation',
20 'BranchEditMenu',21 'BranchEditMenu',
21 'BranchInProductView',22 'BranchInProductView',
@@ -612,7 +613,7 @@
612class BranchNameValidationMixin:613class BranchNameValidationMixin:
613 """Provide name validation logic used by several branch view classes."""614 """Provide name validation logic used by several branch view classes."""
614615
615 def _setBranchExists(self, existing_branch):616 def _setBranchExists(self, existing_branch, field_name='name'):
616 owner = existing_branch.owner617 owner = existing_branch.owner
617 if owner == self.user:618 if owner == self.user:
618 prefix = "You already have"619 prefix = "You already have"
@@ -622,7 +623,7 @@
622 "%s a branch for <em>%s</em> called <em>%s</em>."623 "%s a branch for <em>%s</em> called <em>%s</em>."
623 % (prefix, existing_branch.target.displayname,624 % (prefix, existing_branch.target.displayname,
624 existing_branch.name))625 existing_branch.name))
625 self.setFieldError('name', structured(message))626 self.setFieldError(field_name, structured(message))
626627
627628
628class BranchEditSchema(Interface):629class BranchEditSchema(Interface):
629630
=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml 2010-03-18 17:30:14 +0000
+++ lib/lp/code/browser/configure.zcml 2010-04-07 13:24:38 +0000
@@ -57,13 +57,6 @@
57 permission="zope.Public"57 permission="zope.Public"
58 />58 />
59 <browser:page59 <browser:page
60 for="canonical.launchpad.webapp.interfaces.ILaunchpadApplication"
61 name="+code-import-list"
62 class="lp.registry.browser.productseries.ProductSeriesSourceListView"
63 template="../templates/sources-list.pt"
64 permission="zope.Public"
65 />
66 <browser:page
67 for="zope.interface.Interface"60 for="zope.interface.Interface"
68 name="+test-webservice-js"61 name="+test-webservice-js"
69 template="../../../canonical/launchpad/templates/test-webservice-js.pt"62 template="../../../canonical/launchpad/templates/test-webservice-js.pt"
7063
=== modified file 'lib/lp/code/interfaces/codeimport.py'
--- lib/lp/code/interfaces/codeimport.py 2010-03-26 01:05:26 +0000
+++ lib/lp/code/interfaces/codeimport.py 2010-04-07 13:24:38 +0000
@@ -188,16 +188,6 @@
188 :param target: An `IBranchTarget` that the code is associated with.188 :param target: An `IBranchTarget` that the code is associated with.
189 """189 """
190190
191 def getActiveImports(text=None):
192 """Return an iterable of all 'active' CodeImport objects.
193
194 Active is defined, somewhat arbitrarily, as having
195 review_status==REVIEWED and having completed at least once.
196
197 :param text: If specifed, limit to the results to those that contain
198 ``text`` in the product or project titles and descriptions.
199 """
200
201 def get(id):191 def get(id):
202 """Get a CodeImport by its id.192 """Get a CodeImport by its id.
203193
204194
=== modified file 'lib/lp/code/javascript/tests/test_productseries_setbranch.js'
--- lib/lp/code/javascript/tests/test_productseries_setbranch.js 2010-04-05 18:58:07 +0000
+++ lib/lp/code/javascript/tests/test_productseries_setbranch.js 2010-04-07 13:24:38 +0000
@@ -61,10 +61,10 @@
61 var custom_events = Y.Event.getListeners(field, 'click');61 var custom_events = Y.Event.getListeners(field, 'click');
62 var click_event = custom_events[0];62 var click_event = custom_events[0];
63 var subscribers = click_event.subscribers;63 var subscribers = click_event.subscribers;
64 for (var sub in subscribers) {64 Y.each(subscribers, function(sub) {
65 Y.Assert.isTrue(subscribers[sub].contains(expected),65 Y.Assert.isTrue(sub.contains(expected),
66 'branch_type_onclick handler setup');66 'branch_type_onclick handler setup');
67 };67 });
68 };68 };
6969
70 check_handler(this.link_lp_bzr, module.onclick_branch_type);70 check_handler(this.link_lp_bzr, module.onclick_branch_type);
7171
=== modified file 'lib/lp/code/model/codeimport.py'
--- lib/lp/code/model/codeimport.py 2010-03-18 15:39:58 +0000
+++ lib/lp/code/model/codeimport.py 2010-04-07 13:24:38 +0000
@@ -30,10 +30,9 @@
30from canonical.database.constants import DEFAULT30from canonical.database.constants import DEFAULT
31from canonical.database.datetimecol import UtcDateTimeCol31from canonical.database.datetimecol import UtcDateTimeCol
32from canonical.database.enumcol import EnumCol32from canonical.database.enumcol import EnumCol
33from canonical.database.sqlbase import SQLBase, quote, sqlvalues33from canonical.database.sqlbase import SQLBase
34from canonical.launchpad.interfaces import IStore34from canonical.launchpad.interfaces import IStore
35from lp.code.model.codeimportjob import CodeImportJobWorkflow35from lp.code.model.codeimportjob import CodeImportJobWorkflow
36from lp.registry.model.productseries import ProductSeries
37from canonical.launchpad.webapp.interfaces import NotFoundError36from canonical.launchpad.webapp.interfaces import NotFoundError
38from lp.code.enums import (37from lp.code.enums import (
39 BranchType, CodeImportJobState, CodeImportResultStatus,38 BranchType, CodeImportJobState, CodeImportResultStatus,
@@ -41,8 +40,6 @@
41from lp.code.interfaces.codeimport import ICodeImport, ICodeImportSet40from lp.code.interfaces.codeimport import ICodeImport, ICodeImportSet
42from lp.code.interfaces.codeimportevent import ICodeImportEventSet41from lp.code.interfaces.codeimportevent import ICodeImportEventSet
43from lp.code.interfaces.codeimportjob import ICodeImportJobWorkflow42from lp.code.interfaces.codeimportjob import ICodeImportJobWorkflow
44from lp.code.interfaces.branchnamespace import (
45 get_branch_namespace)
46from lp.code.model.codeimportresult import CodeImportResult43from lp.code.model.codeimportresult import CodeImportResult
47from lp.code.mail.codeimport import code_import_updated44from lp.code.mail.codeimport import code_import_updated
48from lp.registry.interfaces.person import validate_public_person45from lp.registry.interfaces.person import validate_public_person
@@ -256,51 +253,6 @@
256 CodeImportJob.delete(code_import.import_job.id)253 CodeImportJob.delete(code_import.import_job.id)
257 CodeImport.delete(code_import.id)254 CodeImport.delete(code_import.id)
258255
259 def getActiveImports(self, text=None):
260 """See `ICodeImportSet`."""
261 query = self.composeQueryString(text)
262 return CodeImport.select(
263 query, orderBy=['product.name', 'branch.name'],
264 clauseTables=['Product', 'Branch'])
265
266 def composeQueryString(self, text=None):
267 """Build SQL "where" clause for `CodeImport` search.
268
269 :param text: Text to search for in the product and project titles and
270 descriptions.
271 """
272 conditions = [
273 "date_last_successful IS NOT NULL",
274 "review_status=%s" % sqlvalues(CodeImportReviewStatus.REVIEWED),
275 "CodeImport.branch = Branch.id",
276 "Branch.product = Product.id",
277 ]
278 if text == u'':
279 text = None
280
281 # First filter on text, if supplied.
282 if text is not None:
283 conditions.append("""
284 ((Project.fti @@ ftq(%s) AND Product.project IS NOT NULL) OR
285 Product.fti @@ ftq(%s))""" % (quote(text), quote(text)))
286
287 # Exclude deactivated products.
288 conditions.append('Product.active IS TRUE')
289
290 # Exclude deactivated projects, too.
291 conditions.append(
292 "((Product.project = Project.id AND Project.active) OR"
293 " Product.project IS NULL)")
294
295 # And build the query.
296 query = " AND ".join(conditions)
297 return """
298 codeimport.id IN
299 (SELECT codeimport.id FROM codeimport, branch, product, project
300 WHERE %s)
301 AND codeimport.branch = branch.id
302 AND branch.product = product.id""" % query
303
304 def get(self, id):256 def get(self, id):
305 """See `ICodeImportSet`."""257 """See `ICodeImportSet`."""
306 try:258 try:
307259
=== modified file 'lib/lp/code/model/tests/test_codeimport.py'
--- lib/lp/code/model/tests/test_codeimport.py 2010-03-18 17:49:21 +0000
+++ lib/lp/code/model/tests/test_codeimport.py 2010-04-07 13:24:38 +0000
@@ -11,14 +11,11 @@
11from storm.store import Store11from storm.store import Store
12from zope.component import getUtility12from zope.component import getUtility
1313
14from lp.codehosting.codeimport.tests.test_workermonitor import (
15 nuke_codeimport_sample_data)
16from lp.code.model.codeimport import CodeImportSet14from lp.code.model.codeimport import CodeImportSet
17from lp.code.model.codeimportevent import CodeImportEvent15from lp.code.model.codeimportevent import CodeImportEvent
18from lp.code.model.codeimportjob import CodeImportJob, CodeImportJobSet16from lp.code.model.codeimportjob import CodeImportJob, CodeImportJobSet
19from lp.code.model.codeimportresult import CodeImportResult17from lp.code.model.codeimportresult import CodeImportResult
20from lp.code.interfaces.branchtarget import IBranchTarget18from lp.code.interfaces.branchtarget import IBranchTarget
21from lp.code.interfaces.codeimport import ICodeImportSet
22from lp.registry.interfaces.person import IPersonSet19from lp.registry.interfaces.person import IPersonSet
23from lp.code.enums import (20from lp.code.enums import (
24 CodeImportResultStatus, CodeImportReviewStatus, RevisionControlSystems)21 CodeImportResultStatus, CodeImportReviewStatus, RevisionControlSystems)
@@ -551,196 +548,5 @@
551 requester, code_import.import_job.requesting_user)548 requester, code_import.import_job.requesting_user)
552549
553550
554def make_active_import(factory, project_name=None, product_name=None,
555 branch_name=None, svn_branch_url=None,
556 cvs_root=None, cvs_module=None, git_repo_url=None,
557 hg_repo_url=None, last_update=None, rcs_type=None):
558 """Make a new CodeImport for a new Product, maybe in a new Project.
559
560 The import will be 'active' in the sense used by
561 `ICodeImportSet.getActiveImports`.
562 """
563 if project_name is not None:
564 project = factory.makeProject(name=project_name)
565 else:
566 project = None
567 product = factory.makeProduct(
568 name=product_name, displayname=product_name, project=project)
569 code_import = factory.makeProductCodeImport(
570 product=product, branch_name=branch_name,
571 svn_branch_url=svn_branch_url, cvs_root=cvs_root,
572 cvs_module=cvs_module, git_repo_url=git_repo_url,
573 hg_repo_url=hg_repo_url, rcs_type=None)
574 make_import_active(factory, code_import, last_update)
575 return code_import
576
577
578def make_import_active(factory, code_import, last_update=None):
579 """Make `code_import` active as per `ICodeImportSet.getActiveImports`."""
580 from zope.security.proxy import removeSecurityProxy
581 naked_import = removeSecurityProxy(code_import)
582 if naked_import.review_status != CodeImportReviewStatus.REVIEWED:
583 naked_import.updateFromData(
584 {'review_status': CodeImportReviewStatus.REVIEWED},
585 factory.makePerson())
586 if last_update is None:
587 # If last_update is not specfied, presumably we don't care what it is
588 # so we just use some made up value.
589 last_update = datetime(2008, 1, 1, tzinfo=pytz.UTC)
590 naked_import.date_last_successful = last_update
591
592
593def deactivate(project_or_product):
594 """Mark `project_or_product` as not active."""
595 from zope.security.proxy import removeSecurityProxy
596 removeSecurityProxy(project_or_product).active = False
597
598
599class TestGetActiveImports(TestCaseWithFactory):
600 """Tests for CodeImportSet.getActiveImports()."""
601
602 layer = DatabaseFunctionalLayer
603
604 def setUp(self):
605 """Prepare by deleting all the import data in the sample data.
606
607 This means that the tests only have to care about the import
608 data they create.
609 """
610 super(TestGetActiveImports, self).setUp()
611 nuke_codeimport_sample_data()
612 login('no-priv@canonical.com')
613
614 def tearDown(self):
615 super(TestGetActiveImports, self).tearDown()
616 logout()
617
618 def testEmpty(self):
619 # We start out with no code imports, so getActiveImports() returns no
620 # results.
621 results = getUtility(ICodeImportSet).getActiveImports()
622 self.assertEquals(list(results), [])
623
624 def testOneSeries(self):
625 # When there is one active import, it is returned.
626 code_import = make_active_import(self.factory)
627 results = getUtility(ICodeImportSet).getActiveImports()
628 self.assertEquals(list(results), [code_import])
629
630 def testOneSeriesWithProject(self):
631 # Code imports for products with a project should be returned too.
632 code_import = make_active_import(
633 self.factory, project_name="whatever")
634 results = getUtility(ICodeImportSet).getActiveImports()
635 self.assertEquals(list(results), [code_import])
636
637 def testExcludeDeactivatedProducts(self):
638 # Deactivating a product means that code imports associated to it are
639 # no longer returned.
640 code_import = make_active_import(self.factory)
641 self.failUnless(code_import.branch.product.active)
642 results = getUtility(ICodeImportSet).getActiveImports()
643 self.assertEquals(list(results), [code_import])
644 deactivate(code_import.branch.product)
645 results = getUtility(ICodeImportSet).getActiveImports()
646 self.assertEquals(list(results), [])
647
648 def testExcludeDeactivatedProjects(self):
649 # Deactivating a project means that code imports associated to
650 # products in it are no longer returned.
651 code_import = make_active_import(
652 self.factory, project_name="whatever")
653 self.failUnless(code_import.branch.product.project.active)
654 results = getUtility(ICodeImportSet).getActiveImports()
655 self.assertEquals(list(results), [code_import])
656 deactivate(code_import.branch.product.project)
657 results = getUtility(ICodeImportSet).getActiveImports()
658 self.assertEquals(list(results), [])
659
660 def testSorting(self):
661 # Returned code imports are sorted by product name, then branch name.
662 prod1_a = make_active_import(
663 self.factory, product_name='prod1', branch_name='a')
664 prod2_a = make_active_import(
665 self.factory, product_name='prod2', branch_name='a')
666 prod1_b = self.factory.makeProductCodeImport(
667 product=prod1_a.branch.product, branch_name='b')
668 make_import_active(self.factory, prod1_b)
669 results = getUtility(ICodeImportSet).getActiveImports()
670 self.assertEquals(
671 list(results), [prod1_a, prod1_b, prod2_a])
672
673 def testSearchByProduct(self):
674 # Searching can filter by product name and other texts.
675 code_import = make_active_import(
676 self.factory, product_name='product')
677 results = getUtility(ICodeImportSet).getActiveImports(
678 text='product')
679 self.assertEquals(
680 list(results), [code_import])
681
682 def testSearchByProductWithProject(self):
683 # Searching can filter by product name and other texts, and returns
684 # matching imports even if the associated product is in a project
685 # which does not match.
686 code_import = make_active_import(
687 self.factory, project_name='whatever', product_name='product')
688 results = getUtility(ICodeImportSet).getActiveImports(
689 text='product')
690 self.assertEquals(
691 list(results), [code_import])
692
693 def testSearchByProject(self):
694 # Searching can filter by project name and other texts.
695 code_import = make_active_import(
696 self.factory, project_name='project', product_name='product')
697 results = getUtility(ICodeImportSet).getActiveImports(
698 text='project')
699 self.assertEquals(
700 list(results), [code_import])
701
702 def testSearchByProjectWithNonMatchingProduct(self):
703 # If a project matches the text, it's an easy mistake to make to
704 # consider all the products with no project as matching too.
705 code_import_1 = make_active_import(
706 self.factory, product_name='product1')
707 code_import_2 = make_active_import(
708 self.factory, project_name='thisone', product_name='product2')
709 results = getUtility(ICodeImportSet).getActiveImports(
710 text='thisone')
711 self.assertEquals(
712 list(results), [code_import_2])
713
714 def testJoining(self):
715 # Test that the query composed by CodeImportSet.composeQueryString
716 # gets the joins right. We create code imports for each of the
717 # possibilities of active or inactive product and active or inactive
718 # or absent project.
719 expected = set()
720 source = {}
721 for project_active in [True, False, None]:
722 for product_active in [True, False]:
723 if project_active is not None:
724 project_name = self.factory.getUniqueString()
725 else:
726 project_name = None
727 code_import = make_active_import(
728 self.factory, project_name=project_name)
729 if code_import.branch.product.project and not project_active:
730 deactivate(code_import.branch.product.project)
731 if not product_active:
732 deactivate(code_import.branch.product)
733 if project_active != False and product_active:
734 expected.add(code_import)
735 source[code_import] = (product_active, project_active)
736 results = set(getUtility(ICodeImportSet).getActiveImports())
737 errors = []
738 for extra in results - expected:
739 errors.append(('extra', source[extra]))
740 for missing in expected - results:
741 errors.append(('extra', source[missing]))
742 self.assertEquals(errors, [])
743
744
745def test_suite():551def test_suite():
746 return unittest.TestLoader().loadTestsFromName(__name__)552 return unittest.TestLoader().loadTestsFromName(__name__)
747553
=== modified file 'lib/lp/code/stories/branches/xx-bazaar-home.txt'
--- lib/lp/code/stories/branches/xx-bazaar-home.txt 2009-08-28 05:57:37 +0000
+++ lib/lp/code/stories/branches/xx-bazaar-home.txt 2010-04-07 13:24:38 +0000
@@ -16,7 +16,7 @@
16 >>> print extract_text(footer)16 >>> print extract_text(footer)
17 30 branches registered in17 30 branches registered in
18 6 projects18 6 projects
19 0 imported branches19 1 imported branches
20 2 branches associated with bug reports20 2 branches associated with bug reports
21 Launchpad uses Bazaar 0.92.0.21 Launchpad uses Bazaar 0.92.0.
2222
2323
=== modified file 'lib/lp/code/stories/branches/xx-branchmergeproposals.txt'
--- lib/lp/code/stories/branches/xx-branchmergeproposals.txt 2010-02-23 21:48:53 +0000
+++ lib/lp/code/stories/branches/xx-branchmergeproposals.txt 2010-04-07 13:24:38 +0000
@@ -80,13 +80,16 @@
80 ... print extract_text(find_tag_by_id(80 ... print extract_text(find_tag_by_id(
81 ... browser.contents, 'proposal-summary'))81 ... browser.contents, 'proposal-summary'))
82 >>> print_summary(nopriv_browser)82 >>> print_summary(nopriv_browser)
83 Status:...83 Status:
84 ...
84 Proposed branch:85 Proposed branch:
85 lp://dev/~name12/gnome-terminal/klingon86 lp://dev/~name12/gnome-terminal/klingon
86 Merge into:87 Merge into:
87 lp://dev/~name12/gnome-terminal/main88 lp://dev/~name12/gnome-terminal/main
88 Prerequisite:89 Prerequisite:
89 lp://dev/~name12/gnome-terminal/pushed90 lp://dev/~name12/gnome-terminal/pushed
91 To merge this branch:
92 bzr merge lp://dev/~name12/gnome-terminal/klingon
9093
9194
92Editing a commit message95Editing a commit message
9396
=== modified file 'lib/lp/code/stories/branches/xx-propose-for-merging.txt'
--- lib/lp/code/stories/branches/xx-propose-for-merging.txt 2010-01-14 04:32:38 +0000
+++ lib/lp/code/stories/branches/xx-propose-for-merging.txt 2010-04-07 13:24:38 +0000
@@ -35,6 +35,7 @@
35 Status: Needs review35 Status: Needs review
36 Proposed branch: ...36 Proposed branch: ...
37 Merge into: lp://dev/fooix37 Merge into: lp://dev/fooix
38 To merge this branch: bzr merge ...
3839
3940
40Work in progress41Work in progress
@@ -57,3 +58,4 @@
57 Status: Work in progress58 Status: Work in progress
58 Proposed branch: ...59 Proposed branch: ...
59 Merge into: lp://dev/fooix60 Merge into: lp://dev/fooix
61 To merge this branch: bzr merge ...
6062
=== removed file 'lib/lp/code/stories/codeimport/xx-codeimport-list.txt'
--- lib/lp/code/stories/codeimport/xx-codeimport-list.txt 2010-01-12 22:09:23 +0000
+++ lib/lp/code/stories/codeimport/xx-codeimport-list.txt 1970-01-01 00:00:00 +0000
@@ -1,72 +0,0 @@
1There is a page listing all the active code imports on the code
2homepage.
3
4We start by deleting all the code import sample data and creating a
5few imports that will be displayed in the listing.
6
7 >>> import datetime
8 >>> import pytz
9 >>> from lp.codehosting.codeimport.tests.test_workermonitor import (
10 ... nuke_codeimport_sample_data)
11 >>> from lp.code.enums import RevisionControlSystems
12 >>> from lp.code.model.tests.test_codeimport import (
13 ... make_active_import)
14 >>> from lp.code.interfaces.codeimport import ICodeImportSet
15 >>> from lp.testing import login, logout
16 >>> from zope.component import getUtility
17 >>> login('david.allouche@canonical.com')
18 >>> nuke_codeimport_sample_data()
19 >>> code_import_set = getUtility(ICodeImportSet)
20 >>> active_svn_import = make_active_import(
21 ... factory, product_name="myproject", branch_name="trunk",
22 ... svn_branch_url="http://example.com/svn/myproject/trunk",
23 ... last_update=datetime.datetime(2007, 1, 1, tzinfo=pytz.UTC))
24 >>> active_bzr_svn_import = make_active_import(
25 ... factory, product_name="ourproject", branch_name="trunk",
26 ... svn_branch_url="http://example.com/bzr-svn/myproject/trunk",
27 ... rcs_type=RevisionControlSystems.BZR_SVN,
28 ... last_update=datetime.datetime(2007, 1, 2, tzinfo=pytz.UTC))
29 >>> active_cvs_import = make_active_import(
30 ... factory, product_name="hisproj", branch_name="main",
31 ... cvs_root=":pserver:anon@example.com:/cvs", cvs_module="hisproj",
32 ... last_update=datetime.datetime(2007, 1, 3, tzinfo=pytz.UTC))
33 >>> active_git_import = make_active_import(
34 ... factory, product_name="herproj", branch_name="master",
35 ... git_repo_url="git://git.example.org/herproj",
36 ... last_update=datetime.datetime(2007, 1, 4, tzinfo=pytz.UTC))
37 >>> active_hg_import = make_active_import(
38 ... factory, product_name="hg-proj", branch_name="tip",
39 ... hg_repo_url="http://hg.example.org/proj",
40 ... last_update=datetime.datetime(2007, 1, 5, tzinfo=pytz.UTC))
41 >>> len(list(code_import_set.getActiveImports()))
42 5
43 >>> logout()
44
45The page is linked to from the "$N imported branches" text.
46
47 >>> browser.open('http://code.launchpad.dev')
48 >>> browser.getLink('5 imported branches').click()
49 >>> print browser.title
50 Available code imports
51
52It lists the active imports, sorted by product then branch name:
53
54 >>> def print_import_table():
55 ... table = first_tag_by_class(browser.contents, 'listing')
56 ... print extract_text(table)
57
58 >>> print_import_table()
59 Project Branch Name Source Details Last Updated
60 herproj master git://git.example.org/herproj 2007-01-04
61 hg-proj tip http://hg.example.org/proj 2007-01-05
62 hisproj main :pserver:anon@example.com:/cvs hisproj 2007-01-03
63 myproject trunk http://example.com/svn/myproject/trunk 2007-01-01
64 ourproject trunk http://example.com/bzr-svn/myproject/trunk 2007-01-02
65
66You can filter the list by product:
67
68 >>> browser.getControl(name='text').value = 'hisproj'
69 >>> browser.getControl('Search', index=0).click()
70 >>> print_import_table()
71 Project Branch Name Source Details Last Updated
72 hisproj main :pserver:anon@example.com:/cvs hisproj 2007-01-03
730
=== modified file 'lib/lp/code/stories/codeimport/xx-codeimport-view.txt'
--- lib/lp/code/stories/codeimport/xx-codeimport-view.txt 2010-03-18 17:49:21 +0000
+++ lib/lp/code/stories/codeimport/xx-codeimport-view.txt 2010-04-07 13:24:38 +0000
@@ -1,10 +1,10 @@
1Code imports1Code imports
2============2============
33
4For now, there is no link to the page that lists all code imports, so4The code imports overview page is linked of the main code page.
5we browse there directly:
65
7 >>> browser.open('http://code.launchpad.dev/+code-imports')6 >>> browser.open('http://code.launchpad.dev')
7 >>> browser.getLink('1 imported branches').click()
8 >>> print browser.title8 >>> print browser.title
9 Code Imports9 Code Imports
1010
1111
=== modified file 'lib/lp/code/templates/bazaar-index.pt'
--- lib/lp/code/templates/bazaar-index.pt 2010-03-11 06:43:00 +0000
+++ lib/lp/code/templates/bazaar-index.pt 2010-04-07 13:24:38 +0000
@@ -121,7 +121,7 @@
121 </a>121 </a>
122 </div>122 </div>
123 <div>123 <div>
124 <a href="/+code-import-list">124 <a href="/+code-imports">
125 <strong tal:content="view/import_count">123</strong>125 <strong tal:content="view/import_count">123</strong>
126 imported branches126 imported branches
127 </a>127 </a>
128128
=== modified file 'lib/lp/code/templates/branchmergeproposal-pagelet-summary.pt'
--- lib/lp/code/templates/branchmergeproposal-pagelet-summary.pt 2010-02-24 08:05:27 +0000
+++ lib/lp/code/templates/branchmergeproposal-pagelet-summary.pt 2010-04-07 13:24:38 +0000
@@ -131,5 +131,9 @@
131 tal:content="context/preview_diff/conflicts"/>131 tal:content="context/preview_diff/conflicts"/>
132 </td>132 </td>
133 </tr>133 </tr>
134 <tr id="summary-row-merge-instruction">
135 <th>To merge this branch:</th>
136 <td>bzr merge <span class="branch-url" tal:content="context/source_branch/bzr_identity" /></td>
137 </tr>
134 </tbody>138 </tbody>
135</table>139</table>
136140
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml 2010-04-01 18:48:04 +0000
+++ lib/lp/registry/browser/configure.zcml 2010-04-07 13:24:38 +0000
@@ -1664,6 +1664,13 @@
1664 facet="overview"1664 facet="overview"
1665 permission="launchpad.Edit"/>1665 permission="launchpad.Edit"/>
1666 <browser:page1666 <browser:page
1667 for="lp.registry.interfaces.productseries.IProductSeries"
1668 name="+setbranch"
1669 class="lp.registry.browser.productseries.ProductSeriesSetBranchView"
1670 template="../templates/productseries-setbranch.pt"
1671 facet="overview"
1672 permission="launchpad.Edit"/>
1673 <browser:page
1667 name="+review"1674 name="+review"
1668 for="lp.registry.interfaces.productseries.IProductSeries"1675 for="lp.registry.interfaces.productseries.IProductSeries"
1669 class="lp.registry.browser.productseries.ProductSeriesReviewView"1676 class="lp.registry.browser.productseries.ProductSeriesReviewView"
16701677
=== modified file 'lib/lp/registry/browser/productseries.py'
--- lib/lp/registry/browser/productseries.py 2010-03-23 00:39:45 +0000
+++ lib/lp/registry/browser/productseries.py 2010-04-07 13:24:38 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""View classes for `IProductSeries`."""4"""View classes for `IProductSeries`."""
@@ -20,7 +20,7 @@
20 'ProductSeriesOverviewNavigationMenu',20 'ProductSeriesOverviewNavigationMenu',
21 'ProductSeriesRdfView',21 'ProductSeriesRdfView',
22 'ProductSeriesReviewView',22 'ProductSeriesReviewView',
23 'ProductSeriesSourceListView',23 'ProductSeriesSetBranchView',
24 'ProductSeriesSpecificationsMenu',24 'ProductSeriesSpecificationsMenu',
25 'ProductSeriesUbuntuPackagingView',25 'ProductSeriesUbuntuPackagingView',
26 'ProductSeriesView',26 'ProductSeriesView',
@@ -34,6 +34,7 @@
34from zope.component import getUtility34from zope.component import getUtility
35from zope.app.form.browser import TextAreaWidget, TextWidget35from zope.app.form.browser import TextAreaWidget, TextWidget
36from zope.formlib import form36from zope.formlib import form
37from zope.interface import Interface
37from zope.schema import Choice38from zope.schema import Choice
38from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary39from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
3940
@@ -41,7 +42,7 @@
4142
42from canonical.cachedproperty import cachedproperty43from canonical.cachedproperty import cachedproperty
43from canonical.launchpad import _44from canonical.launchpad import _
44from lp.code.browser.branchref import BranchRef45from canonical.launchpad.fields import URIField
45from lp.blueprints.browser.specificationtarget import (46from lp.blueprints.browser.specificationtarget import (
46 HasSpecificationsMenuMixin)47 HasSpecificationsMenuMixin)
47from lp.blueprints.interfaces.specification import (48from lp.blueprints.interfaces.specification import (
@@ -49,9 +50,15 @@
49from lp.bugs.interfaces.bugtask import BugTaskStatus50from lp.bugs.interfaces.bugtask import BugTaskStatus
50from lp.bugs.browser.bugtask import BugTargetTraversalMixin51from lp.bugs.browser.bugtask import BugTargetTraversalMixin
51from canonical.launchpad.helpers import browserLanguages52from canonical.launchpad.helpers import browserLanguages
53from lp.code.browser.branch import BranchNameValidationMixin
54from lp.code.browser.branchref import BranchRef
55from lp.code.enums import BranchType, RevisionControlSystems
56from lp.code.interfaces.branch import (
57 BranchCreationForbidden, BranchExists, IBranch)
52from lp.code.interfaces.branchjob import IRosettaUploadJobSource58from lp.code.interfaces.branchjob import IRosettaUploadJobSource
59from lp.code.interfaces.branchtarget import IBranchTarget
53from lp.code.interfaces.codeimport import (60from lp.code.interfaces.codeimport import (
54 ICodeImportSet)61 ICodeImport, ICodeImportSet)
55from lp.services.worlddata.interfaces.country import ICountry62from lp.services.worlddata.interfaces.country import ICountry
56from lp.bugs.interfaces.bugtask import IBugTaskSet63from lp.bugs.interfaces.bugtask import IBugTaskSet
57from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities64from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
@@ -70,12 +77,13 @@
70 Link, Navigation, NavigationMenu, StandardLaunchpadFacets, stepthrough,77 Link, Navigation, NavigationMenu, StandardLaunchpadFacets, stepthrough,
71 stepto)78 stepto)
72from canonical.launchpad.webapp.authorization import check_permission79from canonical.launchpad.webapp.authorization import check_permission
73from canonical.launchpad.webapp.batching import BatchNavigator
74from canonical.launchpad.webapp.breadcrumb import Breadcrumb80from canonical.launchpad.webapp.breadcrumb import Breadcrumb
75from canonical.launchpad.webapp.interfaces import NotFoundError81from canonical.launchpad.webapp.interfaces import (
82 NotFoundError, UnexpectedFormData)
76from canonical.launchpad.webapp.launchpadform import (83from canonical.launchpad.webapp.launchpadform import (
77 action, custom_widget, LaunchpadEditFormView, LaunchpadFormView)84 action, custom_widget, LaunchpadEditFormView, LaunchpadFormView)
78from canonical.launchpad.webapp.menu import structured85from canonical.launchpad.webapp.menu import structured
86from canonical.widgets.itemswidgets import LaunchpadRadioWidget
79from canonical.widgets.textwidgets import StrippedTextWidget87from canonical.widgets.textwidgets import StrippedTextWidget
8088
81from lp.registry.browser import (89from lp.registry.browser import (
@@ -83,6 +91,9 @@
83from lp.registry.interfaces.series import SeriesStatus91from lp.registry.interfaces.series import SeriesStatus
84from lp.registry.interfaces.productseries import IProductSeries92from lp.registry.interfaces.productseries import IProductSeries
8593
94from lazr.enum import DBItem
95from lazr.restful.interface import copy_field, use_template
96
8697
87def quote(text):98def quote(text):
88 """Escape and quote text."""99 """Escape and quote text."""
@@ -644,7 +655,369 @@
644 self.next_url = canonical_url(product)655 self.next_url = canonical_url(product)
645656
646657
647class ProductSeriesLinkBranchView(LaunchpadEditFormView):658LINK_LP_BZR = 'link-lp-bzr'
659CREATE_NEW = 'create-new'
660IMPORT_EXTERNAL = 'import-external'
661
662
663BRANCH_TYPE_VOCABULARY = SimpleVocabulary((
664 SimpleTerm(LINK_LP_BZR, LINK_LP_BZR,
665 _("Link to a Bazaar branch already on Launchpad")),
666 SimpleTerm(CREATE_NEW, CREATE_NEW,
667 _("Create a new, empty branch in Launchpad and "
668 "link to this series")),
669 SimpleTerm(IMPORT_EXTERNAL, IMPORT_EXTERNAL,
670 _("Import a branch hosted somewhere else")),
671 ))
672
673
674class RevisionControlSystemsExtended(RevisionControlSystems):
675 """External RCS plus Bazaar."""
676 BZR = DBItem(99, """
677 Bazaar
678
679 External Bazaar branch.
680 """)
681
682
683class SetBranchForm(Interface):
684 """The fields presented on the form for setting a branch."""
685
686 use_template(
687 ICodeImport,
688 ['cvs_module'])
689
690 rcs_type = Choice(title=_("Type of RCS"),
691 required=False, vocabulary=RevisionControlSystemsExtended,
692 description=_(
693 "The version control system to import from. "))
694
695 repo_url = URIField(
696 title=_("Branch URL"), required=True,
697 description=_("The URL of the branch."),
698 allowed_schemes=["http", "https"],
699 allow_userinfo=False,
700 allow_port=True,
701 allow_query=False,
702 allow_fragment=False,
703 trailing_slash=False)
704
705 branch_location = copy_field(
706 IProductSeries['branch'],
707 __name__='branch_location',
708 title=_('Branch'),
709 description=_(
710 "The Bazaar branch for this series in Launchpad, "
711 "if one exists."),
712 )
713
714 branch_type = Choice(
715 title=_('Import type'),
716 vocabulary=BRANCH_TYPE_VOCABULARY,
717 description=_("The type of import"),
718 required=True)
719
720 branch_name = copy_field(
721 IBranch['name'],
722 __name__='branch_name',
723 title=_('Branch name'),
724 description=_(''),
725 required=True,
726 )
727
728 branch_owner = copy_field(
729 IBranch['owner'],
730 __name__='branch_owner',
731 title=_('Branch owner'),
732 description=_(''),
733 required=True,
734 )
735
736
737class ProductSeriesSetBranchView(LaunchpadFormView, ProductSeriesView,
738 BranchNameValidationMixin):
739 """The view to set a branch for the ProductSeries."""
740
741 schema = SetBranchForm
742 # Set for_input to True to ensure fields marked read-only will be editable
743 # upon creation.
744 for_input = True
745
746 custom_widget('rcs_type', LaunchpadRadioWidget)
747 custom_widget('branch_type', LaunchpadRadioWidget)
748 initial_values = {
749 'rcs_type': RevisionControlSystemsExtended.BZR,
750 'branch_type': LINK_LP_BZR,
751 }
752
753 def setUpWidgets(self):
754 """See `LaunchpadFormView`."""
755 super(ProductSeriesSetBranchView, self).setUpWidgets()
756
757 def render(widget, term_value, current_value, label=None):
758 term = widget.vocabulary.getTerm(term_value)
759 if term.value == current_value:
760 render = widget.renderSelectedItem
761 else:
762 render = widget.renderItem
763 if label is None:
764 label = term.title
765 value = term.token
766 return render(index=term.value,
767 text=label,
768 value=value,
769 name=widget.name,
770 cssClass='')
771
772 widget = self.widgets['rcs_type']
773 vocab = widget.vocabulary
774 current_value = widget._getFormValue()
775 self.rcs_type_cvs = render(widget, vocab.CVS, current_value, 'CVS')
776 self.rcs_type_svn = render(widget, vocab.BZR_SVN, current_value,
777 'SVN')
778 self.rcs_type_git = render(widget, vocab.GIT, current_value)
779 self.rcs_type_hg = render(widget, vocab.HG, current_value)
780 self.rcs_type_bzr = render(widget, vocab.BZR, current_value)
781 self.rcs_type_emptymarker = widget._emptyMarker()
782
783 widget = self.widgets['branch_type']
784 current_value = widget._getFormValue()
785 vocab = widget.vocabulary
786
787 (self.branch_type_link,
788 self.branch_type_create,
789 self.branch_type_import) = [
790 render(widget, value, current_value)
791 for value in (LINK_LP_BZR, CREATE_NEW, IMPORT_EXTERNAL)]
792
793 def _validateLinkLpBzr(self, data):
794 """Validate data for link-lp-bzr case."""
795 if 'branch_location' not in data:
796 self.setFieldError(
797 'branch_location',
798 'The branch location must be set.')
799
800 def _validateCreateNew(self, data):
801 """Validate data for create new case."""
802 self._validateBranch(data)
803
804 def _validateImportExternal(self, data):
805 """Validate data for import external case."""
806 rcs_type = data.get('rcs_type')
807 repo_url = data.get('repo_url')
808
809 if repo_url is None:
810 self.setFieldError('repo_url',
811 'You must set the external repository URL.')
812 else:
813 # Ensure this URL has not been imported before.
814 code_import = getUtility(ICodeImportSet).getByURL(repo_url)
815 if code_import is not None:
816 self.setFieldError(
817 'repo_url',
818 structured("""
819 This foreign branch URL is already specified for
820 the imported branch <a href="%s">%s</a>.""",
821 canonical_url(code_import.branch),
822 code_import.branch.unique_name))
823
824 # RCS type is mandatory.
825 # This condition should never happen since an initial value is set.
826 if rcs_type is None:
827 # The error shows but does not identify the widget.
828 self.setFieldError(
829 'rcs_type',
830 'You must specify the type of RCS for the remote host.')
831 elif rcs_type == RevisionControlSystemsExtended.CVS:
832 if 'cvs_module' not in data:
833 self.setFieldError(
834 'cvs_module',
835 'The CVS module must be set.')
836 self._validateBranch(data)
837
838 def _validateBranch(self, data):
839 """Validate that branch name and owner are set."""
840 if 'branch_name' not in data:
841 self.setFieldError(
842 'branch_name',
843 'The branch name must be set.')
844 if 'branch_owner' not in data:
845 self.setFieldError(
846 'branch_owner',
847 'The branch owner must be set.')
848
849 def _setRequired(self, names, value):
850 """Mark the widget field as optional."""
851 for name in names:
852 widget = self.widgets[name]
853 # The 'required' property on the widget context is set to False.
854 # The widget also has a 'required' property but it isn't used
855 # during validation.
856 widget.context.required = value
857
858 def _validSchemes(self, rcs_type):
859 """Return the valid schemes for the repository URL."""
860 schemes = set(['http', 'https'])
861 # Extend the allowed schemes for the repository URL based on
862 # rcs_type.
863 extra_schemes = {
864 RevisionControlSystemsExtended.BZR_SVN:['svn'],
865 RevisionControlSystemsExtended.GIT:['git'],
866 }
867 schemes.update(extra_schemes.get(rcs_type, []))
868 return schemes
869
870 def validate_widgets(self, data, names=None):
871 """See `LaunchpadFormView`."""
872 names = ['branch_type', 'rcs_type']
873 super(ProductSeriesSetBranchView, self).validate_widgets(data, names)
874 branch_type = data.get('branch_type')
875 if branch_type == LINK_LP_BZR:
876 # Mark other widgets as non-required.
877 self._setRequired(['rcs_type', 'repo_url', 'cvs_module',
878 'branch_name', 'branch_owner'], False)
879 elif branch_type == CREATE_NEW:
880 self._setRequired(
881 ['branch_location', 'repo_url', 'rcs_type', 'cvs_module'],
882 False)
883 elif branch_type == IMPORT_EXTERNAL:
884 rcs_type = data.get('rcs_type')
885
886 # Set the valid schemes based on rcs_type.
887 self.widgets['repo_url'].field.allowed_schemes = (
888 self._validSchemes(rcs_type))
889 # The branch location is not required for validation.
890 self._setRequired(['branch_location'], False)
891 # The cvs_module is required if it is a CVS import.
892 if rcs_type == RevisionControlSystemsExtended.CVS:
893 self._setRequired(['cvs_module'], True)
894 else:
895 raise AssertionError("Unknown branch type %s" % branch_type)
896 # Perform full validation now.
897 super(ProductSeriesSetBranchView, self).validate_widgets(data)
898
899 def validate(self, data):
900 """See `LaunchpadFormView`."""
901 # If widget validation returned errors then there is no need to
902 # continue as we'd likely just override the errors reported there.
903 if len(self.errors) > 0:
904 return
905 branch_type = data['branch_type']
906 if branch_type == IMPORT_EXTERNAL:
907 self._validateImportExternal(data)
908 elif branch_type == LINK_LP_BZR:
909 self._validateLinkLpBzr(data)
910 elif branch_type == CREATE_NEW:
911 self._validateCreateNew(data)
912 else:
913 raise AssertionError("Unknown branch type %s" % branch_type)
914
915 @property
916 def target(self):
917 """The branch target for the context."""
918 return IBranchTarget(self.context)
919
920 @action(_('Update'), name='update')
921 def update_action(self, action, data):
922 self.next_url = canonical_url(self.context)
923 branch_type = data.get('branch_type')
924 if branch_type == LINK_LP_BZR:
925 branch_location = data.get('branch_location')
926 if branch_location != self.context.branch:
927 self.context.branch = branch_location
928 # Request an initial upload of translation files.
929 getUtility(IRosettaUploadJobSource).create(
930 self.context.branch, NULL_REVISION)
931 else:
932 self.context.branch = branch_location
933 self.request.response.addInfoNotification(
934 'Series code location updated.')
935 else:
936 branch_name = data.get('branch_name')
937 branch_owner = data.get('branch_owner')
938
939 # Create a new branch.
940 if branch_type == CREATE_NEW:
941 branch = self._createBzrBranch(
942 BranchType.HOSTED, branch_name, branch_owner)
943 if branch is not None:
944 self.context.branch = branch
945 self.request.response.addInfoNotification(
946 'New branch created and linked to the series.')
947
948 # Import or mirror an external branch.
949 elif branch_type == IMPORT_EXTERNAL:
950 # Either create an externally hosted bzr branch
951 # (a.k.a. 'mirrored') or create a new code import.
952 rcs_type = data.get('rcs_type')
953 if rcs_type == RevisionControlSystemsExtended.BZR:
954 branch = self._createBzrBranch(
955 BranchType.MIRRORED, branch_name, branch_owner,
956 data['repo_url'])
957
958 if branch is not None:
959 self.context.branch = branch
960 self.request.response.addInfoNotification(
961 'Mirrored branch created and linked to '
962 'the series.')
963 else:
964 # We need to create an import request.
965
966 # Ensure the URL has not already been imported.
967 if rcs_type == RevisionControlSystemsExtended.CVS:
968 cvs_root = data.get('repo_url')
969 cvs_module = data.get('cvs_module')
970 url = None
971 else:
972 cvs_root = None
973 cvs_module = None
974 url = data.get('repo_url')
975 rcs_item = RevisionControlSystems.items[rcs_type.name]
976 code_import = getUtility(ICodeImportSet).new(
977 registrant=branch_owner,
978 target=self.target,
979 branch_name=branch_name,
980 rcs_type=rcs_item,
981 url=url,
982 cvs_root=cvs_root,
983 cvs_module=cvs_module)
984 self.context.branch = code_import.branch
985 self.request.response.addInfoNotification(
986 'Code import created and branch linked to the '
987 'series.')
988 else:
989 raise UnexpectedFormData(branch_type)
990
991 def _createBzrBranch(self, branch_type, branch_name,
992 branch_owner, repo_url=None):
993 """Create a new Bazaar branch. It may be hosted or mirrored.
994
995 Return the branch on success or None.
996 """
997 branch = None
998 try:
999 namespace = self.target.getNamespace(branch_owner)
1000 branch = namespace.createBranch(branch_type=branch_type,
1001 name=branch_name,
1002 registrant=self.user,
1003 url=repo_url)
1004 if branch_type == BranchType.MIRRORED:
1005 branch.requestMirror()
1006 except BranchCreationForbidden:
1007 self.addError(
1008 "You are not allowed to create branches in %s." %
1009 self.context.displayname)
1010 except BranchExists, e:
1011 self._setBranchExists(e.existing_branch, 'branch_name')
1012 return branch
1013
1014 @property
1015 def cancel_url(self):
1016 """See `LaunchpadFormView`."""
1017 return canonical_url(self.context)
1018
1019
1020class ProductSeriesLinkBranchView(LaunchpadEditFormView, ProductSeriesView):
648 """View to set the bazaar branch for a product series."""1021 """View to set the bazaar branch for a product series."""
6491022
650 schema = IProductSeries1023 schema = IProductSeries
@@ -753,23 +1126,6 @@
753 return encodeddata1126 return encodeddata
7541127
7551128
756class ProductSeriesSourceListView(LaunchpadView):
757 """A listing of all the running imports.
758
759 See `ICodeImportSet.getActiveImports` for our definition of running.
760 """
761
762 page_title = 'Available code imports'
763 label = page_title
764
765 def initialize(self):
766 """See `LaunchpadFormView`."""
767 self.text = self.request.get('text')
768 results = getUtility(ICodeImportSet).getActiveImports(text=self.text)
769
770 self.batchnav = BatchNavigator(results, self.request)
771
772
773class ProductSeriesFileBugRedirect(LaunchpadView):1129class ProductSeriesFileBugRedirect(LaunchpadView):
774 """Redirect to the product's +filebug page."""1130 """Redirect to the product's +filebug page."""
7751131
7761132
=== added file 'lib/lp/registry/browser/tests/productseries-setbranch-view.txt'
--- lib/lp/registry/browser/tests/productseries-setbranch-view.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/browser/tests/productseries-setbranch-view.txt 2010-04-07 13:24:38 +0000
@@ -0,0 +1,339 @@
1Set branch
2----------
3
4The productseries +setbranch view allows the user to set a branch for
5this series. The branch can be one that already exists in Launchpad,
6or a new branch in Launchpad can be defined, or it can be a repository
7that exists externally in a variety of version control systems.
8
9 >>> from canonical.launchpad.testing.pages import find_tag_by_id
10 >>> product = factory.makeProduct(name="chevy")
11 >>> series = factory.makeProductSeries(name="impala", product=product)
12 >>> transaction.commit()
13 >>> login_person(product.owner)
14 >>> view = create_initialized_view(series, name='+setbranch',
15 ... principal=product.owner)
16 >>> print find_tag_by_id(view.render(), 'maincontent')
17 <div...
18 ...Link to a Bazaar branch already on Launchpad...
19 ...Create a new, empty branch in Launchpad and link to this series...
20 ...Import a branch hosted somewhere else...
21 ...Branch name:...
22 ...Branch owner:...
23
24
25Linking to an existing branch
26-----------------------------
27
28If linking to an existing branch is selected then the branch location
29must be provided.
30
31 >>> form = {
32 ... 'field.branch_type': 'link-lp-bzr',
33 ... 'field.actions.update': 'Update',
34 ... }
35 >>> view = create_initialized_view(series, name='+setbranch',
36 ... principal=product.owner, form=form)
37 >>> for error in view.errors:
38 ... print error
39 The branch location must be set.
40
41Setting the branch location to an invalid branch results in another
42validation error.
43
44 >>> form = {
45 ... 'field.branch_type': 'link-lp-bzr',
46 ... 'field.branch_location': 'foo',
47 ... 'field.actions.update': 'Update',
48 ... }
49 >>> view = create_initialized_view(series, name='+setbranch',
50 ... principal=product.owner, form=form)
51 >>> for error in view.errors:
52 ... print error
53 ('Invalid value', InvalidValue("token 'foo' not found in vocabulary"))
54
55Providing a valid branch results in a successful linking.
56
57 >>> series.branch is None
58 True
59 >>> branch = factory.makeBranch(name='impala-branch',
60 ... owner=product.owner, product=product)
61 >>> form = {
62 ... 'field.branch_type': 'link-lp-bzr',
63 ... 'field.branch_location': branch.unique_name,
64 ... 'field.actions.update': 'Update',
65 ... }
66 >>> view = create_initialized_view(series, name='+setbranch',
67 ... principal=product.owner, form=form)
68 >>> for error in view.errors:
69 ... print error
70 >>> for notification in view.request.response.notifications:
71 ... print notification.message
72 Series code location updated.
73
74 >>> print series.branch.name
75 impala-branch
76
77
78Creating a new branch
79---------------------
80
81When creating a new branch the branch name and owner must be specified.
82
83 >>> series = factory.makeProductSeries(name="camaro", product=product)
84 >>> transaction.commit()
85
86 >>> form = {
87 ... 'field.branch_type': 'create-new',
88 ... 'field.actions.update': 'Update',
89 ... }
90 >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
91 >>> for notification in view.request.response.notifications:
92 ... print notification.message
93 >>> for error in view.errors:
94 ... print error
95 The branch name must be set.
96 The branch owner must be set.
97
98 >>> from lp.registry.interfaces.person import IPersonSet
99 >>> mark = getUtility(IPersonSet).getByEmail('mark@example.com')
100 >>> form = {
101 ... 'field.branch_type': 'create-new',
102 ... 'field.branch_name': 'camaro-branch',
103 ... 'field.branch_owner': product.owner.name,
104 ... 'field.actions.update': 'Update',
105 ... }
106
107 >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
108 >>> for error in view.errors:
109 ... print error
110 >>> for notification in view.request.response.notifications:
111 ... print notification.message
112 New branch created and linked to the series.
113 >>> print series.branch.name
114 camaro-branch
115
116
117Import a branch hosted elsewhere
118--------------------------------
119
120Importing an externally hosted branch can either be a mirror, if a
121Bazaar branch, or an import, if a git, hg, cvs, or svn branch.
122
123Lots of data are required to create an import.
124
125 >>> series = factory.makeProductSeries(name="blazer", product=product)
126 >>> transaction.commit()
127
128 >>> form = {
129 ... 'field.branch_type': 'import-external',
130 ... 'field.actions.update': 'Update',
131 ... }
132 >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
133 >>> for notification in view.request.response.notifications:
134 ... print notification.message
135 >>> for error in view.errors:
136 ... print error
137 You must set the external repository URL.
138 You must specify the type of RCS for the remote host.
139 The branch name must be set.
140 The branch owner must be set.
141
142For Bazaar branches the scheme may only be http or https.
143
144 >>> form = {
145 ... 'field.branch_type': 'import-external',
146 ... 'field.rcs_type': 'BZR',
147 ... 'field.branch_name': 'blazer-branch',
148 ... 'field.branch_owner': product.owner.name,
149 ... 'field.repo_url': 'bzr://bzr.com/foo',
150 ... 'field.actions.update': 'Update',
151 ... }
152 >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
153 >>> for notification in view.request.response.notifications:
154 ... print notification.message
155 >>> for error in view.errors:
156 ... print error
157 ('repo_url'...The URI scheme "bzr" is not allowed. Only URIs with the following schemes may be
158 used: http, https'))
159
160A correct URL is accepted.
161
162 >>> form = {
163 ... 'field.branch_type': 'import-external',
164 ... 'field.rcs_type': 'BZR',
165 ... 'field.branch_name': 'blazer-branch',
166 ... 'field.branch_owner': product.owner.name,
167 ... 'field.repo_url': 'http://bzr.com/foo',
168 ... 'field.actions.update': 'Update',
169 ... }
170 >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
171 >>> for error in view.errors:
172 ... print error
173 >>> for notification in view.request.response.notifications:
174 ... print notification.message
175 Mirrored branch created and linked to the series.
176 >>> print series.branch.name
177 blazer-branch
178
179Git branches cannnot use svn.
180
181 >>> form = {
182 ... 'field.branch_type': 'import-external',
183 ... 'field.rcs_type': 'GIT',
184 ... 'field.branch_name': 'chevette-branch',
185 ... 'field.branch_owner': product.owner.name,
186 ... 'field.repo_url': 'svn://svn.com/chevette',
187 ... 'field.actions.update': 'Update',
188 ... }
189 >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
190 >>> for notification in view.request.response.notifications:
191 ... print notification.message
192 >>> for error in view.errors:
193 ... print error
194 ('repo_url'...'The URI scheme "svn" is not allowed. Only
195 URIs with the following schemes may be used: git, http, https'))
196
197But Git branches may use git.
198
199 >>> series = factory.makeProductSeries(name="chevette", product=product)
200 >>> transaction.commit()
201 >>> form = {
202 ... 'field.branch_type': 'import-external',
203 ... 'field.rcs_type': 'GIT',
204 ... 'field.branch_name': 'chevette-branch',
205 ... 'field.branch_owner': product.owner.name,
206 ... 'field.repo_url': 'git://github.com/chevette',
207 ... 'field.actions.update': 'Update',
208 ... }
209 >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
210 >>> transaction.commit()
211 >>> for error in view.errors:
212 ... print error
213 >>> for notification in view.request.response.notifications:
214 ... print notification.message
215 Code import created and branch linked to the series.
216 >>> print series.branch.name
217 chevette-branch
218
219But Subversion branches cannnot use git.
220
221 >>> form = {
222 ... 'field.branch_type': 'import-external',
223 ... 'field.rcs_type': 'BZR_SVN',
224 ... 'field.branch_name': 'suburban-branch',
225 ... 'field.branch_owner': product.owner.name,
226 ... 'field.repo_url': 'git://github.com/suburban',
227 ... 'field.actions.update': 'Update',
228 ... }
229 >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
230 >>> for notification in view.request.response.notifications:
231 ... print notification.message
232 >>> for error in view.errors:
233 ... print error
234 ('repo_url'...'The URI scheme "git" is not allowed. Only
235 URIs with the following schemes may be used: http, https, svn'))
236
237But Subversion branches may use svn as the scheme.
238
239 >>> series = factory.makeProductSeries(name="suburban", product=product)
240 >>> transaction.commit()
241 >>> form = {
242 ... 'field.branch_type': 'import-external',
243 ... 'field.rcs_type': 'BZR_SVN',
244 ... 'field.branch_name': 'suburban-branch',
245 ... 'field.branch_owner': product.owner.name,
246 ... 'field.repo_url': 'svn://svn.com/suburban',
247 ... 'field.actions.update': 'Update',
248 ... }
249 >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
250 >>> for error in view.errors:
251 ... print error
252 >>> for notification in view.request.response.notifications:
253 ... print notification.message
254 Code import created and branch linked to the series.
255 >>> print series.branch.name
256 suburban-branch
257
258Mercurial branches must use http or https as the scheme.
259
260 >>> series = factory.makeProductSeries(name="malibu", product=product)
261 >>> transaction.commit()
262 >>> form = {
263 ... 'field.branch_type': 'import-external',
264 ... 'field.rcs_type': 'HG',
265 ... 'field.branch_name': 'malibu-branch',
266 ... 'field.branch_owner': product.owner.name,
267 ... 'field.repo_url': 'https://mercurial.com/branch',
268 ... 'field.actions.update': 'Update',
269 ... }
270 >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
271 >>> for error in view.errors:
272 ... print error
273 >>> for notification in view.request.response.notifications:
274 ... print notification.message
275 Code import created and branch linked to the series.
276 >>> print series.branch.name
277 malibu-branch
278
279CVS branches must use http or https as the scheme and must have the
280CVS module field specified.
281
282 >>> series = factory.makeProductSeries(name="corvair", product=product)
283 >>> transaction.commit()
284 >>> form = {
285 ... 'field.branch_type': 'import-external',
286 ... 'field.rcs_type': 'CVS',
287 ... 'field.branch_name': 'corvair-branch',
288 ... 'field.branch_owner': product.owner.name,
289 ... 'field.repo_url': 'https://cvs.com/branch',
290 ... 'field.actions.update': 'Update',
291 ... }
292 >>> view = create_initialized_view(series, name='+setbranch',
293 ... principal=product.owner, form=form)
294 >>> for notification in view.request.response.notifications:
295 ... print notification.message
296 >>> for error in view.errors:
297 ... print error
298 The CVS module must be set.
299
300 >>> form = {
301 ... 'field.branch_type': 'import-external',
302 ... 'field.rcs_type': 'CVS',
303 ... 'field.branch_name': 'corvair-branch',
304 ... 'field.branch_owner': product.owner.name,
305 ... 'field.repo_url': 'https://cvs.com/branch',
306 ... 'field.cvs_module': 'root',
307 ... 'field.actions.update': 'Update',
308 ... }
309 >>> view = create_initialized_view(series, name='+setbranch',
310 ... principal=product.owner, form=form)
311 >>> for error in view.errors:
312 ... print error
313 >>> for notification in view.request.response.notifications:
314 ... print notification.message
315 Code import created and branch linked to the series.
316 >>> print series.branch.name
317 corvair-branch
318
319Attempting to import a location that has already been imported results
320in an error.
321
322 >>> form = {
323 ... 'field.branch_type': 'import-external',
324 ... 'field.rcs_type': 'GIT',
325 ... 'field.branch_name': 'chevette-branch-dup',
326 ... 'field.branch_owner': product.owner.name,
327 ... 'field.repo_url': 'git://github.com/chevette',
328 ... 'field.actions.update': 'Update',
329 ... }
330 >>> view = create_initialized_view(series, name='+setbranch',
331 ... principal=product.owner, form=form)
332 >>> for error in view.errors:
333 ... print error
334 <BLANKLINE>
335 This foreign branch URL is already specified for
336 the imported branch <a href="http://code.launchpad.dev/~.../chevy/chevette-branch">~.../chevy/chevette-branch</a>.
337
338 >>> for notification in view.request.response.notifications:
339 ... print notification.message
0340
=== added file 'lib/lp/registry/stories/productseries/xx-productseries-set-branch.txt'
--- lib/lp/registry/stories/productseries/xx-productseries-set-branch.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/stories/productseries/xx-productseries-set-branch.txt 2010-04-07 13:24:38 +0000
@@ -0,0 +1,147 @@
1Setting the branch for a product series
2=======================================
3
4A product series should have a branch set for it. The branch can be
5hosted on Launchpad or somewhere else. Foreign branches can be in
6Bazaar, Git, Mercurial, Subversion, or CVS. Though internally
7Launchpad treats those scenarios differently we provide a single page
8to the user to set up the branch.
9
10At present, the unified page for setting up the branch is not linked
11from anywhere, so it must be navigated to directly.
12
13 >>> browser = setupBrowser(auth="Basic test@canonical.com:test")
14 >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch')
15
16The default choice for the type of branch to set is one that
17already exists on Launchpad.
18
19 >>> print_radio_button_field(browser.contents, 'branch_type')
20 (*) Link to a Bazaar branch already on Launchpad
21 ( ) Create a new, empty branch in Launchpad and link to this series
22 ( ) Import a branch hosted somewhere else
23
24
25Linking to an existing branch
26-----------------------------
27
28A user can choose to link to an existing branch on Launchpad.
29
30 >>> login('test@canonical.com')
31 >>> from zope.component import getUtility
32 >>> from lp.registry.interfaces.product import IProductSet
33 >>> productset = getUtility(IProductSet)
34 >>> firefox = productset.getByName('firefox')
35 >>> branch = factory.makeBranch(name="firefox-hosted-branch", product=firefox)
36 >>> branch_name = branch.unique_name
37 >>> logout()
38
39 >>> browser.getControl(name='field.branch_location').value = branch_name
40 >>> browser.getControl('Update').click()
41 >>> for message in get_feedback_messages(browser.contents):
42 ... print extract_text(message)
43 Series code location updated.
44 >>> print browser.url
45 http://launchpad.dev/firefox/trunk
46
47
48Creating a new branch
49---------------------
50
51A brand new, empty branch on Launchpad can be created and set as the
52series branch.
53
54 >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch')
55 >>> browser.getControl('Create a new, empty branch in Launchpad').click()
56 >>> browser.getControl('Update').click()
57 >>> for message in get_feedback_messages(browser.contents):
58 ... print extract_text(message)
59 There is 1 error.
60 Required input is missing.
61
62However in order to create the branch the name and owner must be
63specified. The owner is a pre-populated dropdown list so the default
64can be used.
65
66 >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch')
67 >>> browser.getControl('Create a new, empty branch in Launchpad').click()
68 >>> browser.getControl(name='field.branch_name').value = 'new-firefox-branch'
69 >>> browser.getControl('Update').click()
70 >>> for message in get_feedback_messages(browser.contents):
71 ... print extract_text(message)
72 New branch created and linked to the series.
73 >>> print browser.url
74 http://launchpad.dev/firefox/trunk
75
76
77Linking to an external branch
78-----------------------------
79
80An external branch can be linked. The branch can be a Bazaar branch
81or be a Git, Mercurial, Subversion, or CVS branch.
82
83Each of these types must provide the URL of the external repository,
84the branch name to use in Launchpad, and the branch owner.
85
86 >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch')
87 >>> browser.getControl('Import a branch hosted somewhere else').click()
88 >>> browser.getControl('Branch name').value = 'bzr-firefox-branch'
89 >>> browser.getControl('Bazaar', index=0).click()
90 >>> browser.getControl('Branch URL').value = 'https://bzr.example.com/branch'
91 >>> browser.getControl('Update').click()
92 >>> for message in get_feedback_messages(browser.contents):
93 ... print extract_text(message)
94 Series code location updated.
95 >>> print browser.url
96 http://launchpad.dev/firefox/trunk
97
98The process is the same for a Git external branch, though the novel
99"git://" scheme can also be used.
100
101 >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch')
102 >>> browser.getControl('Import a branch hosted somewhere else').click()
103 >>> browser.getControl('Branch name').value = 'git-firefox-branch'
104 >>> browser.getControl('Git').click()
105 >>> browser.getControl('Branch URL').value = 'git://git.example.com/branch'
106 >>> browser.getControl('Update').click()
107 >>> for message in get_feedback_messages(browser.contents):
108 ... print extract_text(message)
109 Code import created and branch linked to the series.
110 >>> print browser.url
111 http://launchpad.dev/firefox/trunk
112
113Likewise Subversion can use the "svn://" scheme.
114
115 >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch')
116 >>> browser.getControl('Import a branch hosted somewhere else').click()
117 >>> browser.getControl('Branch name').value = 'svn-firefox-branch'
118 >>> browser.getControl('SVN').click()
119 >>> browser.getControl('Branch URL').value = 'svn://svn.example.com/branch'
120 >>> browser.getControl('Update').click()
121 >>> for message in get_feedback_messages(browser.contents):
122 ... print extract_text(message)
123 Code import created and branch linked to the series.
124 >>> print browser.url
125 http://launchpad.dev/firefox/trunk
126
127The branch owner can be the logged in user or one of her teams.
128
129 >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch')
130 >>> browser.getControl('Import a branch hosted somewhere else').click()
131 >>> browser.getControl('Branch name').value = 'hg-firefox-branch'
132 >>> browser.getControl('Mercurial').click()
133 >>> browser.getControl('Branch URL').value = 'http://hg.example.com/branch'
134 >>> browser.getControl('Branch owner').value = ['hwdb-team']
135 >>> browser.getControl('Update').click()
136 >>> for message in get_feedback_messages(browser.contents):
137 ... print extract_text(message)
138 Code import created and branch linked to the series.
139 >>> print browser.url
140 http://launchpad.dev/firefox/trunk
141 >>> login('test@canonical.com')
142 >>> firefox_trunk = firefox.getSeries('trunk')
143 >>> print firefox_trunk.branch.unique_name
144 ~hwdb-team/firefox/hg-firefox-branch
145 >>> print firefox_trunk.branch.owner.name
146 hwdb-team
147 >>> logout()
0148
=== modified file 'lib/lp/registry/templates/productseries-codesummary.pt'
--- lib/lp/registry/templates/productseries-codesummary.pt 2010-03-09 22:06:12 +0000
+++ lib/lp/registry/templates/productseries-codesummary.pt 2010-04-07 13:24:38 +0000
@@ -29,7 +29,7 @@
29 <li>29 <li>
30 <p>30 <p>
31 If the code is in a Bazaar branch not yet on Launchpad31 If the code is in a Bazaar branch not yet on Launchpad
32 you can either32 you can either:
33 </p>33 </p>
3434
35 <ul class="bulleted" style="margin-bottom: 0;">35 <ul class="bulleted" style="margin-bottom: 0;">
@@ -39,7 +39,7 @@
39 registering a mirrored branch</a>39 registering a mirrored branch</a>
40 </li>40 </li>
41 <li id="ssh-key-directions">41 <li id="ssh-key-directions">
42 Push the branch directly to Launchpad. eg. with <br />42 Push the branch directly to Launchpad, e.g. with:<br />
43 <tt><strong>43 <tt><strong>
44 bzr push lp:~<tal:user replace="view/user/name"/>/<tal:project replace="context/product/name"/>/trunk44 bzr push lp:~<tal:user replace="view/user/name"/>/<tal:project replace="context/product/name"/>/trunk
45 </strong></tt>45 </strong></tt>
@@ -58,7 +58,7 @@
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>.
59 </li>59 </li>
60 </ul>60 </ul>
61 61
62 <ul class="horizontal">62 <ul class="horizontal">
63 <li>63 <li>
64 <a tal:replace="structure context/menu:overview/link_branch/fmt:link" />64 <a tal:replace="structure context/menu:overview/link_branch/fmt:link" />
6565
=== modified file 'lib/lp/registry/templates/productseries-linkbranch.pt'
--- lib/lp/registry/templates/productseries-linkbranch.pt 2009-08-11 21:26:30 +0000
+++ lib/lp/registry/templates/productseries-linkbranch.pt 2010-04-07 13:24:38 +0000
@@ -7,8 +7,44 @@
7 i18n:domain="launchpad">7 i18n:domain="launchpad">
8 <body>8 <body>
9 <div metal:fill-slot="main">9 <div metal:fill-slot="main">
10 <div metal:use-macro="context/@@launchpad_form/form" />10 <ul>
11 <li>If the code is already in a Bazaar branch registered with Launchpad,
12 specify it here:
13 <div metal:use-macro="context/@@launchpad_form/form" />
14 </li>
15
16 <li>
17 <p>
18 Otherwise, if the code is in a Bazaar branch not yet on Launchpad
19 you can either:
20 </p>
21
22 <ul class="bulleted" style="margin-bottom: 0;">
23 <li>
24 Have the branch mirrored from a remote location by
25 <a tal:attributes="href context/menu:overview/branch_add/fmt:url">
26 registering a mirrored branch</a>
27 </li>
28 <li id="ssh-key-directions">
29 Push the branch directly to Launchpad, e.g. with:<br />
30 <tt><strong>
31 bzr push lp:~<tal:user replace="view/user/name"/>/<tal:project replace="context/product/name"/>/trunk
32 </strong></tt>
33 <tal:no-keys condition="not:view/user/sshkeys">
34 <br/>To authenticate with the Launchpad branch upload service,
35 you need to
36 <a tal:attributes="href string:${view/user/fmt:url}/+editsshkeys">
37 register a SSH key</a>.
38 </tal:no-keys>
39 </li>
40 </ul>
41 </li>
42
43 <li>
44 If the code is in git, CVS or Subversion you can
45 <a tal:attributes="href view/request_import_link">request that the branch be imported to Bazaar</a>.
46 </li>
47 </ul>
11 </div>48 </div>
12 </body>49 </body>
13</html>50</html>
14
1551
=== added file 'lib/lp/registry/templates/productseries-setbranch.pt'
--- lib/lp/registry/templates/productseries-setbranch.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/templates/productseries-setbranch.pt 2010-04-07 13:24:38 +0000
@@ -0,0 +1,129 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad">
8
9<body>
10
11<metal:block fill-slot="head_epilogue">
12 <style type="text/css">
13 .subordinate {
14 margin: 0.5em 0 0.5em 4em;
15 }
16 </style>
17</metal:block>
18
19<div metal:fill-slot="main">
20
21 <div metal:use-macro="context/@@launchpad_form/form">
22
23 <metal:formbody fill-slot="widgets">
24
25 <table class="form">
26
27 <tr>
28 <td>
29 <label tal:replace="structure view/branch_type_link">
30 Link to a Bazaar branch already in Launchpad
31 </label>
32 <table class="subordinate">
33 <tal:widget define="widget nocall:view/widgets/branch_location">
34 <metal:block use-macro="context/@@launchpad_form/widget_row" />
35 </tal:widget>
36 </table>
37 </td>
38 </tr>
39
40 <tr>
41 <td>
42 <label tal:replace="structure view/branch_type_create">
43 Create a new, empty branch in Launchpad and link
44 to this series
45 </label>
46 </td>
47 </tr>
48
49 <tr>
50 <td>
51 <label tal:replace="structure view/branch_type_import">
52 Import a branch hosted somewhere else
53 </label>
54 <table class="subordinate">
55 <tal:widget define="widget nocall:view/widgets/repo_url">
56 <metal:block use-macro="context/@@launchpad_form/widget_row" />
57 </tal:widget>
58
59 <tr>
60 <td>
61 <label tal:replace="structure view/rcs_type_bzr">
62 Bazaar, hosted externally
63 </label>
64 </td>
65 </tr>
66
67 <tr>
68 <td>
69 <label tal:replace="structure view/rcs_type_git">
70 Git
71 </label>
72 </td>
73 </tr>
74
75 <tr>
76 <td>
77 <label tal:replace="structure view/rcs_type_svn">
78 SVN
79 </label>
80 </td>
81 </tr>
82
83 <tr>
84 <td>
85 <label tal:replace="structure view/rcs_type_hg">
86 Mercurial
87 </label>
88 </td>
89 </tr>
90
91 <tr>
92 <td>
93 <label tal:replace="structure view/rcs_type_cvs">
94 CVS
95 </label>
96 <table class="subordinate">
97 <tal:widget define="widget nocall:view/widgets/cvs_module">
98 <metal:block use-macro="context/@@launchpad_form/widget_row" />
99 </tal:widget>
100 </table>
101 </td>
102 </tr>
103
104 </table>
105 </td>
106 </tr>
107
108 <tal:widget define="widget nocall:view/widgets/branch_name">
109 <metal:block use-macro="context/@@launchpad_form/widget_row" />
110 </tal:widget>
111 <tal:widget define="widget nocall:view/widgets/branch_owner">
112 <metal:block use-macro="context/@@launchpad_form/widget_row" />
113 </tal:widget>
114
115 </table>
116 <input tal:replace="structure view/rcs_type_emptymarker" />
117
118 </metal:formbody>
119 </div>
120
121 <script type="text/javascript">
122 YUI().use('lp.code.productseries_setbranch', function(Y) {
123 Y.on('domready', Y.lp.code.productseries_setbranch.setup);
124 });
125 </script>
126
127</div>
128</body>
129</html>
0130
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2010-04-05 17:40:35 +0000
+++ lib/lp/testing/factory.py 2010-04-07 13:24:38 +0000
@@ -149,8 +149,8 @@
149149
150DIFF = """\150DIFF = """\
151=== zbqvsvrq svyr 'yvo/yc/pbqr/vagresnprf/qvss.cl'151=== zbqvsvrq svyr 'yvo/yc/pbqr/vagresnprf/qvss.cl'
152--- yvo/yc/pbqr/vagresnprf/qvss.cl 2009-10-01 13:25:12 +0000152--- yvo/yc/pbqr/vagresnprf/qvss.cl 2009-10-01 13:25:12 +0000
153+++ yvo/yc/pbqr/vagresnprf/qvss.cl 2010-02-02 15:48:56 +0000153+++ yvo/yc/pbqr/vagresnprf/qvss.cl 2010-02-02 15:48:56 +0000
154@@ -121,6 +121,10 @@154@@ -121,6 +121,10 @@
155 'Gur pbasyvpgf grkg qrfpevovat nal cngu be grkg pbasyvpgf.'),155 'Gur pbasyvpgf grkg qrfpevovat nal cngu be grkg pbasyvpgf.'),
156 ernqbayl=Gehr))156 ernqbayl=Gehr))
@@ -635,7 +635,7 @@
635 def makeProcessorFamily(self, name, title=None, description=None,635 def makeProcessorFamily(self, name, title=None, description=None,
636 restricted=False):636 restricted=False):
637 """Create a new processor family.637 """Create a new processor family.
638 638
639 :param name: Name of the family (e.g. x86)639 :param name: Name of the family (e.g. x86)
640 :param title: Optional title of the family640 :param title: Optional title of the family
641 :param description: Optional extended description641 :param description: Optional extended description
@@ -1568,7 +1568,7 @@
15681568
1569 :param branch: If supplied, the branch to set as1569 :param branch: If supplied, the branch to set as
1570 ProductSeries.branch.1570 ProductSeries.branch.
1571 :param product: If supplied, the name of the series.1571 :param name: If supplied, the name of the series.
1572 :param product: If supplied, the series is created for this product.1572 :param product: If supplied, the series is created for this product.
1573 Otherwise, a new product is created.1573 Otherwise, a new product is created.
1574 """1574 """

Subscribers

People subscribed via source and target branches

to status/vote changes: