Merge lp:~blr/launchpad/ui-project-setbranch into lp:launchpad
- ui-project-setbranch
- Merge into devel
| Status: | Superseded |
|---|---|
| Proposed branch: | lp:~blr/launchpad/ui-project-setbranch |
| Merge into: | lp:launchpad |
| Diff against target: |
2209 lines (+1213/-478) 27 files modified
LICENSE (+276/-0) lib/canonical/launchpad/icing/inline-sprites-1.css.in (+5/-1) lib/canonical/launchpad/icing/style.css (+37/-4) lib/lp/code/browser/branchlisting.py (+5/-6) lib/lp/code/browser/configure.zcml (+28/-1) lib/lp/code/javascript/productseries-setbranch.js (+41/-0) lib/lp/code/model/gitrepository.py (+2/-2) lib/lp/code/model/tests/test_gitrepository.py (+11/-0) lib/lp/code/stories/codeimport/xx-create-codeimport.txt (+2/-1) lib/lp/code/templates/configure-code-macros.pt (+39/-0) lib/lp/code/templates/configure-code.pt (+165/-0) lib/lp/code/templates/product-branch-summary.pt (+1/-2) lib/lp/code/vocabularies/configure.zcml (+11/-0) lib/lp/code/vocabularies/gitrepository.py (+19/-0) lib/lp/code/vocabularies/tests/test_gitrepository_vocabularies.py (+60/-1) lib/lp/registry/browser/configure.zcml (+0/-7) lib/lp/registry/browser/product.py (+414/-8) lib/lp/registry/browser/productseries.py (+12/-315) lib/lp/registry/browser/tests/productseries-setbranch-view.txt (+2/-2) lib/lp/registry/browser/tests/test_product.py (+7/-0) lib/lp/registry/browser/tests/test_product_views.py (+36/-0) lib/lp/registry/interfaces/product.py (+8/-0) lib/lp/registry/model/product.py (+15/-0) lib/lp/registry/stories/product/xx-product-development-focus.txt (+4/-3) lib/lp/registry/templates/product-index.pt (+5/-0) lib/lp/registry/templates/productseries-setbranch.pt (+0/-125) lib/lp/registry/tests/test_product.py (+8/-0) |
| To merge this branch: | bzr merge lp:~blr/launchpad/ui-project-setbranch |
| Related bugs: |
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| William Grant | code | 2015-05-14 | Approve on 2015-06-24 |
|
Review via email:
|
|||
This proposal has been superseded by a proposal from 2015-06-25.
Commit Message
Add Product.
Description of the Change
Provides a new SetBranch view for projects, additionally allowing a default version control system to be defined.
A project's vcs will also be displayed on the product index under Project Information. If not default vcs has been provided, the vcs type is inferred from existing bzr or git branches, with git taking precedence.
Fixes bug in setTargetDefault where an exception was thrown when attempting to set the same targetdefault again.
| William Grant (wgrant) wrote : | # |
Just one last thing: when I suggested that is_series didn't need to be a property, I meant that it could be a plain attribute. Both implements return a constant value.
Preview Diff
| 1 | === modified file 'LICENSE' |
| 2 | --- LICENSE 2015-03-24 13:36:23 +0000 |
| 3 | +++ LICENSE 2015-06-25 03:34:41 +0000 |
| 4 | @@ -13,6 +13,8 @@ |
| 5 | The Launchpad name and logo are trademarks of Canonical, and may not |
| 6 | be used without the prior written permission of Canonical. |
| 7 | |
| 8 | +Git SCM logos are licensed Creative Commons Attribution 3.0 Unported. |
| 9 | + |
| 10 | Third-party copyright in this distribution is noted where applicable. |
| 11 | |
| 12 | All rights not expressly granted are reserved. |
| 13 | @@ -683,3 +685,277 @@ |
| 14 | <http://www.gnu.org/licenses/>. |
| 15 | |
| 16 | ========================================================================= |
| 17 | + |
| 18 | + |
| 19 | +Creative Commons Attribution 3.0 Unported License |
| 20 | + |
| 21 | +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS |
| 22 | +CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS |
| 23 | +PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE |
| 24 | +WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS |
| 25 | +PROHIBITED. |
| 26 | + |
| 27 | +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND |
| 28 | +AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS |
| 29 | +LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU |
| 30 | +THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH |
| 31 | +TERMS AND CONDITIONS. |
| 32 | + |
| 33 | +1. Definitions |
| 34 | + |
| 35 | +"Adaptation" means a work based upon the Work, or upon the Work and |
| 36 | +other pre-existing works, such as a translation, adaptation, |
| 37 | +derivative work, arrangement of music or other alterations of a |
| 38 | +literary or artistic work, or phonogram or performance and includes |
| 39 | +cinematographic adaptations or any other form in which the Work may be |
| 40 | +recast, transformed, or adapted including in any form recognizably |
| 41 | +derived from the original, except that a work that constitutes a |
| 42 | +Collection will not be considered an Adaptation for the purpose of |
| 43 | +this License. For the avoidance of doubt, where the Work is a musical |
| 44 | +work, performance or phonogram, the synchronization of the Work in |
| 45 | +timed-relation with a moving image ("synching") will be considered an |
| 46 | +Adaptation for the purpose of this License. "Collection" means a |
| 47 | +collection of literary or artistic works, such as encyclopedias and |
| 48 | +anthologies, or performances, phonograms or broadcasts, or other works |
| 49 | +or subject matter other than works listed in Section 1(f) below, |
| 50 | +which, by reason of the selection and arrangement of their contents, |
| 51 | +constitute intellectual creations, in which the Work is included in |
| 52 | +its entirety in unmodified form along with one or more other |
| 53 | +contributions, each constituting separate and independent works in |
| 54 | +themselves, which together are assembled into a collective whole. A |
| 55 | +work that constitutes a Collection will not be considered an |
| 56 | +Adaptation (as defined above) for the purposes of this License. |
| 57 | +"Distribute" means to make available to the public the original and |
| 58 | +copies of the Work or Adaptation, as appropriate, through sale or |
| 59 | +other transfer of ownership. "Licensor" means the individual, |
| 60 | +individuals, entity or entities that offer(s) the Work under the terms |
| 61 | +of this License. "Original Author" means, in the case of a literary or |
| 62 | +artistic work, the individual, individuals, entity or entities who |
| 63 | +created the Work or if no individual or entity can be identified, the |
| 64 | +publisher; and in addition (i) in the case of a performance the |
| 65 | +actors, singers, musicians, dancers, and other persons who act, sing, |
| 66 | +deliver, declaim, play in, interpret or otherwise perform literary or |
| 67 | +artistic works or expressions of folklore; (ii) in the case of a |
| 68 | +phonogram the producer being the person or legal entity who first |
| 69 | +fixes the sounds of a performance or other sounds; and, (iii) in the |
| 70 | +case of broadcasts, the organization that transmits the broadcast. |
| 71 | +"Work" means the literary and/or artistic work offered under the terms |
| 72 | +of this License including without limitation any production in the |
| 73 | +literary, scientific and artistic domain, whatever may be the mode or |
| 74 | +form of its expression including digital form, such as a book, |
| 75 | +pamphlet and other writing; a lecture, address, sermon or other work |
| 76 | +of the same nature; a dramatic or dramatico-musical work; a |
| 77 | +choreographic work or entertainment in dumb show; a musical |
| 78 | +composition with or without words; a cinematographic work to which are |
| 79 | +assimilated works expressed by a process analogous to cinematography; |
| 80 | +a work of drawing, painting, architecture, sculpture, engraving or |
| 81 | +lithography; a photographic work to which are assimilated works |
| 82 | +expressed by a process analogous to photography; a work of applied |
| 83 | +art; an illustration, map, plan, sketch or three-dimensional work |
| 84 | +relative to geography, topography, architecture or science; a |
| 85 | +performance; a broadcast; a phonogram; a compilation of data to the |
| 86 | +extent it is protected as a copyrightable work; or a work performed by |
| 87 | +a variety or circus performer to the extent it is not otherwise |
| 88 | +considered a literary or artistic work. "You" means an individual or |
| 89 | +entity exercising rights under this License who has not previously |
| 90 | +violated the terms of this License with respect to the Work, or who |
| 91 | +has received express permission from the Licensor to exercise rights |
| 92 | +under this License despite a previous violation. "Publicly Perform" |
| 93 | +means to perform public recitations of the Work and to communicate to |
| 94 | +the public those public recitations, by any means or process, |
| 95 | +including by wire or wireless means or public digital performances; to |
| 96 | +make available to the public Works in such a way that members of the |
| 97 | +public may access these Works from a place and at a place individually |
| 98 | +chosen by them; to perform the Work to the public by any means or |
| 99 | +process and the communication to the public of the performances of the |
| 100 | +Work, including by public digital performance; to broadcast and |
| 101 | +rebroadcast the Work by any means including signs, sounds or images. |
| 102 | +"Reproduce" means to make copies of the Work by any means including |
| 103 | +without limitation by sound or visual recordings and the right of |
| 104 | +fixation and reproducing fixations of the Work, including storage of a |
| 105 | +protected performance or phonogram in digital form or other electronic |
| 106 | +medium. 2. Fair Dealing Rights. Nothing in this License is intended to |
| 107 | +reduce, limit, or restrict any uses free from copyright or rights |
| 108 | +arising from limitations or exceptions that are provided for in |
| 109 | +connection with the copyright protection under copyright law or other |
| 110 | +applicable laws. |
| 111 | + |
| 112 | +3. License Grant. Subject to the terms and conditions of this License, |
| 113 | +Licensor hereby grants You a worldwide, royalty-free, non-exclusive, |
| 114 | +perpetual (for the duration of the applicable copyright) license to |
| 115 | +exercise the rights in the Work as stated below: |
| 116 | + |
| 117 | +to Reproduce the Work, to incorporate the Work into one or more |
| 118 | +Collections, and to Reproduce the Work as incorporated in the |
| 119 | +Collections; to create and Reproduce Adaptations provided that any |
| 120 | +such Adaptation, including any translation in any medium, takes |
| 121 | +reasonable steps to clearly label, demarcate or otherwise identify |
| 122 | +that changes were made to the original Work. For example, a |
| 123 | +translation could be marked "The original work was translated from |
| 124 | +English to Spanish," or a modification could indicate "The original |
| 125 | +work has been modified."; to Distribute and Publicly Perform the Work |
| 126 | +including as incorporated in Collections; and, to Distribute and |
| 127 | +Publicly Perform Adaptations. For the avoidance of doubt: |
| 128 | + |
| 129 | +Non-waivable Compulsory License Schemes. In those jurisdictions in |
| 130 | +which the right to collect royalties through any statutory or |
| 131 | +compulsory licensing scheme cannot be waived, the Licensor reserves |
| 132 | +the exclusive right to collect such royalties for any exercise by You |
| 133 | +of the rights granted under this License; Waivable Compulsory License |
| 134 | +Schemes. In those jurisdictions in which the right to collect |
| 135 | +royalties through any statutory or compulsory licensing scheme can be |
| 136 | +waived, the Licensor waives the exclusive right to collect such |
| 137 | +royalties for any exercise by You of the rights granted under this |
| 138 | +License; and, Voluntary License Schemes. The Licensor waives the right |
| 139 | +to collect royalties, whether individually or, in the event that the |
| 140 | +Licensor is a member of a collecting society that administers |
| 141 | +voluntary licensing schemes, via that society, from any exercise by |
| 142 | +You of the rights granted under this License. The above rights may be |
| 143 | +exercised in all media and formats whether now known or hereafter |
| 144 | +devised. The above rights include the right to make such modifications |
| 145 | +as are technically necessary to exercise the rights in other media and |
| 146 | +formats. Subject to Section 8(f), all rights not expressly granted by |
| 147 | +Licensor are hereby reserved. |
| 148 | + |
| 149 | +4. Restrictions. The license granted in Section 3 above is expressly |
| 150 | +made subject to and limited by the following restrictions: |
| 151 | + |
| 152 | +You may Distribute or Publicly Perform the Work only under the terms |
| 153 | +of this License. You must include a copy of, or the Uniform Resource |
| 154 | +Identifier (URI) for, this License with every copy of the Work You |
| 155 | +Distribute or Publicly Perform. You may not offer or impose any terms |
| 156 | +on the Work that restrict the terms of this License or the ability of |
| 157 | +the recipient of the Work to exercise the rights granted to that |
| 158 | +recipient under the terms of the License. You may not sublicense the |
| 159 | +Work. You must keep intact all notices that refer to this License and |
| 160 | +to the disclaimer of warranties with every copy of the Work You |
| 161 | +Distribute or Publicly Perform. When You Distribute or Publicly |
| 162 | +Perform the Work, You may not impose any effective technological |
| 163 | +measures on the Work that restrict the ability of a recipient of the |
| 164 | +Work from You to exercise the rights granted to that recipient under |
| 165 | +the terms of the License. This Section 4(a) applies to the Work as |
| 166 | +incorporated in a Collection, but this does not require the Collection |
| 167 | +apart from the Work itself to be made subject to the terms of this |
| 168 | +License. If You create a Collection, upon notice from any Licensor You |
| 169 | +must, to the extent practicable, remove from the Collection any credit |
| 170 | +as required by Section 4(b), as requested. If You create an |
| 171 | +Adaptation, upon notice from any Licensor You must, to the extent |
| 172 | +practicable, remove from the Adaptation any credit as required by |
| 173 | +Section 4(b), as requested. If You Distribute, or Publicly Perform the |
| 174 | +Work or any Adaptations or Collections, You must, unless a request has |
| 175 | +been made pursuant to Section 4(a), keep intact all copyright notices |
| 176 | +for the Work and provide, reasonable to the medium or means You are |
| 177 | +utilizing: (i) the name of the Original Author (or pseudonym, if |
| 178 | +applicable) if supplied, and/or if the Original Author and/or Licensor |
| 179 | +designate another party or parties (e.g., a sponsor institute, |
| 180 | +publishing entity, journal) for attribution ("Attribution Parties") in |
| 181 | +Licensor's copyright notice, terms of service or by other reasonable |
| 182 | +means, the name of such party or parties; (ii) the title of the Work |
| 183 | +if supplied; (iii) to the extent reasonably practicable, the URI, if |
| 184 | +any, that Licensor specifies to be associated with the Work, unless |
| 185 | +such URI does not refer to the copyright notice or licensing |
| 186 | +information for the Work; and (iv) , consistent with Section 3(b), in |
| 187 | +the case of an Adaptation, a credit identifying the use of the Work in |
| 188 | +the Adaptation (e.g., "French translation of the Work by Original |
| 189 | +Author," or "Screenplay based on original Work by Original Author"). |
| 190 | +The credit required by this Section 4 (b) may be implemented in any |
| 191 | +reasonable manner; provided, however, that in the case of a Adaptation |
| 192 | +or Collection, at a minimum such credit will appear, if a credit for |
| 193 | +all contributing authors of the Adaptation or Collection appears, then |
| 194 | +as part of these credits and in a manner at least as prominent as the |
| 195 | +credits for the other contributing authors. For the avoidance of |
| 196 | +doubt, You may only use the credit required by this Section for the |
| 197 | +purpose of attribution in the manner set out above and, by exercising |
| 198 | +Your rights under this License, You may not implicitly or explicitly |
| 199 | +assert or imply any connection with, sponsorship or endorsement by the |
| 200 | +Original Author, Licensor and/or Attribution Parties, as appropriate, |
| 201 | +of You or Your use of the Work, without the separate, express prior |
| 202 | +written permission of the Original Author, Licensor and/or Attribution |
| 203 | +Parties. Except as otherwise agreed in writing by the Licensor or as |
| 204 | +may be otherwise permitted by applicable law, if You Reproduce, |
| 205 | +Distribute or Publicly Perform the Work either by itself or as part of |
| 206 | +any Adaptations or Collections, You must not distort, mutilate, modify |
| 207 | +or take other derogatory action in relation to the Work which would be |
| 208 | +prejudicial to the Original Author's honor or reputation. Licensor |
| 209 | +agrees that in those jurisdictions (e.g. Japan), in which any exercise |
| 210 | +of the right granted in Section 3(b) of this License (the right to |
| 211 | +make Adaptations) would be deemed to be a distortion, mutilation, |
| 212 | +modification or other derogatory action prejudicial to the Original |
| 213 | +Author's honor and reputation, the Licensor will waive or not assert, |
| 214 | +as appropriate, this Section, to the fullest extent permitted by the |
| 215 | +applicable national law, to enable You to reasonably exercise Your |
| 216 | +right under Section 3(b) of this License (right to make Adaptations) |
| 217 | +but not otherwise. 5. Representations, Warranties and Disclaimer |
| 218 | + |
| 219 | +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, |
| 220 | +LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR |
| 221 | +WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, |
| 222 | +STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF |
| 223 | +TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, |
| 224 | +NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, |
| 225 | +OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. |
| 226 | +SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, |
| 227 | +SO SUCH EXCLUSION MAY NOT APPLY TO YOU. |
| 228 | + |
| 229 | +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY |
| 230 | +APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY |
| 231 | +LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR |
| 232 | +EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, |
| 233 | +EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. |
| 234 | + |
| 235 | +7. Termination |
| 236 | + |
| 237 | +This License and the rights granted hereunder will terminate |
| 238 | +automatically upon any breach by You of the terms of this License. |
| 239 | +Individuals or entities who have received Adaptations or Collections |
| 240 | +from You under this License, however, will not have their licenses |
| 241 | +terminated provided such individuals or entities remain in full |
| 242 | +compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will |
| 243 | +survive any termination of this License. Subject to the above terms |
| 244 | +and conditions, the license granted here is perpetual (for the |
| 245 | +duration of the applicable copyright in the Work). Notwithstanding the |
| 246 | +above, Licensor reserves the right to release the Work under different |
| 247 | +license terms or to stop distributing the Work at any time; provided, |
| 248 | +however that any such election will not serve to withdraw this License |
| 249 | +(or any other license that has been, or is required to be, granted |
| 250 | +under the terms of this License), and this License will continue in |
| 251 | +full force and effect unless terminated as stated above. 8. |
| 252 | +Miscellaneous |
| 253 | + |
| 254 | +Each time You Distribute or Publicly Perform the Work or a Collection, |
| 255 | +the Licensor offers to the recipient a license to the Work on the same |
| 256 | +terms and conditions as the license granted to You under this License. |
| 257 | +Each time You Distribute or Publicly Perform an Adaptation, Licensor |
| 258 | +offers to the recipient a license to the original Work on the same |
| 259 | +terms and conditions as the license granted to You under this License. |
| 260 | +If any provision of this License is invalid or unenforceable under |
| 261 | +applicable law, it shall not affect the validity or enforceability of |
| 262 | +the remainder of the terms of this License, and without further action |
| 263 | +by the parties to this agreement, such provision shall be reformed to |
| 264 | +the minimum extent necessary to make such provision valid and |
| 265 | +enforceable. No term or provision of this License shall be deemed |
| 266 | +waived and no breach consented to unless such waiver or consent shall |
| 267 | +be in writing and signed by the party to be charged with such waiver |
| 268 | +or consent. This License constitutes the entire agreement between the |
| 269 | +parties with respect to the Work licensed here. There are no |
| 270 | +understandings, agreements or representations with respect to the Work |
| 271 | +not specified here. Licensor shall not be bound by any additional |
| 272 | +provisions that may appear in any communication from You. This License |
| 273 | +may not be modified without the mutual written agreement of the |
| 274 | +Licensor and You. The rights granted under, and the subject matter |
| 275 | +referenced, in this License were drafted utilizing the terminology of |
| 276 | +the Berne Convention for the Protection of Literary and Artistic Works |
| 277 | +(as amended on September 28, 1979), the Rome Convention of 1961, the |
| 278 | +WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms |
| 279 | +Treaty of 1996 and the Universal Copyright Convention (as revised on |
| 280 | +July 24, 1971). These rights and subject matter take effect in the |
| 281 | +relevant jurisdiction in which the License terms are sought to be |
| 282 | +enforced according to the corresponding provisions of the |
| 283 | +implementation of those treaty provisions in the applicable national |
| 284 | +law. If the standard suite of rights granted under applicable |
| 285 | +copyright law includes additional rights not granted under this |
| 286 | +License, such additional rights are deemed to be included in the |
| 287 | +License; this License is not intended to restrict the license of any |
| 288 | +rights under applicable law. |
| 289 | + |
| 290 | +========================================================================= |
| 291 | \ No newline at end of file |
| 292 | |
| 293 | === modified file 'lib/canonical/launchpad/icing/inline-sprites-1.css.in' |
| 294 | --- lib/canonical/launchpad/icing/inline-sprites-1.css.in 2015-03-24 13:36:23 +0000 |
| 295 | +++ lib/canonical/launchpad/icing/inline-sprites-1.css.in 2015-06-25 03:34:41 +0000 |
| 296 | @@ -84,7 +84,11 @@ |
| 297 | .branch { |
| 298 | background-image: url(/@@/branch.png); /* sprite-ref: icon-sprites */ |
| 299 | background-repeat: no-repeat; |
| 300 | - } |
| 301 | +} |
| 302 | +.gitbranch { |
| 303 | + background-image: url(/@@/gitbranch.png); /* sprite-ref: icon-sprites */ |
| 304 | + background-repeat: no-repeat; |
| 305 | +} |
| 306 | .distribution { |
| 307 | background-image: url(/@@/distribution.png); /* sprite-ref: icon-sprites */ |
| 308 | background-repeat: no-repeat; |
| 309 | |
| 310 | === modified file 'lib/canonical/launchpad/icing/style.css' |
| 311 | --- lib/canonical/launchpad/icing/style.css 2015-04-07 23:43:43 +0000 |
| 312 | +++ lib/canonical/launchpad/icing/style.css 2015-06-25 03:34:41 +0000 |
| 313 | @@ -64,10 +64,24 @@ |
| 314 | padding-left: 1em; |
| 315 | } |
| 316 | |
| 317 | +form label { |
| 318 | + font-weight: bold; |
| 319 | +} |
| 320 | + |
| 321 | form.primary.search { |
| 322 | margin-bottom: 2em; |
| 323 | } |
| 324 | |
| 325 | +.infobox { |
| 326 | + color: #666; |
| 327 | + display: inline-block; |
| 328 | + padding: 0.5em 0.5em 0.5em 2em; |
| 329 | + background: url(/@@/info) 0.5em 0.5em no-repeat; |
| 330 | + background-color: rgb(249, 249, 249); |
| 331 | + border: 1px solid #ddd; |
| 332 | + border-radius: 3px |
| 333 | +} |
| 334 | + |
| 335 | div#bugs-search-form.dynamic_bug_listing { |
| 336 | margin-bottom: 10px; |
| 337 | padding: 3px 0; |
| 338 | @@ -538,17 +552,36 @@ |
| 339 | |
| 340 | /* --- Code --- */ |
| 341 | |
| 342 | +code.command { |
| 343 | + background-color: #fff; |
| 344 | + border: 1px solid #ddd; |
| 345 | + border-radius: 3px; |
| 346 | + color: #626262; |
| 347 | + padding: 4px; |
| 348 | + font-family: "DejaVu Sans Mono", "Courier New", monospace; |
| 349 | + font-size: 1.05em; |
| 350 | +} |
| 351 | +code.command-block { |
| 352 | + display: block; |
| 353 | + margin-bottom: 1em; |
| 354 | + padding: 6px; |
| 355 | +} |
| 356 | +code.command-block:last-child { |
| 357 | + margin: 0; |
| 358 | +} |
| 359 | table.code { |
| 360 | margin-bottom: 1em; |
| 361 | - } |
| 362 | +} |
| 363 | table.code th { |
| 364 | text-align: left; |
| 365 | font-weight: bold; |
| 366 | - } |
| 367 | +} |
| 368 | table.code th, table.code td { |
| 369 | padding-right: 3em; |
| 370 | - } |
| 371 | - |
| 372 | +} |
| 373 | +#git-expander-content { |
| 374 | + margin-top: 1em; |
| 375 | +} |
| 376 | .branch-no-dev-focus { |
| 377 | background: #FFF59C; |
| 378 | vertical-align: middle; |
| 379 | |
| 380 | === added file 'lib/canonical/launchpad/images/gitbranch-large.png' |
| 381 | 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-25 03:34:41 +0000 differ |
| 382 | === added file 'lib/canonical/launchpad/images/gitbranch.png' |
| 383 | Binary files lib/canonical/launchpad/images/gitbranch.png 1970-01-01 00:00:00 +0000 and lib/canonical/launchpad/images/gitbranch.png 2015-06-25 03:34:41 +0000 differ |
| 384 | === modified file 'lib/lp/code/browser/branchlisting.py' |
| 385 | --- lib/lp/code/browser/branchlisting.py 2015-06-15 05:28:12 +0000 |
| 386 | +++ lib/lp/code/browser/branchlisting.py 2015-06-25 03:34:41 +0000 |
| 387 | @@ -1133,13 +1133,12 @@ |
| 388 | @property |
| 389 | def configure_codehosting(self): |
| 390 | """Get the menu link for configuring code hosting.""" |
| 391 | - if not check_permission( |
| 392 | - 'launchpad.Edit', self.context.development_focus): |
| 393 | + if not check_permission('launchpad.Edit', self.context): |
| 394 | return None |
| 395 | - series_menu = MenuAPI(self.context.development_focus).overview |
| 396 | - set_branch = series_menu['set_branch'] |
| 397 | - set_branch.text = 'Configure Code' |
| 398 | - return set_branch |
| 399 | + menu = MenuAPI(self.context).overview |
| 400 | + configure_code = menu['configure_code'] |
| 401 | + configure_code.text = 'Configure Code' |
| 402 | + return configure_code |
| 403 | |
| 404 | |
| 405 | class ProductBranchStatisticsView(BranchCountSummaryView, |
| 406 | |
| 407 | === modified file 'lib/lp/code/browser/configure.zcml' |
| 408 | --- lib/lp/code/browser/configure.zcml 2015-06-18 20:18:16 +0000 |
| 409 | +++ lib/lp/code/browser/configure.zcml 2015-06-25 03:34:41 +0000 |
| 410 | @@ -37,7 +37,34 @@ |
| 411 | template="../templates/code-in-branches.pt" |
| 412 | permission="zope.Public" |
| 413 | /> |
| 414 | - |
| 415 | + <browser:page |
| 416 | + name="+configure-code" |
| 417 | + for="lp.registry.interfaces.product.IProduct" |
| 418 | + class="lp.registry.browser.product.ProductSetBranchView" |
| 419 | + permission="launchpad.Edit" |
| 420 | + template="../templates/configure-code.pt" |
| 421 | + /> |
| 422 | + <browser:page |
| 423 | + name="+setbranch" |
| 424 | + for="lp.registry.interfaces.productseries.IProductSeries" |
| 425 | + class="lp.registry.browser.productseries.ProductSeriesSetBranchView" |
| 426 | + permission="launchpad.Edit" |
| 427 | + template="../templates/configure-code.pt" |
| 428 | + /> |
| 429 | + <browser:page |
| 430 | + name="+configure-code-macros" |
| 431 | + for="lp.registry.interfaces.productseries.IProductSeries" |
| 432 | + class="lp.app.browser.launchpad.Macro" |
| 433 | + permission="zope.Public" |
| 434 | + template="../templates/configure-code-macros.pt" |
| 435 | + /> |
| 436 | + <browser:page |
| 437 | + name="+configure-code-macros" |
| 438 | + for="lp.registry.interfaces.product.IProduct" |
| 439 | + class="lp.app.browser.launchpad.Macro" |
| 440 | + permission="zope.Public" |
| 441 | + template="../templates/configure-code-macros.pt" |
| 442 | + /> |
| 443 | <browser:page |
| 444 | for="lp.services.webapp.interfaces.ILaunchpadApplication" |
| 445 | name="+project-cloud" |
| 446 | |
| 447 | === modified file 'lib/lp/code/javascript/productseries-setbranch.js' |
| 448 | --- lib/lp/code/javascript/productseries-setbranch.js 2015-05-19 03:01:06 +0000 |
| 449 | +++ lib/lp/code/javascript/productseries-setbranch.js 2015-06-25 03:34:41 +0000 |
| 450 | @@ -24,6 +24,17 @@ |
| 451 | return selected; |
| 452 | }; |
| 453 | |
| 454 | + module._get_selected_default_vcs = function () { |
| 455 | + var vcs = document.getElementsByName('field.default_vcs'); |
| 456 | + var i; |
| 457 | + for (i = 0; i < vcs.length; i++) { |
| 458 | + if (vcs[i].checked) { |
| 459 | + return vcs[i].value; |
| 460 | + } |
| 461 | + } |
| 462 | + return null; |
| 463 | + }; |
| 464 | + |
| 465 | |
| 466 | module.__rcs_types = null; |
| 467 | |
| 468 | @@ -39,6 +50,32 @@ |
| 469 | field.disabled = !is_enabled; |
| 470 | }; |
| 471 | |
| 472 | + module.setup_expanders = function() { |
| 473 | + var git_expander = new Y.lp.app.widgets.expander.Expander( |
| 474 | + Y.one('#git-expander-icon'), Y.one('#git-expander-content') |
| 475 | + ); |
| 476 | + |
| 477 | + var bzr_expander = new Y.lp.app.widgets.expander.Expander( |
| 478 | + Y.one('#bzr-expander-icon'), Y.one('#bzr-expander-content') |
| 479 | + ); |
| 480 | + module.git_expander = git_expander.setUp(); |
| 481 | + module.bzr_expander = bzr_expander.setUp(); |
| 482 | + }; |
| 483 | + |
| 484 | + module.onclick_default_vcs = function(e) { |
| 485 | + /* Which project vcs was selected? */ |
| 486 | + var selectedDefaultVCS = |
| 487 | + module._get_selected_default_vcs(); |
| 488 | + |
| 489 | + if (selectedDefaultVCS === 'GIT') { |
| 490 | + module.git_expander.render(true); |
| 491 | + module.bzr_expander.render(false); |
| 492 | + } else { |
| 493 | + module.bzr_expander.render(true); |
| 494 | + module.git_expander.render(false); |
| 495 | + } |
| 496 | + }; |
| 497 | + |
| 498 | module.onclick_rcs_type = function(e) { |
| 499 | /* Which rcs type radio button has been selected? */ |
| 500 | // CVS |
| 501 | @@ -81,10 +118,14 @@ |
| 502 | 'click', module.onclick_rcs_type); |
| 503 | Y.all('input[name="field.branch_type"]').on( |
| 504 | 'click', module.onclick_branch_type); |
| 505 | + Y.all('input[name="field.default_vcs"]').on( |
| 506 | + 'click', module.onclick_default_vcs); |
| 507 | |
| 508 | // Set the initial state. |
| 509 | + module.setup_expanders(); |
| 510 | module.onclick_rcs_type(); |
| 511 | module.onclick_branch_type(); |
| 512 | + module.onclick_default_vcs(); |
| 513 | }; |
| 514 | |
| 515 | }, "0.1", {"requires": ["node", "DOM"]}); |
| 516 | |
| 517 | === modified file 'lib/lp/code/model/gitrepository.py' |
| 518 | --- lib/lp/code/model/gitrepository.py 2015-06-15 09:55:47 +0000 |
| 519 | +++ lib/lp/code/model/gitrepository.py 2015-06-25 03:34:41 +0000 |
| 520 | @@ -296,7 +296,7 @@ |
| 521 | repository_set = getUtility(IGitRepositorySet) |
| 522 | existing = repository_set.getDefaultRepositoryForOwner( |
| 523 | self.owner, self.target) |
| 524 | - if existing is not None: |
| 525 | + if existing is not None and existing != self: |
| 526 | raise GitDefaultConflict( |
| 527 | existing, self.target, owner=self.owner) |
| 528 | self.owner_default = value |
| 529 | @@ -307,7 +307,7 @@ |
| 530 | # Check for an existing target default. |
| 531 | existing = getUtility(IGitRepositorySet).getDefaultRepository( |
| 532 | self.target) |
| 533 | - if existing is not None: |
| 534 | + if existing is not None and existing != self: |
| 535 | raise GitDefaultConflict(existing, self.target) |
| 536 | self.target_default = value |
| 537 | if IProduct.providedBy(self.target): |
| 538 | |
| 539 | === modified file 'lib/lp/code/model/tests/test_gitrepository.py' |
| 540 | --- lib/lp/code/model/tests/test_gitrepository.py 2015-06-18 14:13:40 +0000 |
| 541 | +++ lib/lp/code/model/tests/test_gitrepository.py 2015-06-25 03:34:41 +0000 |
| 542 | @@ -1986,6 +1986,17 @@ |
| 543 | self.repository_set.setDefaultRepositoryForOwner, |
| 544 | person, person, repository, user) |
| 545 | |
| 546 | + def test_setDefaultRepository_for_same_targetdefault_noops(self): |
| 547 | + # If a repository is already the target default, |
| 548 | + # setting the defaultRepository again should no-op. |
| 549 | + project = self.factory.makeProduct() |
| 550 | + repository = self.factory.makeGitRepository(target=project) |
| 551 | + with person_logged_in(project.owner): |
| 552 | + self.repository_set.setDefaultRepository(project, repository) |
| 553 | + result = self.repository_set.setDefaultRepository( |
| 554 | + project, repository) |
| 555 | + self.assertEqual(None, result) |
| 556 | + |
| 557 | |
| 558 | class TestGitRepositorySetDefaultsMixin: |
| 559 | |
| 560 | |
| 561 | === modified file 'lib/lp/code/stories/codeimport/xx-create-codeimport.txt' |
| 562 | --- lib/lp/code/stories/codeimport/xx-create-codeimport.txt 2015-06-15 05:28:12 +0000 |
| 563 | +++ lib/lp/code/stories/codeimport/xx-create-codeimport.txt 2015-06-25 03:34:41 +0000 |
| 564 | @@ -38,11 +38,12 @@ |
| 565 | >>> owner_browser = setupBrowser(auth="Basic test@canonical.com:test") |
| 566 | >>> owner_browser.open('http://code.launchpad.dev/firefox') |
| 567 | >>> owner_browser.getLink('Configure Code').click() |
| 568 | + >>> owner_browser.getControl('Bazaar', index=0).click() |
| 569 | >>> owner_browser.getControl( |
| 570 | ... 'Import a branch').click() |
| 571 | >>> owner_browser.getControl('Branch URL').value = ( |
| 572 | ... 'git://example.com/firefox') |
| 573 | - >>> owner_browser.getControl('Git').click() |
| 574 | + >>> owner_browser.getControl('Git', index=1).click() |
| 575 | >>> owner_browser.getControl('Branch name').value = 'trunk' |
| 576 | >>> owner_browser.getControl('Update').click() |
| 577 | |
| 578 | |
| 579 | === added file 'lib/lp/code/templates/configure-code-macros.pt' |
| 580 | --- lib/lp/code/templates/configure-code-macros.pt 1970-01-01 00:00:00 +0000 |
| 581 | +++ lib/lp/code/templates/configure-code-macros.pt 2015-06-25 03:34:41 +0000 |
| 582 | @@ -0,0 +1,39 @@ |
| 583 | +<tal:root |
| 584 | + xmlns:tal="http://xml.zope.org/namespaces/tal" |
| 585 | + xmlns:metal="http://xml.zope.org/namespaces/metal" |
| 586 | + xmlns:i18n="http://xml.zope.org/namespaces/i18n" |
| 587 | + omit-tag=""> |
| 588 | + |
| 589 | + <div metal:define-macro="push-instructions-bzr" id="push-instructions-bzr" class="scm-tip"> |
| 590 | + <h3>Push a new branch</h3> |
| 591 | + <p>You can push a Bazaar branch directly to Launchpad with the command:</p> |
| 592 | + <p> |
| 593 | + <code class="command command-block"> |
| 594 | + bzr push lp:<tal:project replace="context/name"/> |
| 595 | + </code> |
| 596 | + </p> |
| 597 | + </div> |
| 598 | + |
| 599 | + <div metal:define-macro="push-instructions-git" id="push-instructions-git" class="scm-tip"> |
| 600 | + <h3>Push a new repository</h3> |
| 601 | + <p>You can add a remote for your Git repository with the |
| 602 | + command:</p> |
| 603 | + <p> |
| 604 | + <code class="command command-block"> |
| 605 | + git remote add origin <tal:project replace="modules/lp.services.config/config/codehosting/git_ssh_root"/><tal:project replace="context/name"/><br /> |
| 606 | + </code></p> |
| 607 | + <p>… and push the Git branch to Launchpad with:</p> |
| 608 | + <p> |
| 609 | + <code class="command command-block"> |
| 610 | + git push origin master |
| 611 | + </code> |
| 612 | + </p> |
| 613 | + </div> |
| 614 | + |
| 615 | + <div metal:define-macro="no-keys" condition="not:view/user/sshkeys"> |
| 616 | + <p class="infobox">To authenticate with the Launchpad branch upload service, you need to |
| 617 | + <a tal:attributes="href string:${view/user/fmt:url}/+editsshkeys"> |
| 618 | + register an SSH key</a>.</p> |
| 619 | + </div> |
| 620 | + |
| 621 | +</tal:root> |
| 622 | |
| 623 | === added file 'lib/lp/code/templates/configure-code.pt' |
| 624 | --- lib/lp/code/templates/configure-code.pt 1970-01-01 00:00:00 +0000 |
| 625 | +++ lib/lp/code/templates/configure-code.pt 2015-06-25 03:34:41 +0000 |
| 626 | @@ -0,0 +1,165 @@ |
| 627 | +<html |
| 628 | + xmlns="http://www.w3.org/1999/xhtml" |
| 629 | + xmlns:tal="http://xml.zope.org/namespaces/tal" |
| 630 | + xmlns:metal="http://xml.zope.org/namespaces/metal" |
| 631 | + xmlns:i18n="http://xml.zope.org/namespaces/i18n" |
| 632 | + metal:use-macro="view/macro:page/main_only" |
| 633 | + i18n:domain="launchpad"> |
| 634 | + |
| 635 | + <body> |
| 636 | + |
| 637 | + <metal:block fill-slot="head_epilogue"> |
| 638 | + <style type="text/css"> |
| 639 | + .subordinate { |
| 640 | + margin: 0.5em 0 0.5em 4em; |
| 641 | + } |
| 642 | + </style> |
| 643 | + </metal:block> |
| 644 | + |
| 645 | + <div metal:fill-slot="main"> |
| 646 | + |
| 647 | + <div metal:use-macro="context/@@launchpad_form/form"> |
| 648 | + |
| 649 | + <metal:formbody fill-slot="widgets"> |
| 650 | + <tal:block condition="not: view/is_series"> |
| 651 | + <h3>Version control system</h3> |
| 652 | + <div id="default_vcs"> |
| 653 | + <ul> |
| 654 | + <li> |
| 655 | + <label tal:replace="structure view/default_vcs_bzr"> |
| 656 | + Bazaar |
| 657 | + </label> |
| 658 | + </li> |
| 659 | + <li> |
| 660 | + <label tal:replace="structure view/default_vcs_git"> |
| 661 | + Git |
| 662 | + </label> <div style="display: inline" |
| 663 | + class="beta"><img alt="[BETA]" src="/@@/beta" /></div> |
| 664 | + </li> |
| 665 | + </ul> |
| 666 | + <p>Your project may have both Git repositories and Bazaar branches. |
| 667 | + </p> |
| 668 | + </div> |
| 669 | + </tal:block> |
| 670 | + |
| 671 | + <div id="show-hide-bzr"> |
| 672 | + <a href="#" id="bzr-expander-icon" class="expander-icon js-action"> |
| 673 | + <span class="sprite branch">Bazaar settings</span> |
| 674 | + </a> |
| 675 | + <div id="bzr-expander-content"> |
| 676 | + <div class="push-instructions"> |
| 677 | + <div metal:use-macro="context/@@+configure-code-macros/push-instructions-bzr"></div> |
| 678 | + <div metal:use-macro="context/@@+configure-code-macros/no-keys"></div> |
| 679 | + </div> |
| 680 | + |
| 681 | + <table id="form_bzr" class="form"> |
| 682 | + <tr> |
| 683 | + <td> |
| 684 | + <h3>Link or import an existing branch</h3> |
| 685 | + <label tal:replace="structure view/branch_type_link"> |
| 686 | + Link to a Bazaar branch already on Launchpad |
| 687 | + </label> |
| 688 | + <table class="subordinate"> |
| 689 | + <tal:widget define="widget nocall:view/widgets/branch_location"> |
| 690 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
| 691 | + </tal:widget> |
| 692 | + </table> |
| 693 | + </td> |
| 694 | + </tr> |
| 695 | + |
| 696 | + <tr id="branch_mirror"> |
| 697 | + <td> |
| 698 | + <label tal:replace="structure view/branch_type_import"> |
| 699 | + Import a branch hosted somewhere else |
| 700 | + </label> |
| 701 | + <table class="subordinate"> |
| 702 | + <tal:widget define="widget nocall:view/widgets/branch_name"> |
| 703 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
| 704 | + </tal:widget> |
| 705 | + <tal:widget define="widget nocall:view/widgets/branch_owner"> |
| 706 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
| 707 | + </tal:widget> |
| 708 | + |
| 709 | + <tal:widget define="widget nocall:view/widgets/repo_url"> |
| 710 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
| 711 | + </tal:widget> |
| 712 | + |
| 713 | + <tr> |
| 714 | + <td> |
| 715 | + <label tal:replace="structure view/rcs_type_bzr"> |
| 716 | + Bazaar, hosted externally |
| 717 | + </label> |
| 718 | + </td> |
| 719 | + </tr> |
| 720 | + |
| 721 | + <tr> |
| 722 | + <td> |
| 723 | + <label tal:replace="structure view/rcs_type_git"> |
| 724 | + Git |
| 725 | + </label> |
| 726 | + </td> |
| 727 | + </tr> |
| 728 | + |
| 729 | + <tr> |
| 730 | + <td> |
| 731 | + <label tal:replace="structure view/rcs_type_svn"> |
| 732 | + SVN |
| 733 | + </label> |
| 734 | + </td> |
| 735 | + </tr> |
| 736 | + |
| 737 | + <tr> |
| 738 | + <td> |
| 739 | + <label tal:replace="structure view/rcs_type_cvs"> |
| 740 | + CVS |
| 741 | + </label> |
| 742 | + <table class="subordinate"> |
| 743 | + <tal:widget define="widget nocall:view/widgets/cvs_module"> |
| 744 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
| 745 | + </tal:widget> |
| 746 | + </table> |
| 747 | + </td> |
| 748 | + </tr> |
| 749 | + </table> |
| 750 | + </td> |
| 751 | + </tr> |
| 752 | + </table> |
| 753 | + </div> |
| 754 | + <input tal:replace="structure view/rcs_type_emptymarker" /> |
| 755 | + </div> |
| 756 | + |
| 757 | + <tal:block condition="not: view/is_series"> |
| 758 | + <div id="show-hide-git"> |
| 759 | + <a href="#" id="git-expander-icon" class="expander-icon js-action"> |
| 760 | + <span class="sprite gitbranch">Git settings</span> |
| 761 | + </a> |
| 762 | + <div id="git-expander-content"> |
| 763 | + <div id="form_git" class="form"> |
| 764 | + <div class="push-instructions"> |
| 765 | + <div metal:use-macro="context/@@+configure-code-macros/push-instructions-git"></div> |
| 766 | + <div metal:use-macro="context/@@+configure-code-macros/no-keys"></div> |
| 767 | + </div> |
| 768 | + |
| 769 | + <h3>Link an existing repository</h3> |
| 770 | + <p>Link to an existing Git repository already on Launchpad</p> |
| 771 | + <tal:widget define="widget nocall:view/widgets/git_repository_location"> |
| 772 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
| 773 | + </tal:widget> |
| 774 | + </div> |
| 775 | + </div> |
| 776 | + </div> |
| 777 | + </tal:block> |
| 778 | + |
| 779 | + |
| 780 | + </metal:formbody> |
| 781 | + </div> |
| 782 | + |
| 783 | + <script type="text/javascript"> |
| 784 | + LPJS.use('lp.code.productseries_setbranch', function(Y) { |
| 785 | + Y.on('domready', Y.lp.code.productseries_setbranch.setup); |
| 786 | + }); |
| 787 | + </script> |
| 788 | + |
| 789 | + </div> |
| 790 | + </body> |
| 791 | +</html> |
| 792 | |
| 793 | === modified file 'lib/lp/code/templates/product-branch-summary.pt' |
| 794 | --- lib/lp/code/templates/product-branch-summary.pt 2015-06-04 06:54:09 +0000 |
| 795 | +++ lib/lp/code/templates/product-branch-summary.pt 2015-06-25 03:34:41 +0000 |
| 796 | @@ -56,8 +56,7 @@ |
| 797 | </p> |
| 798 | </div> |
| 799 | |
| 800 | - <tal:no-branches |
| 801 | - condition="not: view/branch_count"> |
| 802 | + <tal:no-branches condition="not: view/branch_count"> |
| 803 | There are no branches for <tal:project-name replace="context/displayname"/> |
| 804 | in Launchpad. |
| 805 | <tal:can-configure condition="view/can_configure_branches"> |
| 806 | |
| 807 | === modified file 'lib/lp/code/vocabularies/configure.zcml' |
| 808 | --- lib/lp/code/vocabularies/configure.zcml 2015-04-13 19:02:15 +0000 |
| 809 | +++ lib/lp/code/vocabularies/configure.zcml 2015-06-25 03:34:41 +0000 |
| 810 | @@ -66,4 +66,15 @@ |
| 811 | <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary"/> |
| 812 | </class> |
| 813 | |
| 814 | + <securedutility |
| 815 | + name="GitRepositoryRestrictedOnProduct" |
| 816 | + component=".gitrepository.GitRepositoryRestrictedOnProductVocabulary" |
| 817 | + provides="zope.schema.interfaces.IVocabularyFactory"> |
| 818 | + <allow interface="zope.schema.interfaces.IVocabularyFactory"/> |
| 819 | + </securedutility> |
| 820 | + |
| 821 | + <class class=".gitrepository.GitRepositoryRestrictedOnProductVocabulary"> |
| 822 | + <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary"/> |
| 823 | + </class> |
| 824 | + |
| 825 | </configure> |
| 826 | |
| 827 | === modified file 'lib/lp/code/vocabularies/gitrepository.py' |
| 828 | --- lib/lp/code/vocabularies/gitrepository.py 2015-04-28 15:22:46 +0000 |
| 829 | +++ lib/lp/code/vocabularies/gitrepository.py 2015-06-25 03:34:41 +0000 |
| 830 | @@ -6,6 +6,7 @@ |
| 831 | __metaclass__ = type |
| 832 | |
| 833 | __all__ = [ |
| 834 | + 'GitRepositoryRestrictedOnProductVocabulary', |
| 835 | 'GitRepositoryVocabulary', |
| 836 | ] |
| 837 | |
| 838 | @@ -15,6 +16,7 @@ |
| 839 | |
| 840 | from lp.code.interfaces.gitcollection import IAllGitRepositories |
| 841 | from lp.code.model.gitrepository import GitRepository |
| 842 | +from lp.registry.interfaces.product import IProduct |
| 843 | from lp.services.webapp.interfaces import ILaunchBag |
| 844 | from lp.services.webapp.vocabulary import ( |
| 845 | CountableIterator, |
| 846 | @@ -62,3 +64,20 @@ |
| 847 | |
| 848 | def _getCollection(self): |
| 849 | return getUtility(IAllGitRepositories) |
| 850 | + |
| 851 | +class GitRepositoryRestrictedOnProductVocabulary(GitRepositoryVocabulary): |
| 852 | + """A vocabulary for searching git repositories restricted on product.""" |
| 853 | + |
| 854 | + def __init__(self, context): |
| 855 | + super(GitRepositoryRestrictedOnProductVocabulary, self).__init__( |
| 856 | + context) |
| 857 | + if IProduct.providedBy(self.context): |
| 858 | + self.product = self.context |
| 859 | + else: |
| 860 | + # An unexpected type. |
| 861 | + raise AssertionError('Unexpected context type') |
| 862 | + |
| 863 | + def _getCollection(self): |
| 864 | + return getUtility(IAllGitRepositories).inProject( |
| 865 | + self.product).isExclusive() |
| 866 | + |
| 867 | |
| 868 | === modified file 'lib/lp/code/vocabularies/tests/test_gitrepository_vocabularies.py' |
| 869 | --- lib/lp/code/vocabularies/tests/test_gitrepository_vocabularies.py 2015-05-14 13:57:51 +0000 |
| 870 | +++ lib/lp/code/vocabularies/tests/test_gitrepository_vocabularies.py 2015-06-25 03:34:41 +0000 |
| 871 | @@ -5,9 +5,15 @@ |
| 872 | |
| 873 | __metaclass__ = type |
| 874 | |
| 875 | -from lp.code.vocabularies.gitrepository import GitRepositoryVocabulary |
| 876 | +from zope.component import getUtility |
| 877 | + |
| 878 | +from lp.code.vocabularies.gitrepository import ( |
| 879 | + GitRepositoryRestrictedOnProductVocabulary, |
| 880 | + GitRepositoryVocabulary, |
| 881 | + ) |
| 882 | from lp.testing import TestCaseWithFactory |
| 883 | from lp.testing.layers import DatabaseFunctionalLayer |
| 884 | +from lp.registry.interfaces.product import IProductSet |
| 885 | |
| 886 | |
| 887 | class TestGitRepositoryVocabulary(TestCaseWithFactory): |
| 888 | @@ -50,3 +56,56 @@ |
| 889 | # If there are more than one search result, a LookupError is still |
| 890 | # raised. |
| 891 | self.assertRaises(LookupError, self.vocab.getTermByToken, "fizzbuzz") |
| 892 | + |
| 893 | + |
| 894 | +class TestRestrictedGitRepositoryVocabularyOnProduct(TestCaseWithFactory): |
| 895 | + """Test the GitRepositoryRestrictedOnProductVocabulary behaves as expected. |
| 896 | + |
| 897 | + When a GitRepositoryRestrictedOnProductVocabulary is used with a project, |
| 898 | + the project of the git repository in the vocabulary match the product give |
| 899 | + as the context. |
| 900 | + """ |
| 901 | + |
| 902 | + layer = DatabaseFunctionalLayer |
| 903 | + |
| 904 | + def setUp(self): |
| 905 | + super(TestRestrictedGitRepositoryVocabularyOnProduct, self).setUp() |
| 906 | + self._createRepositories() |
| 907 | + self.vocab = GitRepositoryRestrictedOnProductVocabulary( |
| 908 | + context=self._getVocabRestriction()) |
| 909 | + |
| 910 | + def _getVocabRestriction(self): |
| 911 | + """Restrict using the widget product.""" |
| 912 | + return getUtility(IProductSet).getByName('widget') |
| 913 | + |
| 914 | + def _createRepositories(self): |
| 915 | + test_product = self.factory.makeProduct(name='widget') |
| 916 | + other_product = self.factory.makeProduct(name='sprocket') |
| 917 | + person = self.factory.makePerson(name=u'scotty') |
| 918 | + self.factory.makeGitRepository( |
| 919 | + owner=person, target=test_product, name=u'mountain') |
| 920 | + self.factory.makeGitRepository( |
| 921 | + owner=person, target=other_product, name=u'mountain') |
| 922 | + self.product = test_product |
| 923 | + self.other_product = test_product |
| 924 | + |
| 925 | + def test_product_restriction(self): |
| 926 | + """Look for widget's target default repository. |
| 927 | + |
| 928 | + The result set should not show ~scotty/sprocket/mountain. |
| 929 | + """ |
| 930 | + results = self.vocab.searchForTerms('mountain') |
| 931 | + expected = [u'~scotty/widget/+git/mountain'] |
| 932 | + repo_names = sorted([repo.token for repo in results]) |
| 933 | + self.assertEqual(expected, repo_names) |
| 934 | + |
| 935 | + def test_singleQueryResult(self): |
| 936 | + # If there is a single search result that matches, use that |
| 937 | + # as the result. |
| 938 | + term = self.vocab.getTermByToken('mountain') |
| 939 | + self.assertEqual(u'~scotty/widget/+git/mountain', term.value.unique_name) |
| 940 | + |
| 941 | + def test_multipleQueryResult(self): |
| 942 | + # If there are more than one search result, a LookupError is still |
| 943 | + # raised. |
| 944 | + self.assertRaises(LookupError, self.vocab.getTermByToken, 'scotty') |
| 945 | |
| 946 | === modified file 'lib/lp/registry/browser/configure.zcml' |
| 947 | --- lib/lp/registry/browser/configure.zcml 2015-06-10 10:19:25 +0000 |
| 948 | +++ lib/lp/registry/browser/configure.zcml 2015-06-25 03:34:41 +0000 |
| 949 | @@ -2002,13 +2002,6 @@ |
| 950 | template="../templates/milestone-add.pt" |
| 951 | /> |
| 952 | <browser:page |
| 953 | - name="+setbranch" |
| 954 | - for="lp.registry.interfaces.productseries.IProductSeries" |
| 955 | - class="lp.registry.browser.productseries.ProductSeriesSetBranchView" |
| 956 | - permission="launchpad.Edit" |
| 957 | - template="../templates/productseries-setbranch.pt" |
| 958 | - /> |
| 959 | - <browser:page |
| 960 | name="+review" |
| 961 | for="lp.registry.interfaces.productseries.IProductSeries" |
| 962 | class="lp.registry.browser.productseries.ProductSeriesReviewView" |
| 963 | |
| 964 | === modified file 'lib/lp/registry/browser/product.py' |
| 965 | --- lib/lp/registry/browser/product.py 2015-06-15 05:28:12 +0000 |
| 966 | +++ lib/lp/registry/browser/product.py 2015-06-25 03:34:41 +0000 |
| 967 | @@ -1,4 +1,4 @@ |
| 968 | -# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
| 969 | +# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
| 970 | # GNU Affero General Public License version 3 (see the file LICENSE). |
| 971 | |
| 972 | """Browser views for products.""" |
| 973 | @@ -30,6 +30,7 @@ |
| 974 | 'ProductRdfView', |
| 975 | 'ProductReviewLicenseView', |
| 976 | 'ProductSeriesSetView', |
| 977 | + 'ProductSetBranchView', |
| 978 | 'ProductSetBreadcrumb', |
| 979 | 'ProductSetNavigation', |
| 980 | 'ProductSetReviewLicensesView', |
| 981 | @@ -44,8 +45,12 @@ |
| 982 | |
| 983 | from operator import attrgetter |
| 984 | |
| 985 | +from bzrlib.revision import NULL_REVISION |
| 986 | from lazr.delegates import delegates |
| 987 | -from lazr.restful.interface import copy_field |
| 988 | +from lazr.restful.interface import ( |
| 989 | + copy_field, |
| 990 | + use_template, |
| 991 | + ) |
| 992 | from lazr.restful.interfaces import IJSONRequestCache |
| 993 | from z3c.ptcompat import ViewPageTemplateFile |
| 994 | from zope.component import getUtility |
| 995 | @@ -66,6 +71,7 @@ |
| 996 | from zope.schema import ( |
| 997 | Bool, |
| 998 | Choice, |
| 999 | + TextLine, |
| 1000 | ) |
| 1001 | from zope.schema.vocabulary import ( |
| 1002 | SimpleTerm, |
| 1003 | @@ -80,6 +86,7 @@ |
| 1004 | custom_widget, |
| 1005 | LaunchpadEditFormView, |
| 1006 | LaunchpadFormView, |
| 1007 | + render_radio_widget_part, |
| 1008 | ReturnToReferrerMixin, |
| 1009 | safe_action, |
| 1010 | ) |
| 1011 | @@ -103,7 +110,10 @@ |
| 1012 | PUBLIC_PROPRIETARY_INFORMATION_TYPES, |
| 1013 | ServiceUsage, |
| 1014 | ) |
| 1015 | -from lp.app.errors import NotFoundError |
| 1016 | +from lp.app.errors import ( |
| 1017 | + NotFoundError, |
| 1018 | + UnexpectedFormData, |
| 1019 | + ) |
| 1020 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
| 1021 | from lp.app.utilities import json_dump_information_types |
| 1022 | from lp.app.vocabularies import InformationTypeVocabulary |
| 1023 | @@ -131,9 +141,28 @@ |
| 1024 | StructuralSubscriptionTargetTraversalMixin, |
| 1025 | ) |
| 1026 | from lp.bugs.interfaces.bugtask import RESOLVED_BUGTASK_STATUSES |
| 1027 | +from lp.code.browser.branch import BranchNameValidationMixin |
| 1028 | from lp.code.browser.branchref import BranchRef |
| 1029 | +from lp.code.browser.codeimport import validate_import_url |
| 1030 | from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin |
| 1031 | +from lp.code.enums import ( |
| 1032 | + BranchType, |
| 1033 | + RevisionControlSystems, |
| 1034 | + ) |
| 1035 | +from lp.code.errors import ( |
| 1036 | + BranchCreationForbidden, |
| 1037 | + BranchExists, |
| 1038 | + ) |
| 1039 | +from lp.code.interfaces.branch import IBranch |
| 1040 | +from lp.code.interfaces.branchjob import IRosettaUploadJobSource |
| 1041 | +from lp.code.interfaces.branchtarget import IBranchTarget |
| 1042 | +from lp.code.interfaces.codeimport import ( |
| 1043 | + ICodeImport, |
| 1044 | + ICodeImportSet, |
| 1045 | + ) |
| 1046 | +from lp.code.interfaces.gitrepository import IGitRepositorySet |
| 1047 | from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin |
| 1048 | + |
| 1049 | from lp.registry.browser import ( |
| 1050 | add_subscribe_link, |
| 1051 | BaseRdfView, |
| 1052 | @@ -150,6 +179,7 @@ |
| 1053 | PillarNavigationMixin, |
| 1054 | PillarViewMixin, |
| 1055 | ) |
| 1056 | +from lp.registry.enums import VCSType |
| 1057 | from lp.registry.interfaces.pillar import IPillarNameSet |
| 1058 | from lp.registry.interfaces.product import ( |
| 1059 | IProduct, |
| 1060 | @@ -171,6 +201,7 @@ |
| 1061 | from lp.services.fields import ( |
| 1062 | PillarAliases, |
| 1063 | PublicPersonChoice, |
| 1064 | + URIField, |
| 1065 | ) |
| 1066 | from lp.services.librarian.interfaces import ILibraryFileAliasSet |
| 1067 | from lp.services.propertycache import cachedproperty |
| 1068 | @@ -347,7 +378,6 @@ |
| 1069 | 'configured' -- a boolean representing the configuration status. |
| 1070 | """ |
| 1071 | overview_menu = MenuAPI(self.context).overview |
| 1072 | - series_menu = MenuAPI(self.context.development_focus).overview |
| 1073 | configuration_names = [ |
| 1074 | 'configure_bugtracker', |
| 1075 | 'configure_translations', |
| 1076 | @@ -363,11 +393,11 @@ |
| 1077 | configured=config_statuses[key])) |
| 1078 | |
| 1079 | # Add the branch configuration in separately. |
| 1080 | - set_branch = series_menu['set_branch'] |
| 1081 | - set_branch.text = 'Code' |
| 1082 | - set_branch.summary = "Specify the location of this project's code." |
| 1083 | + configure_code = overview_menu['configure_code'] |
| 1084 | + configure_code.text = 'Code' |
| 1085 | + configure_code.summary = "Specify the location of this project's code." |
| 1086 | config_list.insert(0, |
| 1087 | - dict(link=set_branch, |
| 1088 | + dict(link=configure_code, |
| 1089 | configured=config_statuses['configure_codehosting'])) |
| 1090 | return config_list |
| 1091 | |
| 1092 | @@ -506,6 +536,7 @@ |
| 1093 | 'packages', |
| 1094 | 'series', |
| 1095 | 'series_add', |
| 1096 | + 'configure_code', |
| 1097 | 'milestones', |
| 1098 | 'downloads', |
| 1099 | 'announce', |
| 1100 | @@ -559,6 +590,14 @@ |
| 1101 | 'RDF</abbr> metadata') |
| 1102 | return Link('+rdf', text, icon='download') |
| 1103 | |
| 1104 | + @enabled_with_permission('launchpad.Edit') |
| 1105 | + def configure_code(self): |
| 1106 | + """Return a link to configure code for this project.""" |
| 1107 | + text = 'Configure code' |
| 1108 | + icon = 'edit' |
| 1109 | + summary = 'Configure code for this project' |
| 1110 | + return Link('+configure-code', text, summary, icon=icon) |
| 1111 | + |
| 1112 | def downloads(self): |
| 1113 | text = 'Downloads' |
| 1114 | return Link('+download', text, icon='info') |
| 1115 | @@ -1594,6 +1633,373 @@ |
| 1116 | return BatchNavigator(decorated_result, self.request) |
| 1117 | |
| 1118 | |
| 1119 | +LINK_LP_BZR = 'link-lp-bzr' |
| 1120 | +IMPORT_EXTERNAL = 'import-external' |
| 1121 | + |
| 1122 | + |
| 1123 | +BRANCH_TYPE_VOCABULARY = SimpleVocabulary(( |
| 1124 | + SimpleTerm(LINK_LP_BZR, LINK_LP_BZR, |
| 1125 | + _("Link to a Bazaar branch already on Launchpad")), |
| 1126 | + SimpleTerm(IMPORT_EXTERNAL, IMPORT_EXTERNAL, |
| 1127 | + _("Import a branch hosted somewhere else")), |
| 1128 | + )) |
| 1129 | + |
| 1130 | + |
| 1131 | +class SetBranchForm(Interface): |
| 1132 | + """The fields presented on the form for setting a branch.""" |
| 1133 | + |
| 1134 | + use_template(ICodeImport, ['cvs_module']) |
| 1135 | + |
| 1136 | + rcs_type = Choice(title=_("Type of RCS"), |
| 1137 | + required=False, vocabulary=RevisionControlSystems, |
| 1138 | + description=_( |
| 1139 | + "The version control system to import from. ")) |
| 1140 | + |
| 1141 | + repo_url = URIField( |
| 1142 | + title=_("Branch URL"), required=True, |
| 1143 | + description=_("The URL of the branch."), |
| 1144 | + allowed_schemes=["http", "https"], |
| 1145 | + allow_userinfo=False, allow_port=True, allow_query=False, |
| 1146 | + allow_fragment=False, trailing_slash=False) |
| 1147 | + |
| 1148 | + branch_location = copy_field( |
| 1149 | + IProductSeries['branch'], __name__='branch_location', |
| 1150 | + title=_('Branch'), |
| 1151 | + description=_( |
| 1152 | + "The Bazaar branch for this series in Launchpad, " |
| 1153 | + "if one exists.")) |
| 1154 | + |
| 1155 | + branch_type = Choice( |
| 1156 | + title=_('Import type'), vocabulary=BRANCH_TYPE_VOCABULARY, |
| 1157 | + description=_("The type of import"), required=True) |
| 1158 | + |
| 1159 | + branch_name = copy_field( |
| 1160 | + IBranch['name'], __name__='branch_name', title=_('Branch name'), |
| 1161 | + description=_(''), required=True) |
| 1162 | + |
| 1163 | + branch_owner = copy_field( |
| 1164 | + IBranch['owner'], __name__='branch_owner', title=_('Branch owner'), |
| 1165 | + description=_(''), required=True) |
| 1166 | + |
| 1167 | + |
| 1168 | +def create_git_fields(): |
| 1169 | + return form.Fields( |
| 1170 | + Choice(__name__='default_vcs', |
| 1171 | + title=_("Project VCS"), |
| 1172 | + required=True, vocabulary=VCSType, |
| 1173 | + description=_("The version control system for " |
| 1174 | + "this project.")), |
| 1175 | + Choice(__name__='git_repository_location', |
| 1176 | + title=_('Git repository'), |
| 1177 | + required=False, |
| 1178 | + vocabulary='GitRepositoryRestrictedOnProduct', |
| 1179 | + description=_( |
| 1180 | + "The Git repository for this project in Launchpad, " |
| 1181 | + "if one exists, in the form: " |
| 1182 | + "~user/project-name/+git/repo-name")) |
| 1183 | + ) |
| 1184 | + |
| 1185 | + |
| 1186 | +class ProductSetBranchView(ReturnToReferrerMixin, LaunchpadFormView, |
| 1187 | + ProductView, |
| 1188 | + BranchNameValidationMixin): |
| 1189 | + """The view to set a branch default for the Product.""" |
| 1190 | + |
| 1191 | + label = 'Configure code' |
| 1192 | + page_title = label |
| 1193 | + schema = SetBranchForm |
| 1194 | + # Set for_input to True to ensure fields marked read-only will be editable |
| 1195 | + # upon creation. |
| 1196 | + for_input = True |
| 1197 | + |
| 1198 | + custom_widget('rcs_type', LaunchpadRadioWidget) |
| 1199 | + custom_widget('branch_type', LaunchpadRadioWidget) |
| 1200 | + custom_widget('default_vcs', LaunchpadRadioWidget) |
| 1201 | + |
| 1202 | + errors_in_action = False |
| 1203 | + is_series = False |
| 1204 | + |
| 1205 | + @property |
| 1206 | + def series(self): |
| 1207 | + return self.context.development_focus |
| 1208 | + |
| 1209 | + @property |
| 1210 | + def initial_values(self): |
| 1211 | + return dict( |
| 1212 | + rcs_type=RevisionControlSystems.BZR, |
| 1213 | + default_vcs=(self.context.pillar.inferred_vcs or VCSType.BZR), |
| 1214 | + branch_type=LINK_LP_BZR, |
| 1215 | + branch_location=self.series.branch) |
| 1216 | + |
| 1217 | + @property |
| 1218 | + def next_url(self): |
| 1219 | + """Return the next_url. |
| 1220 | + |
| 1221 | + Use the value from `ReturnToReferrerMixin` or None if there |
| 1222 | + are errors. |
| 1223 | + """ |
| 1224 | + if self.errors_in_action: |
| 1225 | + return None |
| 1226 | + return super(ProductSetBranchView, self).next_url |
| 1227 | + |
| 1228 | + def setUpFields(self): |
| 1229 | + """See `LaunchpadFormView`.""" |
| 1230 | + super(ProductSetBranchView, self).setUpFields() |
| 1231 | + if not self.is_series: |
| 1232 | + self.form_fields = (self.form_fields + create_git_fields()) |
| 1233 | + |
| 1234 | + def setUpWidgets(self): |
| 1235 | + """See `LaunchpadFormView`.""" |
| 1236 | + super(ProductSetBranchView, self).setUpWidgets() |
| 1237 | + widget = self.widgets['rcs_type'] |
| 1238 | + vocab = widget.vocabulary |
| 1239 | + current_value = widget._getFormValue() |
| 1240 | + self.rcs_type_cvs = render_radio_widget_part( |
| 1241 | + widget, vocab.CVS, current_value, 'CVS') |
| 1242 | + self.rcs_type_svn = render_radio_widget_part( |
| 1243 | + widget, vocab.BZR_SVN, current_value, 'SVN') |
| 1244 | + self.rcs_type_git = render_radio_widget_part( |
| 1245 | + widget, vocab.GIT, current_value) |
| 1246 | + self.rcs_type_bzr = render_radio_widget_part( |
| 1247 | + widget, vocab.BZR, current_value) |
| 1248 | + self.rcs_type_emptymarker = widget._emptyMarker() |
| 1249 | + |
| 1250 | + widget = self.widgets['branch_type'] |
| 1251 | + current_value = widget._getFormValue() |
| 1252 | + vocab = widget.vocabulary |
| 1253 | + |
| 1254 | + (self.branch_type_link, |
| 1255 | + self.branch_type_import) = [ |
| 1256 | + render_radio_widget_part(widget, value, current_value) |
| 1257 | + for value in (LINK_LP_BZR, IMPORT_EXTERNAL)] |
| 1258 | + |
| 1259 | + if not self.is_series: |
| 1260 | + widget = self.widgets['default_vcs'] |
| 1261 | + vocab = widget.vocabulary |
| 1262 | + current_value = widget._getFormValue() |
| 1263 | + self.default_vcs_git = render_radio_widget_part( |
| 1264 | + widget, vocab.GIT, current_value, 'Git') |
| 1265 | + self.default_vcs_bzr = render_radio_widget_part( |
| 1266 | + widget, vocab.BZR, current_value, 'Bazaar') |
| 1267 | + |
| 1268 | + def _validateLinkLpBzr(self, data): |
| 1269 | + """Validate data for link-lp-bzr case.""" |
| 1270 | + if 'branch_location' not in data: |
| 1271 | + self.setFieldError( |
| 1272 | + 'branch_location', 'The branch location must be set.') |
| 1273 | + |
| 1274 | + def _validateLinkLpGit(self, data): |
| 1275 | + """Validate data for link-lp-git case.""" |
| 1276 | + if data.get('git_repository_location'): |
| 1277 | + repo = data.get('git_repository_location') |
| 1278 | + if not repo: |
| 1279 | + self.setFieldError( |
| 1280 | + 'git_repository_location', |
| 1281 | + 'The repository does not exist.') |
| 1282 | + |
| 1283 | + def _validateImportExternal(self, data): |
| 1284 | + """Validate data for import external case.""" |
| 1285 | + rcs_type = data.get('rcs_type') |
| 1286 | + repo_url = data.get('repo_url') |
| 1287 | + |
| 1288 | + # Private teams are forbidden from owning code imports. |
| 1289 | + branch_owner = data.get('branch_owner') |
| 1290 | + if branch_owner is not None and branch_owner.private: |
| 1291 | + self.setFieldError( |
| 1292 | + 'branch_owner', 'Private teams are forbidden from owning ' |
| 1293 | + 'external imports.') |
| 1294 | + |
| 1295 | + if repo_url is None: |
| 1296 | + self.setFieldError( |
| 1297 | + 'repo_url', 'You must set the external repository URL.') |
| 1298 | + else: |
| 1299 | + reason = validate_import_url(repo_url, rcs_type) |
| 1300 | + if reason: |
| 1301 | + self.setFieldError('repo_url', reason) |
| 1302 | + |
| 1303 | + # RCS type is mandatory. |
| 1304 | + # This condition should never happen since an initial value is set. |
| 1305 | + if rcs_type is None: |
| 1306 | + # The error shows but does not identify the widget. |
| 1307 | + self.setFieldError( |
| 1308 | + 'rcs_type', |
| 1309 | + 'You must specify the type of RCS for the remote host.') |
| 1310 | + elif rcs_type == RevisionControlSystems.CVS: |
| 1311 | + if 'cvs_module' not in data: |
| 1312 | + self.setFieldError('cvs_module', 'The CVS module must be set.') |
| 1313 | + self._validateBranch(data) |
| 1314 | + |
| 1315 | + def _validateBranch(self, data): |
| 1316 | + """Validate that branch name and owner are set.""" |
| 1317 | + if 'branch_name' not in data: |
| 1318 | + self.setFieldError('branch_name', 'The branch name must be set.') |
| 1319 | + if 'branch_owner' not in data: |
| 1320 | + self.setFieldError('branch_owner', 'The branch owner must be set.') |
| 1321 | + |
| 1322 | + def _setRequired(self, names, value): |
| 1323 | + """Mark the widget field as optional.""" |
| 1324 | + for name in names: |
| 1325 | + widget = self.widgets[name] |
| 1326 | + # The 'required' property on the widget context is set to False. |
| 1327 | + # The widget also has a 'required' property but it isn't used |
| 1328 | + # during validation. |
| 1329 | + widget.context.required = value |
| 1330 | + |
| 1331 | + def _validSchemes(self, rcs_type): |
| 1332 | + """Return the valid schemes for the repository URL.""" |
| 1333 | + schemes = set(['http', 'https']) |
| 1334 | + # Extend the allowed schemes for the repository URL based on |
| 1335 | + # rcs_type. |
| 1336 | + extra_schemes = { |
| 1337 | + RevisionControlSystems.BZR_SVN: ['svn'], |
| 1338 | + RevisionControlSystems.GIT: ['git'], |
| 1339 | + RevisionControlSystems.BZR: ['bzr'], |
| 1340 | + } |
| 1341 | + schemes.update(extra_schemes.get(rcs_type, [])) |
| 1342 | + return schemes |
| 1343 | + |
| 1344 | + def validate_widgets(self, data, names=None): |
| 1345 | + """See `LaunchpadFormView`.""" |
| 1346 | + names = ['branch_type', 'rcs_type', 'default_vcs'] |
| 1347 | + super(ProductSetBranchView, self).validate_widgets(data, names) |
| 1348 | + branch_type = data.get('branch_type') |
| 1349 | + |
| 1350 | + if branch_type == LINK_LP_BZR: |
| 1351 | + # Mark other widgets as non-required. |
| 1352 | + self._setRequired(['rcs_type', 'repo_url', 'cvs_module', |
| 1353 | + 'branch_name', 'branch_owner'], False) |
| 1354 | + elif branch_type == IMPORT_EXTERNAL: |
| 1355 | + rcs_type = data.get('rcs_type') |
| 1356 | + |
| 1357 | + # Set the valid schemes based on rcs_type. |
| 1358 | + self.widgets['repo_url'].field.allowed_schemes = ( |
| 1359 | + self._validSchemes(rcs_type)) |
| 1360 | + # The branch location is not required for validation. |
| 1361 | + self._setRequired(['branch_location'], False) |
| 1362 | + # The cvs_module is required if it is a CVS import. |
| 1363 | + if rcs_type == RevisionControlSystems.CVS: |
| 1364 | + self._setRequired(['cvs_module'], True) |
| 1365 | + else: |
| 1366 | + raise AssertionError("Unknown branch type %s" % branch_type) |
| 1367 | + # Perform full validation now. |
| 1368 | + super(ProductSetBranchView, self).validate_widgets(data) |
| 1369 | + |
| 1370 | + def validate(self, data): |
| 1371 | + """See `LaunchpadFormView`.""" |
| 1372 | + # If widget validation returned errors then there is no need to |
| 1373 | + # continue as we'd likely just override the errors reported there. |
| 1374 | + if len(self.errors) > 0: |
| 1375 | + return |
| 1376 | + branch_type = data.get('branch_type') |
| 1377 | + if not self.is_series: |
| 1378 | + self._validateLinkLpGit(data) |
| 1379 | + if branch_type == IMPORT_EXTERNAL: |
| 1380 | + self._validateImportExternal(data) |
| 1381 | + elif branch_type == LINK_LP_BZR: |
| 1382 | + self._validateLinkLpBzr(data) |
| 1383 | + else: |
| 1384 | + raise AssertionError("Unknown branch type %s" % branch_type) |
| 1385 | + |
| 1386 | + @property |
| 1387 | + def target(self): |
| 1388 | + """The branch target for the context.""" |
| 1389 | + return IBranchTarget(self.context) |
| 1390 | + |
| 1391 | + def abort_update(self): |
| 1392 | + """Abort transaction. |
| 1393 | + |
| 1394 | + This is normally handled by LaunchpadFormView, but this can be called |
| 1395 | + from the success handler.""" |
| 1396 | + |
| 1397 | + self.errors_in_action = True |
| 1398 | + self._abort() |
| 1399 | + |
| 1400 | + def add_update_notification(self): |
| 1401 | + self.request.response.addInfoNotification( |
| 1402 | + 'Project settings updated.') |
| 1403 | + |
| 1404 | + @action(_('Update'), name='update') |
| 1405 | + def update_action(self, action, data): |
| 1406 | + branch_type = data.get('branch_type') |
| 1407 | + |
| 1408 | + if not self.is_series: |
| 1409 | + default_vcs = data.get('default_vcs') |
| 1410 | + if default_vcs: |
| 1411 | + self.context.vcs = default_vcs |
| 1412 | + |
| 1413 | + repo = data.get('git_repository_location') |
| 1414 | + getUtility(IGitRepositorySet).setDefaultRepository( |
| 1415 | + self.context, repo) |
| 1416 | + if branch_type == LINK_LP_BZR: |
| 1417 | + branch_location = data.get('branch_location') |
| 1418 | + if branch_location != self.series.branch: |
| 1419 | + self.series.branch = branch_location |
| 1420 | + # Request an initial upload of translation files. |
| 1421 | + getUtility(IRosettaUploadJobSource).create( |
| 1422 | + self.series.branch, NULL_REVISION) |
| 1423 | + else: |
| 1424 | + self.series.branch = branch_location |
| 1425 | + self.add_update_notification() |
| 1426 | + else: |
| 1427 | + branch_name = data.get('branch_name') |
| 1428 | + branch_owner = data.get('branch_owner') |
| 1429 | + |
| 1430 | + if branch_type == IMPORT_EXTERNAL: |
| 1431 | + rcs_type = data.get('rcs_type') |
| 1432 | + if rcs_type == RevisionControlSystems.CVS: |
| 1433 | + cvs_root = data.get('repo_url') |
| 1434 | + cvs_module = data.get('cvs_module') |
| 1435 | + url = None |
| 1436 | + else: |
| 1437 | + cvs_root = None |
| 1438 | + cvs_module = None |
| 1439 | + url = data.get('repo_url') |
| 1440 | + rcs_item = RevisionControlSystems.items[rcs_type.name] |
| 1441 | + try: |
| 1442 | + code_import = getUtility(ICodeImportSet).new( |
| 1443 | + owner=branch_owner, |
| 1444 | + registrant=self.user, |
| 1445 | + target=IBranchTarget(self.target), |
| 1446 | + branch_name=branch_name, |
| 1447 | + rcs_type=rcs_item, |
| 1448 | + url=url, |
| 1449 | + cvs_root=cvs_root, |
| 1450 | + cvs_module=cvs_module) |
| 1451 | + except BranchExists as e: |
| 1452 | + self._setBranchExists(e.existing_branch, 'branch_name') |
| 1453 | + self.abort_update() |
| 1454 | + return |
| 1455 | + self.series.branch = code_import.branch |
| 1456 | + self.request.response.addInfoNotification( |
| 1457 | + 'Code import created and branch linked to the series.') |
| 1458 | + else: |
| 1459 | + raise UnexpectedFormData(branch_type) |
| 1460 | + |
| 1461 | + def _createBzrBranch(self, branch_name, branch_owner, repo_url=None): |
| 1462 | + """Create a new hosted Bazaar branch. |
| 1463 | + |
| 1464 | + Return the branch on success or None. |
| 1465 | + """ |
| 1466 | + branch = None |
| 1467 | + try: |
| 1468 | + namespace = self.target.getNamespace(branch_owner) |
| 1469 | + branch = namespace.createBranch( |
| 1470 | + branch_type=BranchType.HOSTED, name=branch_name, |
| 1471 | + registrant=self.user, url=repo_url) |
| 1472 | + except BranchCreationForbidden: |
| 1473 | + self.addError( |
| 1474 | + "You are not allowed to create branches in %s." % |
| 1475 | + self.context.displayname) |
| 1476 | + except BranchExists as e: |
| 1477 | + self._setBranchExists(e.existing_branch, 'branch_name') |
| 1478 | + if branch is None: |
| 1479 | + self.errors_in_action = True |
| 1480 | + # Abort transaction. This is normally handled by |
| 1481 | + # LaunchpadFormView, but we are already in the success handler. |
| 1482 | + self._abort() |
| 1483 | + return branch |
| 1484 | + |
| 1485 | + |
| 1486 | class ProductRdfView(BaseRdfView): |
| 1487 | """A view that sets its mime-type to application/rdf+xml""" |
| 1488 | |
| 1489 | |
| 1490 | === modified file 'lib/lp/registry/browser/productseries.py' |
| 1491 | --- lib/lp/registry/browser/productseries.py 2015-06-12 14:20:12 +0000 |
| 1492 | +++ lib/lp/registry/browser/productseries.py 2015-06-25 03:34:41 +0000 |
| 1493 | @@ -28,11 +28,6 @@ |
| 1494 | |
| 1495 | from operator import attrgetter |
| 1496 | |
| 1497 | -from bzrlib.revision import NULL_REVISION |
| 1498 | -from lazr.restful.interface import ( |
| 1499 | - copy_field, |
| 1500 | - use_template, |
| 1501 | - ) |
| 1502 | from z3c.ptcompat import ViewPageTemplateFile |
| 1503 | from zope.component import getUtility |
| 1504 | from zope.formlib import form |
| 1505 | @@ -57,17 +52,11 @@ |
| 1506 | custom_widget, |
| 1507 | LaunchpadEditFormView, |
| 1508 | LaunchpadFormView, |
| 1509 | - render_radio_widget_part, |
| 1510 | - ReturnToReferrerMixin, |
| 1511 | ) |
| 1512 | from lp.app.browser.tales import MenuAPI |
| 1513 | from lp.app.enums import ServiceUsage |
| 1514 | -from lp.app.errors import ( |
| 1515 | - NotFoundError, |
| 1516 | - UnexpectedFormData, |
| 1517 | - ) |
| 1518 | +from lp.app.errors import NotFoundError |
| 1519 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
| 1520 | -from lp.app.widgets.itemswidgets import LaunchpadRadioWidget |
| 1521 | from lp.app.widgets.textwidgets import StrippedTextWidget |
| 1522 | from lp.blueprints.browser.specificationtarget import ( |
| 1523 | HasSpecificationsMenuMixin, |
| 1524 | @@ -81,24 +70,9 @@ |
| 1525 | StructuralSubscriptionTargetTraversalMixin, |
| 1526 | ) |
| 1527 | from lp.bugs.interfaces.bugtask import IBugTaskSet |
| 1528 | -from lp.code.browser.branch import BranchNameValidationMixin |
| 1529 | from lp.code.browser.branchref import BranchRef |
| 1530 | -from lp.code.browser.codeimport import validate_import_url |
| 1531 | -from lp.code.enums import ( |
| 1532 | - BranchType, |
| 1533 | - RevisionControlSystems, |
| 1534 | - ) |
| 1535 | -from lp.code.errors import ( |
| 1536 | - BranchCreationForbidden, |
| 1537 | - BranchExists, |
| 1538 | - ) |
| 1539 | -from lp.code.interfaces.branch import IBranch |
| 1540 | -from lp.code.interfaces.branchjob import IRosettaUploadJobSource |
| 1541 | +from lp.code.enums import RevisionControlSystems |
| 1542 | from lp.code.interfaces.branchtarget import IBranchTarget |
| 1543 | -from lp.code.interfaces.codeimport import ( |
| 1544 | - ICodeImport, |
| 1545 | - ICodeImportSet, |
| 1546 | - ) |
| 1547 | from lp.registry.browser import ( |
| 1548 | add_subscribe_link, |
| 1549 | BaseRdfView, |
| 1550 | @@ -110,6 +84,7 @@ |
| 1551 | InvolvedMenu, |
| 1552 | PillarInvolvementView, |
| 1553 | ) |
| 1554 | +from lp.registry.browser.product import ProductSetBranchView |
| 1555 | from lp.registry.errors import CannotPackageProprietaryProduct |
| 1556 | from lp.registry.interfaces.packaging import ( |
| 1557 | IPackaging, |
| 1558 | @@ -117,7 +92,6 @@ |
| 1559 | ) |
| 1560 | from lp.registry.interfaces.productseries import IProductSeries |
| 1561 | from lp.registry.interfaces.series import SeriesStatus |
| 1562 | -from lp.services.fields import URIField |
| 1563 | from lp.services.propertycache import cachedproperty |
| 1564 | from lp.services.webapp import ( |
| 1565 | ApplicationMenu, |
| 1566 | @@ -756,300 +730,23 @@ |
| 1567 | self.next_url = canonical_url(product) |
| 1568 | |
| 1569 | |
| 1570 | -LINK_LP_BZR = 'link-lp-bzr' |
| 1571 | -IMPORT_EXTERNAL = 'import-external' |
| 1572 | - |
| 1573 | - |
| 1574 | -BRANCH_TYPE_VOCABULARY = SimpleVocabulary(( |
| 1575 | - SimpleTerm(LINK_LP_BZR, LINK_LP_BZR, |
| 1576 | - _("Link to a Bazaar branch already on Launchpad")), |
| 1577 | - SimpleTerm(IMPORT_EXTERNAL, IMPORT_EXTERNAL, |
| 1578 | - _("Import a branch hosted somewhere else")), |
| 1579 | - )) |
| 1580 | - |
| 1581 | - |
| 1582 | -class SetBranchForm(Interface): |
| 1583 | - """The fields presented on the form for setting a branch.""" |
| 1584 | - |
| 1585 | - use_template(ICodeImport, ['cvs_module']) |
| 1586 | - |
| 1587 | - rcs_type = Choice(title=_("Type of RCS"), |
| 1588 | - required=False, vocabulary=RevisionControlSystems, |
| 1589 | - description=_( |
| 1590 | - "The version control system to import from. ")) |
| 1591 | - |
| 1592 | - repo_url = URIField( |
| 1593 | - title=_("Branch URL"), required=True, |
| 1594 | - description=_("The URL of the branch."), |
| 1595 | - allowed_schemes=["http", "https"], |
| 1596 | - allow_userinfo=False, allow_port=True, allow_query=False, |
| 1597 | - allow_fragment=False, trailing_slash=False) |
| 1598 | - |
| 1599 | - branch_location = copy_field( |
| 1600 | - IProductSeries['branch'], __name__='branch_location', |
| 1601 | - title=_('Branch'), |
| 1602 | - description=_( |
| 1603 | - "The Bazaar branch for this series in Launchpad, " |
| 1604 | - "if one exists.")) |
| 1605 | - |
| 1606 | - branch_type = Choice( |
| 1607 | - title=_('Import type'), vocabulary=BRANCH_TYPE_VOCABULARY, |
| 1608 | - description=_("The type of import"), required=True) |
| 1609 | - |
| 1610 | - branch_name = copy_field( |
| 1611 | - IBranch['name'], __name__='branch_name', title=_('Branch name'), |
| 1612 | - description=_(''), required=True) |
| 1613 | - |
| 1614 | - branch_owner = copy_field( |
| 1615 | - IBranch['owner'], __name__='branch_owner', title=_('Branch owner'), |
| 1616 | - description=_(''), required=True) |
| 1617 | - |
| 1618 | - |
| 1619 | -class ProductSeriesSetBranchView(ReturnToReferrerMixin, LaunchpadFormView, |
| 1620 | - ProductSeriesView, |
| 1621 | - BranchNameValidationMixin): |
| 1622 | +class ProductSeriesSetBranchView(ProductSetBranchView, ProductSeriesView): |
| 1623 | """The view to set a branch for the ProductSeries.""" |
| 1624 | |
| 1625 | - schema = SetBranchForm |
| 1626 | - # Set for_input to True to ensure fields marked read-only will be editable |
| 1627 | - # upon creation. |
| 1628 | - for_input = True |
| 1629 | - |
| 1630 | - custom_widget('rcs_type', LaunchpadRadioWidget) |
| 1631 | - custom_widget('branch_type', LaunchpadRadioWidget) |
| 1632 | - |
| 1633 | - errors_in_action = False |
| 1634 | - |
| 1635 | - @property |
| 1636 | - def initial_values(self): |
| 1637 | - return dict( |
| 1638 | - rcs_type=RevisionControlSystems.BZR, |
| 1639 | - branch_type=LINK_LP_BZR, |
| 1640 | - branch_location=self.context.branch) |
| 1641 | - |
| 1642 | - @property |
| 1643 | - def next_url(self): |
| 1644 | - """Return the next_url. |
| 1645 | - |
| 1646 | - Use the value from `ReturnToReferrerMixin` or None if there |
| 1647 | - are errors. |
| 1648 | - """ |
| 1649 | - if self.errors_in_action: |
| 1650 | - return None |
| 1651 | - return super(ProductSeriesSetBranchView, self).next_url |
| 1652 | - |
| 1653 | - def setUpWidgets(self): |
| 1654 | - """See `LaunchpadFormView`.""" |
| 1655 | - super(ProductSeriesSetBranchView, self).setUpWidgets() |
| 1656 | - widget = self.widgets['rcs_type'] |
| 1657 | - vocab = widget.vocabulary |
| 1658 | - current_value = widget._getFormValue() |
| 1659 | - self.rcs_type_cvs = render_radio_widget_part( |
| 1660 | - widget, vocab.CVS, current_value, 'CVS') |
| 1661 | - self.rcs_type_svn = render_radio_widget_part( |
| 1662 | - widget, vocab.BZR_SVN, current_value, 'SVN') |
| 1663 | - self.rcs_type_git = render_radio_widget_part( |
| 1664 | - widget, vocab.GIT, current_value) |
| 1665 | - self.rcs_type_bzr = render_radio_widget_part( |
| 1666 | - widget, vocab.BZR, current_value) |
| 1667 | - self.rcs_type_emptymarker = widget._emptyMarker() |
| 1668 | - |
| 1669 | - widget = self.widgets['branch_type'] |
| 1670 | - current_value = widget._getFormValue() |
| 1671 | - vocab = widget.vocabulary |
| 1672 | - |
| 1673 | - (self.branch_type_link, |
| 1674 | - self.branch_type_import) = [ |
| 1675 | - render_radio_widget_part(widget, value, current_value) |
| 1676 | - for value in (LINK_LP_BZR, IMPORT_EXTERNAL)] |
| 1677 | - |
| 1678 | - def _validateLinkLpBzr(self, data): |
| 1679 | - """Validate data for link-lp-bzr case.""" |
| 1680 | - if 'branch_location' not in data: |
| 1681 | - self.setFieldError( |
| 1682 | - 'branch_location', 'The branch location must be set.') |
| 1683 | - |
| 1684 | - def _validateImportExternal(self, data): |
| 1685 | - """Validate data for import external case.""" |
| 1686 | - rcs_type = data.get('rcs_type') |
| 1687 | - repo_url = data.get('repo_url') |
| 1688 | - |
| 1689 | - # Private teams are forbidden from owning code imports. |
| 1690 | - branch_owner = data.get('branch_owner') |
| 1691 | - if branch_owner is not None and branch_owner.private: |
| 1692 | - self.setFieldError( |
| 1693 | - 'branch_owner', 'Private teams are forbidden from owning ' |
| 1694 | - 'external imports.') |
| 1695 | - |
| 1696 | - if repo_url is None: |
| 1697 | - self.setFieldError( |
| 1698 | - 'repo_url', 'You must set the external repository URL.') |
| 1699 | - else: |
| 1700 | - reason = validate_import_url(repo_url, rcs_type) |
| 1701 | - if reason: |
| 1702 | - self.setFieldError('repo_url', reason) |
| 1703 | - |
| 1704 | - # RCS type is mandatory. |
| 1705 | - # This condition should never happen since an initial value is set. |
| 1706 | - if rcs_type is None: |
| 1707 | - # The error shows but does not identify the widget. |
| 1708 | - self.setFieldError( |
| 1709 | - 'rcs_type', |
| 1710 | - 'You must specify the type of RCS for the remote host.') |
| 1711 | - elif rcs_type == RevisionControlSystems.CVS: |
| 1712 | - if 'cvs_module' not in data: |
| 1713 | - self.setFieldError('cvs_module', 'The CVS module must be set.') |
| 1714 | - self._validateBranch(data) |
| 1715 | - |
| 1716 | - def _validateBranch(self, data): |
| 1717 | - """Validate that branch name and owner are set.""" |
| 1718 | - if 'branch_name' not in data: |
| 1719 | - self.setFieldError('branch_name', 'The branch name must be set.') |
| 1720 | - if 'branch_owner' not in data: |
| 1721 | - self.setFieldError('branch_owner', 'The branch owner must be set.') |
| 1722 | - |
| 1723 | - def _setRequired(self, names, value): |
| 1724 | - """Mark the widget field as optional.""" |
| 1725 | - for name in names: |
| 1726 | - widget = self.widgets[name] |
| 1727 | - # The 'required' property on the widget context is set to False. |
| 1728 | - # The widget also has a 'required' property but it isn't used |
| 1729 | - # during validation. |
| 1730 | - widget.context.required = value |
| 1731 | - |
| 1732 | - def _validSchemes(self, rcs_type): |
| 1733 | - """Return the valid schemes for the repository URL.""" |
| 1734 | - schemes = set(['http', 'https']) |
| 1735 | - # Extend the allowed schemes for the repository URL based on |
| 1736 | - # rcs_type. |
| 1737 | - extra_schemes = { |
| 1738 | - RevisionControlSystems.BZR_SVN: ['svn'], |
| 1739 | - RevisionControlSystems.GIT: ['git'], |
| 1740 | - RevisionControlSystems.BZR: ['bzr'], |
| 1741 | - } |
| 1742 | - schemes.update(extra_schemes.get(rcs_type, [])) |
| 1743 | - return schemes |
| 1744 | - |
| 1745 | - def validate_widgets(self, data, names=None): |
| 1746 | - """See `LaunchpadFormView`.""" |
| 1747 | - names = ['branch_type', 'rcs_type'] |
| 1748 | - super(ProductSeriesSetBranchView, self).validate_widgets(data, names) |
| 1749 | - branch_type = data.get('branch_type') |
| 1750 | - if branch_type == LINK_LP_BZR: |
| 1751 | - # Mark other widgets as non-required. |
| 1752 | - self._setRequired(['rcs_type', 'repo_url', 'cvs_module', |
| 1753 | - 'branch_name', 'branch_owner'], False) |
| 1754 | - elif branch_type == IMPORT_EXTERNAL: |
| 1755 | - rcs_type = data.get('rcs_type') |
| 1756 | - |
| 1757 | - # Set the valid schemes based on rcs_type. |
| 1758 | - self.widgets['repo_url'].field.allowed_schemes = ( |
| 1759 | - self._validSchemes(rcs_type)) |
| 1760 | - # The branch location is not required for validation. |
| 1761 | - self._setRequired(['branch_location'], False) |
| 1762 | - # The cvs_module is required if it is a CVS import. |
| 1763 | - if rcs_type == RevisionControlSystems.CVS: |
| 1764 | - self._setRequired(['cvs_module'], True) |
| 1765 | - else: |
| 1766 | - raise AssertionError("Unknown branch type %s" % branch_type) |
| 1767 | - # Perform full validation now. |
| 1768 | - super(ProductSeriesSetBranchView, self).validate_widgets(data) |
| 1769 | - |
| 1770 | - def validate(self, data): |
| 1771 | - """See `LaunchpadFormView`.""" |
| 1772 | - # If widget validation returned errors then there is no need to |
| 1773 | - # continue as we'd likely just override the errors reported there. |
| 1774 | - if len(self.errors) > 0: |
| 1775 | - return |
| 1776 | - branch_type = data['branch_type'] |
| 1777 | - if branch_type == IMPORT_EXTERNAL: |
| 1778 | - self._validateImportExternal(data) |
| 1779 | - elif branch_type == LINK_LP_BZR: |
| 1780 | - self._validateLinkLpBzr(data) |
| 1781 | - else: |
| 1782 | - raise AssertionError("Unknown branch type %s" % branch_type) |
| 1783 | + is_series = True |
| 1784 | + |
| 1785 | + @property |
| 1786 | + def series(self): |
| 1787 | + return self.context |
| 1788 | |
| 1789 | @property |
| 1790 | def target(self): |
| 1791 | """The branch target for the context.""" |
| 1792 | return IBranchTarget(self.context.product) |
| 1793 | |
| 1794 | - @action(_('Update'), name='update') |
| 1795 | - def update_action(self, action, data): |
| 1796 | - branch_type = data.get('branch_type') |
| 1797 | - if branch_type == LINK_LP_BZR: |
| 1798 | - branch_location = data.get('branch_location') |
| 1799 | - if branch_location != self.context.branch: |
| 1800 | - self.context.branch = branch_location |
| 1801 | - # Request an initial upload of translation files. |
| 1802 | - getUtility(IRosettaUploadJobSource).create( |
| 1803 | - self.context.branch, NULL_REVISION) |
| 1804 | - else: |
| 1805 | - self.context.branch = branch_location |
| 1806 | - self.request.response.addInfoNotification( |
| 1807 | - 'Series code location updated.') |
| 1808 | - else: |
| 1809 | - branch_name = data.get('branch_name') |
| 1810 | - branch_owner = data.get('branch_owner') |
| 1811 | - |
| 1812 | - if branch_type == IMPORT_EXTERNAL: |
| 1813 | - rcs_type = data.get('rcs_type') |
| 1814 | - if rcs_type == RevisionControlSystems.CVS: |
| 1815 | - cvs_root = data.get('repo_url') |
| 1816 | - cvs_module = data.get('cvs_module') |
| 1817 | - url = None |
| 1818 | - else: |
| 1819 | - cvs_root = None |
| 1820 | - cvs_module = None |
| 1821 | - url = data.get('repo_url') |
| 1822 | - rcs_item = RevisionControlSystems.items[rcs_type.name] |
| 1823 | - try: |
| 1824 | - code_import = getUtility(ICodeImportSet).new( |
| 1825 | - owner=branch_owner, |
| 1826 | - registrant=self.user, |
| 1827 | - target=IBranchTarget(self.context.product), |
| 1828 | - branch_name=branch_name, |
| 1829 | - rcs_type=rcs_item, |
| 1830 | - url=url, |
| 1831 | - cvs_root=cvs_root, |
| 1832 | - cvs_module=cvs_module) |
| 1833 | - except BranchExists as e: |
| 1834 | - self._setBranchExists(e.existing_branch, 'branch_name') |
| 1835 | - self.errors_in_action = True |
| 1836 | - # Abort transaction. This is normally handled |
| 1837 | - # by LaunchpadFormView, but we are already in |
| 1838 | - # the success handler. |
| 1839 | - self._abort() |
| 1840 | - return |
| 1841 | - self.context.branch = code_import.branch |
| 1842 | - self.request.response.addInfoNotification( |
| 1843 | - 'Code import created and branch linked to the series.') |
| 1844 | - else: |
| 1845 | - raise UnexpectedFormData(branch_type) |
| 1846 | - |
| 1847 | - def _createBzrBranch(self, branch_name, branch_owner, repo_url=None): |
| 1848 | - """Create a new hosted Bazaar branch. |
| 1849 | - |
| 1850 | - Return the branch on success or None. |
| 1851 | - """ |
| 1852 | - branch = None |
| 1853 | - try: |
| 1854 | - namespace = self.target.getNamespace(branch_owner) |
| 1855 | - branch = namespace.createBranch( |
| 1856 | - branch_type=BranchType.HOSTED, name=branch_name, |
| 1857 | - registrant=self.user, url=repo_url) |
| 1858 | - except BranchCreationForbidden: |
| 1859 | - self.addError( |
| 1860 | - "You are not allowed to create branches in %s." % |
| 1861 | - self.context.displayname) |
| 1862 | - except BranchExists as e: |
| 1863 | - self._setBranchExists(e.existing_branch, 'branch_name') |
| 1864 | - if branch is None: |
| 1865 | - self.errors_in_action = True |
| 1866 | - # Abort transaction. This is normally handled by |
| 1867 | - # LaunchpadFormView, but we are already in the success handler. |
| 1868 | - self._abort() |
| 1869 | - return branch |
| 1870 | + def add_update_notification(self): |
| 1871 | + self.request.response.addInfoNotification( |
| 1872 | + 'Series code location updated.') |
| 1873 | |
| 1874 | |
| 1875 | class ProductSeriesReviewView(LaunchpadEditFormView): |
| 1876 | |
| 1877 | === modified file 'lib/lp/registry/browser/tests/productseries-setbranch-view.txt' |
| 1878 | --- lib/lp/registry/browser/tests/productseries-setbranch-view.txt 2015-06-12 14:20:12 +0000 |
| 1879 | +++ lib/lp/registry/browser/tests/productseries-setbranch-view.txt 2015-06-25 03:34:41 +0000 |
| 1880 | @@ -27,8 +27,8 @@ |
| 1881 | |
| 1882 | The user can see instructions to push a branch. |
| 1883 | |
| 1884 | - >>> instructions = find_tag_by_id(content, 'push-instructions') |
| 1885 | - >>> 'bzr push lp:~' in str(instructions) |
| 1886 | + >>> instructions = find_tag_by_id(content, 'push-instructions-bzr') |
| 1887 | + >>> 'bzr push lp:' in str(instructions) |
| 1888 | True |
| 1889 | |
| 1890 | |
| 1891 | |
| 1892 | === modified file 'lib/lp/registry/browser/tests/test_product.py' |
| 1893 | --- lib/lp/registry/browser/tests/test_product.py 2014-06-19 06:38:53 +0000 |
| 1894 | +++ lib/lp/registry/browser/tests/test_product.py 2015-06-25 03:34:41 +0000 |
| 1895 | @@ -34,6 +34,7 @@ |
| 1896 | from lp.registry.enums import ( |
| 1897 | EXCLUSIVE_TEAM_POLICY, |
| 1898 | TeamMembershipPolicy, |
| 1899 | + VCSType, |
| 1900 | ) |
| 1901 | from lp.registry.interfaces.product import ( |
| 1902 | IProductSet, |
| 1903 | @@ -309,6 +310,12 @@ |
| 1904 | view = create_initialized_view(self.product, '+index') |
| 1905 | self.assertTrue(view.show_programming_languages) |
| 1906 | |
| 1907 | + def test_show_default_vcs(self): |
| 1908 | + with person_logged_in(self.product.owner): |
| 1909 | + self.product.vcs = VCSType.GIT |
| 1910 | + view = create_initialized_view(self.product, '+index') |
| 1911 | + self.assertTrue(view.show_vcs) |
| 1912 | + |
| 1913 | def test_show_license_info_without_other_license(self): |
| 1914 | # show_license_info is false when one of the "other" licences is |
| 1915 | # not selected. |
| 1916 | |
| 1917 | === added file 'lib/lp/registry/browser/tests/test_product_views.py' |
| 1918 | --- lib/lp/registry/browser/tests/test_product_views.py 1970-01-01 00:00:00 +0000 |
| 1919 | +++ lib/lp/registry/browser/tests/test_product_views.py 2015-06-25 03:34:41 +0000 |
| 1920 | @@ -0,0 +1,36 @@ |
| 1921 | +# Copyright 2011-2015 Canonical Ltd. This software is licensed under the |
| 1922 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
| 1923 | + |
| 1924 | +"""View tests for Product pages.""" |
| 1925 | + |
| 1926 | +__metaclass__ = type |
| 1927 | + |
| 1928 | +import soupmatchers |
| 1929 | +from zope.security.proxy import removeSecurityProxy |
| 1930 | + |
| 1931 | +from lp.services.webapp import canonical_url |
| 1932 | +from lp.testing import BrowserTestCase |
| 1933 | +from lp.testing.layers import DatabaseFunctionalLayer |
| 1934 | + |
| 1935 | + |
| 1936 | +class TestProductSetBranchView(BrowserTestCase): |
| 1937 | + |
| 1938 | + layer = DatabaseFunctionalLayer |
| 1939 | + |
| 1940 | + def getBrowser(self, project, view_name=None): |
| 1941 | + project = removeSecurityProxy(project) |
| 1942 | + url = canonical_url(project, view_name=view_name) |
| 1943 | + return self.getUserBrowser(url, project.owner) |
| 1944 | + |
| 1945 | + def test_link_existing_git_repository(self): |
| 1946 | + repo = removeSecurityProxy(self.factory.makeGitRepository( |
| 1947 | + target=self.factory.makeProduct())) |
| 1948 | + browser = self.getBrowser(repo.project, '+configure-code') |
| 1949 | + browser.getControl('Git', index=0).click() |
| 1950 | + browser.getControl('Git repository').value = repo.shortened_path |
| 1951 | + browser.getControl('Update').click() |
| 1952 | + |
| 1953 | + tag = soupmatchers.Tag( |
| 1954 | + 'success-div', 'div', attrs={'class': 'informational message'}, |
| 1955 | + text='Project settings updated.') |
| 1956 | + self.assertThat(browser.contents, soupmatchers.HTMLContains(tag)) |
| 1957 | |
| 1958 | === modified file 'lib/lp/registry/interfaces/product.py' |
| 1959 | --- lib/lp/registry/interfaces/product.py 2015-05-13 05:03:33 +0000 |
| 1960 | +++ lib/lp/registry/interfaces/product.py 2015-06-25 03:34:41 +0000 |
| 1961 | @@ -761,6 +761,14 @@ |
| 1962 | description=_( |
| 1963 | "Version control system for this project's code."))) |
| 1964 | |
| 1965 | + inferred_vcs = exported( |
| 1966 | + Choice( |
| 1967 | + title=_("Inferred VCS"), |
| 1968 | + readonly=True, |
| 1969 | + vocabulary=VCSType, |
| 1970 | + description=_( |
| 1971 | + "Inferred version control system for this project's code."))) |
| 1972 | + |
| 1973 | def getAllowedBugInformationTypes(): |
| 1974 | """Get the information types that a bug in this project can have. |
| 1975 | |
| 1976 | |
| 1977 | === modified file 'lib/lp/registry/model/product.py' |
| 1978 | --- lib/lp/registry/model/product.py 2015-05-13 05:25:30 +0000 |
| 1979 | +++ lib/lp/registry/model/product.py 2015-06-25 03:34:41 +0000 |
| 1980 | @@ -116,6 +116,8 @@ |
| 1981 | ) |
| 1982 | from lp.code.enums import BranchType |
| 1983 | from lp.code.interfaces.branch import DEFAULT_BRANCH_STATUS_IN_LISTING |
| 1984 | +from lp.code.interfaces.branchcollection import IBranchCollection |
| 1985 | +from lp.code.interfaces.gitcollection import IGitCollection |
| 1986 | from lp.code.interfaces.gitrepository import IGitRepositorySet |
| 1987 | from lp.code.model.branch import Branch |
| 1988 | from lp.code.model.branchnamespace import BRANCH_POLICY_ALLOWED_TYPES |
| 1989 | @@ -445,6 +447,19 @@ |
| 1990 | """ |
| 1991 | return None |
| 1992 | |
| 1993 | + @property |
| 1994 | + def inferred_vcs(self): |
| 1995 | + """Use vcs, otherwise infer from existence of git or bzr branches. |
| 1996 | + |
| 1997 | + Bzr take precedence over git, if no project vcs set. |
| 1998 | + """ |
| 1999 | + if self.vcs: |
| 2000 | + return self.vcs |
| 2001 | + if not IBranchCollection(self).is_empty(): |
| 2002 | + return VCSType.BZR |
| 2003 | + elif not IGitCollection(self).is_empty(): |
| 2004 | + return VCSType.GIT |
| 2005 | + |
| 2006 | @date_next_suggest_packaging.setter # pyflakes:ignore |
| 2007 | def date_next_suggest_packaging(self, value): |
| 2008 | """See `IProduct` |
| 2009 | |
| 2010 | === modified file 'lib/lp/registry/stories/product/xx-product-development-focus.txt' |
| 2011 | --- lib/lp/registry/stories/product/xx-product-development-focus.txt 2015-06-14 23:48:24 +0000 |
| 2012 | +++ lib/lp/registry/stories/product/xx-product-development-focus.txt 2015-06-25 03:34:41 +0000 |
| 2013 | @@ -74,7 +74,7 @@ |
| 2014 | (http://launchpad.dev/fooix/+edit) |
| 2015 | >>> print_involvement_portlet(owner_browser) |
| 2016 | Code |
| 2017 | - http://launchpad.dev/fooix/trunk/+setbranch |
| 2018 | + http://launchpad.dev/fooix/+configure-code |
| 2019 | Bugs |
| 2020 | http://launchpad.dev/fooix/+configure-bugtracker |
| 2021 | Translations |
| 2022 | @@ -84,12 +84,13 @@ |
| 2023 | |
| 2024 | The owner can specify the development focus branch from the overview page. |
| 2025 | |
| 2026 | - >>> owner_browser.getLink(url='+setbranch').click() |
| 2027 | + >>> owner_browser.getLink(url='+configure-code').click() |
| 2028 | + >>> owner_browser.getControl('Bazaar', index=0).click() |
| 2029 | >>> owner_browser.getControl(name='field.branch_location').value = ( |
| 2030 | ... '~eric/fooix/trunk') |
| 2031 | >>> owner_browser.getControl('Update').click() |
| 2032 | >>> print_feedback_messages(owner_browser.contents) |
| 2033 | - Series code location updated. |
| 2034 | + Project settings updated. |
| 2035 | |
| 2036 | The owner is taken back to the project page. |
| 2037 | |
| 2038 | |
| 2039 | === modified file 'lib/lp/registry/templates/product-index.pt' |
| 2040 | --- lib/lp/registry/templates/product-index.pt 2015-01-29 16:28:30 +0000 |
| 2041 | +++ lib/lp/registry/templates/product-index.pt 2015-06-25 03:34:41 +0000 |
| 2042 | @@ -129,6 +129,11 @@ |
| 2043 | <dd tal:content="structure view/languages_edit_widget" /> |
| 2044 | </dl> |
| 2045 | |
| 2046 | + <dl id="product-vcs" tal:condition="context/inferred_vcs"> |
| 2047 | + <dt>Version control system:</dt> |
| 2048 | + <dd tal:content="context/inferred_vcs/title">Git</dd> |
| 2049 | + </dl> |
| 2050 | + |
| 2051 | <dl id="licences"> |
| 2052 | <dt>Licences:</dt> |
| 2053 | <dd> |
| 2054 | |
| 2055 | === removed file 'lib/lp/registry/templates/productseries-setbranch.pt' |
| 2056 | --- lib/lp/registry/templates/productseries-setbranch.pt 2015-05-01 13:18:54 +0000 |
| 2057 | +++ lib/lp/registry/templates/productseries-setbranch.pt 1970-01-01 00:00:00 +0000 |
| 2058 | @@ -1,125 +0,0 @@ |
| 2059 | -<html |
| 2060 | - xmlns="http://www.w3.org/1999/xhtml" |
| 2061 | - xmlns:tal="http://xml.zope.org/namespaces/tal" |
| 2062 | - xmlns:metal="http://xml.zope.org/namespaces/metal" |
| 2063 | - xmlns:i18n="http://xml.zope.org/namespaces/i18n" |
| 2064 | - metal:use-macro="view/macro:page/main_only" |
| 2065 | - i18n:domain="launchpad"> |
| 2066 | - |
| 2067 | -<body> |
| 2068 | - |
| 2069 | -<metal:block fill-slot="head_epilogue"> |
| 2070 | - <style type="text/css"> |
| 2071 | - .subordinate { |
| 2072 | - margin: 0.5em 0 0.5em 4em; |
| 2073 | - } |
| 2074 | - </style> |
| 2075 | -</metal:block> |
| 2076 | - |
| 2077 | -<div metal:fill-slot="main"> |
| 2078 | - |
| 2079 | - <p id="push-instructions"> |
| 2080 | - You can push a Bazaar branch directly to Launchpad with the command:<br /> |
| 2081 | - <tt class="command"> |
| 2082 | - bzr push lp:~<tal:user replace="view/user/name"/>/<tal:project replace="context/product/name"/>/<tal:series replace="context/name"/> |
| 2083 | - </tt> |
| 2084 | - <tal:no-keys condition="not:view/user/sshkeys"> |
| 2085 | - <br/>To authenticate with the Launchpad branch upload service, |
| 2086 | - you need to |
| 2087 | - <a tal:attributes="href string:${view/user/fmt:url}/+editsshkeys"> |
| 2088 | - register a SSH key</a>. |
| 2089 | - </tal:no-keys> |
| 2090 | - </p> |
| 2091 | - |
| 2092 | - <div metal:use-macro="context/@@launchpad_form/form"> |
| 2093 | - |
| 2094 | - <metal:formbody fill-slot="widgets"> |
| 2095 | - |
| 2096 | - <table class="form"> |
| 2097 | - |
| 2098 | - <tr> |
| 2099 | - <td> |
| 2100 | - <label tal:replace="structure view/branch_type_link"> |
| 2101 | - Link to a Bazaar branch already in Launchpad |
| 2102 | - </label> |
| 2103 | - <table class="subordinate"> |
| 2104 | - <tal:widget define="widget nocall:view/widgets/branch_location"> |
| 2105 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
| 2106 | - </tal:widget> |
| 2107 | - </table> |
| 2108 | - </td> |
| 2109 | - </tr> |
| 2110 | - |
| 2111 | - <tr> |
| 2112 | - <td> |
| 2113 | - <label tal:replace="structure view/branch_type_import"> |
| 2114 | - Import a branch hosted somewhere else |
| 2115 | - </label> |
| 2116 | - <table class="subordinate"> |
| 2117 | - <tal:widget define="widget nocall:view/widgets/repo_url"> |
| 2118 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
| 2119 | - </tal:widget> |
| 2120 | - |
| 2121 | - <tr> |
| 2122 | - <td> |
| 2123 | - <label tal:replace="structure view/rcs_type_bzr"> |
| 2124 | - Bazaar, hosted externally |
| 2125 | - </label> |
| 2126 | - </td> |
| 2127 | - </tr> |
| 2128 | - |
| 2129 | - <tr> |
| 2130 | - <td> |
| 2131 | - <label tal:replace="structure view/rcs_type_git"> |
| 2132 | - Git |
| 2133 | - </label> |
| 2134 | - </td> |
| 2135 | - </tr> |
| 2136 | - |
| 2137 | - <tr> |
| 2138 | - <td> |
| 2139 | - <label tal:replace="structure view/rcs_type_svn"> |
| 2140 | - SVN |
| 2141 | - </label> |
| 2142 | - </td> |
| 2143 | - </tr> |
| 2144 | - |
| 2145 | - <tr> |
| 2146 | - <td> |
| 2147 | - <label tal:replace="structure view/rcs_type_cvs"> |
| 2148 | - CVS |
| 2149 | - </label> |
| 2150 | - <table class="subordinate"> |
| 2151 | - <tal:widget define="widget nocall:view/widgets/cvs_module"> |
| 2152 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
| 2153 | - </tal:widget> |
| 2154 | - </table> |
| 2155 | - </td> |
| 2156 | - </tr> |
| 2157 | - |
| 2158 | - </table> |
| 2159 | - </td> |
| 2160 | - </tr> |
| 2161 | - |
| 2162 | - <tal:widget define="widget nocall:view/widgets/branch_name"> |
| 2163 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
| 2164 | - </tal:widget> |
| 2165 | - <tal:widget define="widget nocall:view/widgets/branch_owner"> |
| 2166 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
| 2167 | - </tal:widget> |
| 2168 | - |
| 2169 | - </table> |
| 2170 | - <input tal:replace="structure view/rcs_type_emptymarker" /> |
| 2171 | - |
| 2172 | - </metal:formbody> |
| 2173 | - </div> |
| 2174 | - |
| 2175 | - <script type="text/javascript"> |
| 2176 | - LPJS.use('lp.code.productseries_setbranch', function(Y) { |
| 2177 | - Y.on('domready', Y.lp.code.productseries_setbranch.setup); |
| 2178 | - }); |
| 2179 | - </script> |
| 2180 | - |
| 2181 | -</div> |
| 2182 | -</body> |
| 2183 | -</html> |
| 2184 | |
| 2185 | === modified file 'lib/lp/registry/tests/test_product.py' |
| 2186 | --- lib/lp/registry/tests/test_product.py 2015-05-13 05:03:33 +0000 |
| 2187 | +++ lib/lp/registry/tests/test_product.py 2015-06-25 03:34:41 +0000 |
| 2188 | @@ -65,6 +65,7 @@ |
| 2189 | SharingPermission, |
| 2190 | SpecificationSharingPolicy, |
| 2191 | TeamMembershipPolicy, |
| 2192 | + VCSType, |
| 2193 | ) |
| 2194 | from lp.registry.errors import ( |
| 2195 | CannotChangeInformationType, |
| 2196 | @@ -309,6 +310,13 @@ |
| 2197 | [u'trunk', u'active-series'], |
| 2198 | [series.name for series in active_series]) |
| 2199 | |
| 2200 | + def test_inferred_vcs(self): |
| 2201 | + """VCS is inferred correctly from existing branch or repo.""" |
| 2202 | + git_repo = self.factory.makeGitRepository() |
| 2203 | + bzr_branch = self.factory.makeBranch() |
| 2204 | + self.assertEqual(VCSType.GIT, git_repo.target.inferred_vcs) |
| 2205 | + self.assertEqual(VCSType.BZR, bzr_branch.product.inferred_vcs) |
| 2206 | + |
| 2207 | def test_owner_cannot_be_open_team(self): |
| 2208 | """Product owners cannot be open teams.""" |
| 2209 | for policy in INCLUSIVE_TEAM_POLICY: |

Various code/style comments inline, UX design comments here:
In almost all cases the user will care about at most one VCS. Everything relating to the other VCS is just noise unless that VCS is selected (or a migration is in progress, or they're doing something weird). So the page should start by asking for the project's VCS (not a "default VCS" in the UI), defaulting to Bazaar until our Git support is more complete.
Once you've selected a VCS (not a "default VCS" in the UI) there are three options: push a new repo, choose an existing repo, or mirror a repo from elsewhere. The other VCS's config has to be available too, but it needn't be visible without an extra click.
The "push a new repo" case is handled by the push instructions. But they can be simpler, because the project owner can always create defaults if they don't exist: the Git URL should be git+ssh: //user@ host/project, and the Bazaar URL lp:project. The initial push instructions will also work fine to write to LP-hosted Bazaar branches or Git repositories, but they shouldn't be shown if the series Bazaar branch is already set and is an import, since those can't be written to directly.
The "choose an existing repo" case requires a selector widget, which can currently just be a textbox to specify a branch or repo path. The Bazaar case should stay as it is now, setting product. development_ focus.branch, and the Git case should use setDefaultRepos itory.
The "mirror a repo from elsewhere" case can currently only exist for Bazaar, as we don't have Git mirroring yet. We can work out how that looks for Git when we get there.