launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #18588
[Merge] lp:~blr/launchpad/ui-project-setbranch into lp:launchpad
Bayard 'kit' Randel has proposed merging lp:~blr/launchpad/ui-project-setbranch into lp:launchpad.
Commit message:
Add Product.ProductSetBranchView and UI support for setting product.vcs.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~blr/launchpad/ui-project-setbranch/+merge/259069
Provides a new SetBranch view for projects, additionally allowing a default version control system to be defined.
A project's vcs will also be displayed on the product index under Project Information. If not default vcs has been provided, the vcs type is inferred from existing bzr or git branches, with git taking precedence.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~blr/launchpad/ui-project-setbranch into lp:launchpad.
=== added file 'lib/canonical/launchpad/icing/css/components/yui_tabview.css'
--- lib/canonical/launchpad/icing/css/components/yui_tabview.css 1970-01-01 00:00:00 +0000
+++ lib/canonical/launchpad/icing/css/components/yui_tabview.css 2015-05-19 02:48:29 +0000
@@ -0,0 +1,112 @@
+/*
+YUI 3.10.3 (build 2fb5187)
+Copyright 2013 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+http://yuilibrary.com/license/
+*/
+
+.yui3-tab-panel {
+ display: none;
+ }
+
+.yui3-tab-panel-selected {
+ display: block;
+ }
+
+.yui3-tabview-list,
+.yui3-tab {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ }
+
+.yui3-tabview {
+ position: relative;
+ }
+
+.yui3-tabview,
+.yui3-tabview-list,
+.yui3-tabview-panel,
+.yui3-tab,
+.yui3-tab-panel {
+ zoom: 1;
+ }
+
+.yui3-tab {
+ display: inline-block;
+ *display: inline;
+ vertical-align: bottom;
+ cursor: pointer;
+ }
+
+.yui3-tab-label {
+ display: block;
+ display: inline-block;
+ padding: 6px 10px;
+ position: relative;
+ text-decoration: none;
+ vertical-align: bottom;
+ }
+
+.yui3-skin-sam .yui3-tabview-list {
+ zoom: 1;
+ }
+
+.yui3-skin-sam .yui3-tab {
+ margin: 0 .2em 0 0;
+ padding: 1px 0 0;
+ zoom: 1;
+ }
+
+.yui3-skin-sam .yui3-tab-selected {
+ margin-bottom: -1px;
+ }
+
+.yui3-skin-sam .yui3-tab-label {
+ background: #d8d8d8 url(../../../../assets/skins/sam/sprite.png) repeat-x;
+ border: solid #a3a3a3;
+ border-width: 1px 1px 0 1px;
+ color: #000;
+ cursor: pointer;
+ font-size: 85%;
+ padding: .3em .75em;
+ text-decoration: none;
+ border-radius: 5px 5px 0 0;
+ }
+
+.yui3-skin-sam .yui3-tab-label:hover,
+.yui3-skin-sam .yui3-tab-label:focus {
+ background: #bfdaff url(../../../../assets/skins/sam/sprite.png) repeat-x left -1300px;
+ outline: 0;
+ }
+
+.yui3-skin-sam .yui3-tab-selected .yui3-tab-label,
+.yui3-skin-sam .yui3-tab-selected .yui3-tab-label:focus,
+.yui3-skin-sam .yui3-tab-selected .yui3-tab-label:hover {
+ background: #fbfbfb url(../../../../assets/skins/sam/sprite.png) repeat-x left -1400px;
+ color: #333;
+ font-weight: bold;
+ }
+
+.yui3-skin-sam .yui3-tab-selected .yui3-tab-label {
+ padding: .4em .75em;
+ }
+
+.yui3-skin-sam .yui3-tab-selected .yui3-tab-label {
+ border-color: #808080;
+ }
+
+.yui3-skin-sam .yui3-tabview-panel {
+ background: #fbfbfb;
+ border-radius: 0px 3px 3px 3px;
+ }
+
+.yui3-skin-sam .yui3-tabview-panel {
+ border: 1px solid #808080;
+ border-top-color: #243356;
+ padding: 1em .5em;
+ }
+
+#yui3-css-stamp.skin-sam-tabview {
+ display: none;
+ }
=== modified file 'lib/canonical/launchpad/icing/inline-sprites-1.css.in'
--- lib/canonical/launchpad/icing/inline-sprites-1.css.in 2015-03-24 13:36:23 +0000
+++ lib/canonical/launchpad/icing/inline-sprites-1.css.in 2015-05-19 02:48:29 +0000
@@ -84,7 +84,11 @@
.branch {
background-image: url(/@@/branch.png); /* sprite-ref: icon-sprites */
background-repeat: no-repeat;
- }
+}
+.gitbranch {
+ background-image: url(/@@/gitbranch.png); /* sprite-ref: icon-sprites */
+ background-repeat: no-repeat;
+}
.distribution {
background-image: url(/@@/distribution.png); /* sprite-ref: icon-sprites */
background-repeat: no-repeat;
=== modified file 'lib/canonical/launchpad/icing/style.css'
--- lib/canonical/launchpad/icing/style.css 2015-04-07 23:43:43 +0000
+++ lib/canonical/launchpad/icing/style.css 2015-05-19 02:48:29 +0000
@@ -537,6 +537,33 @@
/* --- Code --- */
+code.command {
+ background-color: #FFF;
+ border: 1px solid #ddd;
+ border-radius: 3px;
+ box-sizing: border-box;
+ color: #626262;
+ padding: 4px;
+ font-family: "DejaVu Sans Mono", "Courier New", monospace;
+ font-size: 1.05em;
+}
+code.command-block {
+ display: block;
+ margin-bottom: 1em;
+ padding: 6px;
+}
+div.scm-tip {
+ margin-bottom: 1em;
+ box-sizing: border-box;
+ overflow: hidden;
+ padding: 5px;
+}
+div.scm-tip:after {
+ clear: left;
+}
+div#push-instructions-tab {
+ width: 750px;
+}
table.code {
margin-bottom: 1em;
=== added file 'lib/canonical/launchpad/images/gitbranch-large.png'
Binary files lib/canonical/launchpad/images/gitbranch-large.png 1970-01-01 00:00:00 +0000 and lib/canonical/launchpad/images/gitbranch-large.png 2015-05-19 02:48:29 +0000 differ
=== added file 'lib/canonical/launchpad/images/gitbranch.png'
Binary files lib/canonical/launchpad/images/gitbranch.png 1970-01-01 00:00:00 +0000 and lib/canonical/launchpad/images/gitbranch.png 2015-05-19 02:48:29 +0000 differ
=== modified file 'lib/lp/blueprints/stories/standalone/xx-batching.txt'
--- lib/lp/blueprints/stories/standalone/xx-batching.txt 2010-09-16 16:47:37 +0000
+++ lib/lp/blueprints/stories/standalone/xx-batching.txt 2015-05-19 02:48:29 +0000
@@ -28,7 +28,7 @@
>>> browser.open("http://blueprints.launchpad.dev/big-project")
>>> print extract_text(find_main_content(browser.contents))
- Blueprints...does not know how...Configure blueprints...
+ Blueprints...does not know how...Configure Blueprints...
But it's easy to change that.
=== modified file 'lib/lp/bugs/stories/bugs/xx-front-page-info.txt'
--- lib/lp/bugs/stories/bugs/xx-front-page-info.txt 2011-12-24 15:18:32 +0000
+++ lib/lp/bugs/stories/bugs/xx-front-page-info.txt 2015-05-19 02:48:29 +0000
@@ -34,7 +34,7 @@
>>> enable_tracker = find_tag_by_id(
... admin_browser.contents, 'no-malone-edit')
>>> print extract_text(enable_tracker)
- Configure bug tracker
+ Configure Bugs
The +bugs page for a project using Launchpad for bug tracking
shows controls for setting bug supervisor and states that no
=== modified file 'lib/lp/code/javascript/productseries-setbranch.js'
--- lib/lp/code/javascript/productseries-setbranch.js 2012-07-02 12:50:33 +0000
+++ lib/lp/code/javascript/productseries-setbranch.js 2015-05-19 02:48:29 +0000
@@ -12,77 +12,88 @@
var module = Y.namespace('lp.code.productseries_setbranch');
module._get_selected_rcs = function() {
- var rcs_types = module._rcs_types();
- var selected = 'None';
- for (var i = 0; i < rcs_types.length; i++) {
- if (rcs_types[i].checked) {
- selected = rcs_types[i].value;
- break;
- }
- }
- return selected;
+ var rcs_types = module._rcs_types();
+ var selected = 'None';
+ for (var i = 0; i < rcs_types.length; i++) {
+ if (rcs_types[i].checked) {
+ selected = rcs_types[i].value;
+ break;
+ }
+ }
+ return selected;
};
module.__rcs_types = null;
module._rcs_types = function() {
- if (module.__rcs_types === null) {
- module.__rcs_types = document.getElementsByName('field.rcs_type');
- }
- return module.__rcs_types;
+ if (module.__rcs_types === null) {
+ module.__rcs_types = document.getElementsByName('field.rcs_type');
+ }
+ return module.__rcs_types;
};
module.set_enabled = function(field_id, is_enabled) {
- var field = Y.DOM.byId(field_id);
- field.disabled = !is_enabled;
+ var field = Y.DOM.byId(field_id);
+ field.disabled = !is_enabled;
};
module.onclick_rcs_type = function(e) {
- /* Which rcs type radio button has been selected? */
- // CVS
- var rcs_types = module._rcs_types();
- var selectedRCS = module._get_selected_rcs();
- module.set_enabled('field.cvs_module', selectedRCS == 'CVS');
+ /* Which rcs type radio button has been selected? */
+ // CVS
+ var rcs_types = module._rcs_types();
+ var selectedRCS = module._get_selected_rcs();
+ module.set_enabled('field.cvs_module', selectedRCS == 'CVS');
};
module.onclick_branch_type = function(e) {
- /* Which branch type radio button was selected? */
- var selectedRCS = module._get_selected_rcs();
- var types = document.getElementsByName('field.branch_type');
- var type = 'None';
- for (var i = 0; i < types.length; i++) {
- if (types[i].checked) {
- type = types[i].value;
- break;
- }
- }
- // Linked
- module.set_enabled('field.branch_location', type == 'link-lp-bzr');
- module.set_enabled('field.branch_name', type != 'link-lp-bzr');
- module.set_enabled('field.branch_owner', type != 'link-lp-bzr');
- // New, empty branch.
- // Import
- var is_external = (type == 'import-external');
- module.set_enabled('field.repo_url', is_external);
- module.set_enabled('field.cvs_module',
- (is_external & selectedRCS == 'CVS'));
- var rcs_types = module._rcs_types();
- for (var j = 0; j < rcs_types.length; j++) {
- rcs_types[j].disabled = !is_external;
- }
+ /* Which branch type radio button was selected? */
+ var selectedRCS = module._get_selected_rcs();
+ var types = document.getElementsByName('field.branch_type');
+ var type = 'None';
+ for (var i = 0; i < types.length; i++) {
+ if (types[i].checked) {
+ type = types[i].value;
+ break;
+ }
+ }
+ // Linked
+ module.set_enabled('field.branch_location', type == 'link-lp-bzr');
+ module.set_enabled('field.branch_name', type != 'link-lp-bzr');
+ module.set_enabled('field.branch_owner', type != 'link-lp-bzr');
+ // New, empty branch.
+ // Import
+ var is_external = (type == 'import-external');
+ module.set_enabled('field.repo_url', is_external);
+ module.set_enabled('field.cvs_module',
+ (is_external & selectedRCS == 'CVS'));
+ var rcs_types = module._rcs_types();
+ for (var j = 0; j < rcs_types.length; j++) {
+ rcs_types[j].disabled = !is_external;
+ }
};
+ module.renderTabs = function() {
+ var tabview = new Y.TabView({
+ srcNode: '#push-instructions-tab'
+ });
+ if (Y.one("#product-push-instructions")) {
+ console.log('exists');
+ tabview.render();
+ }
+ };
+
module.setup = function() {
- Y.all('input[name="field.rcs_type"]').on(
- 'click', module.onclick_rcs_type);
- Y.all('input[name="field.branch_type"]').on(
- 'click', module.onclick_branch_type);
-
- // Set the initial state.
- module.onclick_rcs_type();
- module.onclick_branch_type();
+ Y.all('input[name="field.rcs_type"]').on(
+ 'click', module.onclick_rcs_type);
+ Y.all('input[name="field.branch_type"]').on(
+ 'click', module.onclick_branch_type);
+
+ // Set the initial state.
+ module.onclick_rcs_type();
+ module.onclick_branch_type();
+
+ module.renderTabs();
};
- }, "0.1", {"requires": ["node", "DOM"]}
-);
+}, "0.1", {"requires": ["node", "DOM", "tabview"]});
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml 2015-05-03 08:34:51 +0000
+++ lib/lp/registry/browser/configure.zcml 2015-05-19 02:48:29 +0000
@@ -1752,6 +1752,20 @@
template="../templates/product-review-license.pt"
/>
<browser:page
+ name="+setbranch"
+ for="lp.registry.interfaces.product.IProduct"
+ class="lp.registry.browser.product.ProductSetBranchView"
+ permission="launchpad.Edit"
+ template="../templates/project-setbranch.pt"
+ />
+ <browser:page
+ name="+product-macros"
+ for="*"
+ class="lp.app.browser.launchpad.Macro"
+ permission="zope.Public"
+ template="../templates/product-macros.pt"
+ />
+ <browser:page
name="+edit"
for="lp.registry.interfaces.nameblacklist.INameBlacklist"
class="lp.registry.browser.nameblacklist.NameBlacklistEditView"
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2015-01-29 16:28:30 +0000
+++ lib/lp/registry/browser/product.py 2015-05-19 02:48:29 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Browser views for products."""
@@ -30,6 +30,7 @@
'ProductRdfView',
'ProductReviewLicenseView',
'ProductSeriesSetView',
+ 'ProductSetBranchView',
'ProductSetBreadcrumb',
'ProductSetNavigation',
'ProductSetReviewLicensesView',
@@ -44,8 +45,12 @@
from operator import attrgetter
+from bzrlib.revision import NULL_REVISION
from lazr.delegates import delegates
-from lazr.restful.interface import copy_field
+from lazr.restful.interface import (
+ copy_field,
+ use_template,
+ )
from lazr.restful.interfaces import IJSONRequestCache
from z3c.ptcompat import ViewPageTemplateFile
from zope.component import getUtility
@@ -80,6 +85,7 @@
custom_widget,
LaunchpadEditFormView,
LaunchpadFormView,
+ render_radio_widget_part,
ReturnToReferrerMixin,
safe_action,
)
@@ -103,7 +109,10 @@
PUBLIC_PROPRIETARY_INFORMATION_TYPES,
ServiceUsage,
)
-from lp.app.errors import NotFoundError
+from lp.app.errors import (
+ NotFoundError,
+ UnexpectedFormData,
+ )
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.app.utilities import json_dump_information_types
from lp.app.vocabularies import InformationTypeVocabulary
@@ -131,8 +140,27 @@
StructuralSubscriptionTargetTraversalMixin,
)
from lp.bugs.interfaces.bugtask import RESOLVED_BUGTASK_STATUSES
+from lp.code.browser.branch import BranchNameValidationMixin
from lp.code.browser.branchref import BranchRef
+from lp.code.browser.codeimport import validate_import_url
from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
+from lp.code.enums import (
+ BranchType,
+ RevisionControlSystems,
+ )
+from lp.code.errors import (
+ BranchCreationForbidden,
+ BranchExists,
+ )
+from lp.code.interfaces.branch import IBranch
+from lp.code.interfaces.branchcollection import IBranchCollection
+from lp.code.interfaces.branchjob import IRosettaUploadJobSource
+from lp.code.interfaces.branchtarget import IBranchTarget
+from lp.code.interfaces.codeimport import (
+ ICodeImport,
+ ICodeImportSet,
+ )
+from lp.code.interfaces.gitcollection import IGitCollection
from lp.registry.browser import (
add_subscribe_link,
BaseRdfView,
@@ -149,6 +177,7 @@
PillarNavigationMixin,
PillarViewMixin,
)
+from lp.registry.enums import VCSType
from lp.registry.interfaces.pillar import IPillarNameSet
from lp.registry.interfaces.product import (
IProduct,
@@ -170,6 +199,7 @@
from lp.services.fields import (
PillarAliases,
PublicPersonChoice,
+ URIField,
)
from lp.services.librarian.interfaces import ILibraryFileAliasSet
from lp.services.propertycache import cachedproperty
@@ -346,24 +376,25 @@
'configured' -- a boolean representing the configuration status.
"""
overview_menu = MenuAPI(self.context).overview
- series_menu = MenuAPI(self.context.development_focus).overview
configuration_names = [
'configure_bugtracker',
+ 'configure_translations',
'configure_answers',
- 'configure_translations',
#'configure_blueprints',
]
config_list = []
config_statuses = self.configuration_states
for key in configuration_names:
+ overview_menu[key].text = overview_menu[key].text.replace(
+ 'Configure ', '')
config_list.append(dict(link=overview_menu[key],
configured=config_statuses[key]))
# Add the branch configuration in separately.
- set_branch = series_menu['set_branch']
- set_branch.text = 'Configure project branch'
+ set_branch = overview_menu['set_branch']
+ set_branch.text = 'Code'
set_branch.summary = "Specify the location of this project's code."
- config_list.append(
+ config_list.insert(0,
dict(link=set_branch,
configured=config_statuses['configure_codehosting']))
return config_list
@@ -417,25 +448,25 @@
@enabled_with_permission('launchpad.BugSupervisor')
def configure_bugtracker(self):
- text = 'Configure bug tracker'
+ text = 'Configure Bugs'
summary = 'Specify where bugs are tracked for this project'
return Link('+configure-bugtracker', text, summary, icon='edit')
@enabled_with_permission('launchpad.TranslationsAdmin')
def configure_translations(self):
- text = 'Configure translations'
+ text = 'Configure Translations'
summary = 'Allow users to submit translations for this project'
return Link('+configure-translations', text, summary, icon='edit')
@enabled_with_permission('launchpad.Edit')
def configure_answers(self):
- text = 'Configure support tracker'
+ text = 'Configure Answers'
summary = 'Allow users to ask questions on this project'
return Link('+configure-answers', text, summary, icon='edit')
@enabled_with_permission('launchpad.Edit')
def configure_blueprints(self):
- text = 'Configure blueprints'
+ text = 'Configure Blueprints'
summary = 'Enable tracking of feature planning.'
return Link('+configure-blueprints', text, summary, icon='edit')
@@ -503,6 +534,7 @@
'packages',
'series',
'series_add',
+ 'set_branch',
'milestones',
'downloads',
'announce',
@@ -556,6 +588,19 @@
'RDF</abbr> metadata')
return Link('+rdf', text, icon='download')
+ @enabled_with_permission('launchpad.Edit')
+ def set_branch(self):
+ """Return a link to set the branch or repo for this project."""
+ if self.context.development_focus.branch is None:
+ text = 'Link to branch'
+ icon = 'add'
+ summary = 'Set the branch for this project'
+ else:
+ text = "Change branch"
+ icon = 'edit'
+ summary = 'Change the branch for this project'
+ return Link('+setbranch', text, summary, icon=icon)
+
def downloads(self):
text = 'Downloads'
return Link('+download', text, icon='info')
@@ -935,6 +980,30 @@
return self.context.license_status != LicenseStatus.OPEN_SOURCE
@property
+ def show_vcs(self):
+ """Should VCS be displayed?
+
+ Infer a project VCS for this view if a git or bzr branch
+ exist, otherwise check if vcs attribute has been set.
+ """
+ if (not IBranchCollection(self.context).is_empty() or
+ not IGitCollection(self.context).is_empty()):
+ return True
+ return bool(self.context.vcs)
+
+ @property
+ def vcs(self):
+ """Default project VCS type."""
+ vcs = None
+ if self.context.vcs:
+ return self.context.vcs
+ if not IBranchCollection(self.context).is_empty():
+ vcs = VCSType.BZR
+ if not IGitCollection(self.context).is_empty():
+ vcs = VCSType.GIT
+ return vcs
+
+ @property
def sourceforge_url(self):
if self.context.sourceforgeproject:
return ("http://sourceforge.net/projects/%s"
@@ -1591,6 +1660,328 @@
return BatchNavigator(decorated_result, self.request)
+LINK_LP_BZR = 'link-lp-bzr'
+IMPORT_EXTERNAL = 'import-external'
+
+
+BRANCH_TYPE_VOCABULARY = SimpleVocabulary((
+ SimpleTerm(LINK_LP_BZR, LINK_LP_BZR,
+ _("Link to a Bazaar branch already on Launchpad")),
+ SimpleTerm(IMPORT_EXTERNAL, IMPORT_EXTERNAL,
+ _("Import a branch hosted somewhere else")),
+ ))
+
+
+class SetBranchForm(Interface):
+ """The fields presented on the form for setting a branch."""
+
+ use_template(ICodeImport, ['cvs_module'])
+
+ default_vcs = Choice(title=_("Project VCS"),
+ required=True, vocabulary=VCSType,
+ description=_("The default version control system for this project."))
+
+ rcs_type = Choice(title=_("Type of RCS"),
+ required=False, vocabulary=RevisionControlSystems,
+ 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',
+ title=_('Branch'),
+ description=_(
+ "The Bazaar branch for this series in Launchpad, "
+ "if one exists."))
+
+ branch_type = Choice(
+ title=_('Import type'), vocabulary=BRANCH_TYPE_VOCABULARY,
+ description=_("The type of import"), required=True)
+
+ branch_name = copy_field(
+ IBranch['name'], __name__='branch_name', title=_('Branch name'),
+ description=_(''), required=True)
+
+ branch_owner = copy_field(
+ IBranch['owner'], __name__='branch_owner', title=_('Branch owner'),
+ description=_(''), required=True)
+
+
+class ProductSetBranchView(ReturnToReferrerMixin, LaunchpadFormView,
+ ProductView,
+ BranchNameValidationMixin):
+ """The view to set a branch default for the Product."""
+
+ schema = SetBranchForm
+ # Set for_input to True to ensure fields marked read-only will be editable
+ # upon creation.
+ for_input = True
+
+ custom_widget('rcs_type', LaunchpadRadioWidget)
+ custom_widget('branch_type', LaunchpadRadioWidget)
+ custom_widget('default_vcs', LaunchpadRadioWidget)
+
+ errors_in_action = False
+
+ @property
+ def series(self):
+ return self.context.development_focus
+
+ @property
+ def vcs(self):
+ return self.context.vcs
+
+ @property
+ def initial_values(self):
+ return dict(
+ rcs_type=RevisionControlSystems.BZR,
+ default_vcs=(self.vcs or VCSType.GIT),
+ branch_type=LINK_LP_BZR,
+ branch_location=self.series.branch)
+
+ @property
+ def next_url(self):
+ """Return the next_url.
+
+ Use the value from `ReturnToReferrerMixin` or None if there
+ are errors.
+ """
+ if self.errors_in_action:
+ return None
+ return super(ProductSetBranchView, self).next_url
+
+ def setUpWidgets(self):
+ """See `LaunchpadFormView`."""
+ super(ProductSetBranchView, self).setUpWidgets()
+ widget = self.widgets['rcs_type']
+ vocab = widget.vocabulary
+ current_value = widget._getFormValue()
+ self.rcs_type_cvs = render_radio_widget_part(
+ widget, vocab.CVS, current_value, 'CVS')
+ self.rcs_type_svn = render_radio_widget_part(
+ widget, vocab.BZR_SVN, current_value, 'SVN')
+ self.rcs_type_git = render_radio_widget_part(
+ widget, vocab.GIT, current_value)
+ self.rcs_type_bzr = render_radio_widget_part(
+ widget, vocab.BZR, current_value)
+ self.rcs_type_emptymarker = widget._emptyMarker()
+
+ widget = self.widgets['branch_type']
+ current_value = widget._getFormValue()
+ vocab = widget.vocabulary
+
+ (self.branch_type_link,
+ self.branch_type_import) = [
+ render_radio_widget_part(widget, value, current_value)
+ for value in (LINK_LP_BZR, IMPORT_EXTERNAL)]
+
+ widget = self.widgets['default_vcs']
+ vocab = widget.vocabulary
+ current_value = widget._getFormValue()
+ self.default_vcs_git = render_radio_widget_part(
+ widget, vocab.GIT, current_value, 'Git')
+ self.default_vcs_bzr = render_radio_widget_part(
+ widget, vocab.BZR, current_value, 'Bazaar')
+
+ def _validateLinkLpBzr(self, data):
+ """Validate data for link-lp-bzr case."""
+ if 'branch_location' not in data:
+ self.setFieldError(
+ 'branch_location', 'The branch location must be set.')
+
+ def _validateImportExternal(self, data):
+ """Validate data for import external case."""
+ rcs_type = data.get('rcs_type')
+ repo_url = data.get('repo_url')
+
+ # Private teams are forbidden from owning code imports.
+ branch_owner = data.get('branch_owner')
+ if branch_owner is not None and branch_owner.private:
+ self.setFieldError(
+ 'branch_owner', 'Private teams are forbidden from owning '
+ 'external imports.')
+
+ if repo_url is None:
+ self.setFieldError(
+ 'repo_url', 'You must set the external repository URL.')
+ else:
+ reason = validate_import_url(repo_url)
+ if reason:
+ self.setFieldError('repo_url', reason)
+
+ # RCS type is mandatory.
+ # This condition should never happen since an initial value is set.
+ if rcs_type is None:
+ # The error shows but does not identify the widget.
+ self.setFieldError(
+ 'rcs_type',
+ 'You must specify the type of RCS for the remote host.')
+ elif rcs_type == RevisionControlSystems.CVS:
+ if 'cvs_module' not in data:
+ self.setFieldError('cvs_module', 'The CVS module must be set.')
+ self._validateBranch(data)
+
+ def _validateBranch(self, data):
+ """Validate that branch name and owner are set."""
+ if 'branch_name' not in data:
+ self.setFieldError('branch_name', 'The branch name must be set.')
+ if 'branch_owner' not in data:
+ self.setFieldError('branch_owner', 'The branch owner must be set.')
+
+ def _setRequired(self, names, value):
+ """Mark the widget field as optional."""
+ for name in names:
+ widget = self.widgets[name]
+ # The 'required' property on the widget context is set to False.
+ # The widget also has a 'required' property but it isn't used
+ # during validation.
+ widget.context.required = value
+
+ def _validSchemes(self, rcs_type):
+ """Return the valid schemes for the repository URL."""
+ schemes = set(['http', 'https'])
+ # Extend the allowed schemes for the repository URL based on
+ # rcs_type.
+ extra_schemes = {
+ RevisionControlSystems.BZR_SVN: ['svn'],
+ RevisionControlSystems.GIT: ['git'],
+ RevisionControlSystems.BZR: ['bzr'],
+ }
+ schemes.update(extra_schemes.get(rcs_type, []))
+ return schemes
+
+ def validate_widgets(self, data, names=None):
+ """See `LaunchpadFormView`."""
+ names = ['branch_type', 'rcs_type', 'default_vcs']
+ super(ProductSetBranchView, self).validate_widgets(data, names)
+ branch_type = data.get('branch_type')
+ if branch_type == LINK_LP_BZR:
+ # Mark other widgets as non-required.
+ self._setRequired(['rcs_type', 'repo_url', 'cvs_module',
+ 'branch_name', 'branch_owner'], False)
+ elif branch_type == IMPORT_EXTERNAL:
+ rcs_type = data.get('rcs_type')
+
+ # Set the valid schemes based on rcs_type.
+ self.widgets['repo_url'].field.allowed_schemes = (
+ self._validSchemes(rcs_type))
+ # The branch location is not required for validation.
+ self._setRequired(['branch_location'], False)
+ # The cvs_module is required if it is a CVS import.
+ if rcs_type == RevisionControlSystems.CVS:
+ self._setRequired(['cvs_module'], True)
+ else:
+ raise AssertionError("Unknown branch type %s" % branch_type)
+ # Perform full validation now.
+ super(ProductSetBranchView, self).validate_widgets(data)
+
+ def validate(self, data):
+ """See `LaunchpadFormView`."""
+ # If widget validation returned errors then there is no need to
+ # continue as we'd likely just override the errors reported there.
+ if len(self.errors) > 0:
+ return
+ branch_type = data['branch_type']
+ if branch_type == IMPORT_EXTERNAL:
+ self._validateImportExternal(data)
+ elif branch_type == LINK_LP_BZR:
+ self._validateLinkLpBzr(data)
+ else:
+ raise AssertionError("Unknown branch type %s" % branch_type)
+
+ @property
+ def target(self):
+ """The branch target for the context."""
+ return IBranchTarget(self.context)
+
+ @action(_('Update'), name='update')
+ def update_action(self, action, data):
+ branch_type = data.get('branch_type')
+ default_vcs = data.get('default_vcs')
+
+ if default_vcs:
+ self.context.vcs = default_vcs
+ if branch_type == LINK_LP_BZR:
+ branch_location = data.get('branch_location')
+ if branch_location != self.series.branch:
+ self.series.branch = branch_location
+ # Request an initial upload of translation files.
+ getUtility(IRosettaUploadJobSource).create(
+ self.series.branch, NULL_REVISION)
+ else:
+ self.series.branch = branch_location
+ self.request.response.addInfoNotification(
+ 'Series code location updated.')
+ else:
+ branch_name = data.get('branch_name')
+ branch_owner = data.get('branch_owner')
+
+ if branch_type == IMPORT_EXTERNAL:
+ rcs_type = data.get('rcs_type')
+ if rcs_type == RevisionControlSystems.CVS:
+ cvs_root = data.get('repo_url')
+ cvs_module = data.get('cvs_module')
+ url = None
+ else:
+ cvs_root = None
+ cvs_module = None
+ url = data.get('repo_url')
+ rcs_item = RevisionControlSystems.items[rcs_type.name]
+ try:
+ code_import = getUtility(ICodeImportSet).new(
+ owner=branch_owner,
+ registrant=self.user,
+ target=IBranchTarget(self.target),
+ branch_name=branch_name,
+ rcs_type=rcs_item,
+ url=url,
+ cvs_root=cvs_root,
+ cvs_module=cvs_module)
+ except BranchExists as e:
+ self._setBranchExists(e.existing_branch, 'branch_name')
+ self.errors_in_action = True
+ # Abort transaction. This is normally handled
+ # by LaunchpadFormView, but we are already in
+ # the success handler.
+ self._abort()
+ return
+ self.series.branch = code_import.branch
+ self.request.response.addInfoNotification(
+ 'Code import created and branch linked to the series.')
+ else:
+ raise UnexpectedFormData(branch_type)
+
+ def _createBzrBranch(self, branch_name, branch_owner, repo_url=None):
+ """Create a new hosted Bazaar branch.
+
+ Return the branch on success or None.
+ """
+ branch = None
+ try:
+ namespace = self.target.getNamespace(branch_owner)
+ branch = namespace.createBranch(
+ branch_type=BranchType.HOSTED, name=branch_name,
+ registrant=self.user, url=repo_url)
+ except BranchCreationForbidden:
+ self.addError(
+ "You are not allowed to create branches in %s." %
+ self.context.displayname)
+ except BranchExists as e:
+ self._setBranchExists(e.existing_branch, 'branch_name')
+ if branch is None:
+ self.errors_in_action = True
+ # Abort transaction. This is normally handled by
+ # LaunchpadFormView, but we are already in the success handler.
+ self._abort()
+ return branch
+
+
class ProductRdfView(BaseRdfView):
"""A view that sets its mime-type to application/rdf+xml"""
=== modified file 'lib/lp/registry/browser/productseries.py'
--- lib/lp/registry/browser/productseries.py 2014-12-06 00:16:55 +0000
+++ lib/lp/registry/browser/productseries.py 2015-05-19 02:48:29 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""View classes for `IProductSeries`."""
@@ -28,11 +28,6 @@
from operator import attrgetter
-from bzrlib.revision import NULL_REVISION
-from lazr.restful.interface import (
- copy_field,
- use_template,
- )
from z3c.ptcompat import ViewPageTemplateFile
from zope.component import getUtility
from zope.formlib import form
@@ -57,17 +52,11 @@
custom_widget,
LaunchpadEditFormView,
LaunchpadFormView,
- render_radio_widget_part,
- ReturnToReferrerMixin,
)
from lp.app.browser.tales import MenuAPI
from lp.app.enums import ServiceUsage
-from lp.app.errors import (
- NotFoundError,
- UnexpectedFormData,
- )
+from lp.app.errors import NotFoundError
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
-from lp.app.widgets.itemswidgets import LaunchpadRadioWidget
from lp.app.widgets.textwidgets import StrippedTextWidget
from lp.blueprints.browser.specificationtarget import (
HasSpecificationsMenuMixin,
@@ -81,24 +70,9 @@
StructuralSubscriptionTargetTraversalMixin,
)
from lp.bugs.interfaces.bugtask import IBugTaskSet
-from lp.code.browser.branch import BranchNameValidationMixin
from lp.code.browser.branchref import BranchRef
-from lp.code.browser.codeimport import validate_import_url
-from lp.code.enums import (
- BranchType,
- RevisionControlSystems,
- )
-from lp.code.errors import (
- BranchCreationForbidden,
- BranchExists,
- )
-from lp.code.interfaces.branch import IBranch
-from lp.code.interfaces.branchjob import IRosettaUploadJobSource
+from lp.code.enums import RevisionControlSystems
from lp.code.interfaces.branchtarget import IBranchTarget
-from lp.code.interfaces.codeimport import (
- ICodeImport,
- ICodeImportSet,
- )
from lp.registry.browser import (
add_subscribe_link,
BaseRdfView,
@@ -110,6 +84,7 @@
InvolvedMenu,
PillarInvolvementView,
)
+from lp.registry.browser.product import ProductSetBranchView
from lp.registry.errors import CannotPackageProprietaryProduct
from lp.registry.interfaces.packaging import (
IPackaging,
@@ -117,7 +92,6 @@
)
from lp.registry.interfaces.productseries import IProductSeries
from lp.registry.interfaces.series import SeriesStatus
-from lp.services.fields import URIField
from lp.services.propertycache import cachedproperty
from lp.services.webapp import (
ApplicationMenu,
@@ -756,300 +730,22 @@
self.next_url = canonical_url(product)
-LINK_LP_BZR = 'link-lp-bzr'
-IMPORT_EXTERNAL = 'import-external'
-
-
-BRANCH_TYPE_VOCABULARY = SimpleVocabulary((
- SimpleTerm(LINK_LP_BZR, LINK_LP_BZR,
- _("Link to a Bazaar branch already on Launchpad")),
- SimpleTerm(IMPORT_EXTERNAL, IMPORT_EXTERNAL,
- _("Import a branch hosted somewhere else")),
- ))
-
-
-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=RevisionControlSystems,
- 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',
- title=_('Branch'),
- description=_(
- "The Bazaar branch for this series in Launchpad, "
- "if one exists."))
-
- branch_type = Choice(
- title=_('Import type'), vocabulary=BRANCH_TYPE_VOCABULARY,
- description=_("The type of import"), required=True)
-
- branch_name = copy_field(
- IBranch['name'], __name__='branch_name', title=_('Branch name'),
- description=_(''), required=True)
-
- branch_owner = copy_field(
- IBranch['owner'], __name__='branch_owner', title=_('Branch owner'),
- description=_(''), required=True)
-
-
-class ProductSeriesSetBranchView(ReturnToReferrerMixin, LaunchpadFormView,
- ProductSeriesView,
- BranchNameValidationMixin):
+class ProductSeriesSetBranchView(ProductSetBranchView, ProductSeriesView):
"""The view to set a branch for the ProductSeries."""
- schema = SetBranchForm
- # Set for_input to True to ensure fields marked read-only will be editable
- # upon creation.
- for_input = True
-
- custom_widget('rcs_type', LaunchpadRadioWidget)
- custom_widget('branch_type', LaunchpadRadioWidget)
-
- errors_in_action = False
-
- @property
- def initial_values(self):
- return dict(
- rcs_type=RevisionControlSystems.BZR,
- branch_type=LINK_LP_BZR,
- branch_location=self.context.branch)
-
- @property
- def next_url(self):
- """Return the next_url.
-
- Use the value from `ReturnToReferrerMixin` or None if there
- are errors.
- """
- if self.errors_in_action:
- return None
- return super(ProductSeriesSetBranchView, self).next_url
-
- def setUpWidgets(self):
- """See `LaunchpadFormView`."""
- super(ProductSeriesSetBranchView, self).setUpWidgets()
- widget = self.widgets['rcs_type']
- vocab = widget.vocabulary
- current_value = widget._getFormValue()
- self.rcs_type_cvs = render_radio_widget_part(
- widget, vocab.CVS, current_value, 'CVS')
- self.rcs_type_svn = render_radio_widget_part(
- widget, vocab.BZR_SVN, current_value, 'SVN')
- self.rcs_type_git = render_radio_widget_part(
- widget, vocab.GIT, current_value)
- self.rcs_type_bzr = render_radio_widget_part(
- widget, vocab.BZR, current_value)
- self.rcs_type_emptymarker = widget._emptyMarker()
-
- widget = self.widgets['branch_type']
- current_value = widget._getFormValue()
- vocab = widget.vocabulary
-
- (self.branch_type_link,
- self.branch_type_import) = [
- render_radio_widget_part(widget, value, current_value)
- for value in (LINK_LP_BZR, IMPORT_EXTERNAL)]
-
- def _validateLinkLpBzr(self, data):
- """Validate data for link-lp-bzr case."""
- if 'branch_location' not in data:
- self.setFieldError(
- 'branch_location', 'The branch location must be set.')
-
- def _validateImportExternal(self, data):
- """Validate data for import external case."""
- rcs_type = data.get('rcs_type')
- repo_url = data.get('repo_url')
-
- # Private teams are forbidden from owning code imports.
- branch_owner = data.get('branch_owner')
- if branch_owner is not None and branch_owner.private:
- self.setFieldError(
- 'branch_owner', 'Private teams are forbidden from owning '
- 'external imports.')
-
- if repo_url is None:
- self.setFieldError(
- 'repo_url', 'You must set the external repository URL.')
- else:
- reason = validate_import_url(repo_url)
- if reason:
- self.setFieldError('repo_url', reason)
-
- # RCS type is mandatory.
- # This condition should never happen since an initial value is set.
- if rcs_type is None:
- # The error shows but does not identify the widget.
- self.setFieldError(
- 'rcs_type',
- 'You must specify the type of RCS for the remote host.')
- elif rcs_type == RevisionControlSystems.CVS:
- if 'cvs_module' not in data:
- self.setFieldError('cvs_module', 'The CVS module must be set.')
- self._validateBranch(data)
-
- def _validateBranch(self, data):
- """Validate that branch name and owner are set."""
- if 'branch_name' not in data:
- self.setFieldError('branch_name', 'The branch name must be set.')
- if 'branch_owner' not in data:
- self.setFieldError('branch_owner', 'The branch owner must be set.')
-
- def _setRequired(self, names, value):
- """Mark the widget field as optional."""
- for name in names:
- widget = self.widgets[name]
- # The 'required' property on the widget context is set to False.
- # The widget also has a 'required' property but it isn't used
- # during validation.
- widget.context.required = value
-
- def _validSchemes(self, rcs_type):
- """Return the valid schemes for the repository URL."""
- schemes = set(['http', 'https'])
- # Extend the allowed schemes for the repository URL based on
- # rcs_type.
- extra_schemes = {
- RevisionControlSystems.BZR_SVN: ['svn'],
- RevisionControlSystems.GIT: ['git'],
- RevisionControlSystems.BZR: ['bzr'],
- }
- schemes.update(extra_schemes.get(rcs_type, []))
- return schemes
-
- def validate_widgets(self, data, names=None):
- """See `LaunchpadFormView`."""
- names = ['branch_type', 'rcs_type']
- super(ProductSeriesSetBranchView, self).validate_widgets(data, names)
- branch_type = data.get('branch_type')
- if branch_type == LINK_LP_BZR:
- # Mark other widgets as non-required.
- self._setRequired(['rcs_type', 'repo_url', 'cvs_module',
- 'branch_name', 'branch_owner'], False)
- elif branch_type == IMPORT_EXTERNAL:
- rcs_type = data.get('rcs_type')
-
- # Set the valid schemes based on rcs_type.
- self.widgets['repo_url'].field.allowed_schemes = (
- self._validSchemes(rcs_type))
- # The branch location is not required for validation.
- self._setRequired(['branch_location'], False)
- # The cvs_module is required if it is a CVS import.
- if rcs_type == RevisionControlSystems.CVS:
- self._setRequired(['cvs_module'], True)
- else:
- raise AssertionError("Unknown branch type %s" % branch_type)
- # Perform full validation now.
- super(ProductSeriesSetBranchView, self).validate_widgets(data)
-
- def validate(self, data):
- """See `LaunchpadFormView`."""
- # If widget validation returned errors then there is no need to
- # continue as we'd likely just override the errors reported there.
- if len(self.errors) > 0:
- return
- branch_type = data['branch_type']
- if branch_type == IMPORT_EXTERNAL:
- self._validateImportExternal(data)
- elif branch_type == LINK_LP_BZR:
- self._validateLinkLpBzr(data)
- else:
- raise AssertionError("Unknown branch type %s" % branch_type)
+ @property
+ def series(self):
+ return self.context
@property
def target(self):
"""The branch target for the context."""
return IBranchTarget(self.context.product)
- @action(_('Update'), name='update')
- def update_action(self, action, data):
- branch_type = data.get('branch_type')
- if branch_type == LINK_LP_BZR:
- branch_location = data.get('branch_location')
- if branch_location != self.context.branch:
- self.context.branch = branch_location
- # Request an initial upload of translation files.
- getUtility(IRosettaUploadJobSource).create(
- self.context.branch, NULL_REVISION)
- else:
- self.context.branch = branch_location
- self.request.response.addInfoNotification(
- 'Series code location updated.')
- else:
- branch_name = data.get('branch_name')
- branch_owner = data.get('branch_owner')
-
- if branch_type == IMPORT_EXTERNAL:
- rcs_type = data.get('rcs_type')
- if rcs_type == RevisionControlSystems.CVS:
- cvs_root = data.get('repo_url')
- cvs_module = data.get('cvs_module')
- url = None
- else:
- cvs_root = None
- cvs_module = None
- url = data.get('repo_url')
- rcs_item = RevisionControlSystems.items[rcs_type.name]
- try:
- code_import = getUtility(ICodeImportSet).new(
- owner=branch_owner,
- registrant=self.user,
- target=IBranchTarget(self.context.product),
- branch_name=branch_name,
- rcs_type=rcs_item,
- url=url,
- cvs_root=cvs_root,
- cvs_module=cvs_module)
- except BranchExists as e:
- self._setBranchExists(e.existing_branch, 'branch_name')
- self.errors_in_action = True
- # Abort transaction. This is normally handled
- # by LaunchpadFormView, but we are already in
- # the success handler.
- self._abort()
- return
- self.context.branch = code_import.branch
- self.request.response.addInfoNotification(
- 'Code import created and branch linked to the series.')
- else:
- raise UnexpectedFormData(branch_type)
-
- def _createBzrBranch(self, branch_name, branch_owner, repo_url=None):
- """Create a new hosted Bazaar branch.
-
- Return the branch on success or None.
- """
- branch = None
- try:
- namespace = self.target.getNamespace(branch_owner)
- branch = namespace.createBranch(
- branch_type=BranchType.HOSTED, name=branch_name,
- registrant=self.user, url=repo_url)
- except BranchCreationForbidden:
- self.addError(
- "You are not allowed to create branches in %s." %
- self.context.displayname)
- except BranchExists as e:
- self._setBranchExists(e.existing_branch, 'branch_name')
- if branch is None:
- self.errors_in_action = True
- # Abort transaction. This is normally handled by
- # LaunchpadFormView, but we are already in the success handler.
- self._abort()
- return branch
+ @property
+ def vcs(self):
+ """The default vcs for the product."""
+ return self.context.product.vcs
class ProductSeriesReviewView(LaunchpadEditFormView):
=== modified file 'lib/lp/registry/browser/tests/test_product.py'
--- lib/lp/registry/browser/tests/test_product.py 2014-06-19 06:38:53 +0000
+++ lib/lp/registry/browser/tests/test_product.py 2015-05-19 02:48:29 +0000
@@ -34,6 +34,7 @@
from lp.registry.enums import (
EXCLUSIVE_TEAM_POLICY,
TeamMembershipPolicy,
+ VCSType,
)
from lp.registry.interfaces.product import (
IProductSet,
@@ -309,6 +310,12 @@
view = create_initialized_view(self.product, '+index')
self.assertTrue(view.show_programming_languages)
+ def test_show_default_vcs(self):
+ with person_logged_in(self.product.owner):
+ self.product.vcs = VCSType.GIT
+ view = create_initialized_view(self.product, '+index')
+ self.assertTrue(view.show_vcs)
+
def test_show_license_info_without_other_license(self):
# show_license_info is false when one of the "other" licences is
# not selected.
=== modified file 'lib/lp/registry/stories/product/xx-product-development-focus.txt'
--- lib/lp/registry/stories/product/xx-product-development-focus.txt 2012-11-08 03:55:11 +0000
+++ lib/lp/registry/stories/product/xx-product-development-focus.txt 2015-05-19 02:48:29 +0000
@@ -73,14 +73,14 @@
Change details
(http://launchpad.dev/fooix/+edit)
>>> print_involvement_portlet(owner_browser)
- Configure bug tracker
+ Code
+ http://launchpad.dev/fooix/+setbranch
+ Bugs
http://launchpad.dev/fooix/+configure-bugtracker
- Configure support tracker
+ Translations
+ http://launchpad.dev/fooix/+configure-translations
+ Answers
http://launchpad.dev/fooix/+configure-answers
- Configure translations
- http://launchpad.dev/fooix/+configure-translations
- Configure project branch
- http://launchpad.dev/fooix/trunk/+setbranch
The owner can specify the development focus branch from the overview page.
=== added file 'lib/lp/registry/stories/product/xx-product-set-branch.txt'
--- lib/lp/registry/stories/product/xx-product-set-branch.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/stories/product/xx-product-set-branch.txt 2015-05-19 02:48:29 +0000
@@ -0,0 +1,136 @@
+Setting the branch for a product
+=======================================
+
+A product should have a branch and default vcs set for it. The branch can be
+hosted on Launchpad or somewhere else. Foreign branches can be in
+Bazaar, Git, Subversion, or CVS. Though internally Launchpad treats those
+scenarios differently we provide a single page to the user to set up the
+branch.
+
+At present, the unified page for setting up the branch is not linked
+from anywhere, so it must be navigated to directly.
+
+ >>> browser = setupBrowser(auth="Basic test@xxxxxxxxxxxxx:test")
+ >>> browser.open('http://launchpad.dev/firefox/+setbranch')
+
+The default choice for the type of branch to set is one that
+already exists on Launchpad.
+
+ >>> print_radio_button_field(browser.contents, 'branch_type')
+ (*) Link to a Bazaar branch already on Launchpad
+ ( ) Import a branch hosted somewhere else
+
+
+Linking to an existing branch
+-----------------------------
+
+A user can choose to link to an existing branch on Launchpad.
+
+ >>> login('test@xxxxxxxxxxxxx')
+ >>> from zope.component import getUtility
+ >>> from lp.registry.interfaces.product import IProductSet
+ >>> productset = getUtility(IProductSet)
+ >>> firefox = productset.getByName('firefox')
+ >>> branch = factory.makeBranch(
+ ... name="firefox-hosted-branch", product=firefox)
+ >>> branch_name = branch.unique_name
+ >>> logout()
+
+ >>> browser.getControl(name='field.branch_location').value = branch_name
+ >>> browser.getControl('Update').click()
+ >>> print_feedback_messages(browser.contents)
+ Series code location updated.
+ >>> print browser.url
+ http://launchpad.dev/firefox
+
+
+Setting a default vcstype
+-------------------------
+
+A user can select a vcs type for the project.
+
+ >>> browser.open('http://launchpad.dev/firefox/+setbranch')
+ >>> print_radio_button_field(browser.contents, 'default_vcs')
+ (*) Git
+ ( ) Bazaar
+
+ >>> browser.getControl('Update').click()
+ >>> '<dl id="product-vcs">' in browser.contents
+ True
+ >>> '<dd>Git</dd>' in browser.contents
+ True
+ >>> print browser.url
+ http://launchpad.dev/firefox
+
+
+Linking to an external branch
+-----------------------------
+
+An external branch can be linked. The branch can be a Bazaar branch
+or be a Git, Subversion, or CVS branch.
+
+Each of these types must provide the URL of the external repository,
+the branch name to use in Launchpad, and the branch owner.
+
+ >>> browser.open('http://launchpad.dev/firefox/+setbranch')
+ >>> browser.getControl('Import a branch hosted somewhere else').click()
+ >>> browser.getControl('Branch name').value = 'bzr-firefox-branch'
+ >>> browser.getControl('Bazaar', index=0).click()
+ >>> browser.getControl('Branch URL').value = (
+ ... 'https://bzr.example.com/branch')
+ >>> browser.getControl('Update').click()
+ >>> print_feedback_messages(browser.contents)
+ Code import created and branch linked to the series.
+ >>> print browser.url
+ http://launchpad.dev/firefox
+
+The process is the same for a Git external branch, though the novel
+"git://" scheme can also be used.
+
+ >>> browser.open('http://launchpad.dev/firefox/+setbranch')
+ >>> browser.getControl('Import a branch hosted somewhere else').click()
+ >>> browser.getControl('Branch name').value = 'git-firefox-branch'
+ >>> browser.getControl('Git', index=1).click()
+ >>> browser.getControl('Branch URL').value = (
+ ... 'git://git.example.com/branch')
+ >>> browser.getControl('Update').click()
+ >>> print_feedback_messages(browser.contents)
+ Code import created and branch linked to the series.
+ >>> print browser.url
+ http://launchpad.dev/firefox
+
+Likewise Subversion can use the "svn://" scheme.
+
+ >>> browser.open('http://launchpad.dev/firefox/+setbranch')
+ >>> browser.getControl('Import a branch hosted somewhere else').click()
+ >>> browser.getControl('Branch name').value = 'svn-firefox-branch'
+ >>> browser.getControl('SVN').click()
+ >>> browser.getControl('Branch URL').value = (
+ ... 'svn://svn.example.com/branch')
+ >>> browser.getControl('Update').click()
+ >>> print_feedback_messages(browser.contents)
+ Code import created and branch linked to the series.
+ >>> print browser.url
+ http://launchpad.dev/firefox
+
+The branch owner can be the logged in user or one of her teams.
+
+ >>> browser.open('http://launchpad.dev/firefox/+setbranch')
+ >>> browser.getControl('Import a branch hosted somewhere else').click()
+ >>> browser.getControl('Branch name').value = 'git-firefox-branch'
+ >>> browser.getControl('Git', index=1).click()
+ >>> browser.getControl('Branch URL').value = (
+ ... 'http://git.example.com/branch')
+ >>> browser.getControl('Branch owner').value = ['hwdb-team']
+ >>> browser.getControl('Update').click()
+ >>> print_feedback_messages(browser.contents)
+ Code import created and branch linked to the series.
+ >>> print browser.url
+ http://launchpad.dev/firefox
+ >>> login('test@xxxxxxxxxxxxx')
+ >>> firefox_trunk = firefox.getSeries('trunk')
+ >>> print firefox_trunk.branch.unique_name
+ ~hwdb-team/firefox/git-firefox-branch
+ >>> print firefox_trunk.branch.owner.name
+ hwdb-team
+ >>> logout()
=== modified file 'lib/lp/registry/templates/product-index.pt'
--- lib/lp/registry/templates/product-index.pt 2015-01-29 16:28:30 +0000
+++ lib/lp/registry/templates/product-index.pt 2015-05-19 02:48:29 +0000
@@ -129,6 +129,11 @@
<dd tal:content="structure view/languages_edit_widget" />
</dl>
+ <dl id="product-vcs" tal:condition="view/show_vcs">
+ <dt>Version Control System:</dt>
+ <dd tal:content="view/vcs/title">Git</dd>
+ </dl>
+
<dl id="licences">
<dt>Licences:</dt>
<dd>
=== added file 'lib/lp/registry/templates/product-macros.pt'
--- lib/lp/registry/templates/product-macros.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/templates/product-macros.pt 2015-05-19 02:48:29 +0000
@@ -0,0 +1,42 @@
+<tal:root
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ omit-tag="">
+
+ <div metal:define-macro="push-instructions" id="product-push-instructions" class="yui3-skin-sam">
+ <div id="push-instructions-tab" class="yui3-tabview">
+ <ul>
+ <li><a href="#push-instructions-git"><i class="sprite gitbranch"></i>git</a></li>
+ <li><a href="#push-instructions-bzr"><i class="sprite branch"></i>bzr</a></li>
+ </ul>
+ <div>
+ <div id="push-instructions-git" class="scm-tip">
+ <p>You can add a remote for your git branch with the command:</p>
+ <code class="command command-block">
+ git remote add origin git+ssh://<tal:user replace="view/user/name"/>@git.launchpad.net/~<tal:user replace="view/user/name"/>/<tal:project replace="context/name"/>/master<br />
+ </code>
+ <p>… and push the git branch to Launchpad with:</p>
+ <code class="command command-block">
+ git push origin master
+ </code>
+ </div>
+ <div id="#push-instructions-bzr" class="scm-tip">
+ <p>You can push a Bazaar branch directly to Launchpad with the command:</p>
+ <p>
+ <code class="command command-block">
+ bzr push lp:~<tal:user replace="view/user/name"/>/<tal:project replace="context/name"/>/<tal:series replace="context/development_focus/name"/>
+ </code>
+ </p>
+ </div>
+ </div>
+
+ <tal:no-keys condition="not:view/user/sshkeys">
+ <p>To authenticate with the Launchpad branch upload service,
+ you need to
+ <a tal:attributes="href string:${view/user/fmt:url}/+editsshkeys">
+ register a SSH key</a>.</p>
+ </tal:no-keys>
+ </div>
+ </div>
+</tal:root>
=== modified file 'lib/lp/registry/templates/productseries-setbranch.pt'
--- lib/lp/registry/templates/productseries-setbranch.pt 2015-05-01 13:18:54 +0000
+++ lib/lp/registry/templates/productseries-setbranch.pt 2015-05-19 02:48:29 +0000
@@ -56,6 +56,14 @@
Import a branch hosted somewhere else
</label>
<table class="subordinate">
+
+ <tal:widget define="widget nocall:view/widgets/branch_name">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/branch_owner">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+
<tal:widget define="widget nocall:view/widgets/repo_url">
<metal:block use-macro="context/@@launchpad_form/widget_row" />
</tal:widget>
@@ -101,13 +109,6 @@
</td>
</tr>
- <tal:widget define="widget nocall:view/widgets/branch_name">
- <metal:block use-macro="context/@@launchpad_form/widget_row" />
- </tal:widget>
- <tal:widget define="widget nocall:view/widgets/branch_owner">
- <metal:block use-macro="context/@@launchpad_form/widget_row" />
- </tal:widget>
-
</table>
<input tal:replace="structure view/rcs_type_emptymarker" />
=== added file 'lib/lp/registry/templates/project-setbranch.pt'
--- lib/lp/registry/templates/project-setbranch.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/templates/project-setbranch.pt 2015-05-19 02:48:29 +0000
@@ -0,0 +1,137 @@
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ metal:use-macro="view/macro:page/main_only"
+ i18n:domain="launchpad">
+
+<body>
+
+<metal:block fill-slot="head_epilogue">
+ <style type="text/css">
+ .subordinate {
+ margin: 0.5em 0 0.5em 4em;
+ }
+ </style>
+</metal:block>
+
+<div metal:fill-slot="main">
+
+ <div metal:use-macro="context/@@launchpad_form/form">
+
+ <metal:formbody fill-slot="widgets">
+
+ <div class="push-instructions">
+ <h3>Push Instructions</h3>
+ <div metal:use-macro="context/@@+product-macros/push-instructions"></div>
+ </div>
+ <table class="form">
+ <tr>
+ <td>
+ <h3>Version Control System</h3>
+ <p>Please select a default vcs for your project:</p>
+ <ul>
+ <li>
+ <label tal:replace="structure view/default_vcs_git">
+ Git
+ </label>
+ </li>
+ <li>
+ <label tal:replace="structure view/default_vcs_bzr">
+ Bazaar
+ </label>
+ </li>
+ </ul>
+ <p><span class="formHelp">Your project may still have both
+ git repositories and bazaar branches.</span></p>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <h3>Branch Link/Import</h3>
+ <label tal:replace="structure view/branch_type_link">
+ Link to a Bazaar branch already in Launchpad
+ </label>
+ <table class="subordinate">
+ <tal:widget define="widget nocall:view/widgets/branch_location">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ </table>
+ </td>
+ </tr>
+
+ <tr>
+ <td>
+ <label tal:replace="structure view/branch_type_import">
+ Import a branch hosted somewhere else
+ </label>
+ <table class="subordinate">
+
+ <tal:widget define="widget nocall:view/widgets/branch_name">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/branch_owner">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+
+ <tal:widget define="widget nocall:view/widgets/repo_url">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+
+ <tr>
+ <td>
+ <label tal:replace="structure view/rcs_type_bzr">
+ Bazaar, hosted externally
+ </label>
+ </td>
+ </tr>
+
+ <tr>
+ <td>
+ <label tal:replace="structure view/rcs_type_git">
+ Git
+ </label>
+ </td>
+ </tr>
+
+ <tr>
+ <td>
+ <label tal:replace="structure view/rcs_type_svn">
+ SVN
+ </label>
+ </td>
+ </tr>
+
+ <tr>
+ <td>
+ <label tal:replace="structure view/rcs_type_cvs">
+ CVS
+ </label>
+ <table class="subordinate">
+ <tal:widget define="widget nocall:view/widgets/cvs_module">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ </table>
+ </td>
+ </tr>
+
+ </table>
+ </td>
+ </tr>
+
+ </table>
+ <input tal:replace="structure view/rcs_type_emptymarker" />
+
+ </metal:formbody>
+ </div>
+
+ <script type="text/javascript">
+ LPJS.use('lp.code.productseries_setbranch', function(Y) {
+ Y.on('domready', Y.lp.code.productseries_setbranch.setup);
+ });
+ </script>
+
+</div>
+</body>
+</html>
=== modified file 'lib/lp/translations/stories/productseries/xx-productseries-translations.txt'
--- lib/lp/translations/stories/productseries/xx-productseries-translations.txt 2014-11-24 09:16:35 +0000
+++ lib/lp/translations/stories/productseries/xx-productseries-translations.txt 2015-05-19 02:48:29 +0000
@@ -181,11 +181,11 @@
Launchpad allows communities to translate projects using
imports or a branch.
Getting started with translating your project in Launchpad
- Configure translations
+ Configure Translations
The notice links to the page for configuring translations on the project.
- >>> owner_browser.getLink('Configure translations').click()
+ >>> owner_browser.getLink('Configure Translations').click()
>>> print owner_browser.url
http://.../bazaar/+configure-translations
@@ -204,7 +204,7 @@
Launchpad allows communities to translate projects using
imports or a branch.
Getting started with translating your project in Launchpad
- Configure translations
+ Configure Translations
A Translations admin who is neither a Launchpad admin nor the project
owner (and so won't be able to change the project's settings) sees the
=== modified file 'lib/lp/translations/stories/standalone/xx-product-translations.txt'
--- lib/lp/translations/stories/standalone/xx-product-translations.txt 2014-11-27 22:13:36 +0000
+++ lib/lp/translations/stories/standalone/xx-product-translations.txt 2015-05-19 02:48:29 +0000
@@ -53,7 +53,7 @@
Launchpad allows communities to translate projects using imports or a
branch.
Getting started with translating your project in Launchpad
- Configure translations
+ Configure Translations
>>> registrant.getLink(
... url=('/gnomebaker/trunk/'
@@ -64,7 +64,7 @@
configuration page, where they can configure the project to use
Launchpad for translations if desired.
- >>> registrant.getLink('Configure translations').click()
+ >>> registrant.getLink('Configure Translations').click()
>>> print registrant.url
http://.../gnomebaker/+configure-translations
@@ -133,7 +133,7 @@
If the netapplet project is updated to use Launchpad for translations...
>>> admin_browser.open('http://launchpad.dev/netapplet')
- >>> admin_browser.getLink('Configure translations').click()
+ >>> admin_browser.getLink('Translations', index=1).click()
>>> print_radio_button_field(admin_browser.contents, "translations_usage")
(*) Unknown
( ) Launchpad
@@ -144,9 +144,9 @@
...there are no longer any obsolete entries.
- >>> admin_browser.getLink('Translations').click()
+ >>> admin_browser.getLink('Translations', index=1).click()
>>> print admin_browser.title
- Translations : NetApplet
+ Configure translations : Translations : NetApplet
>>> print find_tag_by_id(admin_browser.contents,
... 'portlet-obsolete-translatable-series')
None
@@ -230,5 +230,5 @@
>>> notice = first_tag_by_class(admin_browser.contents, 'notice')
>>> print extract_text(notice)
Getting started with translating your project in Launchpad
- Configure translations
+ Configure Translations
There are no translations for this project.
=== modified file 'lib/lp/translations/stories/translationfocus/xx-product-translationfocus.txt'
--- lib/lp/translations/stories/translationfocus/xx-product-translationfocus.txt 2011-12-08 18:47:23 +0000
+++ lib/lp/translations/stories/translationfocus/xx-product-translationfocus.txt 2015-05-19 02:48:29 +0000
@@ -18,7 +18,7 @@
>>> print extract_text(
... find_tags_by_class(admin_browser.contents,
... 'menu-link-configure_translations')[0])
- Configure translations
+ Configure Translations
>>> browser.open(fooproject_url)
>>> print extract_text(
=== modified file 'lib/lp/translations/stories/translationgroups/xx-change-translation-policy.txt'
--- lib/lp/translations/stories/translationgroups/xx-change-translation-policy.txt 2012-01-15 13:32:27 +0000
+++ lib/lp/translations/stories/translationgroups/xx-change-translation-policy.txt 2015-05-19 02:48:29 +0000
@@ -23,13 +23,13 @@
>>> re_browser.open(
... 'http://translations.launchpad.dev/chestii')
- >>> re_browser.getLink('Configure translations').click()
+ >>> re_browser.getLink('Configure Translations').click()
>>> print re_browser.url
http://translations.launchpad.dev/chestii/+configure-translations
>>> po_browser.open(
... 'http://translations.launchpad.dev/chestii')
- >>> po_browser.getLink('Configure translations').click()
+ >>> po_browser.getLink('Configure Translations').click()
>>> print po_browser.url
http://translations.launchpad.dev/chestii/+configure-translations
@@ -57,7 +57,7 @@
>>> dtc_browser.open(
... 'http://translations.launchpad.dev/chestii')
- >>> dtc_browser.getLink('Configure translations')
+ >>> dtc_browser.getLink('Configure Translations')
Traceback (most recent call last):
...
LinkNotFoundError...
=== modified file 'lib/lp/translations/stories/translationgroups/xx-translationgroups.txt'
--- lib/lp/translations/stories/translationgroups/xx-translationgroups.txt 2014-02-19 02:11:16 +0000
+++ lib/lp/translations/stories/translationgroups/xx-translationgroups.txt 2015-05-19 02:48:29 +0000
@@ -263,7 +263,7 @@
Other users cannot access this page, nor see the menu link to it.
>>> user_browser.open(anon_browser.url)
- >>> user_browser.getLink('Configure translations').click()
+ >>> user_browser.getLink('Configure Translations').click()
Traceback (most recent call last):
...
LinkNotFoundError
@@ -315,7 +315,7 @@
... auth='Basic test@xxxxxxxxxxxxx:test')
>>> netapplet_owner_browser.open('http://launchpad.dev/netapplet')
>>> netapplet_owner_browser.getLink(
- ... 'Configure translations').click()
+ ... 'Translations', index=1).click()
>>> print netapplet_owner_browser.title
Configure translations : Translations : NetApplet
@@ -341,7 +341,7 @@
group and permissions.
>>> translations_page_url = netapplet_owner_browser.url
- >>> netapplet_owner_browser.getLink('Configure translations').click()
+ >>> netapplet_owner_browser.getLink('Configure Translations').click()
>>> change_translators_url = netapplet_owner_browser.url
>>> print netapplet_owner_browser.title
@@ -356,11 +356,11 @@
... 'Translation group').displayValue
['(nothing selected)']
-Ordinary users cannot see the "Configure translations" link or the page it
+Ordinary users cannot see the "Configure Translations" link or the page it
leads to.
>>> user_browser.open(translations_page_url)
- >>> user_browser.getLink('Configure translations').click()
+ >>> user_browser.getLink('Configure Translations').click()
Traceback (most recent call last):
...
LinkNotFoundError
@@ -1001,7 +1001,7 @@
First, we verify that netapplet is using Launchpad Translations.
>>> admin_browser.open('http://launchpad.dev/netapplet')
- >>> admin_browser.getLink('Configure translations').click()
+ >>> admin_browser.getLink('Translations', index=1).click()
>>> print_radio_button_field(admin_browser.contents, "translations_usage")
( ) Unknown
(*) Launchpad
@@ -1015,7 +1015,7 @@
as the translation group for the netapplet product...
>>> admin_browser.getLink('Translations').click()
- >>> admin_browser.getLink('Configure translations').click()
+ >>> admin_browser.getLink('Configure Translations').click()
>>> admin_browser.getControl('Translation group').displayOptions
['(nothing selected)', 'Single-language Translators',
'The PolyGlot Translation Group', 'Just a testing team']
Follow ups