Merge lp:~julian-edwards/launchpad/ppa-deletion-ui into lp:launchpad/db-devel
- ppa-deletion-ui
- Merge into db-devel
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Guilherme Salgado | ||||
Approved revision: | no longer in the source branch. | ||||
Merge reported by: | Julian Edwards | ||||
Merged at revision: | not available | ||||
Proposed branch: | lp:~julian-edwards/launchpad/ppa-deletion-ui | ||||
Merge into: | lp:launchpad/db-devel | ||||
Diff against target: |
2167 lines (+869/-412) (has conflicts) 39 files modified
lib/canonical/launchpad/doc/canonical_url_examples.txt (+1/-1) lib/canonical/launchpad/icing/style-3-0.css.in (+3/-1) lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py (+24/-36) lib/lp/answers/browser/tests/test_breadcrumbs.py (+6/-12) lib/lp/blueprints/browser/tests/test_breadcrumbs.py (+5/-9) lib/lp/bugs/browser/configure.zcml (+2/-1) lib/lp/bugs/browser/tests/test_breadcrumbs.py (+32/-60) lib/lp/code/browser/codeimportmachine.py (+9/-0) lib/lp/code/browser/configure.zcml (+13/-0) lib/lp/code/browser/sourcepackagerecipe.py (+21/-7) lib/lp/code/browser/tests/test_breadcrumbs.py (+25/-0) lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+48/-2) lib/lp/code/configure.zcml (+0/-6) lib/lp/code/model/tests/test_diff.py (+3/-1) lib/lp/codehosting/codeimport/tests/test_worker.py (+15/-0) lib/lp/codehosting/codeimport/worker.py (+5/-2) lib/lp/registry/browser/tests/test_breadcrumbs.py (+10/-26) lib/lp/services/mailman/doc/reactivate-lists.txt (+1/-1) lib/lp/services/mailman/doc/staging.txt (+2/-1) lib/lp/soyuz/browser/archive.py (+82/-12) lib/lp/soyuz/browser/configure.zcml (+7/-0) lib/lp/soyuz/browser/tests/archive-views.txt (+2/-2) lib/lp/soyuz/browser/tests/test_breadcrumbs.py (+14/-34) lib/lp/soyuz/doc/archive-deletion.txt (+81/-0) lib/lp/soyuz/doc/archive.txt (+2/-0) lib/lp/soyuz/doc/buildd-mass-retry.txt (+39/-0) lib/lp/soyuz/interfaces/archive.py (+17/-0) lib/lp/soyuz/model/archive.py (+27/-1) lib/lp/soyuz/scripts/packagecopier.py (+0/-4) lib/lp/soyuz/scripts/tests/test_copypackage.py (+48/-3) lib/lp/soyuz/stories/ppa/xx-ppa-workflow.txt (+82/-4) lib/lp/soyuz/templates/archive-delete.pt (+32/-0) lib/lp/soyuz/templates/archive-index.pt (+2/-1) lib/lp/soyuz/templates/archive-packages.pt (+2/-1) lib/lp/testing/breadcrumbs.py (+40/-56) lib/lp/testing/publication.py (+57/-0) lib/lp/translations/browser/tests/test_breadcrumbs.py (+99/-128) scripts/ftpmaster-tools/buildd-mass-retry.py (+6/-0) utilities/sourcedeps.conf (+5/-0) Conflict adding file lib/canonical/launchpad/apidoc. Moved existing file to lib/canonical/launchpad/apidoc.moved. Text conflict in utilities/sourcedeps.conf |
||||
To merge this branch: | bzr merge lp:~julian-edwards/launchpad/ppa-deletion-ui | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Guilherme Salgado (community) | code | Approve | |
Michael Nelson (community) | ui | Approve | |
Paul Hummer (community) | ui* | Needs Information | |
Review via email: mp+21925@code.launchpad.net |
Commit message
Add a web user interface to delete PPAs.
Description of the change
Adds a trivial UI for PPA deletion.
Paul Hummer (rockstar) wrote : | # |
<rockstar> bigjools, also, the "Delete PPA" link shouldn't show if the deletion has already been requested.
<bigjools> rockstar: I thought about that, it was easier to leave it and make the page template different!
<bigjools> generally I really hate links appearing and disappearing with no explanation :/
<bigjools> but I can change it
<bigjools> one day I hope we'll present disabled links
<rockstar> bigjools, grey it out then?
<bigjools> rockstar: does Link do that?
<rockstar> bigjools, I don't think so, but it should... :)
<bigjools> heh
<rockstar> bigjools, basically, you shouldn't be able to click it if it's no longer deletable (because it's already deleted)
<bigjools> I didn't think about it too hard because it won't be around longer than 5 minutes
<bigjools> in any case we're changing tack slightly, and only removing the repository, the PPA will be disabled and hidden
<rockstar> bigjools, yeah, I figured that, but to the user, it's "deleted"
<bigjools> rockstar: yeah
<bigjools> rockstar: so I'll make the whole thing disappear and redirect to the user's profile page, what do you think?
<rockstar> bigjools, yeah, that's probably a good idea.
<rockstar> And then the info notification is something like "The PPA deletion has been requested."
<bigjools> right
<bigjools> in red?
<bigjools> :)
<rockstar> bigjools, no, at that point, blue is fine.
<bigjools> rockstar: cool, thanks for the pre-imp
Michael Nelson (michael.nelson) wrote : | # |
Great points Paul and Julian.
I'm just taking a look now, as by the time Paul's day starts, mine will be ending. The UI looks great and the redirection seems natural. I've got notes below, but basically I'm happy to ui=me - unless Paul disagrees of course - with the Delete menu item moved as below, and the Edit/Copy/Delete menu items disabled when the PPA is deleted/deleting.
Great stuff!
Details:
When first browsing:
https:/
I was a bit surprised to see the Delete action as the first presented.
{{{
11:49 < noodles> bigjools: did you intend for the Delete link to be the first presented in the menu? Or is it just because you've listed it alphabetically in the ArchiveIndexAct
11:49 < bigjools> noodles: that did cross my mind, I was considering moving it but was waiting for someone to comment first
11:49 < noodles> I'm wondering if it should be the last action (ie. after 'manage_
11:49 < bigjools> and you did :)
11:50 < noodles> Great.
}}}
I agree, when deleting the archive, redirecting to the profile and including the blue info box is a good solution. Works well.
At that point though, I'm wasn't sure there was any reason for presenting the link on the profile page allowing me to view the empty PPA, but Julian clarified that - the history is still viewable at:
https:/
{{{
11:56 < noodles> bigjools: Is there any reason to present the PPA after the deletion has been requested? Why have a disabled-like PPA link on the profile that can be clicked on and viewed in it's empty state with the "This archive has been deleted" message?
11:57 < bigjools> so you can go in and view its history even after deletion
11:58 < bigjools> StevenK: looks good
11:58 < noodles> bigjools: ah, I misunderstood. So it will stay there permanently? OK.
11:58 < bigjools> StevenK: when that's done, you need to run process-accepted to make it do the re-upload between the librarians
11:59 < bigjools> noodles: yes - this is what wgrant wanted, and is the simpler first step before complete obliteration of all archive objects
}}}
which leaves me wondering whether, rather than saying (on the PPA index page), "This archive has been deleted.", we said "This archive has been deleted ([link]View deleted packages[/link])." which would (1) indicate that there is useful information still available and (2) save you from having to know to click on the Packages link and update the filter to be able to see them. See what you both think. (it would be nice if this extra link only appeared on the index page)
Note: you can use view.archive_label instead of 'archive' in your message if you want it to automatically use "PPA" or "archive".
The only other thoughts I had were around the presentation of the menu options, such as "Copy packages", "Delete packages", "Change details" and "Edit dependencies".
I'm assuming we want to disable the last 3 (currently I can click on Change details and "re-enable" my PPA, which then means that the "This PPA has been deleted" msg no longer appears, even though I assume it's still deleted). Note, disabl...
Julian Edwards (julian-edwards) wrote : | # |
Ok guys, how is it looking now?
Guilherme Salgado (salgado) wrote : | # |
Hi Julian,
This is a nice branch and I'm glad you've asked for a UI review before
asking for a code review. I have some questions/
nothing major.
review needsfixing
On Fri, 2010-03-26 at 14:02 +0000, Julian Edwards wrote:
> You have been requested to review the proposed merge of lp:~julian-edwards/launchpad/ppa-deletion-ui into lp:launchpad.
>
> Adds a trivial UI for PPA deletion.
Since you've done changes after the m-p creation, would you care to run
'make lint' one more time?
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -404,13 +405,32 @@
> archive = view.context
> if not archive.private:
> link.enabled = False
> + if archive.status in (
> + ArchiveStatus.
> + link.enabled = False
> + return link
>
> return link
Am I reading this wrong or is there an extra 'return link' above? Maybe
'make lint' catches this?
>
> @enabled_
> def edit(self):
> text = 'Change details'
> - return Link('+edit', text, icon='edit')
> + link = Link('+edit', text, icon='edit')
> + view = self.context
> + if view.context.status in (
> + ArchiveStatus.
> + link.enabled = False
> + return link
> +
> + @enabled_
> + def delete_ppa(self):
> + text = 'Delete PPA'
> + link = Link('+delete', text, icon='trash-icon')
> + view = self.context
> + if view.context.status in (
> + ArchiveStatus.
> + link.enabled = False
> + return link
>
> def builds(self):
> text = 'View all builds'
> @@ -442,6 +462,10 @@
> # archives without any sources.
> if self.context.
> link.enabled = False
> + view = self.context
> + if view.context.status in (
> + ArchiveStatus.
> + link.enabled = False
> return link
>
> @enabled_
> @@ -458,7 +482,12 @@
> @enabled_
> def edit_dependenci
> text = 'Edit PPA dependencies'
> - return Link('+
> + link = Link('+
> + view = self.context
> + if view.context.status in (
> + ArchiveStatus.
> + link.enabled = False
> + return link
You could have a method/function which takes an archive and returns
whether or not it's (being) deleted, to avoid all the duplication above.
Also, you can pass enabled=False in the link constructor, simplifying
things a bit more. e.g.
def is_archive_
"""Return True if the given archive has not been deleted."""
return archive.staus...
Julian Edwards (julian-edwards) wrote : | # |
On Friday 26 March 2010 15:09:22 Guilherme Salgado wrote:
> This is a nice branch and I'm glad you've asked for a UI review before
> asking for a code review. I have some questions/
> nothing major.
Thanks for the review Salgado - answers inline as usual!
> Since you've done changes after the m-p creation, would you care to run
> 'make lint' one more time?
There's no lint, thanks for reminding me.
> > === modified file 'lib/lp/
> > --- lib/lp/
> > +++ lib/lp/
> > @@ -404,13 +405,32 @@
> > archive = view.context
> > if not archive.private:
> > link.enabled = False
> > + if archive.status in (
> > + ArchiveStatus.
> > + link.enabled = False
> > + return link
> >
> > return link
>
> Am I reading this wrong or is there an extra 'return link' above? Maybe
> 'make lint' catches this?
Yes it's wrong and no it doesn't!
> Although by now I'm starting to wonder if it shouldn't be a property of
> the model class?
I've done that now, dunno why I didn't do it before really!
> > + @property
> > + def can_be_
> > + return self.context.status != ArchiveStatus.
>
> Erm, can you delete an archive which has also been deleted (i.e. status
> == DELETED)? This doesn't seem consistent with the rules for enabling
> the links above...
It is indeed wrong, I've fixed it, thanks for noticing.
> > +Now, all the publications are DELTETED, the archive is disabled and the
>
> typo: DELTETED
Fixed.
> > + def delete(self, deleted_by):
> > + """See `IArchive`."""
>
> Would it make sense to assert that self.status is not DELETED or
> DELETING here?
Yep, done.
> > + >>> print no_priv_
> > + Delete “PPA for No Privileges Person” : PPA for No Privileges Person
> > : No Privileges Person
>
> The above line is too long, but since this is a doctest (and we run them
> with the NORMALIZE_
> it. :)
See, I know this. Why did I do it wrong? :)
> > + <form name="DELETE" action="" method="POST">
> > + <input name="DELETE" type="hidden" value="1"/>
> > + <input type="submit" value="Permanently delete PPA"/>
> > + or <a tal:attributes=
>
> Why did you choose to hand-craft this form? Using a LaunchpadFormView
> you'd even get the cancel link for free when using an @action...
Nearly free :)
I was hacking it up to see what I wanted and didn't get around to changing it
to an LPForm. All fixed now!
> Now I'm left wondering where's the code that will actually delete the
> PPA repository and set its status to DELETED? Does it exist or is that
> something you'll write in another branch?
It's being done in a different branch - the publisher will remove it.
Cheers
J
Guilherme Salgado (salgado) wrote : | # |
review approve code
status approved
Julian Edwards (julian-edwards) wrote : | # |
Because the backend didn't make it last cycle, this change is now going to be delayed a whole cycle as it can't appear on edge unless the backend is in place :(
Preview Diff
1 | === added directory 'lib/canonical/launchpad/apidoc' | |||
2 | === renamed directory 'lib/canonical/launchpad/apidoc' => 'lib/canonical/launchpad/apidoc.moved' | |||
3 | === modified file 'lib/canonical/launchpad/doc/canonical_url_examples.txt' | |||
4 | --- lib/canonical/launchpad/doc/canonical_url_examples.txt 2010-04-16 15:06:55 +0000 | |||
5 | +++ lib/canonical/launchpad/doc/canonical_url_examples.txt 2010-04-29 11:26:16 +0000 | |||
6 | @@ -275,7 +275,7 @@ | |||
7 | 275 | >>> bug_comment = BugComment( | 275 | >>> bug_comment = BugComment( |
8 | 276 | ... 1, bug_one.initial_message, bugtask_one, True) | 276 | ... 1, bug_one.initial_message, bugtask_one, True) |
9 | 277 | >>> canonical_url(bug_comment) | 277 | >>> canonical_url(bug_comment) |
11 | 278 | u'http://launchpad.dev/firefox/+bug/1/comments/1' | 278 | u'http://bugs.launchpad.dev/firefox/+bug/1/comments/1' |
12 | 279 | 279 | ||
13 | 280 | An IBugNomination. | 280 | An IBugNomination. |
14 | 281 | 281 | ||
15 | 282 | 282 | ||
16 | === modified file 'lib/canonical/launchpad/icing/style-3-0.css.in' | |||
17 | --- lib/canonical/launchpad/icing/style-3-0.css.in 2010-04-27 12:38:38 +0000 | |||
18 | +++ lib/canonical/launchpad/icing/style-3-0.css.in 2010-04-29 11:26:16 +0000 | |||
19 | @@ -1656,7 +1656,9 @@ | |||
20 | 1656 | .distromirrorstatusONEHOURBEHIND, | 1656 | .distromirrorstatusONEHOURBEHIND, |
21 | 1657 | .distromirrorstatusTWOHOURSBEHIND, | 1657 | .distromirrorstatusTWOHOURSBEHIND, |
22 | 1658 | .distromirrorstatusFOURHOURSBEHIND, | 1658 | .distromirrorstatusFOURHOURSBEHIND, |
24 | 1659 | .distromirrorstatusSIXHOURSBEHIND, | 1659 | .distromirrorstatusSIXHOURSBEHIND { |
25 | 1660 | color: green; | ||
26 | 1661 | } | ||
27 | 1660 | .distromirrorstatusONEDAYBEHIND { | 1662 | .distromirrorstatusONEDAYBEHIND { |
28 | 1661 | color: #f60; | 1663 | color: #f60; |
29 | 1662 | } | 1664 | } |
30 | 1663 | 1665 | ||
31 | === modified file 'lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py' | |||
32 | --- lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py 2009-09-21 16:21:50 +0000 | |||
33 | +++ lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py 2010-04-29 11:26:16 +0000 | |||
34 | @@ -13,9 +13,8 @@ | |||
35 | 13 | from canonical.launchpad.webapp.interfaces import ICanonicalUrlData | 13 | from canonical.launchpad.webapp.interfaces import ICanonicalUrlData |
36 | 14 | from canonical.launchpad.webapp.publisher import canonical_url | 14 | from canonical.launchpad.webapp.publisher import canonical_url |
37 | 15 | from canonical.launchpad.webapp.servers import LaunchpadTestRequest | 15 | from canonical.launchpad.webapp.servers import LaunchpadTestRequest |
38 | 16 | from canonical.launchpad.webapp.tests.breadcrumbs import ( | ||
39 | 17 | BaseBreadcrumbTestCase) | ||
40 | 18 | from lp.testing import login, TestCase | 16 | from lp.testing import login, TestCase |
41 | 17 | from lp.testing.breadcrumbs import BaseBreadcrumbTestCase | ||
42 | 19 | 18 | ||
43 | 20 | 19 | ||
44 | 21 | class Cookbook: | 20 | class Cookbook: |
45 | @@ -52,19 +51,17 @@ | |||
46 | 52 | self.product_url = canonical_url(self.product) | 51 | self.product_url = canonical_url(self.product) |
47 | 53 | 52 | ||
48 | 54 | def test_default_page(self): | 53 | def test_default_page(self): |
52 | 55 | urls = self._getBreadcrumbsURLs( | 54 | self.assertBreadcrumbUrls([self.product_url], self.product) |
50 | 56 | self.product_url, [self.root, self.product]) | ||
51 | 57 | self.assertEquals(urls, [self.product_url]) | ||
53 | 58 | 55 | ||
54 | 59 | def test_non_default_page(self): | 56 | def test_non_default_page(self): |
55 | 57 | crumbs = self.getBreadcrumbsForObject(self.product, '+download') | ||
56 | 60 | downloads_url = "%s/+download" % self.product_url | 58 | downloads_url = "%s/+download" % self.product_url |
64 | 61 | urls = self._getBreadcrumbsURLs( | 59 | self.assertEquals( |
65 | 62 | downloads_url, [self.root, self.product]) | 60 | [self.product_url, downloads_url], |
66 | 63 | self.assertEquals(urls, [self.product_url, downloads_url]) | 61 | [crumb.url for crumb in crumbs]) |
67 | 64 | texts = self._getBreadcrumbsTexts( | 62 | self.assertEquals( |
68 | 65 | downloads_url, [self.root, self.product]) | 63 | '%s project files' % self.product.displayname, |
69 | 66 | self.assertEquals(texts[-1], | 64 | crumbs[-1].text) |
63 | 67 | '%s project files' % self.product.displayname) | ||
70 | 68 | 65 | ||
71 | 69 | def test_zope_i18n_Messages_are_interpolated(self): | 66 | def test_zope_i18n_Messages_are_interpolated(self): |
72 | 70 | # Views can use zope.i18nmessageid.Message as their title when they | 67 | # Views can use zope.i18nmessageid.Message as their title when they |
73 | @@ -108,31 +105,25 @@ | |||
74 | 108 | self.package_bugtask_url = canonical_url(self.package_bugtask) | 105 | self.package_bugtask_url = canonical_url(self.package_bugtask) |
75 | 109 | 106 | ||
76 | 110 | def test_root_on_mainsite(self): | 107 | def test_root_on_mainsite(self): |
79 | 111 | urls = self._getBreadcrumbsURLs('http://launchpad.dev/', [self.root]) | 108 | crumbs = self.getBreadcrumbsForUrl('http://launchpad.dev/') |
80 | 112 | self.assertEquals(urls, []) | 109 | self.assertEquals(crumbs, []) |
81 | 113 | 110 | ||
82 | 114 | def test_product_on_mainsite(self): | 111 | def test_product_on_mainsite(self): |
86 | 115 | urls = self._getBreadcrumbsURLs( | 112 | self.assertBreadcrumbUrls([self.product_url], self.product) |
84 | 116 | self.product_url, [self.root, self.product]) | ||
85 | 117 | self.assertEquals(urls, [self.product_url]) | ||
87 | 118 | 113 | ||
88 | 119 | def test_root_on_vhost(self): | 114 | def test_root_on_vhost(self): |
92 | 120 | urls = self._getBreadcrumbsURLs( | 115 | crumbs = self.getBreadcrumbsForUrl('http://bugs.launchpad.dev/') |
93 | 121 | 'http://bugs.launchpad.dev/', [self.root]) | 116 | self.assertEquals(crumbs, []) |
91 | 122 | self.assertEquals(urls, []) | ||
94 | 123 | 117 | ||
95 | 124 | def test_product_on_vhost(self): | 118 | def test_product_on_vhost(self): |
99 | 125 | urls = self._getBreadcrumbsURLs( | 119 | self.assertBreadcrumbUrls( |
100 | 126 | self.product_bugs_url, [self.root, self.product]) | 120 | [self.product_url, self.product_bugs_url], |
101 | 127 | self.assertEquals(urls, [self.product_url, self.product_bugs_url]) | 121 | self.product, rootsite='bugs') |
102 | 128 | 122 | ||
103 | 129 | def test_product_bugtask(self): | 123 | def test_product_bugtask(self): |
110 | 130 | urls = self._getBreadcrumbsURLs( | 124 | self.assertBreadcrumbUrls( |
111 | 131 | self.product_bugtask_url, | 125 | [self.product_url, self.product_bugs_url, self.product_bugtask_url], |
112 | 132 | [self.root, self.product, self.product_bugtask]) | 126 | self.product_bugtask) |
107 | 133 | self.assertEquals( | ||
108 | 134 | urls, [self.product_url, self.product_bugs_url, | ||
109 | 135 | self.product_bugtask_url]) | ||
113 | 136 | 127 | ||
114 | 137 | def test_package_bugtask(self): | 128 | def test_package_bugtask(self): |
115 | 138 | target = self.package_bugtask.target | 129 | target = self.package_bugtask.target |
116 | @@ -140,14 +131,11 @@ | |||
117 | 140 | distroseries_url = canonical_url(target.distroseries) | 131 | distroseries_url = canonical_url(target.distroseries) |
118 | 141 | package_url = canonical_url(target) | 132 | package_url = canonical_url(target) |
119 | 142 | package_bugs_url = canonical_url(target, rootsite='bugs') | 133 | package_bugs_url = canonical_url(target, rootsite='bugs') |
126 | 143 | urls = self._getBreadcrumbsURLs( | 134 | |
127 | 144 | self.package_bugtask_url, | 135 | self.assertBreadcrumbUrls( |
122 | 145 | [self.root, target.distribution, target.distroseries, target, | ||
123 | 146 | self.package_bugtask]) | ||
124 | 147 | self.assertEquals( | ||
125 | 148 | urls, | ||
128 | 149 | [distro_url, distroseries_url, package_url, package_bugs_url, | 136 | [distro_url, distroseries_url, package_url, package_bugs_url, |
130 | 150 | self.package_bugtask_url]) | 137 | self.package_bugtask_url], |
131 | 138 | self.package_bugtask) | ||
132 | 151 | 139 | ||
133 | 152 | 140 | ||
134 | 153 | def test_suite(): | 141 | def test_suite(): |
135 | 154 | 142 | ||
136 | === modified file 'lib/lp/answers/browser/tests/test_breadcrumbs.py' | |||
137 | --- lib/lp/answers/browser/tests/test_breadcrumbs.py 2010-02-17 11:13:06 +0000 | |||
138 | +++ lib/lp/answers/browser/tests/test_breadcrumbs.py 2010-04-29 11:26:16 +0000 | |||
139 | @@ -6,10 +6,9 @@ | |||
140 | 6 | import unittest | 6 | import unittest |
141 | 7 | 7 | ||
142 | 8 | from canonical.launchpad.webapp.publisher import canonical_url | 8 | from canonical.launchpad.webapp.publisher import canonical_url |
143 | 9 | from canonical.launchpad.webapp.tests.breadcrumbs import ( | ||
144 | 10 | BaseBreadcrumbTestCase) | ||
145 | 11 | 9 | ||
146 | 12 | from lp.testing import login_person | 10 | from lp.testing import login_person |
147 | 11 | from lp.testing.breadcrumbs import BaseBreadcrumbTestCase | ||
148 | 13 | 12 | ||
149 | 14 | 13 | ||
150 | 15 | class TestQuestionTargetProjectAndPersonBreadcrumbOnAnswersVHost( | 14 | class TestQuestionTargetProjectAndPersonBreadcrumbOnAnswersVHost( |
151 | @@ -36,22 +35,19 @@ | |||
152 | 36 | self.project, rootsite='answers') | 35 | self.project, rootsite='answers') |
153 | 37 | 36 | ||
154 | 38 | def test_product(self): | 37 | def test_product(self): |
157 | 39 | crumbs = self._getBreadcrumbs( | 38 | crumbs = self.getBreadcrumbsForObject(self.product, rootsite='answers') |
156 | 40 | self.product_questions_url, [self.root, self.product]) | ||
158 | 41 | last_crumb = crumbs[-1] | 39 | last_crumb = crumbs[-1] |
159 | 42 | self.assertEquals(last_crumb.url, self.product_questions_url) | 40 | self.assertEquals(last_crumb.url, self.product_questions_url) |
160 | 43 | self.assertEquals(last_crumb.text, 'Questions') | 41 | self.assertEquals(last_crumb.text, 'Questions') |
161 | 44 | 42 | ||
162 | 45 | def test_project(self): | 43 | def test_project(self): |
165 | 46 | crumbs = self._getBreadcrumbs( | 44 | crumbs = self.getBreadcrumbsForObject(self.project, rootsite='answers') |
164 | 47 | self.project_questions_url, [self.root, self.project]) | ||
166 | 48 | last_crumb = crumbs[-1] | 45 | last_crumb = crumbs[-1] |
167 | 49 | self.assertEquals(last_crumb.url, self.project_questions_url) | 46 | self.assertEquals(last_crumb.url, self.project_questions_url) |
168 | 50 | self.assertEquals(last_crumb.text, 'Questions') | 47 | self.assertEquals(last_crumb.text, 'Questions') |
169 | 51 | 48 | ||
170 | 52 | def test_person(self): | 49 | def test_person(self): |
173 | 53 | crumbs = self._getBreadcrumbs( | 50 | crumbs = self.getBreadcrumbsForObject(self.person, rootsite='answers') |
172 | 54 | self.person_questions_url, [self.root, self.person]) | ||
174 | 55 | last_crumb = crumbs[-1] | 51 | last_crumb = crumbs[-1] |
175 | 56 | self.assertEquals(last_crumb.url, self.person_questions_url) | 52 | self.assertEquals(last_crumb.url, self.person_questions_url) |
176 | 57 | self.assertEquals(last_crumb.text, 'Questions') | 53 | self.assertEquals(last_crumb.text, 'Questions') |
177 | @@ -69,16 +65,14 @@ | |||
178 | 69 | self.question = self.factory.makeQuestion( | 65 | self.question = self.factory.makeQuestion( |
179 | 70 | target=self.product, title='Seeds are hard to chew') | 66 | target=self.product, title='Seeds are hard to chew') |
180 | 71 | self.question_url = canonical_url(self.question, rootsite='answers') | 67 | self.question_url = canonical_url(self.question, rootsite='answers') |
183 | 72 | crumbs = self._getBreadcrumbs( | 68 | crumbs = self.getBreadcrumbsForObject(self.question) |
182 | 73 | self.question_url, [self.root, self.product, self.question]) | ||
184 | 74 | last_crumb = crumbs[-1] | 69 | last_crumb = crumbs[-1] |
185 | 75 | self.assertEquals(last_crumb.text, 'Question #%d' % self.question.id) | 70 | self.assertEquals(last_crumb.text, 'Question #%d' % self.question.id) |
186 | 76 | 71 | ||
187 | 77 | def test_faq(self): | 72 | def test_faq(self): |
188 | 78 | self.faq = self.factory.makeFAQ(target=self.product, title='Seedless') | 73 | self.faq = self.factory.makeFAQ(target=self.product, title='Seedless') |
189 | 79 | self.faq_url = canonical_url(self.faq, rootsite='answers') | 74 | self.faq_url = canonical_url(self.faq, rootsite='answers') |
192 | 80 | crumbs = self._getBreadcrumbs( | 75 | crumbs = self.getBreadcrumbsForObject(self.faq) |
191 | 81 | self.faq_url, [self.root, self.product, self.faq]) | ||
193 | 82 | last_crumb = crumbs[-1] | 76 | last_crumb = crumbs[-1] |
194 | 83 | self.assertEquals(last_crumb.text, 'FAQ #%d' % self.faq.id) | 77 | self.assertEquals(last_crumb.text, 'FAQ #%d' % self.faq.id) |
195 | 84 | 78 | ||
196 | 85 | 79 | ||
197 | === modified file 'lib/lp/blueprints/browser/tests/test_breadcrumbs.py' | |||
198 | --- lib/lp/blueprints/browser/tests/test_breadcrumbs.py 2009-09-22 15:02:41 +0000 | |||
199 | +++ lib/lp/blueprints/browser/tests/test_breadcrumbs.py 2010-04-29 11:26:16 +0000 | |||
200 | @@ -6,8 +6,8 @@ | |||
201 | 6 | import unittest | 6 | import unittest |
202 | 7 | 7 | ||
203 | 8 | from canonical.launchpad.webapp.publisher import canonical_url | 8 | from canonical.launchpad.webapp.publisher import canonical_url |
206 | 9 | from canonical.launchpad.webapp.tests.breadcrumbs import ( | 9 | |
207 | 10 | BaseBreadcrumbTestCase) | 10 | from lp.testing.breadcrumbs import BaseBreadcrumbTestCase |
208 | 11 | 11 | ||
209 | 12 | 12 | ||
210 | 13 | class TestHasSpecificationsBreadcrumbOnBlueprintsVHost( | 13 | class TestHasSpecificationsBreadcrumbOnBlueprintsVHost( |
211 | @@ -25,15 +25,13 @@ | |||
212 | 25 | self.product, rootsite='blueprints') | 25 | self.product, rootsite='blueprints') |
213 | 26 | 26 | ||
214 | 27 | def test_product(self): | 27 | def test_product(self): |
217 | 28 | crumbs = self._getBreadcrumbs( | 28 | crumbs = self.getBreadcrumbsForObject(self.product, rootsite='blueprints') |
216 | 29 | self.product_specs_url, [self.root, self.product]) | ||
218 | 30 | last_crumb = crumbs[-1] | 29 | last_crumb = crumbs[-1] |
219 | 31 | self.assertEquals(last_crumb.url, self.product_specs_url) | 30 | self.assertEquals(last_crumb.url, self.product_specs_url) |
220 | 32 | self.assertEquals(last_crumb.text, 'Blueprints') | 31 | self.assertEquals(last_crumb.text, 'Blueprints') |
221 | 33 | 32 | ||
222 | 34 | def test_person(self): | 33 | def test_person(self): |
225 | 35 | crumbs = self._getBreadcrumbs( | 34 | crumbs = self.getBreadcrumbsForObject(self.person, rootsite='blueprints') |
224 | 36 | self.person_specs_url, [self.root, self.person]) | ||
226 | 37 | last_crumb = crumbs[-1] | 35 | last_crumb = crumbs[-1] |
227 | 38 | self.assertEquals(last_crumb.url, self.person_specs_url) | 36 | self.assertEquals(last_crumb.url, self.person_specs_url) |
228 | 39 | self.assertEquals(last_crumb.text, 'Blueprints') | 37 | self.assertEquals(last_crumb.text, 'Blueprints') |
229 | @@ -52,9 +50,7 @@ | |||
230 | 52 | self.specification, rootsite='blueprints') | 50 | self.specification, rootsite='blueprints') |
231 | 53 | 51 | ||
232 | 54 | def test_specification(self): | 52 | def test_specification(self): |
236 | 55 | crumbs = self._getBreadcrumbs( | 53 | crumbs = self.getBreadcrumbsForObject(self.specification) |
234 | 56 | self.specification_url, | ||
235 | 57 | [self.root, self.product, self.specification]) | ||
237 | 58 | last_crumb = crumbs[-1] | 54 | last_crumb = crumbs[-1] |
238 | 59 | self.assertEquals(last_crumb.url, self.specification_url) | 55 | self.assertEquals(last_crumb.url, self.specification_url) |
239 | 60 | self.assertEquals( | 56 | self.assertEquals( |
240 | 61 | 57 | ||
241 | === modified file 'lib/lp/bugs/browser/configure.zcml' | |||
242 | --- lib/lp/bugs/browser/configure.zcml 2010-04-15 15:14:21 +0000 | |||
243 | +++ lib/lp/bugs/browser/configure.zcml 2010-04-29 11:26:16 +0000 | |||
244 | @@ -163,7 +163,8 @@ | |||
245 | 163 | <browser:url | 163 | <browser:url |
246 | 164 | for="canonical.launchpad.interfaces.IBugComment" | 164 | for="canonical.launchpad.interfaces.IBugComment" |
247 | 165 | path_expression="string:comments/${index}" | 165 | path_expression="string:comments/${index}" |
249 | 166 | attribute_to_parent="bugtask"/> | 166 | attribute_to_parent="bugtask" |
250 | 167 | rootsite="bugs"/> | ||
251 | 167 | <browser:page | 168 | <browser:page |
252 | 168 | for="canonical.launchpad.interfaces.IBugComment" | 169 | for="canonical.launchpad.interfaces.IBugComment" |
253 | 169 | name="+index" | 170 | name="+index" |
254 | 170 | 171 | ||
255 | === modified file 'lib/lp/bugs/browser/tests/test_breadcrumbs.py' | |||
256 | --- lib/lp/bugs/browser/tests/test_breadcrumbs.py 2010-03-22 18:39:24 +0000 | |||
257 | +++ lib/lp/bugs/browser/tests/test_breadcrumbs.py 2010-04-29 11:26:16 +0000 | |||
258 | @@ -8,10 +8,10 @@ | |||
259 | 8 | from zope.component import getUtility | 8 | from zope.component import getUtility |
260 | 9 | 9 | ||
261 | 10 | from canonical.launchpad.webapp.publisher import canonical_url | 10 | from canonical.launchpad.webapp.publisher import canonical_url |
264 | 11 | from canonical.launchpad.webapp.tests.breadcrumbs import ( | 11 | |
263 | 12 | BaseBreadcrumbTestCase) | ||
265 | 13 | from lp.bugs.interfaces.bugtracker import IBugTrackerSet | 12 | from lp.bugs.interfaces.bugtracker import IBugTrackerSet |
267 | 14 | from lp.testing import ANONYMOUS, login | 13 | from lp.testing import login_person |
268 | 14 | from lp.testing.breadcrumbs import BaseBreadcrumbTestCase | ||
269 | 15 | 15 | ||
270 | 16 | 16 | ||
271 | 17 | class TestBugTaskBreadcrumb(BaseBreadcrumbTestCase): | 17 | class TestBugTaskBreadcrumb(BaseBreadcrumbTestCase): |
272 | @@ -21,54 +21,35 @@ | |||
273 | 21 | product = self.factory.makeProduct( | 21 | product = self.factory.makeProduct( |
274 | 22 | name='crumb-tester', displayname="Crumb Tester") | 22 | name='crumb-tester', displayname="Crumb Tester") |
275 | 23 | self.bug = self.factory.makeBug(product=product) | 23 | self.bug = self.factory.makeBug(product=product) |
280 | 24 | self.bugtask_url = canonical_url( | 24 | self.bugtask = self.bug.default_bugtask |
281 | 25 | self.bug.default_bugtask, rootsite='bugs') | 25 | self.bugtask_url = canonical_url(self.bugtask, rootsite='bugs') |
278 | 26 | self.traversed_objects = [ | ||
279 | 27 | self.root, product, self.bug.default_bugtask] | ||
282 | 28 | 26 | ||
283 | 29 | def test_bugtask(self): | 27 | def test_bugtask(self): |
290 | 30 | urls = self._getBreadcrumbsURLs( | 28 | crumbs = self.getBreadcrumbsForObject(self.bugtask) |
291 | 31 | self.bugtask_url, self.traversed_objects) | 29 | last_crumb = crumbs[-1] |
292 | 32 | self.assertEquals(urls[-1], self.bugtask_url) | 30 | self.assertEquals(self.bugtask_url, last_crumb.url) |
293 | 33 | texts = self._getBreadcrumbsTexts( | 31 | self.assertEquals("Bug #%d" % self.bug.id, last_crumb.text) |
288 | 34 | self.bugtask_url, self.traversed_objects) | ||
289 | 35 | self.assertEquals(texts[-1], "Bug #%d" % self.bug.id) | ||
294 | 36 | 32 | ||
295 | 37 | def test_bugtask_child(self): | 33 | def test_bugtask_child(self): |
303 | 38 | url = canonical_url( | 34 | crumbs = self.getBreadcrumbsForObject(self.bugtask, view_name='+activity') |
304 | 39 | self.bug.default_bugtask, rootsite='bugs', view_name='+activity') | 35 | self.assertEquals(crumbs[-1].url, "%s/+activity" % self.bugtask_url) |
305 | 40 | urls = self._getBreadcrumbsURLs(url, self.traversed_objects) | 36 | self.assertEquals(crumbs[-2].url, self.bugtask_url) |
306 | 41 | self.assertEquals(urls[-1], "%s/+activity" % self.bugtask_url) | 37 | self.assertEquals(crumbs[-2].text, "Bug #%d" % self.bug.id) |
300 | 42 | self.assertEquals(urls[-2], self.bugtask_url) | ||
301 | 43 | texts = self._getBreadcrumbsTexts(url, self.traversed_objects) | ||
302 | 44 | self.assertEquals(texts[-2], "Bug #%d" % self.bug.id) | ||
307 | 45 | 38 | ||
308 | 46 | def test_bugtask_comment(self): | 39 | def test_bugtask_comment(self): |
310 | 47 | login('foo.bar@canonical.com') | 40 | login_person(self.bug.owner) |
311 | 48 | comment = self.factory.makeBugComment( | 41 | comment = self.factory.makeBugComment( |
312 | 49 | bug=self.bug, owner=self.bug.owner, | 42 | bug=self.bug, owner=self.bug.owner, |
313 | 50 | subject="test comment subject", body="test comment body") | 43 | subject="test comment subject", body="test comment body") |
335 | 51 | url = canonical_url(comment, rootsite='bugs') | 44 | expected_breadcrumbs = [ |
336 | 52 | urls = self._getBreadcrumbsURLs(url, self.traversed_objects) | 45 | ('Crumb Tester', 'http://launchpad.dev/crumb-tester'), |
337 | 53 | texts = self._getBreadcrumbsTexts(url, self.traversed_objects) | 46 | ('Bugs', 'http://bugs.launchpad.dev/crumb-tester'), |
338 | 54 | self.assertEquals(url, "%s/comments/1" % self.bugtask_url) | 47 | ('Bug #%s' % self.bug.id, |
339 | 55 | self.assertEquals(urls[-1], "%s" % self.bugtask_url) | 48 | 'http://bugs.launchpad.dev/crumb-tester/+bug/%s' % self.bug.id), |
340 | 56 | self.assertEquals(texts[-1], "Bug #%d" % self.bug.id) | 49 | ('Comment #1', |
341 | 57 | 50 | 'http://bugs.launchpad.dev/crumb-tester/+bug/%s/comments/1' % self.bug.id), | |
342 | 58 | def test_bugtask_private_bug(self): | 51 | ] |
343 | 59 | # A breadcrumb is not generated for a bug that the user does | 52 | self.assertBreadcrumbs(expected_breadcrumbs, comment) |
323 | 60 | # not have permission to view. | ||
324 | 61 | login('foo.bar@canonical.com') | ||
325 | 62 | self.bug.setPrivate(True, self.bug.owner) | ||
326 | 63 | login(ANONYMOUS) | ||
327 | 64 | url = canonical_url(self.bug.default_bugtask, rootsite='bugs') | ||
328 | 65 | self.assertEquals( | ||
329 | 66 | ['http://launchpad.dev/crumb-tester', | ||
330 | 67 | 'http://bugs.launchpad.dev/crumb-tester'], | ||
331 | 68 | self._getBreadcrumbsURLs(url, self.traversed_objects)) | ||
332 | 69 | self.assertEquals( | ||
333 | 70 | ["Crumb Tester", "Bugs"], | ||
334 | 71 | self._getBreadcrumbsTexts(url, self.traversed_objects)) | ||
344 | 72 | 53 | ||
345 | 73 | 54 | ||
346 | 74 | class TestBugTrackerBreadcrumbs(BaseBreadcrumbTestCase): | 55 | class TestBugTrackerBreadcrumbs(BaseBreadcrumbTestCase): |
347 | @@ -84,28 +65,19 @@ | |||
348 | 84 | 65 | ||
349 | 85 | def test_bug_tracker_set(self): | 66 | def test_bug_tracker_set(self): |
350 | 86 | # Check TestBugTrackerSetBreadcrumb. | 67 | # Check TestBugTrackerSetBreadcrumb. |
359 | 87 | traversed_objects = [ | 68 | expected_breadcrumbs = [ |
360 | 88 | self.root, self.bug_tracker_set] | 69 | ('Bug trackers', self.bug_tracker_set_url), |
361 | 89 | urls = self._getBreadcrumbsURLs( | 70 | ] |
362 | 90 | self.bug_tracker_set_url, traversed_objects) | 71 | self.assertBreadcrumbs(expected_breadcrumbs, self.bug_tracker_set) |
355 | 91 | self.assertEquals(self.bug_tracker_set_url, urls[-1]) | ||
356 | 92 | texts = self._getBreadcrumbsTexts( | ||
357 | 93 | self.bug_tracker_set_url, traversed_objects) | ||
358 | 94 | self.assertEquals("Bug trackers", texts[-1]) | ||
363 | 95 | 72 | ||
364 | 96 | def test_bug_tracker(self): | 73 | def test_bug_tracker(self): |
365 | 97 | # Check TestBugTrackerBreadcrumb (and | 74 | # Check TestBugTrackerBreadcrumb (and |
366 | 98 | # TestBugTrackerSetBreadcrumb). | 75 | # TestBugTrackerSetBreadcrumb). |
377 | 99 | traversed_objects = [ | 76 | expected_breadcrumbs = [ |
378 | 100 | self.root, self.bug_tracker_set, self.bug_tracker] | 77 | ('Bug trackers', self.bug_tracker_set_url), |
379 | 101 | urls = self._getBreadcrumbsURLs( | 78 | (self.bug_tracker.title, self.bug_tracker_url), |
380 | 102 | self.bug_tracker_url, traversed_objects) | 79 | ] |
381 | 103 | self.assertEquals(self.bug_tracker_url, urls[-1]) | 80 | self.assertBreadcrumbs(expected_breadcrumbs, self.bug_tracker) |
372 | 104 | self.assertEquals(self.bug_tracker_set_url, urls[-2]) | ||
373 | 105 | texts = self._getBreadcrumbsTexts( | ||
374 | 106 | self.bug_tracker_url, traversed_objects) | ||
375 | 107 | self.assertEquals(self.bug_tracker.title, texts[-1]) | ||
376 | 108 | self.assertEquals("Bug trackers", texts[-2]) | ||
382 | 109 | 81 | ||
383 | 110 | 82 | ||
384 | 111 | def test_suite(): | 83 | def test_suite(): |
385 | 112 | 84 | ||
386 | === modified file 'lib/lp/code/browser/codeimportmachine.py' | |||
387 | --- lib/lp/code/browser/codeimportmachine.py 2009-08-24 20:28:33 +0000 | |||
388 | +++ lib/lp/code/browser/codeimportmachine.py 2010-04-29 11:26:16 +0000 | |||
389 | @@ -6,6 +6,7 @@ | |||
390 | 6 | __metaclass__ = type | 6 | __metaclass__ = type |
391 | 7 | 7 | ||
392 | 8 | __all__ = [ | 8 | __all__ = [ |
393 | 9 | 'CodeImportMachineBreadcrumb', | ||
394 | 9 | 'CodeImportMachineSetBreadcrumb', | 10 | 'CodeImportMachineSetBreadcrumb', |
395 | 10 | 'CodeImportMachineSetNavigation', | 11 | 'CodeImportMachineSetNavigation', |
396 | 11 | 'CodeImportMachineSetView', | 12 | 'CodeImportMachineSetView', |
397 | @@ -30,6 +31,14 @@ | |||
398 | 30 | from lazr.delegates import delegates | 31 | from lazr.delegates import delegates |
399 | 31 | 32 | ||
400 | 32 | 33 | ||
401 | 34 | class CodeImportMachineBreadcrumb(Breadcrumb): | ||
402 | 35 | """An `IBreadcrumb` that uses the machines hostname.""" | ||
403 | 36 | |||
404 | 37 | @property | ||
405 | 38 | def text(self): | ||
406 | 39 | return self.context.hostname | ||
407 | 40 | |||
408 | 41 | |||
409 | 33 | class CodeImportMachineSetNavigation(Navigation): | 42 | class CodeImportMachineSetNavigation(Navigation): |
410 | 34 | """Navigation methods for ICodeImportMachineSet.""" | 43 | """Navigation methods for ICodeImportMachineSet.""" |
411 | 35 | usedfor = ICodeImportMachineSet | 44 | usedfor = ICodeImportMachineSet |
412 | 36 | 45 | ||
413 | === modified file 'lib/lp/code/browser/configure.zcml' | |||
414 | --- lib/lp/code/browser/configure.zcml 2010-04-19 03:44:27 +0000 | |||
415 | +++ lib/lp/code/browser/configure.zcml 2010-04-29 11:26:16 +0000 | |||
416 | @@ -1055,6 +1055,19 @@ | |||
417 | 1055 | for="lp.code.interfaces.branchmergeproposal.IBranchMergeProposal" | 1055 | for="lp.code.interfaces.branchmergeproposal.IBranchMergeProposal" |
418 | 1056 | factory="lp.code.browser.branchmergeproposal.BranchMergeProposalBreadcrumb" | 1056 | factory="lp.code.browser.branchmergeproposal.BranchMergeProposalBreadcrumb" |
419 | 1057 | permission="zope.Public"/> | 1057 | permission="zope.Public"/> |
420 | 1058 | |||
421 | 1059 | <adapter | ||
422 | 1060 | provides="canonical.launchpad.webapp.interfaces.IBreadcrumb" | ||
423 | 1061 | for="lp.code.interfaces.codeimportmachine.ICodeImportMachine" | ||
424 | 1062 | factory="lp.code.browser.codeimportmachine.CodeImportMachineBreadcrumb" | ||
425 | 1063 | permission="zope.Public"/> | ||
426 | 1064 | |||
427 | 1065 | <adapter | ||
428 | 1066 | provides="canonical.launchpad.webapp.interfaces.IBreadcrumb" | ||
429 | 1067 | for="lp.code.interfaces.codeimportmachine.ICodeImportMachineSet" | ||
430 | 1068 | factory="lp.code.browser.codeimportmachine.CodeImportMachineSetBreadcrumb" | ||
431 | 1069 | permission="zope.Public"/> | ||
432 | 1070 | |||
433 | 1058 | <adapter | 1071 | <adapter |
434 | 1059 | factory="lp.code.browser.branchmergeproposal.PreviewDiffHTMLRepresentation" | 1072 | factory="lp.code.browser.branchmergeproposal.PreviewDiffHTMLRepresentation" |
435 | 1060 | name="lazr.restful.EntryResource"/> | 1073 | name="lazr.restful.EntryResource"/> |
436 | 1061 | 1074 | ||
437 | === modified file 'lib/lp/code/browser/sourcepackagerecipe.py' | |||
438 | --- lib/lp/code/browser/sourcepackagerecipe.py 2010-04-23 03:30:30 +0000 | |||
439 | +++ lib/lp/code/browser/sourcepackagerecipe.py 2010-04-29 11:26:16 +0000 | |||
440 | @@ -45,6 +45,7 @@ | |||
441 | 45 | IArchiveSet) | 45 | IArchiveSet) |
442 | 46 | from lp.registry.interfaces.distroseries import IDistroSeriesSet | 46 | from lp.registry.interfaces.distroseries import IDistroSeriesSet |
443 | 47 | from lp.registry.interfaces.pocket import PackagePublishingPocket | 47 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
444 | 48 | from lp.services.job.interfaces.job import JobStatus | ||
445 | 48 | 49 | ||
446 | 49 | 50 | ||
447 | 50 | class IRecipesForPerson(Interface): | 51 | class IRecipesForPerson(Interface): |
448 | @@ -52,8 +53,7 @@ | |||
449 | 52 | 53 | ||
450 | 53 | 54 | ||
451 | 54 | class RecipesForPersonBreadcrumb(Breadcrumb): | 55 | class RecipesForPersonBreadcrumb(Breadcrumb): |
454 | 55 | """A Breadcrumb that will handle the "Recipes" link for recipe breadcrumbs. | 56 | """A Breadcrumb to handle the "Recipes" link for recipe breadcrumbs.""" |
453 | 56 | """ | ||
455 | 57 | 57 | ||
456 | 58 | rootsite = 'code' | 58 | rootsite = 'code' |
457 | 59 | text = 'Recipes' | 59 | text = 'Recipes' |
458 | @@ -229,14 +229,26 @@ | |||
459 | 229 | 229 | ||
460 | 230 | @property | 230 | @property |
461 | 231 | def eta(self): | 231 | def eta(self): |
463 | 232 | """The datetime when the build job is estimated to begin.""" | 232 | """The datetime when the build job is estimated to complete. |
464 | 233 | |||
465 | 234 | This is the BuildQueue.estimated_duration plus the | ||
466 | 235 | Job.date_started or BuildQueue.getEstimatedJobStartTime. | ||
467 | 236 | """ | ||
468 | 233 | if self.context.buildqueue_record is None: | 237 | if self.context.buildqueue_record is None: |
469 | 234 | return None | 238 | return None |
471 | 235 | return self.context.buildqueue_record.getEstimatedJobStartTime() | 239 | queue_record = self.context.buildqueue_record |
472 | 240 | if queue_record.job.status == JobStatus.WAITING: | ||
473 | 241 | start_time = queue_record.getEstimatedJobStartTime() | ||
474 | 242 | if start_time is None: | ||
475 | 243 | return None | ||
476 | 244 | else: | ||
477 | 245 | start_time = queue_record.job.date_started | ||
478 | 246 | duration = queue_record.estimated_duration | ||
479 | 247 | return start_time + duration | ||
480 | 236 | 248 | ||
481 | 237 | @property | 249 | @property |
482 | 238 | def date(self): | 250 | def date(self): |
484 | 239 | """The date when the build complete or will begin.""" | 251 | """The date when the build completed or is estimated to complete.""" |
485 | 240 | if self.estimate: | 252 | if self.estimate: |
486 | 241 | return self.eta | 253 | return self.eta |
487 | 242 | return self.context.datebuilt | 254 | return self.context.datebuilt |
488 | @@ -244,7 +256,9 @@ | |||
489 | 244 | @property | 256 | @property |
490 | 245 | def estimate(self): | 257 | def estimate(self): |
491 | 246 | """If true, the date value is an estimate.""" | 258 | """If true, the date value is an estimate.""" |
493 | 247 | return (self.context.datebuilt is None and self.eta is not None) | 259 | if self.context.datebuilt is not None: |
494 | 260 | return False | ||
495 | 261 | return self.eta is not None | ||
496 | 248 | 262 | ||
497 | 249 | 263 | ||
498 | 250 | class ISourcePackageAddEditSchema(Interface): | 264 | class ISourcePackageAddEditSchema(Interface): |
499 | @@ -272,7 +286,7 @@ | |||
500 | 272 | def validate(self, data): | 286 | def validate(self, data): |
501 | 273 | try: | 287 | try: |
502 | 274 | parser = RecipeParser(data['recipe_text']) | 288 | parser = RecipeParser(data['recipe_text']) |
504 | 275 | recipe_text = parser.parse() | 289 | parser.parse() |
505 | 276 | except RecipeParseError: | 290 | except RecipeParseError: |
506 | 277 | self.setFieldError( | 291 | self.setFieldError( |
507 | 278 | 'recipe_text', | 292 | 'recipe_text', |
508 | 279 | 293 | ||
509 | === added file 'lib/lp/code/browser/tests/test_breadcrumbs.py' | |||
510 | --- lib/lp/code/browser/tests/test_breadcrumbs.py 1970-01-01 00:00:00 +0000 | |||
511 | +++ lib/lp/code/browser/tests/test_breadcrumbs.py 2010-04-29 11:26:16 +0000 | |||
512 | @@ -0,0 +1,25 @@ | |||
513 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
514 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
515 | 3 | |||
516 | 4 | __metaclass__ = type | ||
517 | 5 | |||
518 | 6 | import unittest | ||
519 | 7 | |||
520 | 8 | from lp.testing.breadcrumbs import BaseBreadcrumbTestCase | ||
521 | 9 | |||
522 | 10 | |||
523 | 11 | class TestCodeImportMachineBreadcrumb(BaseBreadcrumbTestCase): | ||
524 | 12 | """Test breadcrumbs for an `ICodeImportMachine`.""" | ||
525 | 13 | |||
526 | 14 | def test_machine(self): | ||
527 | 15 | machine = self.factory.makeCodeImportMachine(hostname='apollo') | ||
528 | 16 | expected = [ | ||
529 | 17 | ('Code Import System', 'http://code.launchpad.dev/+code-imports'), | ||
530 | 18 | ('Machines', 'http://code.launchpad.dev/+code-imports/+machines'), | ||
531 | 19 | ('apollo', | ||
532 | 20 | 'http://code.launchpad.dev/+code-imports/+machines/apollo')] | ||
533 | 21 | self.assertBreadcrumbs(expected, machine) | ||
534 | 22 | |||
535 | 23 | |||
536 | 24 | def test_suite(): | ||
537 | 25 | return unittest.TestLoader().loadTestsFromName(__name__) | ||
538 | 0 | 26 | ||
539 | === modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py' | |||
540 | --- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-04-23 02:35:47 +0000 | |||
541 | +++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-04-29 11:26:16 +0000 | |||
542 | @@ -19,9 +19,11 @@ | |||
543 | 19 | extract_text, find_main_content, find_tags_by_class) | 19 | extract_text, find_main_content, find_tags_by_class) |
544 | 20 | from canonical.testing import DatabaseFunctionalLayer | 20 | from canonical.testing import DatabaseFunctionalLayer |
545 | 21 | from lp.buildmaster.interfaces.buildbase import BuildStatus | 21 | from lp.buildmaster.interfaces.buildbase import BuildStatus |
547 | 22 | from lp.code.browser.sourcepackagerecipe import SourcePackageRecipeView | 22 | from lp.code.browser.sourcepackagerecipe import ( |
548 | 23 | SourcePackageRecipeView, SourcePackageRecipeBuildView | ||
549 | 24 | ) | ||
550 | 23 | from lp.code.interfaces.sourcepackagerecipe import MINIMAL_RECIPE_TEXT | 25 | from lp.code.interfaces.sourcepackagerecipe import MINIMAL_RECIPE_TEXT |
552 | 24 | from lp.testing import ANONYMOUS, BrowserTestCase, login | 26 | from lp.testing import ANONYMOUS, BrowserTestCase, login, TestCaseWithFactory |
553 | 25 | 27 | ||
554 | 26 | 28 | ||
555 | 27 | class TestCaseForRecipe(BrowserTestCase): | 29 | class TestCaseForRecipe(BrowserTestCase): |
556 | @@ -330,6 +332,50 @@ | |||
557 | 330 | self.assertEqual(['Secret Squirrel', 'Woody'], build_distros) | 332 | self.assertEqual(['Secret Squirrel', 'Woody'], build_distros) |
558 | 331 | 333 | ||
559 | 332 | 334 | ||
560 | 335 | class TestSourcePackageRecipeBuildView(TestCaseWithFactory): | ||
561 | 336 | """Test behaviour of SourcePackageReciptBuildView.""" | ||
562 | 337 | |||
563 | 338 | layer = DatabaseFunctionalLayer | ||
564 | 339 | |||
565 | 340 | def test_estimate(self): | ||
566 | 341 | """Time should be estimated until the job is completed.""" | ||
567 | 342 | build = self.factory.makeSourcePackageRecipeBuild() | ||
568 | 343 | queue_entry = self.factory.makeSourcePackageRecipeBuildJob( | ||
569 | 344 | recipe_build=build) | ||
570 | 345 | self.factory.makeBuilder() | ||
571 | 346 | view = SourcePackageRecipeBuildView(build, None) | ||
572 | 347 | self.assertTrue(view.estimate) | ||
573 | 348 | queue_entry.job.start() | ||
574 | 349 | self.assertTrue(view.estimate) | ||
575 | 350 | removeSecurityProxy(build).datebuilt = datetime.now(utc) | ||
576 | 351 | self.assertFalse(view.estimate) | ||
577 | 352 | |||
578 | 353 | def test_eta(self): | ||
579 | 354 | """ETA should be reasonable. | ||
580 | 355 | |||
581 | 356 | It should be None if there is no builder or queue entry. | ||
582 | 357 | It should be getEstimatedJobStartTime + estimated duration for jobs | ||
583 | 358 | that have not started. | ||
584 | 359 | It should be job.date_started + estimated duration for jobs that have | ||
585 | 360 | started. | ||
586 | 361 | """ | ||
587 | 362 | build = self.factory.makeSourcePackageRecipeBuild() | ||
588 | 363 | view = SourcePackageRecipeBuildView(build, None) | ||
589 | 364 | self.assertIs(None, view.eta) | ||
590 | 365 | queue_entry = self.factory.makeSourcePackageRecipeBuildJob( | ||
591 | 366 | recipe_build=build) | ||
592 | 367 | queue_entry._now = lambda: datetime(1970, 1, 1, 0, 0, 0, 0, utc) | ||
593 | 368 | self.factory.makeBuilder() | ||
594 | 369 | self.assertIsNot(None, view.eta) | ||
595 | 370 | self.assertEqual( | ||
596 | 371 | queue_entry.getEstimatedJobStartTime() + | ||
597 | 372 | queue_entry.estimated_duration, view.eta) | ||
598 | 373 | queue_entry.job.start() | ||
599 | 374 | self.assertEqual( | ||
600 | 375 | queue_entry.job.date_started + queue_entry.estimated_duration, | ||
601 | 376 | view.eta) | ||
602 | 377 | |||
603 | 378 | |||
604 | 333 | class TestSourcePackageRecipeDeleteView(TestCaseForRecipe): | 379 | class TestSourcePackageRecipeDeleteView(TestCaseForRecipe): |
605 | 334 | 380 | ||
606 | 335 | layer = DatabaseFunctionalLayer | 381 | layer = DatabaseFunctionalLayer |
607 | 336 | 382 | ||
608 | === modified file 'lib/lp/code/configure.zcml' | |||
609 | --- lib/lp/code/configure.zcml 2010-04-23 22:48:25 +0000 | |||
610 | +++ lib/lp/code/configure.zcml 2010-04-29 11:26:16 +0000 | |||
611 | @@ -64,12 +64,6 @@ | |||
612 | 64 | appropriate. --> | 64 | appropriate. --> |
613 | 65 | </class> | 65 | </class> |
614 | 66 | 66 | ||
615 | 67 | <adapter | ||
616 | 68 | provides="canonical.launchpad.webapp.interfaces.IBreadcrumb" | ||
617 | 69 | for="lp.code.interfaces.codeimportmachine.ICodeImportMachineSet" | ||
618 | 70 | factory="lp.code.browser.codeimportmachine.CodeImportMachineSetBreadcrumb" | ||
619 | 71 | permission="zope.Public"/> | ||
620 | 72 | |||
621 | 73 | <!-- CodeImportMachineSet --> | 67 | <!-- CodeImportMachineSet --> |
622 | 74 | 68 | ||
623 | 75 | <securedutility | 69 | <securedutility |
624 | 76 | 70 | ||
625 | === modified file 'lib/lp/code/model/tests/test_diff.py' | |||
626 | --- lib/lp/code/model/tests/test_diff.py 2010-04-26 00:24:26 +0000 | |||
627 | +++ lib/lp/code/model/tests/test_diff.py 2010-04-29 11:26:16 +0000 | |||
628 | @@ -263,7 +263,9 @@ | |||
629 | 263 | last_oops_id = errorlog.globalErrorUtility.lastid | 263 | last_oops_id = errorlog.globalErrorUtility.lastid |
630 | 264 | diff_bytes = "not a real diff" | 264 | diff_bytes = "not a real diff" |
631 | 265 | diff = Diff.fromFile(StringIO(diff_bytes), len(diff_bytes)) | 265 | diff = Diff.fromFile(StringIO(diff_bytes), len(diff_bytes)) |
633 | 266 | self.assertNotEqual(last_oops_id, errorlog.globalErrorUtility.lastid) | 266 | # XXX MichaelHudson, 2010-04-29, bug=567257: This test fails |
634 | 267 | # intermittently when run close to midnight. | ||
635 | 268 | #self.assertNotEqual(last_oops_id, errorlog.globalErrorUtility.lastid) | ||
636 | 267 | self.assertIs(None, diff.diffstat) | 269 | self.assertIs(None, diff.diffstat) |
637 | 268 | self.assertIs(None, diff.added_lines_count) | 270 | self.assertIs(None, diff.added_lines_count) |
638 | 269 | self.assertIs(None, diff.removed_lines_count) | 271 | self.assertIs(None, diff.removed_lines_count) |
639 | 270 | 272 | ||
640 | === modified file 'lib/lp/codehosting/codeimport/tests/test_worker.py' | |||
641 | --- lib/lp/codehosting/codeimport/tests/test_worker.py 2010-04-12 01:16:16 +0000 | |||
642 | +++ lib/lp/codehosting/codeimport/tests/test_worker.py 2010-04-29 11:26:16 +0000 | |||
643 | @@ -314,6 +314,21 @@ | |||
644 | 314 | set([revid, revid1, revid2]), | 314 | set([revid, revid1, revid2]), |
645 | 315 | set(retrieved_branch.repository.all_revision_ids())) | 315 | set(retrieved_branch.repository.all_revision_ids())) |
646 | 316 | 316 | ||
647 | 317 | def test_pull_doesnt_bring_backup_directories(self): | ||
648 | 318 | # If the branch has been upgraded in the branch store, `pull` does not | ||
649 | 319 | # copy the backup.bzr directory to `target_path`, just the .bzr | ||
650 | 320 | # directory. | ||
651 | 321 | store = self.makeBranchStore() | ||
652 | 322 | tree = create_branch_with_one_revision('original') | ||
653 | 323 | store.push(self.arbitrary_branch_id, tree.branch, default_format) | ||
654 | 324 | t = get_transport(store._getMirrorURL(self.arbitrary_branch_id)) | ||
655 | 325 | t.mkdir('backup.bzr') | ||
656 | 326 | retrieved_branch = store.pull( | ||
657 | 327 | self.arbitrary_branch_id, 'pulled', default_format, | ||
658 | 328 | needs_tree=False) | ||
659 | 329 | self.assertEqual( | ||
660 | 330 | ['.bzr'], retrieved_branch.bzrdir.root_transport.list_dir('.')) | ||
661 | 331 | |||
662 | 317 | 332 | ||
663 | 318 | class TestImportDataStore(WorkerTest): | 333 | class TestImportDataStore(WorkerTest): |
664 | 319 | """Tests for `ImportDataStore`.""" | 334 | """Tests for `ImportDataStore`.""" |
665 | 320 | 335 | ||
666 | === modified file 'lib/lp/codehosting/codeimport/worker.py' | |||
667 | --- lib/lp/codehosting/codeimport/worker.py 2010-04-15 02:27:31 +0000 | |||
668 | +++ lib/lp/codehosting/codeimport/worker.py 2010-04-29 11:26:16 +0000 | |||
669 | @@ -94,9 +94,12 @@ | |||
670 | 94 | # revisions are in the ancestry of the tip of the remote branch, which | 94 | # revisions are in the ancestry of the tip of the remote branch, which |
671 | 95 | # we strictly don't care about, so we just copy the whole thing down | 95 | # we strictly don't care about, so we just copy the whole thing down |
672 | 96 | # at the vfs level. | 96 | # at the vfs level. |
673 | 97 | control_dir = remote_bzr_dir.root_transport.relpath( | ||
674 | 98 | remote_bzr_dir.transport.abspath('.')) | ||
675 | 97 | target = get_transport(target_path) | 99 | target = get_transport(target_path) |
678 | 98 | target.ensure_base() | 100 | target_control = target.clone(control_dir) |
679 | 99 | remote_bzr_dir.root_transport.copy_tree_to_transport(target) | 101 | target_control.create_prefix() |
680 | 102 | remote_bzr_dir.transport.copy_tree_to_transport(target_control) | ||
681 | 100 | local_bzr_dir = BzrDir.open_from_transport(target) | 103 | local_bzr_dir = BzrDir.open_from_transport(target) |
682 | 101 | if needs_tree: | 104 | if needs_tree: |
683 | 102 | local_bzr_dir.create_workingtree() | 105 | local_bzr_dir.create_workingtree() |
684 | 103 | 106 | ||
685 | === modified file 'lib/lp/registry/browser/tests/test_breadcrumbs.py' | |||
686 | --- lib/lp/registry/browser/tests/test_breadcrumbs.py 2009-11-25 15:08:30 +0000 | |||
687 | +++ lib/lp/registry/browser/tests/test_breadcrumbs.py 2010-04-29 11:26:16 +0000 | |||
688 | @@ -9,8 +9,8 @@ | |||
689 | 9 | 9 | ||
690 | 10 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities | 10 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
691 | 11 | from canonical.launchpad.webapp.publisher import canonical_url | 11 | from canonical.launchpad.webapp.publisher import canonical_url |
694 | 12 | from canonical.launchpad.webapp.tests.breadcrumbs import ( | 12 | |
695 | 13 | BaseBreadcrumbTestCase) | 13 | from lp.testing.breadcrumbs import BaseBreadcrumbTestCase |
696 | 14 | 14 | ||
697 | 15 | 15 | ||
698 | 16 | class TestDistroseriesBreadcrumb(BaseBreadcrumbTestCase): | 16 | class TestDistroseriesBreadcrumb(BaseBreadcrumbTestCase): |
699 | @@ -25,9 +25,7 @@ | |||
700 | 25 | self.distroseries_url = canonical_url(self.distroseries) | 25 | self.distroseries_url = canonical_url(self.distroseries) |
701 | 26 | 26 | ||
702 | 27 | def test_distroseries(self): | 27 | def test_distroseries(self): |
706 | 28 | crumbs = self._getBreadcrumbs( | 28 | crumbs = self.getBreadcrumbsForObject(self.distroseries) |
704 | 29 | self.distroseries_url, | ||
705 | 30 | [self.root, self.distribution, self.distroseries]) | ||
707 | 31 | last_crumb = crumbs[-1] | 29 | last_crumb = crumbs[-1] |
708 | 32 | self.assertEqual(self.distroseries.named_version, last_crumb.text) | 30 | self.assertEqual(self.distroseries.named_version, last_crumb.text) |
709 | 33 | 31 | ||
710 | @@ -46,9 +44,7 @@ | |||
711 | 46 | mirror = self.factory.makeMirror( | 44 | mirror = self.factory.makeMirror( |
712 | 47 | distribution=self.distribution, | 45 | distribution=self.distribution, |
713 | 48 | displayname=displayname) | 46 | displayname=displayname) |
717 | 49 | crumbs = self._getBreadcrumbs( | 47 | crumbs = self.getBreadcrumbsForObject(mirror) |
715 | 50 | canonical_url(mirror), | ||
716 | 51 | [self.root, self.distribution, mirror]) | ||
718 | 52 | last_crumb = crumbs[-1] | 48 | last_crumb = crumbs[-1] |
719 | 53 | self.assertEqual(displayname, last_crumb.text) | 49 | self.assertEqual(displayname, last_crumb.text) |
720 | 54 | 50 | ||
721 | @@ -60,9 +56,7 @@ | |||
722 | 60 | distribution=self.distribution, | 56 | distribution=self.distribution, |
723 | 61 | displayname=None, | 57 | displayname=None, |
724 | 62 | http_url=http_url) | 58 | http_url=http_url) |
728 | 63 | crumbs = self._getBreadcrumbs( | 59 | crumbs = self.getBreadcrumbsForObject(mirror) |
726 | 64 | canonical_url(mirror), | ||
727 | 65 | [self.root, self.distribution, mirror]) | ||
729 | 66 | last_crumb = crumbs[-1] | 60 | last_crumb = crumbs[-1] |
730 | 67 | self.assertEqual("Example.com-archive", last_crumb.text) | 61 | self.assertEqual("Example.com-archive", last_crumb.text) |
731 | 68 | 62 | ||
732 | @@ -74,9 +68,7 @@ | |||
733 | 74 | distribution=self.distribution, | 68 | distribution=self.distribution, |
734 | 75 | displayname=None, | 69 | displayname=None, |
735 | 76 | ftp_url=ftp_url) | 70 | ftp_url=ftp_url) |
739 | 77 | crumbs = self._getBreadcrumbs( | 71 | crumbs = self.getBreadcrumbsForObject(mirror) |
737 | 78 | canonical_url(mirror), | ||
738 | 79 | [self.root, self.distribution, mirror]) | ||
740 | 80 | last_crumb = crumbs[-1] | 72 | last_crumb = crumbs[-1] |
741 | 81 | self.assertEqual("Example.com-archive", last_crumb.text) | 73 | self.assertEqual("Example.com-archive", last_crumb.text) |
742 | 82 | 74 | ||
743 | @@ -93,15 +85,13 @@ | |||
744 | 93 | self.milestone_url = canonical_url(self.milestone) | 85 | self.milestone_url = canonical_url(self.milestone) |
745 | 94 | 86 | ||
746 | 95 | def test_milestone_without_code_name(self): | 87 | def test_milestone_without_code_name(self): |
749 | 96 | crumbs = self._getBreadcrumbs( | 88 | crumbs = self.getBreadcrumbsForObject(self.milestone) |
748 | 97 | self.milestone_url, [self.root, self.project, self.milestone]) | ||
750 | 98 | last_crumb = crumbs[-1] | 89 | last_crumb = crumbs[-1] |
751 | 99 | self.assertEqual(self.milestone.name, last_crumb.text) | 90 | self.assertEqual(self.milestone.name, last_crumb.text) |
752 | 100 | 91 | ||
753 | 101 | def test_milestone_with_code_name(self): | 92 | def test_milestone_with_code_name(self): |
754 | 102 | self.milestone.code_name = "duck" | 93 | self.milestone.code_name = "duck" |
757 | 103 | crumbs = self._getBreadcrumbs( | 94 | crumbs = self.getBreadcrumbsForObject(self.milestone) |
756 | 104 | self.milestone_url, [self.root, self.project, self.milestone]) | ||
758 | 105 | last_crumb = crumbs[-1] | 95 | last_crumb = crumbs[-1] |
759 | 106 | expected_text = '%s "%s"' % ( | 96 | expected_text = '%s "%s"' % ( |
760 | 107 | self.milestone.name, self.milestone.code_name) | 97 | self.milestone.name, self.milestone.code_name) |
761 | @@ -109,10 +99,7 @@ | |||
762 | 109 | 99 | ||
763 | 110 | def test_productrelease(self): | 100 | def test_productrelease(self): |
764 | 111 | release = self.factory.makeProductRelease(milestone=self.milestone) | 101 | release = self.factory.makeProductRelease(milestone=self.milestone) |
769 | 112 | self.release_url = canonical_url(release) | 102 | crumbs = self.getBreadcrumbsForObject(release) |
766 | 113 | crumbs = self._getBreadcrumbs( | ||
767 | 114 | self.release_url, | ||
768 | 115 | [self.root, self.project, self.series, self.milestone]) | ||
770 | 116 | last_crumb = crumbs[-1] | 103 | last_crumb = crumbs[-1] |
771 | 117 | self.assertEqual(self.milestone.name, last_crumb.text) | 104 | self.assertEqual(self.milestone.name, last_crumb.text) |
772 | 118 | 105 | ||
773 | @@ -131,12 +118,9 @@ | |||
774 | 131 | name=name, | 118 | name=name, |
775 | 132 | title=title, | 119 | title=title, |
776 | 133 | proposition=proposition) | 120 | proposition=proposition) |
777 | 134 | self.poll_url = canonical_url(self.poll) | ||
778 | 135 | 121 | ||
779 | 136 | def test_poll(self): | 122 | def test_poll(self): |
783 | 137 | crumbs = self._getBreadcrumbs( | 123 | crumbs = self.getBreadcrumbsForObject(self.poll) |
781 | 138 | self.poll_url, | ||
782 | 139 | [self.root, self.team, self.poll]) | ||
784 | 140 | last_crumb = crumbs[-1] | 124 | last_crumb = crumbs[-1] |
785 | 141 | self.assertEqual(self.poll.title, last_crumb.text) | 125 | self.assertEqual(self.poll.title, last_crumb.text) |
786 | 142 | 126 | ||
787 | 143 | 127 | ||
788 | === modified file 'lib/lp/services/mailman/doc/reactivate-lists.txt' | |||
789 | --- lib/lp/services/mailman/doc/reactivate-lists.txt 2009-09-24 18:51:29 +0000 | |||
790 | +++ lib/lp/services/mailman/doc/reactivate-lists.txt 2010-04-29 11:26:16 +0000 | |||
791 | @@ -50,7 +50,7 @@ | |||
792 | 50 | 50 | ||
793 | 51 | Now the team owner reactivates the list. | 51 | Now the team owner reactivates the list. |
794 | 52 | 52 | ||
796 | 53 | >>> browser.getLink('Create a mailing list').click() | 53 | >>> browser.getLink('Configure mailing list').click() |
797 | 54 | >>> browser.getControl('Reactivate this Mailing List').click() | 54 | >>> browser.getControl('Reactivate this Mailing List').click() |
798 | 55 | >>> xmlrpc_watcher.wait_for_reactivation('itest-one') | 55 | >>> xmlrpc_watcher.wait_for_reactivation('itest-one') |
799 | 56 | 56 | ||
800 | 57 | 57 | ||
801 | === modified file 'lib/lp/services/mailman/doc/staging.txt' | |||
802 | --- lib/lp/services/mailman/doc/staging.txt 2009-12-02 22:56:09 +0000 | |||
803 | +++ lib/lp/services/mailman/doc/staging.txt 2010-04-29 11:26:16 +0000 | |||
804 | @@ -22,7 +22,8 @@ | |||
805 | 22 | 22 | ||
806 | 23 | >>> login('admin@canonical.com') | 23 | >>> login('admin@canonical.com') |
807 | 24 | 24 | ||
809 | 25 | >>> owner = factory.makePerson() | 25 | >>> from zope.security.proxy import removeSecurityProxy |
810 | 26 | >>> owner = removeSecurityProxy(factory.makePerson()) | ||
811 | 26 | >>> team_one = factory.makeTeam(owner=owner, name='staging-one') | 27 | >>> team_one = factory.makeTeam(owner=owner, name='staging-one') |
812 | 27 | >>> team_two = factory.makeTeam(owner=owner, name='staging-two') | 28 | >>> team_two = factory.makeTeam(owner=owner, name='staging-two') |
813 | 28 | >>> transaction.commit() | 29 | >>> transaction.commit() |
814 | 29 | 30 | ||
815 | === modified file 'lib/lp/soyuz/browser/archive.py' | |||
816 | --- lib/lp/soyuz/browser/archive.py 2010-04-16 05:03:03 +0000 | |||
817 | +++ lib/lp/soyuz/browser/archive.py 2010-04-29 11:26:16 +0000 | |||
818 | @@ -10,6 +10,7 @@ | |||
819 | 10 | 'ArchiveActivateView', | 10 | 'ArchiveActivateView', |
820 | 11 | 'ArchiveBadges', | 11 | 'ArchiveBadges', |
821 | 12 | 'ArchiveBuildsView', | 12 | 'ArchiveBuildsView', |
822 | 13 | 'ArchiveDeleteView', | ||
823 | 13 | 'ArchiveEditDependenciesView', | 14 | 'ArchiveEditDependenciesView', |
824 | 14 | 'ArchiveEditView', | 15 | 'ArchiveEditView', |
825 | 15 | 'ArchiveIndexActionsMenu', | 16 | 'ArchiveIndexActionsMenu', |
826 | @@ -59,8 +60,8 @@ | |||
827 | 59 | from lp.soyuz.adapters.archivesourcepublication import ( | 60 | from lp.soyuz.adapters.archivesourcepublication import ( |
828 | 60 | ArchiveSourcePublications) | 61 | ArchiveSourcePublications) |
829 | 61 | from lp.soyuz.interfaces.archive import ( | 62 | from lp.soyuz.interfaces.archive import ( |
832 | 62 | ArchivePurpose, CannotCopy, IArchive, IArchiveEditDependenciesForm, | 63 | ArchivePurpose, ArchiveStatus, CannotCopy, IArchive, |
833 | 63 | IArchiveSet, IPPAActivateForm, NoSuchPPA) | 64 | IArchiveEditDependenciesForm, IArchiveSet, IPPAActivateForm, NoSuchPPA) |
834 | 64 | from lp.soyuz.interfaces.archivepermission import ( | 65 | from lp.soyuz.interfaces.archivepermission import ( |
835 | 65 | ArchivePermissionType, IArchivePermissionSet) | 66 | ArchivePermissionType, IArchivePermissionSet) |
836 | 66 | from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriberSet | 67 | from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriberSet |
837 | @@ -404,15 +405,24 @@ | |||
838 | 404 | # This link should only be available for private archives: | 405 | # This link should only be available for private archives: |
839 | 405 | view = self.context | 406 | view = self.context |
840 | 406 | archive = view.context | 407 | archive = view.context |
842 | 407 | if not archive.private: | 408 | if not archive.private or not archive.is_active: |
843 | 408 | link.enabled = False | 409 | link.enabled = False |
844 | 409 | |||
845 | 410 | return link | 410 | return link |
846 | 411 | 411 | ||
847 | 412 | @enabled_with_permission('launchpad.Edit') | 412 | @enabled_with_permission('launchpad.Edit') |
848 | 413 | def edit(self): | 413 | def edit(self): |
849 | 414 | text = 'Change details' | 414 | text = 'Change details' |
851 | 415 | return Link('+edit', text, icon='edit') | 415 | view = self.context |
852 | 416 | return Link( | ||
853 | 417 | '+edit', text, icon='edit', enabled=view.context.is_active) | ||
854 | 418 | |||
855 | 419 | @enabled_with_permission('launchpad.Edit') | ||
856 | 420 | def delete_ppa(self): | ||
857 | 421 | text = 'Delete PPA' | ||
858 | 422 | view = self.context | ||
859 | 423 | return Link( | ||
860 | 424 | '+delete', text, icon='trash-icon', | ||
861 | 425 | enabled=view.context.is_active) | ||
862 | 416 | 426 | ||
863 | 417 | def builds(self): | 427 | def builds(self): |
864 | 418 | text = 'View all builds' | 428 | text = 'View all builds' |
865 | @@ -444,6 +454,9 @@ | |||
866 | 444 | # archives without any sources. | 454 | # archives without any sources. |
867 | 445 | if self.context.is_copy or not self.context.has_sources: | 455 | if self.context.is_copy or not self.context.has_sources: |
868 | 446 | link.enabled = False | 456 | link.enabled = False |
869 | 457 | view = self.context | ||
870 | 458 | if not view.context.is_active: | ||
871 | 459 | link.enabled = False | ||
872 | 447 | return link | 460 | return link |
873 | 448 | 461 | ||
874 | 449 | @enabled_with_permission('launchpad.AnyPerson') | 462 | @enabled_with_permission('launchpad.AnyPerson') |
875 | @@ -460,7 +473,10 @@ | |||
876 | 460 | @enabled_with_permission('launchpad.Edit') | 473 | @enabled_with_permission('launchpad.Edit') |
877 | 461 | def edit_dependencies(self): | 474 | def edit_dependencies(self): |
878 | 462 | text = 'Edit PPA dependencies' | 475 | text = 'Edit PPA dependencies' |
880 | 463 | return Link('+edit-dependencies', text, icon='edit') | 476 | view = self.context |
881 | 477 | return Link( | ||
882 | 478 | '+edit-dependencies', text, icon='edit', | ||
883 | 479 | enabled=view.context.is_active) | ||
884 | 464 | 480 | ||
885 | 465 | 481 | ||
886 | 466 | class ArchiveNavigationMenu(NavigationMenu, ArchiveMenuMixin): | 482 | class ArchiveNavigationMenu(NavigationMenu, ArchiveMenuMixin): |
887 | @@ -468,9 +484,9 @@ | |||
888 | 468 | 484 | ||
889 | 469 | usedfor = IArchive | 485 | usedfor = IArchive |
890 | 470 | facet = 'overview' | 486 | facet = 'overview' |
894 | 471 | links = ['admin', 'builds', 'builds_building', 'builds_pending', | 487 | links = ['admin', 'builds', 'builds_building', |
895 | 472 | 'builds_successful', 'edit', 'edit_dependencies', 'packages', | 488 | 'builds_pending', 'builds_successful', |
896 | 473 | 'ppa'] | 489 | 'packages', 'ppa'] |
897 | 474 | 490 | ||
898 | 475 | 491 | ||
899 | 476 | class IArchiveIndexActionsMenu(Interface): | 492 | class IArchiveIndexActionsMenu(Interface): |
900 | @@ -481,8 +497,8 @@ | |||
901 | 481 | """Archive index navigation menu.""" | 497 | """Archive index navigation menu.""" |
902 | 482 | usedfor = IArchiveIndexActionsMenu | 498 | usedfor = IArchiveIndexActionsMenu |
903 | 483 | facet = 'overview' | 499 | facet = 'overview' |
906 | 484 | links = ['admin', 'edit', 'edit_dependencies', 'manage_subscribers', | 500 | links = ['admin', 'edit', 'edit_dependencies', |
907 | 485 | 'packages'] | 501 | 'manage_subscribers', 'packages', 'delete_ppa'] |
908 | 486 | 502 | ||
909 | 487 | 503 | ||
910 | 488 | class IArchivePackagesActionMenu(Interface): | 504 | class IArchivePackagesActionMenu(Interface): |
911 | @@ -578,7 +594,7 @@ | |||
912 | 578 | if self.context.is_ppa: | 594 | if self.context.is_ppa: |
913 | 579 | return 'PPA' | 595 | return 'PPA' |
914 | 580 | else: | 596 | else: |
916 | 581 | return 'Archive' | 597 | return 'archive' |
917 | 582 | 598 | ||
918 | 583 | @cachedproperty | 599 | @cachedproperty |
919 | 584 | def build_counters(self): | 600 | def build_counters(self): |
920 | @@ -622,6 +638,19 @@ | |||
921 | 622 | return list(copy_requests) | 638 | return list(copy_requests) |
922 | 623 | 639 | ||
923 | 624 | 640 | ||
924 | 641 | @property | ||
925 | 642 | def disabled_warning_message(self): | ||
926 | 643 | """Return an appropriate message if the archive is disabled.""" | ||
927 | 644 | if self.context.enabled: | ||
928 | 645 | return None | ||
929 | 646 | |||
930 | 647 | if self.context.status in ( | ||
931 | 648 | ArchiveStatus.DELETED, ArchiveStatus.DELETING): | ||
932 | 649 | return "This %s has been deleted." % self.archive_label | ||
933 | 650 | else: | ||
934 | 651 | return "This %s has been disabled." % self.archive_label | ||
935 | 652 | |||
936 | 653 | |||
937 | 625 | class ArchiveSeriesVocabularyFactory: | 654 | class ArchiveSeriesVocabularyFactory: |
938 | 626 | """A factory for generating vocabularies of an archive's series.""" | 655 | """A factory for generating vocabularies of an archive's series.""" |
939 | 627 | 656 | ||
940 | @@ -1916,3 +1945,44 @@ | |||
941 | 1916 | :rtype: bool | 1945 | :rtype: bool |
942 | 1917 | """ | 1946 | """ |
943 | 1918 | return self.context.owner.visibility == PersonVisibility.PRIVATE | 1947 | return self.context.owner.visibility == PersonVisibility.PRIVATE |
944 | 1948 | |||
945 | 1949 | |||
946 | 1950 | class ArchiveDeleteView(LaunchpadFormView): | ||
947 | 1951 | """View class for deleting `IArchive`s""" | ||
948 | 1952 | |||
949 | 1953 | schema = Interface | ||
950 | 1954 | |||
951 | 1955 | @property | ||
952 | 1956 | def page_title(self): | ||
953 | 1957 | return smartquote('Delete "%s"' % self.context.displayname) | ||
954 | 1958 | |||
955 | 1959 | @property | ||
956 | 1960 | def label(self): | ||
957 | 1961 | return self.page_title | ||
958 | 1962 | |||
959 | 1963 | @property | ||
960 | 1964 | def can_be_deleted(self): | ||
961 | 1965 | return self.context.status not in ( | ||
962 | 1966 | ArchiveStatus.DELETING, ArchiveStatus.DELETED) | ||
963 | 1967 | |||
964 | 1968 | @property | ||
965 | 1969 | def waiting_for_deletion(self): | ||
966 | 1970 | return self.context.status == ArchiveStatus.DELETING | ||
967 | 1971 | |||
968 | 1972 | @property | ||
969 | 1973 | def next_url(self): | ||
970 | 1974 | # We redirect back to the PPA owner's profile page on a | ||
971 | 1975 | # successful action. | ||
972 | 1976 | return canonical_url(self.context.owner) | ||
973 | 1977 | |||
974 | 1978 | @property | ||
975 | 1979 | def cancel_url(self): | ||
976 | 1980 | return canonical_url(self.context) | ||
977 | 1981 | |||
978 | 1982 | @action(_("Permanently delete PPA"), name="delete_ppa") | ||
979 | 1983 | def action_delete_ppa(self, action, data): | ||
980 | 1984 | self.context.delete(self.user) | ||
981 | 1985 | self.request.response.addInfoNotification( | ||
982 | 1986 | "Deletion of '%s' has been requested and the repository will be " | ||
983 | 1987 | "removed shortly." % self.context.title) | ||
984 | 1988 | |||
985 | 1919 | 1989 | ||
986 | === modified file 'lib/lp/soyuz/browser/configure.zcml' | |||
987 | --- lib/lp/soyuz/browser/configure.zcml 2010-04-16 05:03:03 +0000 | |||
988 | +++ lib/lp/soyuz/browser/configure.zcml 2010-04-29 11:26:16 +0000 | |||
989 | @@ -276,6 +276,13 @@ | |||
990 | 276 | permission="launchpad.Edit" | 276 | permission="launchpad.Edit" |
991 | 277 | template="../templates/archive-edit.pt"/> | 277 | template="../templates/archive-edit.pt"/> |
992 | 278 | <browser:page | 278 | <browser:page |
993 | 279 | name="+delete" | ||
994 | 280 | facet="overview" | ||
995 | 281 | for="lp.soyuz.interfaces.archive.IArchive" | ||
996 | 282 | class="lp.soyuz.browser.archive.ArchiveDeleteView" | ||
997 | 283 | permission="launchpad.Edit" | ||
998 | 284 | template="../templates/archive-delete.pt"/> | ||
999 | 285 | <browser:page | ||
1000 | 279 | name="+admin" | 286 | name="+admin" |
1001 | 280 | facet="overview" | 287 | facet="overview" |
1002 | 281 | for="lp.soyuz.interfaces.archive.IArchive" | 288 | for="lp.soyuz.interfaces.archive.IArchive" |
1003 | 282 | 289 | ||
1004 | === modified file 'lib/lp/soyuz/browser/tests/archive-views.txt' | |||
1005 | --- lib/lp/soyuz/browser/tests/archive-views.txt 2010-04-15 23:25:47 +0000 | |||
1006 | +++ lib/lp/soyuz/browser/tests/archive-views.txt 2010-04-29 11:26:16 +0000 | |||
1007 | @@ -38,13 +38,13 @@ | |||
1008 | 38 | None | 38 | None |
1009 | 39 | 39 | ||
1010 | 40 | The ArchiveView includes an archive_label property that returns either | 40 | The ArchiveView includes an archive_label property that returns either |
1012 | 41 | the string 'PPA' or 'Archive' depending on whether the archive is a PPA | 41 | the string 'PPA' or 'archive' depending on whether the archive is a PPA |
1013 | 42 | (this is mainly for branding purposes): | 42 | (this is mainly for branding purposes): |
1014 | 43 | 43 | ||
1015 | 44 | >>> print ppa_archive_view.archive_label | 44 | >>> print ppa_archive_view.archive_label |
1016 | 45 | PPA | 45 | PPA |
1017 | 46 | >>> print copy_archive_view.archive_label | 46 | >>> print copy_archive_view.archive_label |
1019 | 47 | Archive | 47 | archive |
1020 | 48 | 48 | ||
1021 | 49 | The ArchiveView is provides the html for the inline description | 49 | The ArchiveView is provides the html for the inline description |
1022 | 50 | editing widget. | 50 | editing widget. |
1023 | 51 | 51 | ||
1024 | === modified file 'lib/lp/soyuz/browser/tests/test_breadcrumbs.py' | |||
1025 | --- lib/lp/soyuz/browser/tests/test_breadcrumbs.py 2009-09-29 07:21:40 +0000 | |||
1026 | +++ lib/lp/soyuz/browser/tests/test_breadcrumbs.py 2010-04-29 11:26:16 +0000 | |||
1027 | @@ -8,11 +8,11 @@ | |||
1028 | 8 | from zope.component import getUtility | 8 | from zope.component import getUtility |
1029 | 9 | 9 | ||
1030 | 10 | from canonical.launchpad.webapp.publisher import canonical_url | 10 | from canonical.launchpad.webapp.publisher import canonical_url |
1033 | 11 | from canonical.launchpad.webapp.tests.breadcrumbs import ( | 11 | |
1032 | 12 | BaseBreadcrumbTestCase) | ||
1034 | 13 | from lp.registry.interfaces.distribution import IDistributionSet | 12 | from lp.registry.interfaces.distribution import IDistributionSet |
1035 | 14 | from lp.soyuz.browser.archivesubscription import PersonalArchiveSubscription | 13 | from lp.soyuz.browser.archivesubscription import PersonalArchiveSubscription |
1036 | 15 | from lp.testing import login, login_person | 14 | from lp.testing import login, login_person |
1037 | 15 | from lp.testing.breadcrumbs import BaseBreadcrumbTestCase | ||
1038 | 16 | 16 | ||
1039 | 17 | 17 | ||
1040 | 18 | class TestDistroArchSeriesBreadcrumb(BaseBreadcrumbTestCase): | 18 | class TestDistroArchSeriesBreadcrumb(BaseBreadcrumbTestCase): |
1041 | @@ -22,39 +22,27 @@ | |||
1042 | 22 | self.ubuntu = getUtility(IDistributionSet).getByName('ubuntu') | 22 | self.ubuntu = getUtility(IDistributionSet).getByName('ubuntu') |
1043 | 23 | self.hoary = self.ubuntu.getSeries('hoary') | 23 | self.hoary = self.ubuntu.getSeries('hoary') |
1044 | 24 | self.hoary_i386 = self.hoary['i386'] | 24 | self.hoary_i386 = self.hoary['i386'] |
1045 | 25 | self.traversed_objects = [ | ||
1046 | 26 | self.root, self.ubuntu, self.hoary, self.hoary_i386] | ||
1047 | 27 | 25 | ||
1048 | 28 | def test_distroarchseries(self): | 26 | def test_distroarchseries(self): |
1049 | 29 | das_url = canonical_url(self.hoary_i386) | 27 | das_url = canonical_url(self.hoary_i386) |
1055 | 30 | urls = self._getBreadcrumbsURLs(das_url, self.traversed_objects) | 28 | crumbs = self.getBreadcrumbsForObject(self.hoary_i386) |
1056 | 31 | texts = self._getBreadcrumbsTexts(das_url, self.traversed_objects) | 29 | self.assertEquals(crumbs[-1].url, das_url) |
1057 | 32 | 30 | self.assertEquals(crumbs[-1].text, "i386") | |
1053 | 33 | self.assertEquals(urls[-1], das_url) | ||
1054 | 34 | self.assertEquals(texts[-1], "i386") | ||
1058 | 35 | 31 | ||
1059 | 36 | def test_distroarchseriesbinarypackage(self): | 32 | def test_distroarchseriesbinarypackage(self): |
1060 | 37 | pmount_hoary_i386 = self.hoary_i386.getBinaryPackage("pmount") | 33 | pmount_hoary_i386 = self.hoary_i386.getBinaryPackage("pmount") |
1061 | 38 | self.traversed_objects.append(pmount_hoary_i386) | ||
1062 | 39 | pmount_url = canonical_url(pmount_hoary_i386) | 34 | pmount_url = canonical_url(pmount_hoary_i386) |
1068 | 40 | urls = self._getBreadcrumbsURLs(pmount_url, self.traversed_objects) | 35 | crumbs = self.getBreadcrumbsForObject(pmount_hoary_i386) |
1069 | 41 | texts = self._getBreadcrumbsTexts(pmount_url, self.traversed_objects) | 36 | self.assertEquals(crumbs[-1].url, pmount_url) |
1070 | 42 | 37 | self.assertEquals(crumbs[-1].text, "pmount") | |
1066 | 43 | self.assertEquals(urls[-1], pmount_url) | ||
1067 | 44 | self.assertEquals(texts[-1], "pmount") | ||
1071 | 45 | 38 | ||
1072 | 46 | def test_distroarchseriesbinarypackagerelease(self): | 39 | def test_distroarchseriesbinarypackagerelease(self): |
1073 | 47 | pmount_hoary_i386 = self.hoary_i386.getBinaryPackage("pmount") | 40 | pmount_hoary_i386 = self.hoary_i386.getBinaryPackage("pmount") |
1074 | 48 | pmount_release = pmount_hoary_i386['0.1-1'] | 41 | pmount_release = pmount_hoary_i386['0.1-1'] |
1075 | 49 | self.traversed_objects.extend([pmount_hoary_i386, pmount_release]) | ||
1076 | 50 | pmount_release_url = canonical_url(pmount_release) | 42 | pmount_release_url = canonical_url(pmount_release) |
1084 | 51 | urls = self._getBreadcrumbsURLs( | 43 | crumbs = self.getBreadcrumbsForObject(pmount_release) |
1085 | 52 | pmount_release_url, self.traversed_objects) | 44 | self.assertEquals(crumbs[-1].url, pmount_release_url) |
1086 | 53 | texts = self._getBreadcrumbsTexts( | 45 | self.assertEquals(crumbs[-1].text, "0.1-1") |
1080 | 54 | pmount_release_url, self.traversed_objects) | ||
1081 | 55 | |||
1082 | 56 | self.assertEquals(urls[-1], pmount_release_url) | ||
1083 | 57 | self.assertEquals(texts[-1], "0.1-1") | ||
1087 | 58 | 46 | ||
1088 | 59 | 47 | ||
1089 | 60 | class TestArchiveSubscriptionBreadcrumb(BaseBreadcrumbTestCase): | 48 | class TestArchiveSubscriptionBreadcrumb(BaseBreadcrumbTestCase): |
1090 | @@ -76,18 +64,10 @@ | |||
1091 | 76 | owner, self.ppa) | 64 | owner, self.ppa) |
1092 | 77 | 65 | ||
1093 | 78 | def test_personal_archive_subscription(self): | 66 | def test_personal_archive_subscription(self): |
1094 | 79 | self.traversed_objects = [ | ||
1095 | 80 | self.root, self.ppa.owner, self.personal_archive_subscription] | ||
1096 | 81 | subscription_url = canonical_url(self.personal_archive_subscription) | 67 | subscription_url = canonical_url(self.personal_archive_subscription) |
1106 | 82 | 68 | crumbs = self.getBreadcrumbsForObject(self.personal_archive_subscription) | |
1107 | 83 | urls = self._getBreadcrumbsURLs( | 69 | self.assertEquals(subscription_url, crumbs[-1].url) |
1108 | 84 | subscription_url, self.traversed_objects) | 70 | self.assertEquals("Access to %s" % self.ppa.displayname, crumbs[-1].text) |
1100 | 85 | texts = self._getBreadcrumbsTexts( | ||
1101 | 86 | subscription_url, self.traversed_objects) | ||
1102 | 87 | |||
1103 | 88 | self.assertEquals(subscription_url, urls[-1]) | ||
1104 | 89 | self.assertEquals( | ||
1105 | 90 | "Access to %s" % self.ppa.displayname, texts[-1]) | ||
1109 | 91 | 71 | ||
1110 | 92 | def test_suite(): | 72 | def test_suite(): |
1111 | 93 | return unittest.TestLoader().loadTestsFromName(__name__) | 73 | return unittest.TestLoader().loadTestsFromName(__name__) |
1112 | 94 | 74 | ||
1113 | === added file 'lib/lp/soyuz/doc/archive-deletion.txt' | |||
1114 | --- lib/lp/soyuz/doc/archive-deletion.txt 1970-01-01 00:00:00 +0000 | |||
1115 | +++ lib/lp/soyuz/doc/archive-deletion.txt 2010-04-29 11:26:16 +0000 | |||
1116 | @@ -0,0 +1,81 @@ | |||
1117 | 1 | = Deleting an archive = | ||
1118 | 2 | |||
1119 | 3 | When deleting an archive, the user calls IArchive.delete(), passing in | ||
1120 | 4 | the IPerson who is requesting the deletion. | ||
1121 | 5 | |||
1122 | 6 | All of the publishing records will be marked as DELETED, the archive is | ||
1123 | 7 | disabled and the status is set to DELETING. | ||
1124 | 8 | |||
1125 | 9 | This status tells the publisher to then delete the repository area. Once | ||
1126 | 10 | it completes that task it will set the status to DELETED. | ||
1127 | 11 | |||
1128 | 12 | >>> from lp.soyuz.interfaces.archive import ( | ||
1129 | 13 | ... ArchiveStatus, IArchiveSet, IArchive) | ||
1130 | 14 | >>> from lp.soyuz.tests.test_publishing import SoyuzTestPublisher | ||
1131 | 15 | >>> login("admin@canonical.com") | ||
1132 | 16 | >>> stp = SoyuzTestPublisher() | ||
1133 | 17 | >>> stp.prepareBreezyAutotest() | ||
1134 | 18 | >>> archive = factory.makeArchive() | ||
1135 | 19 | |||
1136 | 20 | The archive is currently active: | ||
1137 | 21 | |||
1138 | 22 | >>> print archive.enabled | ||
1139 | 23 | True | ||
1140 | 24 | |||
1141 | 25 | >>> print archive.status.name | ||
1142 | 26 | ACTIVE | ||
1143 | 27 | |||
1144 | 28 | We can create some packages in it using the test publisher: | ||
1145 | 29 | |||
1146 | 30 | >>> from lp.soyuz.interfaces.publishing import PackagePublishingStatus | ||
1147 | 31 | >>> ignore = stp.getPubBinaries( | ||
1148 | 32 | ... archive=archive, binaryname="foo-bin1", | ||
1149 | 33 | ... status=PackagePublishingStatus.PENDING) | ||
1150 | 34 | >>> ignore = stp.getPubBinaries( | ||
1151 | 35 | ... archive=archive, binaryname="foo-bin2", | ||
1152 | 36 | ... status=PackagePublishingStatus.PUBLISHED) | ||
1153 | 37 | >>> from storm.store import Store | ||
1154 | 38 | >>> Store.of(archive).flush() | ||
1155 | 39 | |||
1156 | 40 | Calling delete() will now do the deletion. It is only callable by someone | ||
1157 | 41 | with launchpad.Edit permission on the archive. Here, "duderino" who is | ||
1158 | 42 | some random dude is refused: | ||
1159 | 43 | |||
1160 | 44 | >>> person = factory.makePerson(name="duderino") | ||
1161 | 45 | >>> login_person(person) | ||
1162 | 46 | >>> archive.delete(person) | ||
1163 | 47 | Traceback (most recent call last): | ||
1164 | 48 | ... | ||
1165 | 49 | Unauthorized:... | ||
1166 | 50 | |||
1167 | 51 | However we can delete it using the owner of the archive: | ||
1168 | 52 | |||
1169 | 53 | >>> login_person(archive.owner) | ||
1170 | 54 | >>> archive.delete(archive.owner) | ||
1171 | 55 | |||
1172 | 56 | The deletion code uses a store.execute() command to speed up the operation | ||
1173 | 57 | where many records need to be updated. Therefore we need to invalidate | ||
1174 | 58 | the cache to make Storm re-read the database objects. | ||
1175 | 59 | |||
1176 | 60 | >>> Store.of(archive).invalidate() | ||
1177 | 61 | |||
1178 | 62 | Now, all the publications are DELETED, the archive is disabled and the | ||
1179 | 63 | status is DELETING to tell the publisher to remove the repository: | ||
1180 | 64 | |||
1181 | 65 | >>> publications = list(archive.getPublishedSources()) | ||
1182 | 66 | >>> publications.extend(list(archive.getAllPublishedBinaries())) | ||
1183 | 67 | >>> for pub in publications: | ||
1184 | 68 | ... print "%s, %s by %s" % ( | ||
1185 | 69 | ... pub.displayname, pub.status.name, pub.removed_by.name) | ||
1186 | 70 | foo 666 in breezy-autotest, DELETED by person-name12 | ||
1187 | 71 | foo 666 in breezy-autotest, DELETED by person-name12 | ||
1188 | 72 | foo-bin1 666 in breezy-autotest i386, DELETED by person-name12 | ||
1189 | 73 | foo-bin1 666 in breezy-autotest hppa, DELETED by person-name12 | ||
1190 | 74 | foo-bin2 666 in breezy-autotest i386, DELETED by person-name12 | ||
1191 | 75 | foo-bin2 666 in breezy-autotest hppa, DELETED by person-name12 | ||
1192 | 76 | |||
1193 | 77 | >>> print archive.enabled | ||
1194 | 78 | False | ||
1195 | 79 | |||
1196 | 80 | >>> print archive.status.name | ||
1197 | 81 | DELETING | ||
1198 | 0 | 82 | ||
1199 | === modified file 'lib/lp/soyuz/doc/archive.txt' | |||
1200 | --- lib/lp/soyuz/doc/archive.txt 2010-04-15 10:27:06 +0000 | |||
1201 | +++ lib/lp/soyuz/doc/archive.txt 2010-04-29 11:26:16 +0000 | |||
1202 | @@ -45,6 +45,8 @@ | |||
1203 | 45 | True | 45 | True |
1204 | 46 | >>> cprov_archive.is_main | 46 | >>> cprov_archive.is_main |
1205 | 47 | False | 47 | False |
1206 | 48 | >>> cprov_archive.is_active | ||
1207 | 49 | True | ||
1208 | 48 | >>> cprov_archive.distribution.main_archive.is_main | 50 | >>> cprov_archive.distribution.main_archive.is_main |
1209 | 49 | True | 51 | True |
1210 | 50 | >>> cprov_archive.total_count | 52 | >>> cprov_archive.total_count |
1211 | 51 | 53 | ||
1212 | === modified file 'lib/lp/soyuz/doc/buildd-mass-retry.txt' | |||
1213 | --- lib/lp/soyuz/doc/buildd-mass-retry.txt 2010-04-14 17:34:35 +0000 | |||
1214 | +++ lib/lp/soyuz/doc/buildd-mass-retry.txt 2010-04-29 11:26:16 +0000 | |||
1215 | @@ -47,6 +47,45 @@ | |||
1216 | 47 | INFO Dry-run. | 47 | INFO Dry-run. |
1217 | 48 | <BLANKLINE> | 48 | <BLANKLINE> |
1218 | 49 | 49 | ||
1219 | 50 | Superseded builds won't be retried; buildd-manager will just skip the build | ||
1220 | 51 | and set it to SUPERSEDED. | ||
1221 | 52 | |||
1222 | 53 | >>> from zope.security.proxy import removeSecurityProxy | ||
1223 | 54 | >>> from lp.soyuz.interfaces.binarypackagebuild import ( | ||
1224 | 55 | ... IBinaryPackageBuildSet) | ||
1225 | 56 | >>> from lp.soyuz.interfaces.publishing import PackagePublishingStatus | ||
1226 | 57 | >>> build = getUtility(IBinaryPackageBuildSet).getByBuildID(12) | ||
1227 | 58 | >>> pub = removeSecurityProxy(build.current_source_publication) | ||
1228 | 59 | |||
1229 | 60 | Let's mark the build from the previous run superseded. | ||
1230 | 61 | |||
1231 | 62 | >>> pub.status = PackagePublishingStatus.SUPERSEDED | ||
1232 | 63 | >>> print build.current_source_publication | ||
1233 | 64 | None | ||
1234 | 65 | >>> transaction.commit() | ||
1235 | 66 | |||
1236 | 67 | A new run doesn't pick it up. | ||
1237 | 68 | |||
1238 | 69 | >>> process = subprocess.Popen([sys.executable, script, "-v", "-NFDC", | ||
1239 | 70 | ... "-s", "hoary"], | ||
1240 | 71 | ... stdout=subprocess.PIPE, | ||
1241 | 72 | ... stderr=subprocess.PIPE,) | ||
1242 | 73 | >>> stdout, stderr = process.communicate() | ||
1243 | 74 | >>> process.returncode | ||
1244 | 75 | 0 | ||
1245 | 76 | >>> print stderr | ||
1246 | 77 | DEBUG Intitialising connection. | ||
1247 | 78 | INFO Initialising Build Mass-Retry for 'The Hoary Hedgehog Release/RELEASE' | ||
1248 | 79 | INFO Processing builds in 'Failed to build' | ||
1249 | 80 | INFO Processing builds in 'Dependency wait' | ||
1250 | 81 | DEBUG Skipping superseded i386 build of libstdc++ b8p in ubuntu hoary RELEASE (12) | ||
1251 | 82 | INFO Processing builds in 'Chroot problem' | ||
1252 | 83 | INFO Success. | ||
1253 | 84 | INFO Dry-run. | ||
1254 | 85 | <BLANKLINE> | ||
1255 | 86 | |||
1256 | 87 | >>> pub.status = PackagePublishingStatus.PUBLISHED | ||
1257 | 88 | >>> transaction.commit() | ||
1258 | 50 | 89 | ||
1259 | 51 | Passing an architecture, which contains no failed build records, | 90 | Passing an architecture, which contains no failed build records, |
1260 | 52 | nothing is done: | 91 | nothing is done: |
1261 | 53 | 92 | ||
1262 | === modified file 'lib/lp/soyuz/interfaces/archive.py' | |||
1263 | --- lib/lp/soyuz/interfaces/archive.py 2010-03-24 14:40:26 +0000 | |||
1264 | +++ lib/lp/soyuz/interfaces/archive.py 2010-04-29 11:26:16 +0000 | |||
1265 | @@ -222,6 +222,10 @@ | |||
1266 | 222 | is_main = Bool( | 222 | is_main = Bool( |
1267 | 223 | title=_("True if archive is a main archive type"), required=False) | 223 | title=_("True if archive is a main archive type"), required=False) |
1268 | 224 | 224 | ||
1269 | 225 | is_active = Bool( | ||
1270 | 226 | title=_("True if the archive is in the active state"), | ||
1271 | 227 | required=False, readonly=True) | ||
1272 | 228 | |||
1273 | 225 | series_with_sources = Attribute( | 229 | series_with_sources = Attribute( |
1274 | 226 | "DistroSeries to which this archive has published sources") | 230 | "DistroSeries to which this archive has published sources") |
1275 | 227 | number_of_sources = Attribute( | 231 | number_of_sources = Attribute( |
1276 | @@ -1151,6 +1155,19 @@ | |||
1277 | 1151 | def disable(): | 1155 | def disable(): |
1278 | 1152 | """Disable the archive.""" | 1156 | """Disable the archive.""" |
1279 | 1153 | 1157 | ||
1280 | 1158 | def delete(deleted_by): | ||
1281 | 1159 | """Delete this archive. | ||
1282 | 1160 | |||
1283 | 1161 | :param deleted_by: The `IPerson` requesting the deletion. | ||
1284 | 1162 | |||
1285 | 1163 | The ArchiveStatus will be set to DELETING and any published | ||
1286 | 1164 | packages will be marked as DELETED by deleted_by. | ||
1287 | 1165 | |||
1288 | 1166 | The publisher is responsible for deleting the repository area | ||
1289 | 1167 | when it sees the status change and sets it to DELETED once | ||
1290 | 1168 | processed. | ||
1291 | 1169 | """ | ||
1292 | 1170 | |||
1293 | 1154 | 1171 | ||
1294 | 1155 | class IArchive(IArchivePublic, IArchiveAppend, IArchiveEdit, IArchiveView): | 1172 | class IArchive(IArchivePublic, IArchiveAppend, IArchiveEdit, IArchiveView): |
1295 | 1156 | """Main Archive interface.""" | 1173 | """Main Archive interface.""" |
1296 | 1157 | 1174 | ||
1297 | === modified file 'lib/lp/soyuz/model/archive.py' | |||
1298 | --- lib/lp/soyuz/model/archive.py 2010-04-15 10:27:06 +0000 | |||
1299 | +++ lib/lp/soyuz/model/archive.py 2010-04-29 11:26:16 +0000 | |||
1300 | @@ -150,7 +150,8 @@ | |||
1301 | 150 | dbName='purpose', unique=False, notNull=True, schema=ArchivePurpose) | 150 | dbName='purpose', unique=False, notNull=True, schema=ArchivePurpose) |
1302 | 151 | 151 | ||
1303 | 152 | status = EnumCol( | 152 | status = EnumCol( |
1305 | 153 | dbName="status", unique=False, notNull=True, schema=ArchiveStatus) | 153 | dbName="status", unique=False, notNull=True, schema=ArchiveStatus, |
1306 | 154 | default=ArchiveStatus.ACTIVE) | ||
1307 | 154 | 155 | ||
1308 | 155 | _enabled = BoolCol(dbName='enabled', notNull=True, default=True) | 156 | _enabled = BoolCol(dbName='enabled', notNull=True, default=True) |
1309 | 156 | enabled = property(lambda x: x._enabled) | 157 | enabled = property(lambda x: x._enabled) |
1310 | @@ -269,6 +270,11 @@ | |||
1311 | 269 | return self.purpose in MAIN_ARCHIVE_PURPOSES | 270 | return self.purpose in MAIN_ARCHIVE_PURPOSES |
1312 | 270 | 271 | ||
1313 | 271 | @property | 272 | @property |
1314 | 273 | def is_active(self): | ||
1315 | 274 | """See `IArchive`.""" | ||
1316 | 275 | return self.status == ArchiveStatus.ACTIVE | ||
1317 | 276 | |||
1318 | 277 | @property | ||
1319 | 272 | def series_with_sources(self): | 278 | def series_with_sources(self): |
1320 | 273 | """See `IArchive`.""" | 279 | """See `IArchive`.""" |
1321 | 274 | store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) | 280 | store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) |
1322 | @@ -1409,6 +1415,26 @@ | |||
1323 | 1409 | self._enabled = False | 1415 | self._enabled = False |
1324 | 1410 | self._setBuildStatuses(JobStatus.SUSPENDED) | 1416 | self._setBuildStatuses(JobStatus.SUSPENDED) |
1325 | 1411 | 1417 | ||
1326 | 1418 | def delete(self, deleted_by): | ||
1327 | 1419 | """See `IArchive`.""" | ||
1328 | 1420 | assert self.status not in ( | ||
1329 | 1421 | ArchiveStatus.DELETING, ArchiveStatus.DELETED, | ||
1330 | 1422 | "This archive is already deleted.") | ||
1331 | 1423 | |||
1332 | 1424 | # Set all the publications to DELETED. | ||
1333 | 1425 | statuses = ( | ||
1334 | 1426 | PackagePublishingStatus.PENDING, | ||
1335 | 1427 | PackagePublishingStatus.PUBLISHED) | ||
1336 | 1428 | sources = list(self.getPublishedSources(status=statuses)) | ||
1337 | 1429 | getUtility(IPublishingSet).requestDeletion( | ||
1338 | 1430 | sources, removed_by=deleted_by, | ||
1339 | 1431 | removal_comment="Removed when deleting archive") | ||
1340 | 1432 | |||
1341 | 1433 | # Mark the archive's status as DELETING so the repository can be | ||
1342 | 1434 | # removed by the publisher. | ||
1343 | 1435 | self.status = ArchiveStatus.DELETING | ||
1344 | 1436 | self.disable() | ||
1345 | 1437 | |||
1346 | 1412 | 1438 | ||
1347 | 1413 | class ArchiveSet: | 1439 | class ArchiveSet: |
1348 | 1414 | implements(IArchiveSet) | 1440 | implements(IArchiveSet) |
1349 | 1415 | 1441 | ||
1350 | === modified file 'lib/lp/soyuz/scripts/packagecopier.py' | |||
1351 | --- lib/lp/soyuz/scripts/packagecopier.py 2010-04-14 09:53:04 +0000 | |||
1352 | +++ lib/lp/soyuz/scripts/packagecopier.py 2010-04-29 11:26:16 +0000 | |||
1353 | @@ -251,10 +251,6 @@ | |||
1354 | 251 | 251 | ||
1355 | 252 | inventory_conflicts = self.getConflicts(source) | 252 | inventory_conflicts = self.getConflicts(source) |
1356 | 253 | 253 | ||
1357 | 254 | if (destination_archive_conflicts.count() == 0 and | ||
1358 | 255 | len(inventory_conflicts) == 0): | ||
1359 | 256 | return | ||
1360 | 257 | |||
1361 | 258 | # Cache the conflicting publications because they will be iterated | 254 | # Cache the conflicting publications because they will be iterated |
1362 | 259 | # more than once. | 255 | # more than once. |
1363 | 260 | destination_archive_conflicts = list(destination_archive_conflicts) | 256 | destination_archive_conflicts = list(destination_archive_conflicts) |
1364 | 261 | 257 | ||
1365 | === modified file 'lib/lp/soyuz/scripts/tests/test_copypackage.py' | |||
1366 | --- lib/lp/soyuz/scripts/tests/test_copypackage.py 2010-04-14 09:53:04 +0000 | |||
1367 | +++ lib/lp/soyuz/scripts/tests/test_copypackage.py 2010-04-29 11:26:16 +0000 | |||
1368 | @@ -2456,10 +2456,10 @@ | |||
1369 | 2456 | """ | 2456 | """ |
1370 | 2457 | ubuntu = getUtility(IDistributionSet).getByName('ubuntu') | 2457 | ubuntu = getUtility(IDistributionSet).getByName('ubuntu') |
1371 | 2458 | warty = ubuntu.getSeries('warty') | 2458 | warty = ubuntu.getSeries('warty') |
1372 | 2459 | hoary = ubuntu.getSeries('hoary') | ||
1373 | 2460 | test_publisher = self.getTestPublisher(warty) | 2459 | test_publisher = self.getTestPublisher(warty) |
1374 | 2461 | test_publisher.addFakeChroots(warty) | 2460 | test_publisher.addFakeChroots(warty) |
1375 | 2462 | 2461 | ||
1376 | 2462 | orig_tarball = 'test-source_1.0.orig.tar.gz' | ||
1377 | 2463 | proposed_source = test_publisher.getPubSource( | 2463 | proposed_source = test_publisher.getPubSource( |
1378 | 2464 | sourcename='test-source', version='1.0-2', | 2464 | sourcename='test-source', version='1.0-2', |
1379 | 2465 | distroseries=warty, archive=warty.main_archive, | 2465 | distroseries=warty, archive=warty.main_archive, |
1380 | @@ -2467,7 +2467,7 @@ | |||
1381 | 2467 | status=PackagePublishingStatus.PUBLISHED, | 2467 | status=PackagePublishingStatus.PUBLISHED, |
1382 | 2468 | section='net') | 2468 | section='net') |
1383 | 2469 | proposed_tar = test_publisher.addMockFile( | 2469 | proposed_tar = test_publisher.addMockFile( |
1385 | 2470 | 'test-source_1.0.orig.tar.gz', filecontent='aaabbbccc') | 2470 | orig_tarball, filecontent='aaabbbccc') |
1386 | 2471 | proposed_source.sourcepackagerelease.addFile(proposed_tar) | 2471 | proposed_source.sourcepackagerelease.addFile(proposed_tar) |
1387 | 2472 | updates_source = test_publisher.getPubSource( | 2472 | updates_source = test_publisher.getPubSource( |
1388 | 2473 | sourcename='test-source', version='1.0-1', | 2473 | sourcename='test-source', version='1.0-1', |
1389 | @@ -2476,7 +2476,7 @@ | |||
1390 | 2476 | status=PackagePublishingStatus.PUBLISHED, | 2476 | status=PackagePublishingStatus.PUBLISHED, |
1391 | 2477 | section='misc') | 2477 | section='misc') |
1392 | 2478 | updates_tar = test_publisher.addMockFile( | 2478 | updates_tar = test_publisher.addMockFile( |
1394 | 2479 | 'test-source_1.0.orig.tar.gz', filecontent='zzzyyyxxx') | 2479 | orig_tarball, filecontent='zzzyyyxxx') |
1395 | 2480 | updates_source.sourcepackagerelease.addFile(updates_tar) | 2480 | updates_source.sourcepackagerelease.addFile(updates_tar) |
1396 | 2481 | # Commit to ensure librarian files are written. | 2481 | # Commit to ensure librarian files are written. |
1397 | 2482 | self.layer.txn.commit() | 2482 | self.layer.txn.commit() |
1398 | @@ -2489,6 +2489,51 @@ | |||
1399 | 2489 | checker.checkCopy, proposed_source, warty, | 2489 | checker.checkCopy, proposed_source, warty, |
1400 | 2490 | PackagePublishingPocket.UPDATES) | 2490 | PackagePublishingPocket.UPDATES) |
1401 | 2491 | 2491 | ||
1402 | 2492 | def testCopySourceWithConflictingFilesInPPAs(self): | ||
1403 | 2493 | """We can copy source if the source files match, both in name and | ||
1404 | 2494 | contents. We can't if they don't. | ||
1405 | 2495 | """ | ||
1406 | 2496 | joe = self.factory.makePerson(email='joe@example.com') | ||
1407 | 2497 | ubuntu = getUtility(IDistributionSet).getByName('ubuntu') | ||
1408 | 2498 | warty = ubuntu.getSeries('warty') | ||
1409 | 2499 | test_publisher = self.getTestPublisher(warty) | ||
1410 | 2500 | test_publisher.addFakeChroots(warty) | ||
1411 | 2501 | dest_ppa = self.factory.makeArchive( | ||
1412 | 2502 | distribution=ubuntu, owner=joe, purpose=ArchivePurpose.PPA, | ||
1413 | 2503 | name='test1') | ||
1414 | 2504 | src_ppa = self.factory.makeArchive( | ||
1415 | 2505 | distribution=ubuntu, owner=joe, purpose=ArchivePurpose.PPA, | ||
1416 | 2506 | name='test2') | ||
1417 | 2507 | test1_source = test_publisher.getPubSource( | ||
1418 | 2508 | sourcename='test-source', version='1.0-1', | ||
1419 | 2509 | distroseries=warty, archive=dest_ppa, | ||
1420 | 2510 | pocket=PackagePublishingPocket.RELEASE, | ||
1421 | 2511 | status=PackagePublishingStatus.PUBLISHED, | ||
1422 | 2512 | section='misc') | ||
1423 | 2513 | orig_tarball = 'test-source_1.0.orig.tar.gz' | ||
1424 | 2514 | test1_tar = test_publisher.addMockFile( | ||
1425 | 2515 | orig_tarball, filecontent='aaabbbccc') | ||
1426 | 2516 | test1_source.sourcepackagerelease.addFile(test1_tar) | ||
1427 | 2517 | test2_source = test_publisher.getPubSource( | ||
1428 | 2518 | sourcename='test-source', version='1.0-2', | ||
1429 | 2519 | distroseries=warty, archive=src_ppa, | ||
1430 | 2520 | pocket=PackagePublishingPocket.RELEASE, | ||
1431 | 2521 | status=PackagePublishingStatus.PUBLISHED, | ||
1432 | 2522 | section='misc') | ||
1433 | 2523 | test2_tar = test_publisher.addMockFile( | ||
1434 | 2524 | orig_tarball, filecontent='zzzyyyxxx') | ||
1435 | 2525 | test2_source.sourcepackagerelease.addFile(test2_tar) | ||
1436 | 2526 | # Commit to ensure librarian files are written. | ||
1437 | 2527 | self.layer.txn.commit() | ||
1438 | 2528 | |||
1439 | 2529 | checker = CopyChecker(dest_ppa, include_binaries=False) | ||
1440 | 2530 | self.assertRaisesWithContent( | ||
1441 | 2531 | CannotCopy, | ||
1442 | 2532 | "test-source_1.0.orig.tar.gz already exists in destination " | ||
1443 | 2533 | "archive with different contents.", | ||
1444 | 2534 | checker.checkCopy, test2_source, warty, | ||
1445 | 2535 | PackagePublishingPocket.RELEASE) | ||
1446 | 2536 | |||
1447 | 2492 | 2537 | ||
1448 | 2493 | def test_suite(): | 2538 | def test_suite(): |
1449 | 2494 | return unittest.TestLoader().loadTestsFromName(__name__) | 2539 | return unittest.TestLoader().loadTestsFromName(__name__) |
1450 | 2495 | 2540 | ||
1451 | === modified file 'lib/lp/soyuz/stories/ppa/xx-ppa-workflow.txt' | |||
1452 | --- lib/lp/soyuz/stories/ppa/xx-ppa-workflow.txt 2010-02-25 16:49:16 +0000 | |||
1453 | +++ lib/lp/soyuz/stories/ppa/xx-ppa-workflow.txt 2010-04-29 11:26:16 +0000 | |||
1454 | @@ -392,7 +392,7 @@ | |||
1455 | 392 | 392 | ||
1456 | 393 | >>> for msg in get_feedback_messages(admin_browser.contents): | 393 | >>> for msg in get_feedback_messages(admin_browser.contents): |
1457 | 394 | ... print msg | 394 | ... print msg |
1459 | 395 | This archive has been disabled. | 395 | This PPA has been disabled. |
1460 | 396 | 396 | ||
1461 | 397 | We need go back to the "Administer archive" page to see the build score and | 397 | We need go back to the "Administer archive" page to see the build score and |
1462 | 398 | external dependencies changes that were made: | 398 | external dependencies changes that were made: |
1463 | @@ -718,7 +718,8 @@ | |||
1464 | 718 | 718 | ||
1465 | 719 | >>> no_priv_browser = setupBrowser( | 719 | >>> no_priv_browser = setupBrowser( |
1466 | 720 | ... auth='Basic no-priv@canonical.com:test') | 720 | ... auth='Basic no-priv@canonical.com:test') |
1468 | 721 | >>> no_priv_browser.open("http://launchpad.dev/~no-priv/+archive/ppa/+edit") | 721 | >>> no_priv_browser.open( |
1469 | 722 | ... "http://launchpad.dev/~no-priv/+archive/ppa/+edit") | ||
1470 | 722 | 723 | ||
1471 | 723 | Initially, the PPA is enabled and publishes. | 724 | Initially, the PPA is enabled and publishes. |
1472 | 724 | 725 | ||
1473 | @@ -734,11 +735,12 @@ | |||
1474 | 734 | >>> no_priv_browser.getControl('Save').click() | 735 | >>> no_priv_browser.getControl('Save').click() |
1475 | 735 | >>> print extract_text( | 736 | >>> print extract_text( |
1476 | 736 | ... first_tag_by_class(no_priv_browser.contents, 'warning message')) | 737 | ... first_tag_by_class(no_priv_browser.contents, 'warning message')) |
1478 | 737 | This archive has been disabled. | 738 | This PPA has been disabled. |
1479 | 738 | 739 | ||
1480 | 739 | Going back to the edit page, we can see the publish flag was cleared: | 740 | Going back to the edit page, we can see the publish flag was cleared: |
1481 | 740 | 741 | ||
1483 | 741 | >>> no_priv_browser.open("http://launchpad.dev/~no-priv/+archive/ppa/+edit") | 742 | >>> no_priv_browser.open( |
1484 | 743 | ... "http://launchpad.dev/~no-priv/+archive/ppa/+edit") | ||
1485 | 742 | >>> print no_priv_browser.getControl(name='field.publish').value | 744 | >>> print no_priv_browser.getControl(name='field.publish').value |
1486 | 743 | False | 745 | False |
1487 | 744 | 746 | ||
1488 | @@ -754,3 +756,79 @@ | |||
1489 | 754 | True | 756 | True |
1490 | 755 | 757 | ||
1491 | 756 | 758 | ||
1492 | 759 | == Deleting a PPA == | ||
1493 | 760 | |||
1494 | 761 | Users with launchpad.Edit permission see a "Delete PPA" link in the | ||
1495 | 762 | navigation menu. | ||
1496 | 763 | |||
1497 | 764 | >>> anon_browser.open("http://launchpad.dev/~no-priv/+archive/ppa") | ||
1498 | 765 | >>> print anon_browser.getLink("Delete PPA") | ||
1499 | 766 | Traceback (most recent call last): | ||
1500 | 767 | ... | ||
1501 | 768 | LinkNotFoundError | ||
1502 | 769 | |||
1503 | 770 | >>> no_priv_browser.open("http://launchpad.dev/~no-priv/+archive/ppa") | ||
1504 | 771 | >>> no_priv_browser.getLink("Delete PPA").click() | ||
1505 | 772 | |||
1506 | 773 | Clicking this link takes the user to a page that allows deletion of a PPA: | ||
1507 | 774 | |||
1508 | 775 | >>> print no_priv_browser.title | ||
1509 | 776 | Delete “PPA for No Privileges Person” : PPA for No Privileges Person : | ||
1510 | 777 | No Privileges Person | ||
1511 | 778 | |||
1512 | 779 | The page contains a stern warning that this action is final and irreversible: | ||
1513 | 780 | |||
1514 | 781 | >>> print extract_text(find_main_content(no_priv_browser.contents)) | ||
1515 | 782 | Delete “PPA for No Privileges Person” | ||
1516 | 783 | ... | ||
1517 | 784 | Deleting a PPA will destroy all of its packages, files and the | ||
1518 | 785 | repository area. | ||
1519 | 786 | This deletion is PERMANENT and cannot be undone. | ||
1520 | 787 | Are you sure ? | ||
1521 | 788 | ... | ||
1522 | 789 | |||
1523 | 790 | If the user changes his mind, he can click on the cancel link to go back | ||
1524 | 791 | a page: | ||
1525 | 792 | |||
1526 | 793 | >>> print no_priv_browser.getLink("Cancel").url | ||
1527 | 794 | http://launchpad.dev/~no-priv/+archive/ppa | ||
1528 | 795 | |||
1529 | 796 | Otherwise, he has a button to press to confirm the deletion. | ||
1530 | 797 | |||
1531 | 798 | >>> no_priv_browser.getControl("Permanently delete PPA").click() | ||
1532 | 799 | |||
1533 | 800 | This action will redirect the user back to his profile page, which will | ||
1534 | 801 | contain a notification message that the deletion is in progress. | ||
1535 | 802 | |||
1536 | 803 | >>> print no_priv_browser.url | ||
1537 | 804 | http://launchpad.dev/~no-priv | ||
1538 | 805 | |||
1539 | 806 | >>> for msg in get_feedback_messages(no_priv_browser.contents): | ||
1540 | 807 | ... print msg | ||
1541 | 808 | Deletion of 'PPA for No Privileges Person' has been requested and | ||
1542 | 809 | the repository will be removed shortly. | ||
1543 | 810 | |||
1544 | 811 | The deleted PPA is still available to browse via a link on the profile page | ||
1545 | 812 | so you can see its build history, etc.: | ||
1546 | 813 | |||
1547 | 814 | >>> no_priv_browser.getLink("PPA for No Privileges Person").click() | ||
1548 | 815 | |||
1549 | 816 | However, most of the action links are removed for deleted PPAs, so you can | ||
1550 | 817 | no longer "Delete packages", "Edit PPA dependencies", or "Change details". | ||
1551 | 818 | |||
1552 | 819 | >>> print no_priv_browser.getLink("Change details") | ||
1553 | 820 | Traceback (most recent call last): | ||
1554 | 821 | ... | ||
1555 | 822 | LinkNotFoundError | ||
1556 | 823 | |||
1557 | 824 | >>> print no_priv_browser.getLink("Edit PPA dependencies") | ||
1558 | 825 | Traceback (most recent call last): | ||
1559 | 826 | ... | ||
1560 | 827 | LinkNotFoundError | ||
1561 | 828 | |||
1562 | 829 | >>> no_priv_browser.getLink("View package details").click() | ||
1563 | 830 | >>> print no_priv_browser.getLink("Delete packages") | ||
1564 | 831 | Traceback (most recent call last): | ||
1565 | 832 | ... | ||
1566 | 833 | LinkNotFoundError | ||
1567 | 834 | |||
1568 | 757 | 835 | ||
1569 | === added file 'lib/lp/soyuz/templates/archive-delete.pt' | |||
1570 | --- lib/lp/soyuz/templates/archive-delete.pt 1970-01-01 00:00:00 +0000 | |||
1571 | +++ lib/lp/soyuz/templates/archive-delete.pt 2010-04-29 11:26:16 +0000 | |||
1572 | @@ -0,0 +1,32 @@ | |||
1573 | 1 | <html | ||
1574 | 2 | xmlns="http://www.w3.org/1999/xhtml" | ||
1575 | 3 | xmlns:tal="http://xml.zope.org/namespaces/tal" | ||
1576 | 4 | xmlns:metal="http://xml.zope.org/namespaces/metal" | ||
1577 | 5 | xmlns:i18n="http://xml.zope.org/namespaces/i18n" | ||
1578 | 6 | metal:use-macro="view/macro:page/main_only" | ||
1579 | 7 | i18n:domain="launchpad" | ||
1580 | 8 | > | ||
1581 | 9 | <body> | ||
1582 | 10 | |||
1583 | 11 | <div metal:fill-slot="main"> | ||
1584 | 12 | |||
1585 | 13 | <div class="top-portlet" | ||
1586 | 14 | tal:condition="view/can_be_deleted"> | ||
1587 | 15 | <p>Deleting a PPA will destroy all of its packages, | ||
1588 | 16 | files and the repository area.</p> | ||
1589 | 17 | <p>This deletion is PERMANENT and cannot be undone.</p> | ||
1590 | 18 | <p>Are you sure ?</p> | ||
1591 | 19 | |||
1592 | 20 | <div metal:use-macro="context/@@launchpad_form/form" /> | ||
1593 | 21 | </div> | ||
1594 | 22 | |||
1595 | 23 | <div class="top-portlet" | ||
1596 | 24 | tal:condition="view/waiting_for_deletion"> | ||
1597 | 25 | <p>This archive is marked for deletion and will be removed | ||
1598 | 26 | shortly.</p> | ||
1599 | 27 | </div> | ||
1600 | 28 | |||
1601 | 29 | </div> <!-- main --> | ||
1602 | 30 | |||
1603 | 31 | </body> | ||
1604 | 32 | </html> | ||
1605 | 0 | 33 | ||
1606 | === modified file 'lib/lp/soyuz/templates/archive-index.pt' | |||
1607 | --- lib/lp/soyuz/templates/archive-index.pt 2010-02-25 10:47:50 +0000 | |||
1608 | +++ lib/lp/soyuz/templates/archive-index.pt 2010-04-29 11:26:16 +0000 | |||
1609 | @@ -26,7 +26,8 @@ | |||
1610 | 26 | 26 | ||
1611 | 27 | <div class="top-portlet" style="padding-top:0.5em;"> | 27 | <div class="top-portlet" style="padding-top:0.5em;"> |
1612 | 28 | <p tal:condition="not: context/enabled" | 28 | <p tal:condition="not: context/enabled" |
1614 | 29 | style="clear: right;" class="warning message"> | 29 | style="clear: right;" class="warning message" |
1615 | 30 | tal:content="view/disabled_warning_message"> | ||
1616 | 30 | This archive has been disabled. | 31 | This archive has been disabled. |
1617 | 31 | </p> | 32 | </p> |
1618 | 32 | 33 | ||
1619 | 33 | 34 | ||
1620 | === modified file 'lib/lp/soyuz/templates/archive-packages.pt' | |||
1621 | --- lib/lp/soyuz/templates/archive-packages.pt 2009-12-03 18:33:22 +0000 | |||
1622 | +++ lib/lp/soyuz/templates/archive-packages.pt 2010-04-29 11:26:16 +0000 | |||
1623 | @@ -104,7 +104,8 @@ | |||
1624 | 104 | tal:content="structure view/@@+global-actions" /> | 104 | tal:content="structure view/@@+global-actions" /> |
1625 | 105 | 105 | ||
1626 | 106 | <p tal:condition="not: context/enabled" | 106 | <p tal:condition="not: context/enabled" |
1628 | 107 | style="clear: right;" class="warning message"> | 107 | style="clear: right;" class="warning message" |
1629 | 108 | tal:content="view/disabled_warning_message"> | ||
1630 | 108 | This archive has been disabled. | 109 | This archive has been disabled. |
1631 | 109 | </p> | 110 | </p> |
1632 | 110 | 111 | ||
1633 | 111 | 112 | ||
1634 | === renamed file 'lib/canonical/launchpad/webapp/tests/breadcrumbs.py' => 'lib/lp/testing/breadcrumbs.py' | |||
1635 | --- lib/canonical/launchpad/webapp/tests/breadcrumbs.py 2009-09-17 18:05:29 +0000 | |||
1636 | +++ lib/lp/testing/breadcrumbs.py 2010-04-29 11:26:16 +0000 | |||
1637 | @@ -3,67 +3,51 @@ | |||
1638 | 3 | 3 | ||
1639 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
1640 | 5 | 5 | ||
1647 | 6 | from zope.app import zapi | 6 | from canonical.launchpad.webapp.publisher import canonical_url |
1642 | 7 | from zope.component import ComponentLookupError, getMultiAdapter | ||
1643 | 8 | |||
1644 | 9 | from canonical.lazr.testing.menus import make_fake_request | ||
1645 | 10 | from canonical.launchpad.layers import setFirstLayer | ||
1646 | 11 | from canonical.launchpad.webapp.publisher import RootObject | ||
1648 | 12 | from canonical.testing import DatabaseFunctionalLayer | 7 | from canonical.testing import DatabaseFunctionalLayer |
1649 | 8 | |||
1650 | 13 | from lp.testing import TestCaseWithFactory | 9 | from lp.testing import TestCaseWithFactory |
1651 | 10 | from lp.testing.views import create_initialized_view | ||
1652 | 11 | from lp.testing.publication import test_traverse | ||
1653 | 14 | 12 | ||
1654 | 15 | 13 | ||
1655 | 16 | class BaseBreadcrumbTestCase(TestCaseWithFactory): | 14 | class BaseBreadcrumbTestCase(TestCaseWithFactory): |
1656 | 17 | 15 | ||
1657 | 18 | layer = DatabaseFunctionalLayer | 16 | layer = DatabaseFunctionalLayer |
1670 | 19 | request_layer = None | 17 | |
1671 | 20 | 18 | def assertBreadcrumbs(self, expected, obj, view_name=None, rootsite=None): | |
1672 | 21 | def setUp(self): | 19 | """Assert that the breadcrumbs for obj match the expected values. |
1673 | 22 | super(BaseBreadcrumbTestCase, self).setUp() | 20 | |
1674 | 23 | self.root = RootObject() | 21 | :param expected: A list of tuples containing (text, url) pairs. |
1675 | 24 | 22 | """ | |
1676 | 25 | def _getHierarchyView(self, url, traversed_objects): | 23 | crumbs = self.getBreadcrumbsForObject(obj, view_name, rootsite) |
1677 | 26 | request = self._make_request(url, traversed_objects) | 24 | self.assertEqual( |
1678 | 27 | return getMultiAdapter((self.root, request), name='+hierarchy') | 25 | expected, |
1679 | 28 | 26 | [(crumb.text, crumb.url) for crumb in crumbs]) | |
1680 | 29 | def _getBreadcrumbs(self, url, traversed_objects): | 27 | |
1681 | 30 | view = self._getHierarchyView(url, traversed_objects) | 28 | def assertBreadcrumbTexts(self, expected, obj, view_name=None, |
1682 | 29 | rootsite=None): | ||
1683 | 30 | """The text of the breadcrumbs for obj match the expected values.""" | ||
1684 | 31 | crumbs = self.getBreadcrumbsForObject(obj, view_name, rootsite) | ||
1685 | 32 | self.assertEqual(expected, [crumb.text for crumb in crumbs]) | ||
1686 | 33 | |||
1687 | 34 | def assertBreadcrumbUrls(self, expected, obj, view_name=None, | ||
1688 | 35 | rootsite=None): | ||
1689 | 36 | """The urls of the breadcrumbs for obj match the expected values.""" | ||
1690 | 37 | crumbs = self.getBreadcrumbsForObject(obj, view_name, rootsite) | ||
1691 | 38 | self.assertEqual(expected, [crumb.url for crumb in crumbs]) | ||
1692 | 39 | |||
1693 | 40 | def getBreadcrumbsForObject(self, obj, view_name=None, rootsite=None): | ||
1694 | 41 | """Get the breadcrumbs for the specified object. | ||
1695 | 42 | |||
1696 | 43 | Traverse to the canonical_url of the object, and use the request from | ||
1697 | 44 | that to feed into the initialized hierarchy view so we get the | ||
1698 | 45 | traversed objects. | ||
1699 | 46 | """ | ||
1700 | 47 | url = canonical_url(obj, view_name=view_name, rootsite=rootsite) | ||
1701 | 48 | return self.getBreadcrumbsForUrl(url) | ||
1702 | 49 | |||
1703 | 50 | def getBreadcrumbsForUrl(self, url): | ||
1704 | 51 | obj, view, request = test_traverse(url) | ||
1705 | 52 | view = create_initialized_view(obj, '+hierarchy', request=request) | ||
1706 | 31 | return view.items | 53 | return view.items |
1707 | 32 | |||
1708 | 33 | def _getBreadcrumbsTexts(self, url, traversed_objects): | ||
1709 | 34 | crumbs = self._getBreadcrumbs(url, traversed_objects) | ||
1710 | 35 | return [crumb.text for crumb in crumbs] | ||
1711 | 36 | |||
1712 | 37 | def _getBreadcrumbsURLs(self, url, traversed_objects): | ||
1713 | 38 | crumbs = self._getBreadcrumbs(url, traversed_objects) | ||
1714 | 39 | return [crumb.url for crumb in crumbs] | ||
1715 | 40 | |||
1716 | 41 | def _make_request(self, url, traversed_objects): | ||
1717 | 42 | """Create and return a LaunchpadTestRequest. | ||
1718 | 43 | |||
1719 | 44 | Set the given list of traversed objects as request.traversed_objects, | ||
1720 | 45 | also appending the view that the given URL points to, to mimic how | ||
1721 | 46 | request.traversed_objects behave in a real request. | ||
1722 | 47 | |||
1723 | 48 | XXX: salgado, bug=432025, 2009-09-17: Instead of setting | ||
1724 | 49 | request.traversed_objects manually, we should duplicate parts of | ||
1725 | 50 | zope.publisher.publish.publish here (or in make_fake_request()) so | ||
1726 | 51 | that tests don't have to specify the list of traversed objects for us | ||
1727 | 52 | to set here. | ||
1728 | 53 | """ | ||
1729 | 54 | request = make_fake_request(url, traversed_objects=traversed_objects) | ||
1730 | 55 | if self.request_layer is not None: | ||
1731 | 56 | setFirstLayer(request, self.request_layer) | ||
1732 | 57 | last_segment = request._traversed_names[-1] | ||
1733 | 58 | if traversed_objects: | ||
1734 | 59 | obj = traversed_objects[-1] | ||
1735 | 60 | # Assume the last_segment is the name of the view on the last | ||
1736 | 61 | # traversed object, and if we fail to find a view with that name, | ||
1737 | 62 | # use the default view. | ||
1738 | 63 | try: | ||
1739 | 64 | view = getMultiAdapter((obj, request), name=last_segment) | ||
1740 | 65 | except ComponentLookupError: | ||
1741 | 66 | default_view_name = zapi.getDefaultViewName(obj, request) | ||
1742 | 67 | view = getMultiAdapter((obj, request), name=default_view_name) | ||
1743 | 68 | request.traversed_objects.append(view) | ||
1744 | 69 | return request | ||
1745 | 70 | 54 | ||
1746 | === modified file 'lib/lp/testing/publication.py' | |||
1747 | --- lib/lp/testing/publication.py 2009-07-17 02:25:09 +0000 | |||
1748 | +++ lib/lp/testing/publication.py 2010-04-29 11:26:16 +0000 | |||
1749 | @@ -7,14 +7,24 @@ | |||
1750 | 7 | __all__ = [ | 7 | __all__ = [ |
1751 | 8 | 'get_request_and_publication', | 8 | 'get_request_and_publication', |
1752 | 9 | 'print_request_and_publication', | 9 | 'print_request_and_publication', |
1753 | 10 | 'test_traverse', | ||
1754 | 10 | ] | 11 | ] |
1755 | 11 | 12 | ||
1756 | 12 | from cStringIO import StringIO | 13 | from cStringIO import StringIO |
1757 | 13 | 14 | ||
1758 | 14 | # Z3 doesn't make this available as a utility. | 15 | # Z3 doesn't make this available as a utility. |
1759 | 16 | from zope.app import zapi | ||
1760 | 15 | from zope.app.publication.requestpublicationregistry import factoryRegistry | 17 | from zope.app.publication.requestpublicationregistry import factoryRegistry |
1761 | 18 | from zope.component import getUtility | ||
1762 | 19 | from zope.interface import providedBy | ||
1763 | 20 | from zope.publisher.interfaces.browser import IDefaultSkin | ||
1764 | 21 | |||
1765 | 22 | from canonical.launchpad.interfaces.launchpad import IOpenLaunchBag | ||
1766 | 23 | import canonical.launchpad.layers as layers | ||
1767 | 24 | from canonical.launchpad.webapp import urlsplit | ||
1768 | 16 | from canonical.launchpad.webapp.servers import ProtocolErrorPublication | 25 | from canonical.launchpad.webapp.servers import ProtocolErrorPublication |
1769 | 17 | 26 | ||
1770 | 27 | |||
1771 | 18 | # Defines an helper function that returns the appropriate | 28 | # Defines an helper function that returns the appropriate |
1772 | 19 | # IRequest and IPublication. | 29 | # IRequest and IPublication. |
1773 | 20 | def get_request_and_publication(host='localhost', port=None, | 30 | def get_request_and_publication(host='localhost', port=None, |
1774 | @@ -39,6 +49,7 @@ | |||
1775 | 39 | publication = publication_factory(None) | 49 | publication = publication_factory(None) |
1776 | 40 | return request, publication | 50 | return request, publication |
1777 | 41 | 51 | ||
1778 | 52 | |||
1779 | 42 | def print_request_and_publication(host='localhost', port=None, | 53 | def print_request_and_publication(host='localhost', port=None, |
1780 | 43 | method='GET', | 54 | method='GET', |
1781 | 44 | mime_type='text/html', | 55 | mime_type='text/html', |
1782 | @@ -56,3 +67,49 @@ | |||
1783 | 56 | print " %s: %s" % (name, value) | 67 | print " %s: %s" % (name, value) |
1784 | 57 | else: | 68 | else: |
1785 | 58 | print publication_classname | 69 | print publication_classname |
1786 | 70 | |||
1787 | 71 | |||
1788 | 72 | def test_traverse(url): | ||
1789 | 73 | """Traverse the url in the same way normal publishing occurs. | ||
1790 | 74 | |||
1791 | 75 | Returns a tuple of (object, view, request) where: | ||
1792 | 76 | object is the last model object in the traversal chain | ||
1793 | 77 | view is the defined view for the object at the specified url (if | ||
1794 | 78 | the url didn't directly specify a view, then the view is the | ||
1795 | 79 | default view for the object. | ||
1796 | 80 | request is the request object resulting from the traversal. This | ||
1797 | 81 | contains a populated traversed_objects list just as a browser | ||
1798 | 82 | request would from a normal call into the app servers. | ||
1799 | 83 | |||
1800 | 84 | This call uses the currently logged in user, and does not start a new | ||
1801 | 85 | transaction. | ||
1802 | 86 | """ | ||
1803 | 87 | url_parts = urlsplit(url) | ||
1804 | 88 | server_url = '://'.join(url_parts[0:2]) | ||
1805 | 89 | path_info = url_parts[2] | ||
1806 | 90 | request, publication = get_request_and_publication( | ||
1807 | 91 | host=url_parts[1], extra_environment={ | ||
1808 | 92 | 'SERVER_URL': server_url, | ||
1809 | 93 | 'PATH_INFO': path_info}) | ||
1810 | 94 | |||
1811 | 95 | request.setPublication(publication) | ||
1812 | 96 | # We avoid calling publication.beforePublication because this starts a new | ||
1813 | 97 | # transaction, which causes an abort of the existing transaction, and the | ||
1814 | 98 | # removal of any created and uncommitted objects. | ||
1815 | 99 | |||
1816 | 100 | # Set the default layer. | ||
1817 | 101 | adapters = zapi.getGlobalSiteManager().adapters | ||
1818 | 102 | layer = adapters.lookup((providedBy(request),), IDefaultSkin, '') | ||
1819 | 103 | if layer is not None: | ||
1820 | 104 | layers.setAdditionalLayer(request, layer) | ||
1821 | 105 | |||
1822 | 106 | principal = publication.getPrincipal(request) | ||
1823 | 107 | request.setPrincipal(principal) | ||
1824 | 108 | |||
1825 | 109 | getUtility(IOpenLaunchBag).clear() | ||
1826 | 110 | app = publication.getApplication(request) | ||
1827 | 111 | view = request.traverse(app) | ||
1828 | 112 | # Since the last traversed object is the view, the second last should be | ||
1829 | 113 | # the object that the view is on. | ||
1830 | 114 | obj = request.traversed_objects[-2] | ||
1831 | 115 | return obj, view, request | ||
1832 | 59 | 116 | ||
1833 | === modified file 'lib/lp/translations/browser/tests/test_breadcrumbs.py' | |||
1834 | --- lib/lp/translations/browser/tests/test_breadcrumbs.py 2009-11-07 01:45:32 +0000 | |||
1835 | +++ lib/lp/translations/browser/tests/test_breadcrumbs.py 2010-04-29 11:26:16 +0000 | |||
1836 | @@ -7,12 +7,8 @@ | |||
1837 | 7 | 7 | ||
1838 | 8 | from canonical.lazr.utils import smartquote | 8 | from canonical.lazr.utils import smartquote |
1839 | 9 | 9 | ||
1840 | 10 | from canonical.launchpad.layers import TranslationsLayer | ||
1841 | 11 | from canonical.launchpad.webapp.publisher import canonical_url | ||
1842 | 12 | from canonical.launchpad.webapp.tests.breadcrumbs import ( | ||
1843 | 13 | BaseBreadcrumbTestCase) | ||
1844 | 14 | |||
1845 | 15 | from lp.services.worlddata.interfaces.language import ILanguageSet | 10 | from lp.services.worlddata.interfaces.language import ILanguageSet |
1846 | 11 | from lp.testing.breadcrumbs import BaseBreadcrumbTestCase | ||
1847 | 16 | from lp.translations.interfaces.distroserieslanguage import ( | 12 | from lp.translations.interfaces.distroserieslanguage import ( |
1848 | 17 | IDistroSeriesLanguageSet) | 13 | IDistroSeriesLanguageSet) |
1849 | 18 | from lp.translations.interfaces.productserieslanguage import ( | 14 | from lp.translations.interfaces.productserieslanguage import ( |
1850 | @@ -20,111 +16,82 @@ | |||
1851 | 20 | from lp.translations.interfaces.translationgroup import ITranslationGroupSet | 16 | from lp.translations.interfaces.translationgroup import ITranslationGroupSet |
1852 | 21 | 17 | ||
1853 | 22 | 18 | ||
1875 | 23 | class BaseTranslationsBreadcrumbTestCase(BaseBreadcrumbTestCase): | 19 | class TestTranslationsVHostBreadcrumb(BaseBreadcrumbTestCase): |
1855 | 24 | request_layer = TranslationsLayer | ||
1856 | 25 | |||
1857 | 26 | def setUp(self): | ||
1858 | 27 | super(BaseTranslationsBreadcrumbTestCase, self).setUp() | ||
1859 | 28 | self.traversed_objects = [self.root] | ||
1860 | 29 | |||
1861 | 30 | def _testContextBreadcrumbs(self, traversal_list, links, texts, url=None): | ||
1862 | 31 | self.traversed_objects.extend(traversal_list) | ||
1863 | 32 | if url is None: | ||
1864 | 33 | url = canonical_url(traversal_list[-1], rootsite='translations') | ||
1865 | 34 | |||
1866 | 35 | self.assertEquals( | ||
1867 | 36 | links, | ||
1868 | 37 | self._getBreadcrumbsURLs(url, self.traversed_objects)) | ||
1869 | 38 | self.assertEquals( | ||
1870 | 39 | texts, | ||
1871 | 40 | self._getBreadcrumbsTexts(url, self.traversed_objects)) | ||
1872 | 41 | |||
1873 | 42 | |||
1874 | 43 | class TestTranslationsVHostBreadcrumb(BaseTranslationsBreadcrumbTestCase): | ||
1876 | 44 | 20 | ||
1877 | 45 | def test_product(self): | 21 | def test_product(self): |
1878 | 46 | product = self.factory.makeProduct( | 22 | product = self.factory.makeProduct( |
1879 | 47 | name='crumb-tester', displayname="Crumb Tester") | 23 | name='crumb-tester', displayname="Crumb Tester") |
1885 | 48 | self._testContextBreadcrumbs( | 24 | self.assertBreadcrumbs( |
1886 | 49 | [product], | 25 | [("Crumb Tester", 'http://launchpad.dev/crumb-tester'), |
1887 | 50 | ['http://launchpad.dev/crumb-tester', | 26 | ("Translations", 'http://translations.launchpad.dev/crumb-tester')], |
1888 | 51 | 'http://translations.launchpad.dev/crumb-tester'], | 27 | product, rootsite='translations') |
1884 | 52 | ["Crumb Tester", "Translations"]) | ||
1889 | 53 | 28 | ||
1890 | 54 | def test_productseries(self): | 29 | def test_productseries(self): |
1891 | 55 | product = self.factory.makeProduct( | 30 | product = self.factory.makeProduct( |
1892 | 56 | name='crumb-tester', displayname="Crumb Tester") | 31 | name='crumb-tester', displayname="Crumb Tester") |
1893 | 57 | series = self.factory.makeProductSeries(name="test", product=product) | 32 | series = self.factory.makeProductSeries(name="test", product=product) |
1900 | 58 | self._testContextBreadcrumbs( | 33 | self.assertBreadcrumbs( |
1901 | 59 | [product, series], | 34 | [("Crumb Tester", 'http://launchpad.dev/crumb-tester'), |
1902 | 60 | ['http://launchpad.dev/crumb-tester', | 35 | ("Series test", 'http://launchpad.dev/crumb-tester/test'), |
1903 | 61 | 'http://launchpad.dev/crumb-tester/test', | 36 | ("Translations", 'http://translations.launchpad.dev/crumb-tester/test')], |
1904 | 62 | 'http://translations.launchpad.dev/crumb-tester/test'], | 37 | series, rootsite='translations') |
1899 | 63 | ["Crumb Tester", "Series test", "Translations"]) | ||
1905 | 64 | 38 | ||
1906 | 65 | def test_distribution(self): | 39 | def test_distribution(self): |
1907 | 66 | distribution = self.factory.makeDistribution( | 40 | distribution = self.factory.makeDistribution( |
1908 | 67 | name='crumb-tester', displayname="Crumb Tester") | 41 | name='crumb-tester', displayname="Crumb Tester") |
1914 | 68 | self._testContextBreadcrumbs( | 42 | self.assertBreadcrumbs( |
1915 | 69 | [distribution], | 43 | [("Crumb Tester", 'http://launchpad.dev/crumb-tester'), |
1916 | 70 | ['http://launchpad.dev/crumb-tester', | 44 | ("Translations", 'http://translations.launchpad.dev/crumb-tester')], |
1917 | 71 | 'http://translations.launchpad.dev/crumb-tester'], | 45 | distribution, rootsite='translations') |
1913 | 72 | ["Crumb Tester", "Translations"]) | ||
1918 | 73 | 46 | ||
1919 | 74 | def test_distroseries(self): | 47 | def test_distroseries(self): |
1920 | 75 | distribution = self.factory.makeDistribution( | 48 | distribution = self.factory.makeDistribution( |
1921 | 76 | name='crumb-tester', displayname="Crumb Tester") | 49 | name='crumb-tester', displayname="Crumb Tester") |
1922 | 77 | series = self.factory.makeDistroRelease( | 50 | series = self.factory.makeDistroRelease( |
1923 | 78 | name="test", version="1.0", distribution=distribution) | 51 | name="test", version="1.0", distribution=distribution) |
1930 | 79 | self._testContextBreadcrumbs( | 52 | self.assertBreadcrumbs( |
1931 | 80 | [distribution, series], | 53 | [("Crumb Tester", 'http://launchpad.dev/crumb-tester'), |
1932 | 81 | ['http://launchpad.dev/crumb-tester', | 54 | ("Test (1.0)", 'http://launchpad.dev/crumb-tester/test'), |
1933 | 82 | 'http://launchpad.dev/crumb-tester/test', | 55 | ("Translations", 'http://translations.launchpad.dev/crumb-tester/test')], |
1934 | 83 | 'http://translations.launchpad.dev/crumb-tester/test'], | 56 | series, rootsite='translations') |
1929 | 84 | ["Crumb Tester", "Test (1.0)", "Translations"]) | ||
1935 | 85 | 57 | ||
1936 | 86 | def test_project(self): | 58 | def test_project(self): |
1937 | 87 | project = self.factory.makeProject( | 59 | project = self.factory.makeProject( |
1938 | 88 | name='crumb-tester', displayname="Crumb Tester") | 60 | name='crumb-tester', displayname="Crumb Tester") |
1944 | 89 | self._testContextBreadcrumbs( | 61 | self.assertBreadcrumbs( |
1945 | 90 | [project], | 62 | [("Crumb Tester", 'http://launchpad.dev/crumb-tester'), |
1946 | 91 | ['http://launchpad.dev/crumb-tester', | 63 | ("Translations", 'http://translations.launchpad.dev/crumb-tester')], |
1947 | 92 | 'http://translations.launchpad.dev/crumb-tester'], | 64 | project, rootsite='translations') |
1943 | 93 | ["Crumb Tester", "Translations"]) | ||
1948 | 94 | 65 | ||
1949 | 95 | def test_person(self): | 66 | def test_person(self): |
1950 | 96 | person = self.factory.makePerson( | 67 | person = self.factory.makePerson( |
1951 | 97 | name='crumb-tester', displayname="Crumb Tester") | 68 | name='crumb-tester', displayname="Crumb Tester") |
1960 | 98 | self._testContextBreadcrumbs( | 69 | self.assertBreadcrumbs( |
1961 | 99 | [person], | 70 | [("Crumb Tester", 'http://launchpad.dev/~crumb-tester'), |
1962 | 100 | ['http://launchpad.dev/~crumb-tester', | 71 | ("Translations", 'http://translations.launchpad.dev/~crumb-tester')], |
1963 | 101 | 'http://translations.launchpad.dev/~crumb-tester'], | 72 | person, rootsite='translations') |
1964 | 102 | ["Crumb Tester", "Translations"]) | 73 | |
1965 | 103 | 74 | ||
1966 | 104 | 75 | class TestTranslationGroupsBreadcrumbs(BaseBreadcrumbTestCase): | |
1959 | 105 | class TestTranslationGroupsBreadcrumbs(BaseTranslationsBreadcrumbTestCase): | ||
1967 | 106 | 76 | ||
1968 | 107 | def test_translationgroupset(self): | 77 | def test_translationgroupset(self): |
1969 | 108 | group_set = getUtility(ITranslationGroupSet) | 78 | group_set = getUtility(ITranslationGroupSet) |
1976 | 109 | url = canonical_url(group_set, rootsite='translations') | 79 | self.assertBreadcrumbs( |
1977 | 110 | self._testContextBreadcrumbs( | 80 | [("Translation groups", 'http://translations.launchpad.dev/+groups')], |
1978 | 111 | [group_set], | 81 | group_set, rootsite='translations') |
1973 | 112 | ['http://translations.launchpad.dev/+groups'], | ||
1974 | 113 | ['Translation groups'], | ||
1975 | 114 | url=url) | ||
1979 | 115 | 82 | ||
1980 | 116 | def test_translationgroup(self): | 83 | def test_translationgroup(self): |
1981 | 117 | group_set = getUtility(ITranslationGroupSet) | ||
1982 | 118 | group = self.factory.makeTranslationGroup( | 84 | group = self.factory.makeTranslationGroup( |
1983 | 119 | name='test-translators', title='Test translators') | 85 | name='test-translators', title='Test translators') |
1992 | 120 | self._testContextBreadcrumbs( | 86 | self.assertBreadcrumbs( |
1993 | 121 | [group_set, group], | 87 | [("Translation groups", 'http://translations.launchpad.dev/+groups'), |
1994 | 122 | ["http://translations.launchpad.dev/+groups", | 88 | ("Test translators", |
1995 | 123 | "http://translations.launchpad.dev/+groups/test-translators"], | 89 | 'http://translations.launchpad.dev/+groups/test-translators')], |
1996 | 124 | ["Translation groups", "Test translators"]) | 90 | group, rootsite='translations') |
1997 | 125 | 91 | ||
1998 | 126 | 92 | ||
1999 | 127 | class TestSeriesLanguageBreadcrumbs(BaseTranslationsBreadcrumbTestCase): | 93 | class TestSeriesLanguageBreadcrumbs(BaseBreadcrumbTestCase): |
2000 | 94 | |||
2001 | 128 | def setUp(self): | 95 | def setUp(self): |
2002 | 129 | super(TestSeriesLanguageBreadcrumbs, self).setUp() | 96 | super(TestSeriesLanguageBreadcrumbs, self).setUp() |
2003 | 130 | self.language = getUtility(ILanguageSet)['sr'] | 97 | self.language = getUtility(ILanguageSet)['sr'] |
2004 | @@ -134,15 +101,18 @@ | |||
2005 | 134 | name='crumb-tester', displayname="Crumb Tester") | 101 | name='crumb-tester', displayname="Crumb Tester") |
2006 | 135 | series = self.factory.makeDistroRelease( | 102 | series = self.factory.makeDistroRelease( |
2007 | 136 | name="test", version="1.0", distribution=distribution) | 103 | name="test", version="1.0", distribution=distribution) |
2008 | 104 | series.hide_all_translations = False | ||
2009 | 137 | serieslanguage = getUtility(IDistroSeriesLanguageSet).getDummy( | 105 | serieslanguage = getUtility(IDistroSeriesLanguageSet).getDummy( |
2010 | 138 | series, self.language) | 106 | series, self.language) |
2018 | 139 | self._testContextBreadcrumbs( | 107 | |
2019 | 140 | [distribution, series, serieslanguage], | 108 | self.assertBreadcrumbs( |
2020 | 141 | ["http://launchpad.dev/crumb-tester", | 109 | [("Crumb Tester", "http://launchpad.dev/crumb-tester"), |
2021 | 142 | "http://launchpad.dev/crumb-tester/test", | 110 | ("Test (1.0)", "http://launchpad.dev/crumb-tester/test"), |
2022 | 143 | "http://translations.launchpad.dev/crumb-tester/test", | 111 | ("Translations", |
2023 | 144 | "http://translations.launchpad.dev/crumb-tester/test/+lang/sr"], | 112 | "http://translations.launchpad.dev/crumb-tester/test"), |
2024 | 145 | ["Crumb Tester", "Test (1.0)", "Translations", "Serbian (sr)"]) | 113 | ("Serbian (sr)", |
2025 | 114 | "http://translations.launchpad.dev/crumb-tester/test/+lang/sr")], | ||
2026 | 115 | serieslanguage) | ||
2027 | 146 | 116 | ||
2028 | 147 | def test_productserieslanguage(self): | 117 | def test_productserieslanguage(self): |
2029 | 148 | product = self.factory.makeProduct( | 118 | product = self.factory.makeProduct( |
2030 | @@ -151,57 +121,58 @@ | |||
2031 | 151 | name="test", product=product) | 121 | name="test", product=product) |
2032 | 152 | serieslanguage = getUtility(IProductSeriesLanguageSet).getDummy( | 122 | serieslanguage = getUtility(IProductSeriesLanguageSet).getDummy( |
2033 | 153 | series, self.language) | 123 | series, self.language) |
2044 | 154 | self._testContextBreadcrumbs( | 124 | |
2045 | 155 | [product, series, serieslanguage], | 125 | self.assertBreadcrumbs( |
2046 | 156 | ["http://launchpad.dev/crumb-tester", | 126 | [("Crumb Tester", "http://launchpad.dev/crumb-tester"), |
2047 | 157 | "http://launchpad.dev/crumb-tester/test", | 127 | ("Series test", "http://launchpad.dev/crumb-tester/test"), |
2048 | 158 | "http://translations.launchpad.dev/crumb-tester/test", | 128 | ("Translations", |
2049 | 159 | "http://translations.launchpad.dev/crumb-tester/test/+lang/sr"], | 129 | "http://translations.launchpad.dev/crumb-tester/test"), |
2050 | 160 | ["Crumb Tester", "Series test", "Translations", "Serbian (sr)"]) | 130 | ("Serbian (sr)", |
2051 | 161 | 131 | "http://translations.launchpad.dev/crumb-tester/test/+lang/sr")], | |
2052 | 162 | 132 | serieslanguage) | |
2053 | 163 | class TestPOTemplateBreadcrumbs(BaseTranslationsBreadcrumbTestCase): | 133 | |
2054 | 134 | |||
2055 | 135 | class TestPOTemplateBreadcrumbs(BaseBreadcrumbTestCase): | ||
2056 | 164 | def test_potemplate(self): | 136 | def test_potemplate(self): |
2057 | 165 | product = self.factory.makeProduct( | 137 | product = self.factory.makeProduct( |
2059 | 166 | name='crumb-tester', displayname="Crumb Tester") | 138 | name='crumb-tester', displayname="Crumb Tester", |
2060 | 139 | official_rosetta=True) | ||
2061 | 167 | series = self.factory.makeProductSeries( | 140 | series = self.factory.makeProductSeries( |
2062 | 168 | name="test", product=product) | 141 | name="test", product=product) |
2077 | 169 | potemplate = self.factory.makePOTemplate(name="template", | 142 | potemplate = self.factory.makePOTemplate( |
2078 | 170 | productseries=series) | 143 | name="template", productseries=series) |
2079 | 171 | self._testContextBreadcrumbs( | 144 | self.assertBreadcrumbs( |
2080 | 172 | [product, series, potemplate], | 145 | [("Crumb Tester", "http://launchpad.dev/crumb-tester"), |
2081 | 173 | ["http://launchpad.dev/crumb-tester", | 146 | ("Series test", "http://launchpad.dev/crumb-tester/test"), |
2082 | 174 | "http://launchpad.dev/crumb-tester/test", | 147 | ("Translations", |
2083 | 175 | "http://translations.launchpad.dev/crumb-tester/test", | 148 | "http://translations.launchpad.dev/crumb-tester/test"), |
2084 | 176 | "http://translations.launchpad.dev/crumb-tester/test" | 149 | (smartquote('Template "template"'), |
2085 | 177 | "/+pots/template"], | 150 | "http://translations.launchpad.dev/crumb-tester/test/+pots/template")], |
2086 | 178 | ["Crumb Tester", "Series test", "Translations", | 151 | potemplate) |
2087 | 179 | smartquote('Template "template"')]) | 152 | |
2088 | 180 | 153 | ||
2089 | 181 | 154 | class TestPOFileBreadcrumbs(BaseBreadcrumbTestCase): | |
2076 | 182 | class TestPOFileBreadcrumbs(BaseTranslationsBreadcrumbTestCase): | ||
2090 | 183 | 155 | ||
2091 | 184 | def setUp(self): | 156 | def setUp(self): |
2092 | 185 | super(TestPOFileBreadcrumbs, self).setUp() | 157 | super(TestPOFileBreadcrumbs, self).setUp() |
2093 | 186 | self.language = getUtility(ILanguageSet)['eo'] | ||
2094 | 187 | self.product = self.factory.makeProduct( | ||
2095 | 188 | name='crumb-tester', displayname="Crumb Tester") | ||
2096 | 189 | self.series = self.factory.makeProductSeries( | ||
2097 | 190 | name="test", product=self.product) | ||
2098 | 191 | self.potemplate = self.factory.makePOTemplate(self.series, | ||
2099 | 192 | name="test-template") | ||
2100 | 193 | self.pofile = self.factory.makePOFile('eo', self.potemplate) | ||
2101 | 194 | 158 | ||
2102 | 195 | def test_pofiletranslate(self): | 159 | def test_pofiletranslate(self): |
2115 | 196 | self._testContextBreadcrumbs( | 160 | product = self.factory.makeProduct( |
2116 | 197 | [self.product, self.series, self.potemplate, self.pofile], | 161 | name='crumb-tester', displayname="Crumb Tester", |
2117 | 198 | ["http://launchpad.dev/crumb-tester", | 162 | official_rosetta=True) |
2118 | 199 | "http://launchpad.dev/crumb-tester/test", | 163 | series = self.factory.makeProductSeries(name="test", product=product) |
2119 | 200 | "http://translations.launchpad.dev/crumb-tester/test", | 164 | potemplate = self.factory.makePOTemplate(series, name="test-template") |
2120 | 201 | "http://translations.launchpad.dev/crumb-tester/test" | 165 | pofile = self.factory.makePOFile('eo', potemplate) |
2121 | 202 | "/+pots/test-template", | 166 | |
2122 | 203 | "http://translations.launchpad.dev/crumb-tester/test" | 167 | self.assertBreadcrumbs( |
2123 | 204 | "/+pots/test-template/eo", | 168 | [("Crumb Tester", "http://launchpad.dev/crumb-tester"), |
2124 | 205 | ], | 169 | ("Series test", "http://launchpad.dev/crumb-tester/test"), |
2125 | 206 | ["Crumb Tester", "Series test", "Translations", | 170 | ("Translations", |
2126 | 207 | smartquote('Template "test-template"'), "Esperanto (eo)"]) | 171 | "http://translations.launchpad.dev/crumb-tester/test"), |
2127 | 172 | (smartquote('Template "test-template"'), | ||
2128 | 173 | "http://translations.launchpad.dev/crumb-tester/test" | ||
2129 | 174 | "/+pots/test-template"), | ||
2130 | 175 | ("Esperanto (eo)", | ||
2131 | 176 | "http://translations.launchpad.dev/crumb-tester/test" | ||
2132 | 177 | "/+pots/test-template/eo")], | ||
2133 | 178 | pofile) | ||
2134 | 208 | 179 | ||
2135 | === modified file 'scripts/ftpmaster-tools/buildd-mass-retry.py' | |||
2136 | --- scripts/ftpmaster-tools/buildd-mass-retry.py 2010-04-22 17:30:35 +0000 | |||
2137 | +++ scripts/ftpmaster-tools/buildd-mass-retry.py 2010-04-29 11:26:16 +0000 | |||
2138 | @@ -121,6 +121,12 @@ | |||
2139 | 121 | build_state=target_state, pocket=pocket) | 121 | build_state=target_state, pocket=pocket) |
2140 | 122 | 122 | ||
2141 | 123 | for build in target_builds: | 123 | for build in target_builds: |
2142 | 124 | # Skip builds for superseded sources; they won't ever | ||
2143 | 125 | # actually build. | ||
2144 | 126 | if not build.current_source_publication: | ||
2145 | 127 | log.debug( | ||
2146 | 128 | 'Skipping superseded %s (%s)' % (build.title, build.id)) | ||
2147 | 129 | continue | ||
2148 | 124 | 130 | ||
2149 | 125 | if not build.can_be_retried: | 131 | if not build.can_be_retried: |
2150 | 126 | log.warn('Can not retry %s (%s)' % (build.title, build.id)) | 132 | log.warn('Can not retry %s (%s)' % (build.title, build.id)) |
2151 | 127 | 133 | ||
2152 | === modified file 'utilities/sourcedeps.conf' | |||
2153 | --- utilities/sourcedeps.conf 2010-04-27 01:35:56 +0000 | |||
2154 | +++ utilities/sourcedeps.conf 2010-04-29 11:26:16 +0000 | |||
2155 | @@ -5,7 +5,12 @@ | |||
2156 | 5 | bzr-svn lp:~launchpad-pqm/bzr-svn/devel;revno=2708 | 5 | bzr-svn lp:~launchpad-pqm/bzr-svn/devel;revno=2708 |
2157 | 6 | cscvs lp:~launchpad-pqm/launchpad-cscvs/devel;revno=432 | 6 | cscvs lp:~launchpad-pqm/launchpad-cscvs/devel;revno=432 |
2158 | 7 | dulwich lp:~launchpad-pqm/dulwich/devel;revno=418 | 7 | dulwich lp:~launchpad-pqm/dulwich/devel;revno=418 |
2159 | 8 | <<<<<<< TREE | ||
2160 | 8 | loggerhead lp:~launchpad-pqm/loggerhead/devel;revno=174 | 9 | loggerhead lp:~launchpad-pqm/loggerhead/devel;revno=174 |
2161 | 10 | ======= | ||
2162 | 11 | launchpad-loggerhead lp:~launchpad-pqm/launchpad-loggerhead/devel;revno=54 | ||
2163 | 12 | loggerhead lp:~launchpad-pqm/loggerhead/devel;revno=176 | ||
2164 | 13 | >>>>>>> MERGE-SOURCE | ||
2165 | 9 | lpreview lp:~launchpad-pqm/bzr-lpreview/devel;revno=23 | 14 | lpreview lp:~launchpad-pqm/bzr-lpreview/devel;revno=23 |
2166 | 10 | mailman lp:~launchpad-pqm/mailman/2.1;revno=976 | 15 | mailman lp:~launchpad-pqm/mailman/2.1;revno=976 |
2167 | 11 | old_xmlplus lp:~launchpad-pqm/dtdparser/trunk;revno=4 | 16 | old_xmlplus lp:~launchpad-pqm/dtdparser/trunk;revno=4 |
<rockstar> bigjools, so, I don't feel like I know enough about Soyuz to really grok what I'm trying to review here (and there's no movie/screen shot). How can I get to the view? launchpad. dev/~cprov/ +archive/ ppa
<bigjools> rockstar: go to http://
<bigjools> sorry I assume too much :)
<rockstar> bigjools, :) It's okay.
<rockstar> bigjools, so, once a PPA is requested for deletion, can it be uploaded to?
<rockstar> Also, "Delete PPA" shouldn't show if the deletion request has already been made.
<bigjools> OTP, will type when I can :)
<rockstar> I also wonder if a red notification for "Deletion in progress" is probably better, since it's more likely to grab your attention.
<rockstar> Although the red often means "It's broken. It's broken! It's BROKEN!!!!"
So, the more I think about it, I think blue is the wrong notification color. We need some way of saying "THIS PPA IS GOING AWAY." I'd suggest we make it a red box.