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', >+ title=_('Branch'), >+ description=_( >+ "The Bazaar branch for this series in Launchpad, " >+ "if one exists."), >+ ) >+ >+ branch_type = Choice( >+ title=_('Import type'), >+ vocabulary=_getBranchTypeVocabulary(), >+ 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(LaunchpadFormView, ProductSeriesView, >+ BranchNameValidationMixin): >+ """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) >+ initial_values = { >+ 'rcs_type': RevisionControlSystemsExtended.BZR, >+ 'branch_type': LINK_LP_BZR, >+ } >+ >+ def setUpWidgets(self): >+ super(ProductSeriesSetBranchView, self).setUpWidgets() >+ >+ # Extract the radio buttons from the rcs_type widget, so we can >+ # display them separately in the form. >+ soup = BeautifulSoup(self.widgets['rcs_type']()) >+ fields = soup.findAll('input') >+ [cvs_button, svn_button, git_button, hg_button, >+ bzr_button, empty_marker] = [ >+ field for field in fields >+ if field.get('value') in ['CVS', 'BZR_SVN', 'GIT', 'HG', >+ 'BZR', '1']] >+ # The following attributes are used only in the page template. >+ self.rcs_type_cvs = str(cvs_button) >+ self.rcs_type_svn = str(svn_button) >+ self.rcs_type_git = str(git_button) >+ self.rcs_type_hg = str(hg_button) >+ self.rcs_type_bzr = str(bzr_button) >+ self.rcs_type_emptymarker = str(empty_marker) It seems like this could be simplified like this: def render(term, current_value, count): if term.value == current_value: render = self.renderSelectedItem else: render = self.renderItem rendered_item = render(count, self.textForValue(term) term.token, self.name, cssClass) current_value = self.widgets['rcs_type']._getFormValue() vocab = self.widgets['rcs_type'].vocabulary self.rcs_type_cvs = render(vocab.CVS, current_value, 0) self.rcs_type_svn = render(vocab.BZR_SVN, current_value, 1) ... self.rcs_type_emptymarker = self.widgets['rcs_type']._emptyMarker() or with all the rendering in the template as: > >+ >+ soup = BeautifulSoup(self.widgets['branch_type']()) >+ fields = soup.findAll('input') >+ (link_button, create_button, import_button, emptymarker) = fields >+ self.branch_type_link = str(link_button) >+ self.branch_type_create = str(create_button) >+ self.branch_type_import = str(import_button) >+ self.branch_type_emptymarker = str(emptymarker) Same here. >+ >+ 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 _validateCreateNew(self, data): >+ """Validate data for create new case.""" >+ self._validateBranch(data) >+ >+ def validateImportExternal(self, data): >+ """Validate data for import external case.""" >+ rcs_type = data.get('rcs_type') >+ repo_url = data.get('repo_url') >+ >+ if repo_url is None: >+ self.setFieldError('repo_url', >+ 'You must set the external repository URL.') This overwrites the error message from the URIField, so you won't see: The URI scheme "svn" is not allowed. Only URIs with the following schemes may be used: git, http, https I'm not sure why your view tests aren't discovering that. >+ >+ # 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 == RevisionControlSystemsExtended.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 = { >+ RevisionControlSystemsExtended.BZR_SVN:['svn'], >+ RevisionControlSystemsExtended.GIT:['git'], >+ } >+ 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 == CREATE_NEW: >+ self._setRequired(['branch_location', 'rcs_type', 'cvs_module'], >+ 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 == RevisionControlSystemsExtended.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`.""" >+ branch_type = data['branch_type'] >+ if branch_type == IMPORT_EXTERNAL: >+ self.validateImportExternal(data) >+ elif branch_type == LINK_LP_BZR: >+ self._validateLinkLpBzr(data) >+ elif branch_type == CREATE_NEW: >+ self._validateCreateNew(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): >+ self.next_url = canonical_url(self.context) >+ 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') >+ >+ # Create a new branch. >+ if branch_type == CREATE_NEW: >+ branch = self._createBzrBranch( >+ BranchType.HOSTED, branch_name, branch_owner) >+ if branch is not None: >+ self.context.branch = branch >+ self.request.response.addInfoNotification( >+ 'New branch created and linked to the series.') >+ >+ # Import or mirror an external branch. >+ elif branch_type == IMPORT_EXTERNAL: >+ # Either create an externally hosted bzr branch >+ # (a.k.a. 'mirrored') or create a new code import. >+ rcs_type = data.get('rcs_type') >+ if rcs_type == RevisionControlSystemsExtended.BZR: >+ branch = self._createBzrBranch( >+ BranchType.MIRRORED, branch_name, branch_owner, >+ data['repo_url']) >+ if branch is not None: >+ self.context.branch = branch >+ self.request.response.addInfoNotification( >+ 'Mirrored branch created and linked to ' >+ 'the series.') >+ else: >+ # We need to create an import request. >+ if rcs_type == RevisionControlSystemsExtended.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') >+ code_import = getUtility(ICodeImportSet).new( >+ registrant=self.user, >+ target=self.target, >+ branch_name=branch_name, >+ rcs_type=RevisionControlSystems.items[rcs_type.name], >+ url=url, >+ cvs_root=cvs_root, >+ cvs_module=cvs_module) >+ 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_type, branch_name, >+ branch_owner, repo_url=None): >+ """Create a new Bazaar branch. It may be hosted or mirrored. >+ >+ Return the branch on success or None. >+ """ >+ branch = None >+ try: >+ namespace = self.target.getNamespace(branch_owner) >+ branch = namespace.createBranch(branch_type=branch_type, >+ name=branch_name, >+ registrant=self.user, >+ url=repo_url) >+ if branch_type == BranchType.MIRRORED: >+ branch.requestMirror() >+ except BranchCreationForbidden: >+ self.addError( >+ "You are not allowed to create branches in %s." % >+ self.context.displayname) >+ except BranchExists, e: >+ self._setBranchExists(e.existing_branch, 'branch_name') >+ return branch >+ >+ @property >+ def cancel_url(self): >+ """See `LaunchpadFormView`.""" >+ return canonical_url(self.context) >+ >+ >+class ProductSeriesLinkBranchView(LaunchpadEditFormView, ProductSeriesView): > """View to set the bazaar branch for a product series.""" > > schema = IProductSeries > >=== 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-03-26 15:28:25 +0000 >@@ -0,0 +1,313 @@ >+ >>> from canonical.launchpad.testing.pages import find_tag_by_id >+ This looks weird at the very top of the file. Can you move that somewhere below the first heading? >+Set branch >+---------- >+ >+The productseries +setbranch view allows the user to set a branch for >+this series. The branch can one that already exists in Launchpad, or Missing verb: "can one that" >+a new branch in Launchpad can be defined, or it can be a repository >+that exists externally in a variety of version control systems. >+ >+ >>> product = factory.makeProduct(name="chevy") >+ >>> series = factory.makeProductSeries(name="impala", product=product) >+ >>> transaction.commit() >+ >>> login_person(product.owner) >+ >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner) >+ >>> print find_tag_by_id(view.render(), 'maincontent') >+ + Link to a Bazaar branch already in Launchpad... >+ Create a new, empty branch in Launchpad and link to this series... >+ Import a branch hosted somewhere else... >+ ...Branch name:... >+ ...Branch owner:... >+ >+ >+Linking to an existing branch >+----------------------------- >+ >+If linking to an existing branch is selected then the branch location must be provided. >+ >+ >>> form = { >+ ... 'field.branch_type': 'link-lp-bzr', >+ ... 'field.actions.update': 'Update', >+ ... } >+ >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form) Wrap line. >+ >>> for error in view.errors: >+ ... print error >+ The branch location must be set. >+ >+Setting the branch location to an invalid branch results in another >+validation error. >+ >+ >>> form = { >+ ... 'field.branch_type': 'link-lp-bzr', >+ ... 'field.branch_location': 'foo', >+ ... 'field.actions.update': 'Update', >+ ... } >+ >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form) Wrap line. >+ >>> for error in view.errors: >+ ... print error >+ ('Invalid value', InvalidValue("token 'foo' not found in vocabulary")) >+ The branch location must be set. >+ >+Providing a valid branch results in a successful linking. >+ >+ >>> series.branch is None >+ True >+ >>> branch = factory.makeBranch(name='impala-branch', owner=product.owner, product=product) Wrap line. >+ >>> form = { >+ ... 'field.branch_type': 'link-lp-bzr', >+ ... 'field.branch_location': branch.unique_name, >+ ... 'field.actions.update': 'Update', >+ ... } >+ >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form) Wrap line. >+ >>> for error in view.errors: >+ ... print error >+ >>> for notification in view.request.response.notifications: >+ ... print notification.message >+ Series code location updated. >+ >+ >>> print series.branch.name >+ impala-branch >+ >+ >+Creating a new branch >+--------------------- >+ >+When creating a new branch the branch name and owner must be specified. >+ >+ >>> series = factory.makeProductSeries(name="camaro", product=product) >+ >>> transaction.commit() >+ >+ >>> form = { >+ ... 'field.branch_type': 'create-new', >+ ... 'field.actions.update': 'Update', >+ ... } >+ >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form) Wrap line. >+ >>> for notification in view.request.response.notifications: >+ ... print notification.message >+ >>> for error in view.errors: >+ ... print error >+ The branch name must be set. >+ The branch owner must be set. >+ >+ >>> from lp.registry.interfaces.person import IPersonSet >+ >>> mark = getUtility(IPersonSet).getByEmail('