launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #18834
Re: [Merge] lp:~blr/launchpad/ui-project-setbranch into lp:launchpad
Review: Needs Fixing code
Diff comments:
> === modified file 'LICENSE'
> --- LICENSE 2015-03-24 13:36:23 +0000
> +++ LICENSE 2015-06-24 08:54:06 +0000
> @@ -13,6 +13,8 @@
> The Launchpad name and logo are trademarks of Canonical, and may not
> be used without the prior written permission of Canonical.
>
> +Git SCM logos are licensed Creative Commons Attribution 3.0 Unported.
> +
> Third-party copyright in this distribution is noted where applicable.
>
> All rights not expressly granted are reserved.
> @@ -683,3 +685,277 @@
> <http://www.gnu.org/licenses/>.
>
> =========================================================================
> +
> +
> +Creative Commons Attribution 3.0 Unported License
> +
> +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS
> +CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS
> +PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE
> +WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS
> +PROHIBITED.
> +
> +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND
> +AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS
> +LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU
> +THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH
> +TERMS AND CONDITIONS.
> +
> +1. Definitions
> +
> +"Adaptation" means a work based upon the Work, or upon the Work and
> +other pre-existing works, such as a translation, adaptation,
> +derivative work, arrangement of music or other alterations of a
> +literary or artistic work, or phonogram or performance and includes
> +cinematographic adaptations or any other form in which the Work may be
> +recast, transformed, or adapted including in any form recognizably
> +derived from the original, except that a work that constitutes a
> +Collection will not be considered an Adaptation for the purpose of
> +this License. For the avoidance of doubt, where the Work is a musical
> +work, performance or phonogram, the synchronization of the Work in
> +timed-relation with a moving image ("synching") will be considered an
> +Adaptation for the purpose of this License. "Collection" means a
> +collection of literary or artistic works, such as encyclopedias and
> +anthologies, or performances, phonograms or broadcasts, or other works
> +or subject matter other than works listed in Section 1(f) below,
> +which, by reason of the selection and arrangement of their contents,
> +constitute intellectual creations, in which the Work is included in
> +its entirety in unmodified form along with one or more other
> +contributions, each constituting separate and independent works in
> +themselves, which together are assembled into a collective whole. A
> +work that constitutes a Collection will not be considered an
> +Adaptation (as defined above) for the purposes of this License.
> +"Distribute" means to make available to the public the original and
> +copies of the Work or Adaptation, as appropriate, through sale or
> +other transfer of ownership. "Licensor" means the individual,
> +individuals, entity or entities that offer(s) the Work under the terms
> +of this License. "Original Author" means, in the case of a literary or
> +artistic work, the individual, individuals, entity or entities who
> +created the Work or if no individual or entity can be identified, the
> +publisher; and in addition (i) in the case of a performance the
> +actors, singers, musicians, dancers, and other persons who act, sing,
> +deliver, declaim, play in, interpret or otherwise perform literary or
> +artistic works or expressions of folklore; (ii) in the case of a
> +phonogram the producer being the person or legal entity who first
> +fixes the sounds of a performance or other sounds; and, (iii) in the
> +case of broadcasts, the organization that transmits the broadcast.
> +"Work" means the literary and/or artistic work offered under the terms
> +of this License including without limitation any production in the
> +literary, scientific and artistic domain, whatever may be the mode or
> +form of its expression including digital form, such as a book,
> +pamphlet and other writing; a lecture, address, sermon or other work
> +of the same nature; a dramatic or dramatico-musical work; a
> +choreographic work or entertainment in dumb show; a musical
> +composition with or without words; a cinematographic work to which are
> +assimilated works expressed by a process analogous to cinematography;
> +a work of drawing, painting, architecture, sculpture, engraving or
> +lithography; a photographic work to which are assimilated works
> +expressed by a process analogous to photography; a work of applied
> +art; an illustration, map, plan, sketch or three-dimensional work
> +relative to geography, topography, architecture or science; a
> +performance; a broadcast; a phonogram; a compilation of data to the
> +extent it is protected as a copyrightable work; or a work performed by
> +a variety or circus performer to the extent it is not otherwise
> +considered a literary or artistic work. "You" means an individual or
> +entity exercising rights under this License who has not previously
> +violated the terms of this License with respect to the Work, or who
> +has received express permission from the Licensor to exercise rights
> +under this License despite a previous violation. "Publicly Perform"
> +means to perform public recitations of the Work and to communicate to
> +the public those public recitations, by any means or process,
> +including by wire or wireless means or public digital performances; to
> +make available to the public Works in such a way that members of the
> +public may access these Works from a place and at a place individually
> +chosen by them; to perform the Work to the public by any means or
> +process and the communication to the public of the performances of the
> +Work, including by public digital performance; to broadcast and
> +rebroadcast the Work by any means including signs, sounds or images.
> +"Reproduce" means to make copies of the Work by any means including
> +without limitation by sound or visual recordings and the right of
> +fixation and reproducing fixations of the Work, including storage of a
> +protected performance or phonogram in digital form or other electronic
> +medium. 2. Fair Dealing Rights. Nothing in this License is intended to
> +reduce, limit, or restrict any uses free from copyright or rights
> +arising from limitations or exceptions that are provided for in
> +connection with the copyright protection under copyright law or other
> +applicable laws.
> +
> +3. License Grant. Subject to the terms and conditions of this License,
> +Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
> +perpetual (for the duration of the applicable copyright) license to
> +exercise the rights in the Work as stated below:
> +
> +to Reproduce the Work, to incorporate the Work into one or more
> +Collections, and to Reproduce the Work as incorporated in the
> +Collections; to create and Reproduce Adaptations provided that any
> +such Adaptation, including any translation in any medium, takes
> +reasonable steps to clearly label, demarcate or otherwise identify
> +that changes were made to the original Work. For example, a
> +translation could be marked "The original work was translated from
> +English to Spanish," or a modification could indicate "The original
> +work has been modified."; to Distribute and Publicly Perform the Work
> +including as incorporated in Collections; and, to Distribute and
> +Publicly Perform Adaptations. For the avoidance of doubt:
> +
> +Non-waivable Compulsory License Schemes. In those jurisdictions in
> +which the right to collect royalties through any statutory or
> +compulsory licensing scheme cannot be waived, the Licensor reserves
> +the exclusive right to collect such royalties for any exercise by You
> +of the rights granted under this License; Waivable Compulsory License
> +Schemes. In those jurisdictions in which the right to collect
> +royalties through any statutory or compulsory licensing scheme can be
> +waived, the Licensor waives the exclusive right to collect such
> +royalties for any exercise by You of the rights granted under this
> +License; and, Voluntary License Schemes. The Licensor waives the right
> +to collect royalties, whether individually or, in the event that the
> +Licensor is a member of a collecting society that administers
> +voluntary licensing schemes, via that society, from any exercise by
> +You of the rights granted under this License. The above rights may be
> +exercised in all media and formats whether now known or hereafter
> +devised. The above rights include the right to make such modifications
> +as are technically necessary to exercise the rights in other media and
> +formats. Subject to Section 8(f), all rights not expressly granted by
> +Licensor are hereby reserved.
> +
> +4. Restrictions. The license granted in Section 3 above is expressly
> +made subject to and limited by the following restrictions:
> +
> +You may Distribute or Publicly Perform the Work only under the terms
> +of this License. You must include a copy of, or the Uniform Resource
> +Identifier (URI) for, this License with every copy of the Work You
> +Distribute or Publicly Perform. You may not offer or impose any terms
> +on the Work that restrict the terms of this License or the ability of
> +the recipient of the Work to exercise the rights granted to that
> +recipient under the terms of the License. You may not sublicense the
> +Work. You must keep intact all notices that refer to this License and
> +to the disclaimer of warranties with every copy of the Work You
> +Distribute or Publicly Perform. When You Distribute or Publicly
> +Perform the Work, You may not impose any effective technological
> +measures on the Work that restrict the ability of a recipient of the
> +Work from You to exercise the rights granted to that recipient under
> +the terms of the License. This Section 4(a) applies to the Work as
> +incorporated in a Collection, but this does not require the Collection
> +apart from the Work itself to be made subject to the terms of this
> +License. If You create a Collection, upon notice from any Licensor You
> +must, to the extent practicable, remove from the Collection any credit
> +as required by Section 4(b), as requested. If You create an
> +Adaptation, upon notice from any Licensor You must, to the extent
> +practicable, remove from the Adaptation any credit as required by
> +Section 4(b), as requested. If You Distribute, or Publicly Perform the
> +Work or any Adaptations or Collections, You must, unless a request has
> +been made pursuant to Section 4(a), keep intact all copyright notices
> +for the Work and provide, reasonable to the medium or means You are
> +utilizing: (i) the name of the Original Author (or pseudonym, if
> +applicable) if supplied, and/or if the Original Author and/or Licensor
> +designate another party or parties (e.g., a sponsor institute,
> +publishing entity, journal) for attribution ("Attribution Parties") in
> +Licensor's copyright notice, terms of service or by other reasonable
> +means, the name of such party or parties; (ii) the title of the Work
> +if supplied; (iii) to the extent reasonably practicable, the URI, if
> +any, that Licensor specifies to be associated with the Work, unless
> +such URI does not refer to the copyright notice or licensing
> +information for the Work; and (iv) , consistent with Section 3(b), in
> +the case of an Adaptation, a credit identifying the use of the Work in
> +the Adaptation (e.g., "French translation of the Work by Original
> +Author," or "Screenplay based on original Work by Original Author").
> +The credit required by this Section 4 (b) may be implemented in any
> +reasonable manner; provided, however, that in the case of a Adaptation
> +or Collection, at a minimum such credit will appear, if a credit for
> +all contributing authors of the Adaptation or Collection appears, then
> +as part of these credits and in a manner at least as prominent as the
> +credits for the other contributing authors. For the avoidance of
> +doubt, You may only use the credit required by this Section for the
> +purpose of attribution in the manner set out above and, by exercising
> +Your rights under this License, You may not implicitly or explicitly
> +assert or imply any connection with, sponsorship or endorsement by the
> +Original Author, Licensor and/or Attribution Parties, as appropriate,
> +of You or Your use of the Work, without the separate, express prior
> +written permission of the Original Author, Licensor and/or Attribution
> +Parties. Except as otherwise agreed in writing by the Licensor or as
> +may be otherwise permitted by applicable law, if You Reproduce,
> +Distribute or Publicly Perform the Work either by itself or as part of
> +any Adaptations or Collections, You must not distort, mutilate, modify
> +or take other derogatory action in relation to the Work which would be
> +prejudicial to the Original Author's honor or reputation. Licensor
> +agrees that in those jurisdictions (e.g. Japan), in which any exercise
> +of the right granted in Section 3(b) of this License (the right to
> +make Adaptations) would be deemed to be a distortion, mutilation,
> +modification or other derogatory action prejudicial to the Original
> +Author's honor and reputation, the Licensor will waive or not assert,
> +as appropriate, this Section, to the fullest extent permitted by the
> +applicable national law, to enable You to reasonably exercise Your
> +right under Section 3(b) of this License (right to make Adaptations)
> +but not otherwise. 5. Representations, Warranties and Disclaimer
> +
> +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING,
> +LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR
> +WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED,
> +STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF
> +TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE,
> +NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY,
> +OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE.
> +SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES,
> +SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
> +
> +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY
> +APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY
> +LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR
> +EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK,
> +EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
> +
> +7. Termination
> +
> +This License and the rights granted hereunder will terminate
> +automatically upon any breach by You of the terms of this License.
> +Individuals or entities who have received Adaptations or Collections
> +from You under this License, however, will not have their licenses
> +terminated provided such individuals or entities remain in full
> +compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will
> +survive any termination of this License. Subject to the above terms
> +and conditions, the license granted here is perpetual (for the
> +duration of the applicable copyright in the Work). Notwithstanding the
> +above, Licensor reserves the right to release the Work under different
> +license terms or to stop distributing the Work at any time; provided,
> +however that any such election will not serve to withdraw this License
> +(or any other license that has been, or is required to be, granted
> +under the terms of this License), and this License will continue in
> +full force and effect unless terminated as stated above. 8.
> +Miscellaneous
> +
> +Each time You Distribute or Publicly Perform the Work or a Collection,
> +the Licensor offers to the recipient a license to the Work on the same
> +terms and conditions as the license granted to You under this License.
> +Each time You Distribute or Publicly Perform an Adaptation, Licensor
> +offers to the recipient a license to the original Work on the same
> +terms and conditions as the license granted to You under this License.
> +If any provision of this License is invalid or unenforceable under
> +applicable law, it shall not affect the validity or enforceability of
> +the remainder of the terms of this License, and without further action
> +by the parties to this agreement, such provision shall be reformed to
> +the minimum extent necessary to make such provision valid and
> +enforceable. No term or provision of this License shall be deemed
> +waived and no breach consented to unless such waiver or consent shall
> +be in writing and signed by the party to be charged with such waiver
> +or consent. This License constitutes the entire agreement between the
> +parties with respect to the Work licensed here. There are no
> +understandings, agreements or representations with respect to the Work
> +not specified here. Licensor shall not be bound by any additional
> +provisions that may appear in any communication from You. This License
> +may not be modified without the mutual written agreement of the
> +Licensor and You. The rights granted under, and the subject matter
> +referenced, in this License were drafted utilizing the terminology of
> +the Berne Convention for the Protection of Literary and Artistic Works
> +(as amended on September 28, 1979), the Rome Convention of 1961, the
> +WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms
> +Treaty of 1996 and the Universal Copyright Convention (as revised on
> +July 24, 1971). These rights and subject matter take effect in the
> +relevant jurisdiction in which the License terms are sought to be
> +enforced according to the corresponding provisions of the
> +implementation of those treaty provisions in the applicable national
> +law. If the standard suite of rights granted under applicable
> +copyright law includes additional rights not granted under this
> +License, such additional rights are deemed to be included in the
> +License; this License is not intended to restrict the license of any
> +rights under applicable law.
> +
> +=========================================================================
> \ No newline at end of file
>
> === 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-06-24 08:54:06 +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-06-24 08:54:06 +0000
> @@ -64,10 +64,24 @@
> padding-left: 1em;
> }
>
> +form label {
> + font-weight: bold;
> +}
> +
> form.primary.search {
> margin-bottom: 2em;
> }
>
> +.note {
> + color: #666;
> + display: inline-block;
> + padding: 0.5em 0.5em 0.5em 2em;
> + background: url(/@@/info) 0.5em 0.5em no-repeat;
> + background-color: rgb(249, 249, 249);
> + border: 1px solid #ddd;
> + border-radius: 3px
> +}
This erroneously applies to the span.note in distroseriesdifferences_details.js
> +
> div#bugs-search-form.dynamic_bug_listing {
> margin-bottom: 10px;
> padding: 3px 0;
> @@ -538,17 +552,36 @@
>
> /* --- Code --- */
>
> +code.command {
> + background-color: #fff;
> + border: 1px solid #ddd;
> + border-radius: 3px;
> + 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;
> +}
> +code.command-block:last-child {
> + margin: 0;
> +}
> table.code {
> margin-bottom: 1em;
> - }
> +}
> table.code th {
> text-align: left;
> font-weight: bold;
> - }
> +}
> table.code th, table.code td {
> padding-right: 3em;
> - }
> -
> +}
> +#git-expander-content {
> + margin-top: 1em;
> +}
> .branch-no-dev-focus {
> background: #FFF59C;
> vertical-align: middle;
>
> === 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-06-24 08:54:06 +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-06-24 08:54:06 +0000 differ
> === modified file 'lib/lp/code/browser/branchlisting.py'
> --- lib/lp/code/browser/branchlisting.py 2015-06-15 05:28:12 +0000
> +++ lib/lp/code/browser/branchlisting.py 2015-06-24 08:54:06 +0000
> @@ -1133,13 +1133,12 @@
> @property
> def configure_codehosting(self):
> """Get the menu link for configuring code hosting."""
> - if not check_permission(
> - 'launchpad.Edit', self.context.development_focus):
> + if not check_permission('launchpad.Edit', self.context):
> return None
> - series_menu = MenuAPI(self.context.development_focus).overview
> - set_branch = series_menu['set_branch']
> - set_branch.text = 'Configure Code'
> - return set_branch
> + menu = MenuAPI(self.context).overview
> + configure_code = menu['configure_code']
> + configure_code.text = 'Configure Code'
> + return configure_code
What does this override from the original? Does anything still use the original?
>
>
> class ProductBranchStatisticsView(BranchCountSummaryView,
>
> === modified file 'lib/lp/code/browser/configure.zcml'
> --- lib/lp/code/browser/configure.zcml 2015-06-18 20:18:16 +0000
> +++ lib/lp/code/browser/configure.zcml 2015-06-24 08:54:06 +0000
> @@ -37,7 +37,34 @@
> template="../templates/code-in-branches.pt"
> permission="zope.Public"
> />
> -
> + <browser:page
> + name="+configure-code"
> + for="lp.registry.interfaces.product.IProduct"
> + class="lp.registry.browser.product.ProductSetBranchView"
> + permission="launchpad.Edit"
> + template="../templates/configure-code.pt"
> + />
> + <browser:page
> + name="+setbranch"
> + for="lp.registry.interfaces.productseries.IProductSeries"
> + class="lp.registry.browser.productseries.ProductSeriesSetBranchView"
> + permission="launchpad.Edit"
> + template="../templates/configure-code.pt"
> + />
> + <browser:page
> + name="+configure-code-macros"
> + for="lp.registry.interfaces.productseries.IProductSeries"
> + class="lp.app.browser.launchpad.Macro"
> + permission="zope.Public"
> + template="../templates/configure-code-macros.pt"
> + />
> + <browser:page
> + name="+configure-code-macros"
> + for="lp.registry.interfaces.product.IProduct"
> + class="lp.app.browser.launchpad.Macro"
> + permission="zope.Public"
> + template="../templates/configure-code-macros.pt"
> + />
> <browser:page
> for="lp.services.webapp.interfaces.ILaunchpadApplication"
> name="+project-cloud"
>
> === modified file 'lib/lp/code/javascript/productseries-setbranch.js'
> --- lib/lp/code/javascript/productseries-setbranch.js 2015-05-19 03:01:06 +0000
> +++ lib/lp/code/javascript/productseries-setbranch.js 2015-06-24 08:54:06 +0000
> @@ -24,6 +24,17 @@
> return selected;
> };
>
> + module._get_selected_default_vcs = function () {
> + var vcs = document.getElementsByName('field.default_vcs');
> + var i;
> + for (i = 0; i < vcs.length; i++) {
> + if (vcs[i].checked) {
> + return vcs[i].value;
> + }
> + }
> + return null;
> + };
> +
>
> module.__rcs_types = null;
>
> @@ -39,6 +50,32 @@
> field.disabled = !is_enabled;
> };
>
> + module.setup_expanders = function() {
> + var git_expander = new Y.lp.app.widgets.expander.Expander(
> + Y.one('#git-expander-icon'), Y.one('#git-expander-content')
> + );
> +
> + var bzr_expander = new Y.lp.app.widgets.expander.Expander(
> + Y.one('#bzr-expander-icon'), Y.one('#bzr-expander-content')
> + );
> + module.git_expander = git_expander.setUp();
> + module.bzr_expander = bzr_expander.setUp();
> + };
> +
> + module.onclick_default_vcs = function(e) {
> + /* Which project vcs was selected? */
> + var selectedDefaultVCS =
> + module._get_selected_default_vcs();
> +
> + if (selectedDefaultVCS === 'GIT') {
> + module.git_expander.render(true);
> + module.bzr_expander.render(false);
> + } else {
> + module.bzr_expander.render(true);
> + module.git_expander.render(false);
> + }
> + };
> +
> module.onclick_rcs_type = function(e) {
> /* Which rcs type radio button has been selected? */
> // CVS
> @@ -81,10 +118,14 @@
> 'click', module.onclick_rcs_type);
> Y.all('input[name="field.branch_type"]').on(
> 'click', module.onclick_branch_type);
> + Y.all('input[name="field.default_vcs"]').on(
> + 'click', module.onclick_default_vcs);
>
> // Set the initial state.
> + module.setup_expanders();
> module.onclick_rcs_type();
> module.onclick_branch_type();
> + module.onclick_default_vcs();
> };
>
> }, "0.1", {"requires": ["node", "DOM"]});
>
> === modified file 'lib/lp/code/model/gitrepository.py'
> --- lib/lp/code/model/gitrepository.py 2015-06-15 09:55:47 +0000
> +++ lib/lp/code/model/gitrepository.py 2015-06-24 08:54:06 +0000
> @@ -307,7 +307,7 @@
> # Check for an existing target default.
> existing = getUtility(IGitRepositorySet).getDefaultRepository(
> self.target)
> - if existing is not None:
> + if existing is not None and existing != self:
> raise GitDefaultConflict(existing, self.target)
> self.target_default = value
> if IProduct.providedBy(self.target):
This should be fixed in setOwnerDefault too.
>
> === modified file 'lib/lp/code/model/tests/test_gitrepository.py'
> --- lib/lp/code/model/tests/test_gitrepository.py 2015-06-18 14:13:40 +0000
> +++ lib/lp/code/model/tests/test_gitrepository.py 2015-06-24 08:54:06 +0000
> @@ -1986,6 +1986,17 @@
> self.repository_set.setDefaultRepositoryForOwner,
> person, person, repository, user)
>
> + def test_setDefaultRepository_for_same_targetdefault_noops(self):
> + # If a repository is already the target default,
> + # setting the defaultRepository again should no-op.
> + project = self.factory.makeProduct()
> + repository = self.factory.makeGitRepository(target=project)
> + with person_logged_in(project.owner):
> + self.repository_set.setDefaultRepository(project, repository)
> + result = self.repository_set.setDefaultRepository(
> + project, repository)
> + self.assertEqual(None, result)
> +
>
> class TestGitRepositorySetDefaultsMixin:
>
>
> === modified file 'lib/lp/code/stories/codeimport/xx-create-codeimport.txt'
> --- lib/lp/code/stories/codeimport/xx-create-codeimport.txt 2015-06-15 05:28:12 +0000
> +++ lib/lp/code/stories/codeimport/xx-create-codeimport.txt 2015-06-24 08:54:06 +0000
> @@ -38,11 +38,12 @@
> >>> owner_browser = setupBrowser(auth="Basic test@xxxxxxxxxxxxx:test")
> >>> owner_browser.open('http://code.launchpad.dev/firefox')
> >>> owner_browser.getLink('Configure Code').click()
> + >>> owner_browser.getControl('Bazaar', index=0).click()
> >>> owner_browser.getControl(
> ... 'Import a branch').click()
> >>> owner_browser.getControl('Branch URL').value = (
> ... 'git://example.com/firefox')
> - >>> owner_browser.getControl('Git').click()
> + >>> owner_browser.getControl('Git', index=1).click()
> >>> owner_browser.getControl('Branch name').value = 'trunk'
> >>> owner_browser.getControl('Update').click()
>
>
> === added file 'lib/lp/code/templates/configure-code-macros.pt'
> --- lib/lp/code/templates/configure-code-macros.pt 1970-01-01 00:00:00 +0000
> +++ lib/lp/code/templates/configure-code-macros.pt 2015-06-24 08:54:06 +0000
> @@ -0,0 +1,39 @@
> +<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-bzr" id="push-instructions-bzr" class="scm-tip">
This indentation is inconsistent with the rest of the file.
> + <h3>Push a new branch</h3>
> + <p>You can push a Bazaar branch directly to Launchpad with the command:</p>
> + <p>
> + <code class="command command-block">
> + bzr push lp:<tal:project replace="context/name"/>
> + </code>
> + </p>
> + </div>
> +
> + <div metal:define-macro="push-instructions-git" id="push-instructions-git" class="scm-tip">
> + <h3>Push a new repository</h3>
> + <p>You can add a remote for your Git branch with the
s/branch/repository/
> + command:</p>
> + <p>
> + <code class="command command-block">
> + git remote add origin <tal:project replace="modules/lp.services.config/config/codehosting/git_ssh_root"/><tal:project replace="context/name"/><br />
> + </code></p>
> + <p>… and push the git branch to Launchpad with:</p>
s/git/Git/
> + <p>
> + <code class="command command-block">
> + git push origin master
> + </code>
> + </p>
> + </div>
> +
> + <div metal:define-macro="no-keys" condition="not:view/user/sshkeys">
> + <p class="note">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>
"an SSH key"
> + </div>
> +
> +</tal:root>
>
> === added file 'lib/lp/code/templates/configure-code.pt'
> --- lib/lp/code/templates/configure-code.pt 1970-01-01 00:00:00 +0000
> +++ lib/lp/code/templates/configure-code.pt 2015-06-24 08:54:06 +0000
> @@ -0,0 +1,167 @@
> +<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">
> + <tal:block condition="not: view/is_series">
> + <h3>Version control system</h3>
> + <div id="default_vcs">
> + <ul>
> + <li>
> + <label tal:replace="structure view/default_vcs_bzr">
> + Bazaar
> + </label>
> + </li>
> + <li>
> + <label tal:replace="structure view/default_vcs_git">
> + Git
> + </label> (beta)
Consider replacing the (beta) with something like <div style="display: inline" class="beta"><img alt="[BETA]" src="/@@/beta"></div>
> + </li>
> + </ul>
> + <p>
> + <span class="note">Your project may have both
> + Git repositories and Bazaar branches.</span><br/>
I'm not sure this is important enough for .note. In the Git case you end up with three (i)s on screen at once, which looks quite odd.
> + </p>
> + </div>
> + </tal:block>
> +
> + <div id="show-hide-bzr">
> + <a href="#" id="bzr-expander-icon" class="expander-icon js-action">
> + <span class="sprite branch">Bazaar settings</span>
> + </a>
> + <div id="bzr-expander-content">
> + <div class="push-instructions">
> + <div metal:use-macro="context/@@+configure-code-macros/push-instructions-bzr"></div>
> + <div metal:use-macro="context/@@+configure-code-macros/no-keys"></div>
> + </div>
> +
> + <table id="form_bzr" class="form">
> + <tr>
> + <td>
> + <h3>Link or Import an existing branch</h3>
s/Import/import/
> + <label tal:replace="structure view/branch_type_link">
> + Link to a Bazaar branch already in Launchpad
s/in/on/
> + </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 id="branch_mirror">
> + <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>
> + </div>
> + <input tal:replace="structure view/rcs_type_emptymarker" />
> + </div>
> +
> + <tal:block condition="not: view/is_series">
> + <div id="show-hide-git">
> + <a href="#" id="git-expander-icon" class="expander-icon js-action">
> + <span class="sprite gitbranch">Git settings</span>
> + </a>
> + <div id="git-expander-content">
> + <div id="form_git" class="form">
> + <span class="note">Git support is currently in beta.</span>
With the obvious burgundy div.beta on the radio buttons this may be unnecessary clutter. The many (i)s again look a bit weird.
> + <div class="push-instructions">
> + <div metal:use-macro="context/@@+configure-code-macros/push-instructions-git"></div>
> + <div metal:use-macro="context/@@+configure-code-macros/no-keys"></div>
> + </div>
> +
> + <h3>Link an existing repository</h3>
> + <p>Link to an existing Git repository already in Launchpad</p>
s/in/on/
> + <tal:widget define="widget nocall:view/widgets/git_repository_location">
> + <metal:block use-macro="context/@@launchpad_form/widget_row" />
> + </tal:widget>
> + </div>
> + </div>
> + </div>
> + </tal:block>
> +
> +
> + </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/code/templates/product-branch-summary.pt'
> --- lib/lp/code/templates/product-branch-summary.pt 2015-06-04 06:54:09 +0000
> +++ lib/lp/code/templates/product-branch-summary.pt 2015-06-24 08:54:06 +0000
> @@ -56,8 +56,7 @@
> </p>
> </div>
>
> - <tal:no-branches
> - condition="not: view/branch_count">
> + <tal:no-branches condition="not: view/branch_count">
> There are no branches for <tal:project-name replace="context/displayname"/>
> in Launchpad.
> <tal:can-configure condition="view/can_configure_branches">
>
> === modified file 'lib/lp/code/vocabularies/configure.zcml'
> --- lib/lp/code/vocabularies/configure.zcml 2015-04-13 19:02:15 +0000
> +++ lib/lp/code/vocabularies/configure.zcml 2015-06-24 08:54:06 +0000
> @@ -66,4 +66,15 @@
> <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary"/>
> </class>
>
> + <securedutility
Anomalous indentation.
> + name="GitRepositoryRestrictedOnProduct"
> + component=".gitrepository.GitRepositoryRestrictedOnProductVocabulary"
> + provides="zope.schema.interfaces.IVocabularyFactory">
> + <allow interface="zope.schema.interfaces.IVocabularyFactory"/>
> + </securedutility>
> +
> + <class class=".gitrepository.GitRepositoryRestrictedOnProductVocabulary">
> + <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary"/>
> + </class>
> +
> </configure>
>
> === modified file 'lib/lp/code/vocabularies/gitrepository.py'
> --- lib/lp/code/vocabularies/gitrepository.py 2015-04-28 15:22:46 +0000
> +++ lib/lp/code/vocabularies/gitrepository.py 2015-06-24 08:54:06 +0000
> @@ -6,6 +6,7 @@
> __metaclass__ = type
>
> __all__ = [
> + 'GitRepositoryRestrictedOnProductVocabulary',
> 'GitRepositoryVocabulary',
> ]
>
> @@ -15,6 +16,7 @@
>
> from lp.code.interfaces.gitcollection import IAllGitRepositories
> from lp.code.model.gitrepository import GitRepository
> +from lp.registry.interfaces.product import IProduct
> from lp.services.webapp.interfaces import ILaunchBag
> from lp.services.webapp.vocabulary import (
> CountableIterator,
> @@ -62,3 +64,20 @@
>
> def _getCollection(self):
> return getUtility(IAllGitRepositories)
> +
> +class GitRepositoryRestrictedOnProductVocabulary(GitRepositoryVocabulary):
> + """A vocabulary for searching git repositories restricted on product."""
> +
> + def __init__(self, context=None):
> + super(GitRepositoryRestrictedOnProductVocabulary, self).__init__(
> + context)
> + if IProduct.providedBy(self.context):
> + self.product = self.context
> + else:
> + # An unexpected type.
> + raise AssertionError('Unexpected context type')
context is an optional keyword argument, but if omitted it will blow up. I'd drop the =None.
> +
> + def _getCollection(self):
> + return getUtility(IAllGitRepositories).inProject(
> + self.product).isExclusive()
> +
>
> === modified file 'lib/lp/code/vocabularies/tests/test_gitrepository_vocabularies.py'
> --- lib/lp/code/vocabularies/tests/test_gitrepository_vocabularies.py 2015-05-14 13:57:51 +0000
> +++ lib/lp/code/vocabularies/tests/test_gitrepository_vocabularies.py 2015-06-24 08:54:06 +0000
> @@ -5,9 +5,15 @@
>
> __metaclass__ = type
>
> -from lp.code.vocabularies.gitrepository import GitRepositoryVocabulary
> +from zope.component import getUtility
> +
> +from lp.code.vocabularies.gitrepository import (
> + GitRepositoryRestrictedOnProductVocabulary,
> + GitRepositoryVocabulary,
> + )
> from lp.testing import TestCaseWithFactory
> from lp.testing.layers import DatabaseFunctionalLayer
> +from lp.registry.interfaces.product import IProductSet
>
>
> class TestGitRepositoryVocabulary(TestCaseWithFactory):
> @@ -50,3 +56,42 @@
> # If there are more than one search result, a LookupError is still
> # raised.
> self.assertRaises(LookupError, self.vocab.getTermByToken, "fizzbuzz")
> +
> +
> +class TestRestrictedGitRepositoryVocabularyOnProduct(TestCaseWithFactory):
> + """Test the GitRepositoryRestrictedOnProductVocabulary behaves as expected.
> +
> + When a GitRepositoryRestrictedOnProductVocabulary is used with a project,
> + the project of the git repository in the vocabulary match the product give
> + as the context.
> + """
> +
> + layer = DatabaseFunctionalLayer
> +
> + def setUp(self):
> + super(TestRestrictedGitRepositoryVocabularyOnProduct, self).setUp()
> + self._createRepositories()
> + self.vocab = GitRepositoryRestrictedOnProductVocabulary(
> + context=self._getVocabRestriction())
> +
> + def _getVocabRestriction(self):
> + """Restrict using the widget product."""
> + return getUtility(IProductSet).getByName('widget')
> +
> + def _createRepositories(self):
> + test_product = self.factory.makeProduct(name='widget')
> + person = self.factory.makePerson(name=u'scotty')
> + self.factory.makeProductGitRepository(
> + owner=person, project=test_product, name=u'mountain')
> + self.product = test_product
I'd create a repo for another Product so tests will fail if the inProject(...) breaks.
> +
> + def test_singleQueryResult(self):
> + # If there is a single search result that matches, use that
> + # as the result.
> + term = self.vocab.getTermByToken('mountain')
> + self.assertEqual(u'~scotty/widget/+git/mountain', term.value.unique_name)
> +
> + def test_multipleQueryResult(self):
> + # If there are more than one search result, a LookupError is still
> + # raised.
> + self.assertRaises(LookupError, self.vocab.getTermByToken, 'scotty')
>
> === modified file 'lib/lp/registry/browser/configure.zcml'
> --- lib/lp/registry/browser/configure.zcml 2015-06-10 10:19:25 +0000
> +++ lib/lp/registry/browser/configure.zcml 2015-06-24 08:54:06 +0000
> @@ -2002,13 +2002,6 @@
> template="../templates/milestone-add.pt"
> />
> <browser:page
> - name="+setbranch"
> - for="lp.registry.interfaces.productseries.IProductSeries"
> - class="lp.registry.browser.productseries.ProductSeriesSetBranchView"
> - permission="launchpad.Edit"
> - template="../templates/productseries-setbranch.pt"
> - />
> - <browser:page
> name="+review"
> for="lp.registry.interfaces.productseries.IProductSeries"
> class="lp.registry.browser.productseries.ProductSeriesReviewView"
>
> === modified file 'lib/lp/registry/browser/product.py'
> --- lib/lp/registry/browser/product.py 2015-06-15 05:28:12 +0000
> +++ lib/lp/registry/browser/product.py 2015-06-24 08:54:06 +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
> @@ -66,6 +71,7 @@
> from zope.schema import (
> Bool,
> Choice,
> + TextLine,
> )
> from zope.schema.vocabulary import (
> SimpleTerm,
> @@ -80,6 +86,7 @@
> custom_widget,
> LaunchpadEditFormView,
> LaunchpadFormView,
> + render_radio_widget_part,
> ReturnToReferrerMixin,
> safe_action,
> )
> @@ -103,7 +110,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,9 +141,28 @@
> 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.branchjob import IRosettaUploadJobSource
> +from lp.code.interfaces.branchtarget import IBranchTarget
> +from lp.code.interfaces.codeimport import (
> + ICodeImport,
> + ICodeImportSet,
> + )
> +from lp.code.interfaces.gitrepository import IGitRepositorySet
> from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin
> +
> from lp.registry.browser import (
> add_subscribe_link,
> BaseRdfView,
> @@ -150,6 +179,7 @@
> PillarNavigationMixin,
> PillarViewMixin,
> )
> +from lp.registry.enums import VCSType
> from lp.registry.interfaces.pillar import IPillarNameSet
> from lp.registry.interfaces.product import (
> IProduct,
> @@ -171,6 +201,7 @@
> from lp.services.fields import (
> PillarAliases,
> PublicPersonChoice,
> + URIField,
> )
> from lp.services.librarian.interfaces import ILibraryFileAliasSet
> from lp.services.propertycache import cachedproperty
> @@ -347,7 +378,6 @@
> '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',
> @@ -363,11 +393,11 @@
> configured=config_statuses[key]))
>
> # Add the branch configuration in separately.
> - set_branch = series_menu['set_branch']
> - set_branch.text = 'Code'
> - set_branch.summary = "Specify the location of this project's code."
> + configure_code = overview_menu['configure_code']
> + configure_code.text = 'Code'
> + configure_code.summary = "Specify the location of this project's code."
> config_list.insert(0,
> - dict(link=set_branch,
> + dict(link=configure_code,
> configured=config_statuses['configure_codehosting']))
> return config_list
>
> @@ -506,6 +536,7 @@
> 'packages',
> 'series',
> 'series_add',
> + 'configure_code',
> 'milestones',
> 'downloads',
> 'announce',
> @@ -559,6 +590,14 @@
> 'RDF</abbr> metadata')
> return Link('+rdf', text, icon='download')
>
> + @enabled_with_permission('launchpad.Edit')
> + def configure_code(self):
> + """Return a link to configure code for this project."""
> + text = 'Configure code'
> + icon = 'edit'
> + summary = 'Configure code for this project'
> + return Link('+configure-code', text, summary, icon=icon)
> +
> def downloads(self):
> text = 'Downloads'
> return Link('+download', text, icon='info')
> @@ -1594,6 +1633,379 @@
> 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'])
> +
> + 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)
> +
> +
> +def create_git_fields():
> + return form.Fields(
> + Choice(__name__='default_vcs',
> + title=_("Project VCS"),
> + required=True, vocabulary=VCSType,
> + description=_("The version control system for "
> + "this project.")),
> + Choice(__name__='git_repository_location',
> + title=_('Git Repository'),
This is the only title-case field on the form. s/Repository/repository/
> + required=False,
> + vocabulary='GitRepositoryRestrictedOnProduct',
> + description=_(
> + "The Git repository for this project in Launchpad, "
> + "if one exists, in the form: "
> + "~user/project-name/+git/repo-name"))
> + )
> +
> +
> +class ProductSetBranchView(ReturnToReferrerMixin, LaunchpadFormView,
> + ProductView,
> + BranchNameValidationMixin):
> + """The view to set a branch default for the Product."""
> +
> + label = 'Configure code'
> + page_title = label
> + 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 is_series(self):
> + return False
This need not be a property.
> +
> + @property
> + def series(self):
> + return self.context.development_focus
> +
> + @property
> + def initial_values(self):
> + return dict(
> + rcs_type=RevisionControlSystems.BZR,
> + default_vcs=(self.context.pillar.inferred_vcs or VCSType.BZR),
> + 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 setUpFields(self):
> + """See `LaunchpadFormView`."""
> + super(ProductSetBranchView, self).setUpFields()
> + if not self.is_series:
> + self.form_fields = (self.form_fields + create_git_fields())
> +
> + 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)]
> +
> + if not self.is_series:
> + 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 _validateLinkLpGit(self, data):
> + """Validate data for link-lp-git case."""
> + if data.get('git_repository_location'):
> + repo = data.get('git_repository_location')
> + if not repo:
> + self.setFieldError(
> + 'git_repository_location',
> + 'The repository does not exist.')
> + if not (self.context == repo.target):
> + self.setFieldError(
> + 'git_repository_location',
> + 'The repository is in a different project.')
Does the target check need to exist now that the vocab is in place?
> +
> + 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.get('branch_type')
> + self._validateLinkLpGit(data)
This needs to skip Git validation in the series case.
> + 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)
> +
> + def abort_update(self):
> + """Abort transaction.
> +
> + This is normally handled by LaunchpadFormView, but this can be called
> + from the success handler."""
> +
> + self.errors_in_action = True
> + self._abort()
> + return
Pointless return.
> +
> + def add_update_notification(self):
> + self.request.response.addInfoNotification(
> + 'Project code updated.')
No code was updated. The settings were changed, though.
> +
> + @action(_('Update'), name='update')
> + def update_action(self, action, data):
> + branch_type = data.get('branch_type')
> + default_vcs = data.get('default_vcs')
> +
Default VCS and Git settings should be skipped in the series case.
> + if default_vcs:
> + self.context.vcs = default_vcs
> +
> + repo = data.get('git_repository_location')
> + if repo:
> + getUtility(IGitRepositorySet).setDefaultRepository(
> + self.context, repo)
This makes it impossible to unset the default.
> + 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.add_update_notification()
> + 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.abort_update()
> + 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 2015-06-12 14:20:12 +0000
> +++ lib/lp/registry/browser/productseries.py 2015-06-24 08:54:06 +0000
> @@ -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,25 @@
> 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, rcs_type)
> - 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 is_series(self):
> + return True
No need for a property.
> +
> + @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
> + def add_update_notification(self):
> + self.request.response.addInfoNotification(
> + 'Series code location updated.')
>
>
> 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-06-24 08:54:06 +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.
>
> === added file 'lib/lp/registry/browser/tests/test_product_views.py'
> --- lib/lp/registry/browser/tests/test_product_views.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/registry/browser/tests/test_product_views.py 2015-06-24 08:54:06 +0000
> @@ -0,0 +1,35 @@
> +# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""View tests for Product pages."""
> +
> +__metaclass__ = type
> +
> +import soupmatchers
> +from zope.security.proxy import removeSecurityProxy
> +
> +from lp.services.webapp import canonical_url
> +from lp.testing import BrowserTestCase
> +from lp.testing.layers import DatabaseFunctionalLayer
> +
> +
> +class TestProductSetBranchView(BrowserTestCase):
> +
> + layer = DatabaseFunctionalLayer
> +
> + def getBrowser(self, project, view_name=None):
> + project = removeSecurityProxy(project)
> + url = canonical_url(project, view_name=view_name)
> + return self.getUserBrowser(url, project.owner)
> +
> + def test_link_existing_git_repository(self):
> + repo = removeSecurityProxy(self.factory.makeProductGitRepository())
> + browser = self.getBrowser(repo.project, '+configure-code')
> + browser.getControl('Git', index=0).click()
> + browser.getControl('Git Repository').value = repo.shortened_path
> + browser.getControl('Update').click()
> +
> + tag = soupmatchers.Tag(
> + 'success-div', 'div', attrs={'class': 'informational message'},
> + text='Project code updated.')
> + self.assertThat(browser.contents, soupmatchers.HTMLContains(tag))
>
> === modified file 'lib/lp/registry/interfaces/product.py'
> --- lib/lp/registry/interfaces/product.py 2015-05-13 05:03:33 +0000
> +++ lib/lp/registry/interfaces/product.py 2015-06-24 08:54:06 +0000
> @@ -761,6 +761,13 @@
> description=_(
> "Version control system for this project's code.")))
>
> + inferred_vcs = exported(
> + TextLine(
> + title=_("Inferred VCS"),
> + readonly=True,
> + description=_(
> + "Inferred version control system for this project's code.")))
It's not a TextLine but a Choice over the enum.
> +
> def getAllowedBugInformationTypes():
> """Get the information types that a bug in this project can have.
>
>
> === modified file 'lib/lp/registry/model/product.py'
> --- lib/lp/registry/model/product.py 2015-05-13 05:25:30 +0000
> +++ lib/lp/registry/model/product.py 2015-06-24 08:54:06 +0000
> @@ -116,6 +116,8 @@
> )
> from lp.code.enums import BranchType
> from lp.code.interfaces.branch import DEFAULT_BRANCH_STATUS_IN_LISTING
> +from lp.code.interfaces.branchcollection import IBranchCollection
> +from lp.code.interfaces.gitcollection import IGitCollection
> from lp.code.interfaces.gitrepository import IGitRepositorySet
> from lp.code.model.branch import Branch
> from lp.code.model.branchnamespace import BRANCH_POLICY_ALLOWED_TYPES
> @@ -445,6 +447,21 @@
> """
> return None
>
> + @property
> + def inferred_vcs(self):
> + """Use vcs, otherwise infer from existence of git or bzr branches.
> +
> + Bzr take precedence over git, if no project vcs set.
> + """
> + vcs = None
> + if self.vcs:
> + return self.vcs
> + if not IBranchCollection(self).is_empty():
> + vcs = VCSType.BZR
> + elif not IGitCollection(self).is_empty():
> + vcs = VCSType.GIT
> + return vcs
The "vcs" local is pointless.
> +
> @date_next_suggest_packaging.setter # pyflakes:ignore
> def date_next_suggest_packaging(self, value):
> """See `IProduct`
>
> === modified file 'lib/lp/registry/stories/product/xx-product-development-focus.txt'
> --- lib/lp/registry/stories/product/xx-product-development-focus.txt 2015-06-14 23:48:24 +0000
> +++ lib/lp/registry/stories/product/xx-product-development-focus.txt 2015-06-24 08:54:06 +0000
> @@ -74,7 +74,7 @@
> (http://launchpad.dev/fooix/+edit)
> >>> print_involvement_portlet(owner_browser)
> Code
> - http://launchpad.dev/fooix/trunk/+setbranch
> + http://launchpad.dev/fooix/+setbranch
> Bugs
> http://launchpad.dev/fooix/+configure-bugtracker
> Translations
> @@ -85,6 +85,7 @@
> The owner can specify the development focus branch from the overview page.
>
> >>> owner_browser.getLink(url='+setbranch').click()
> + >>> owner_browser.getControl('Bazaar', index=0).click()
> >>> owner_browser.getControl(name='field.branch_location').value = (
> ... '~eric/fooix/trunk')
> >>> owner_browser.getControl('Update').click()
>
> === 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-06-24 08:54:06 +0000
> @@ -129,6 +129,11 @@
> <dd tal:content="structure view/languages_edit_widget" />
> </dl>
>
> + <dl id="product-vcs" tal:condition="context/inferred_vcs">
> + <dt>Version control system:</dt>
> + <dd tal:content="context/inferred_vcs/title">Git</dd>
> + </dl>
> +
> <dl id="licences">
> <dt>Licences:</dt>
> <dd>
>
> === removed 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 1970-01-01 00:00:00 +0000
> @@ -1,125 +0,0 @@
> -<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">
> -
> - <p id="push-instructions">
> - You can push a Bazaar branch directly to Launchpad with the command:<br />
> - <tt class="command">
> - bzr push lp:~<tal:user replace="view/user/name"/>/<tal:project replace="context/product/name"/>/<tal:series replace="context/name"/>
> - </tt>
> - <tal:no-keys condition="not:view/user/sshkeys">
> - <br/>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>.
> - </tal:no-keys>
> - </p>
> -
> - <div metal:use-macro="context/@@launchpad_form/form">
> -
> - <metal:formbody fill-slot="widgets">
> -
> - <table class="form">
> -
> - <tr>
> - <td>
> - <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/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>
> -
> - <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" />
> -
> - </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/registry/tests/test_product.py'
> --- lib/lp/registry/tests/test_product.py 2015-05-13 05:03:33 +0000
> +++ lib/lp/registry/tests/test_product.py 2015-06-24 08:54:06 +0000
> @@ -65,6 +65,7 @@
> SharingPermission,
> SpecificationSharingPolicy,
> TeamMembershipPolicy,
> + VCSType,
> )
> from lp.registry.errors import (
> CannotChangeInformationType,
> @@ -309,6 +310,13 @@
> [u'trunk', u'active-series'],
> [series.name for series in active_series])
>
> + def test_inferred_vcs(self):
> + """VCS is inferred correctly from existing branch or repo."""
> + git_repo = self.factory.makeGitRepository()
> + bzr_branch = self.factory.makeBranch()
> + self.assertEqual(VCSType.GIT, git_repo.target.inferred_vcs)
> + self.assertEqual(VCSType.BZR, bzr_branch.product.inferred_vcs)
> +
> def test_owner_cannot_be_open_team(self):
> """Product owners cannot be open teams."""
> for policy in INCLUSIVE_TEAM_POLICY:
>
> === modified file 'lib/lp/testing/factory.py'
> --- lib/lp/testing/factory.py 2015-06-15 09:51:32 +0000
> +++ lib/lp/testing/factory.py 2015-06-24 08:54:06 +0000
> @@ -1222,6 +1222,15 @@
> product = self.makeProduct()
> return self.makeBranch(product=product, **kwargs)
>
> + def makeProductGitRepository(self, project=None, **kwargs):
> + """Make a git repository on an arbitrary project.
> +
> + See `makeGitRepository` for more information on arguments.
> + """
> + if project is None:
> + project = self.makeProduct()
> + return self.makeGitRepository(target=project, **kwargs)
This isn't much shorter than makeGitRepository(target=self.factory.makeProduct()). I don't think it's worth it.
> +
> def makeAnyBranch(self, **kwargs):
> """Make a branch without caring about its container.
>
>
--
https://code.launchpad.net/~blr/launchpad/ui-project-setbranch/+merge/259069
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.
References