Merge lp:~julian-edwards/launchpad/ppa-deletion-ui into lp:launchpad/db-devel

Proposed by Julian Edwards
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
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.

To post a comment you must log in.
Revision history for this message
Paul Hummer (rockstar) wrote :

<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?
<bigjools> rockstar: go to http://launchpad.dev/~cprov/+archive/ppa
<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.

review: Needs Information (ui*)
Revision history for this message
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

Revision history for this message
Michael Nelson (michael.nelson) wrote :
Download full text (4.4 KiB)

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://launchpad.dev/~cprov/+archive/ppa

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 ArchiveIndexActionsMenu?
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_subscribers' on ArchiveIndexActionsMenu.links)
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://launchpad.dev/~cprov/+archive/ppa/+packages?field.status_filter=

{{{
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...

Read more...

review: Approve (ui)
Revision history for this message
Julian Edwards (julian-edwards) wrote :

Ok guys, how is it looking now?

Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (13.2 KiB)

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/suggestions, but
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/soyuz/browser/archive.py'
> --- lib/lp/soyuz/browser/archive.py 2010-03-12 06:23:04 +0000
> +++ lib/lp/soyuz/browser/archive.py 2010-03-26 13:59:31 +0000
> @@ -404,13 +405,32 @@
> archive = view.context
> if not archive.private:
> link.enabled = False
> + if archive.status in (
> + ArchiveStatus.DELETING, ArchiveStatus.DELETED):
> + 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_with_permission('launchpad.Edit')
> 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.DELETING, ArchiveStatus.DELETED):
> + link.enabled = False
> + return link
> +
> + @enabled_with_permission('launchpad.Edit')
> + def delete_ppa(self):
> + text = 'Delete PPA'
> + link = Link('+delete', text, icon='trash-icon')
> + view = self.context
> + if view.context.status in (
> + ArchiveStatus.DELETING, ArchiveStatus.DELETED):
> + link.enabled = False
> + return link
>
> def builds(self):
> text = 'View all builds'
> @@ -442,6 +462,10 @@
> # archives without any sources.
> if self.context.is_copy or not self.context.has_sources:
> link.enabled = False
> + view = self.context
> + if view.context.status in (
> + ArchiveStatus.DELETING, ArchiveStatus.DELETED):
> + link.enabled = False
> return link
>
> @enabled_with_permission('launchpad.AnyPerson')
> @@ -458,7 +482,12 @@
> @enabled_with_permission('launchpad.Edit')
> def edit_dependencies(self):
> text = 'Edit PPA dependencies'
> - return Link('+edit-dependencies', text, icon='edit')
> + link = Link('+edit-dependencies', text, icon='edit')
> + view = self.context
> + if view.context.status in (
> + ArchiveStatus.DELETING, ArchiveStatus.DELETED):
> + 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_alive(archive):
        """Return True if the given archive has not been deleted."""
        return archive.staus...

review: Needs Fixing
Revision history for this message
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/suggestions, but
> 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/soyuz/browser/archive.py'
> > --- lib/lp/soyuz/browser/archive.py 2010-03-12 06:23:04 +0000
> > +++ lib/lp/soyuz/browser/archive.py 2010-03-26 13:59:31 +0000
> > @@ -404,13 +405,32 @@
> > archive = view.context
> > if not archive.private:
> > link.enabled = False
> > + if archive.status in (
> > + ArchiveStatus.DELETING, ArchiveStatus.DELETED):
> > + 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_deleted(self):
> > + return self.context.status != ArchiveStatus.DELETING
>
> 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_browser.title
> > + 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_WHITESPACE flag) you can add line breaks anywhere in
> 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="href context/fmt:url">Cancel</a>
>
> 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

Revision history for this message
Guilherme Salgado (salgado) wrote :

 review approve code
 status approved

review: Approve (code)
Revision history for this message
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added directory 'lib/canonical/launchpad/apidoc'
=== renamed directory 'lib/canonical/launchpad/apidoc' => 'lib/canonical/launchpad/apidoc.moved'
=== modified file 'lib/canonical/launchpad/doc/canonical_url_examples.txt'
--- lib/canonical/launchpad/doc/canonical_url_examples.txt 2010-04-16 15:06:55 +0000
+++ lib/canonical/launchpad/doc/canonical_url_examples.txt 2010-04-29 11:26:16 +0000
@@ -275,7 +275,7 @@
275 >>> bug_comment = BugComment(275 >>> bug_comment = BugComment(
276 ... 1, bug_one.initial_message, bugtask_one, True)276 ... 1, bug_one.initial_message, bugtask_one, True)
277 >>> canonical_url(bug_comment)277 >>> canonical_url(bug_comment)
278 u'http://launchpad.dev/firefox/+bug/1/comments/1'278 u'http://bugs.launchpad.dev/firefox/+bug/1/comments/1'
279279
280An IBugNomination.280An IBugNomination.
281281
282282
=== modified file 'lib/canonical/launchpad/icing/style-3-0.css.in'
--- lib/canonical/launchpad/icing/style-3-0.css.in 2010-04-27 12:38:38 +0000
+++ lib/canonical/launchpad/icing/style-3-0.css.in 2010-04-29 11:26:16 +0000
@@ -1656,7 +1656,9 @@
1656.distromirrorstatusONEHOURBEHIND,1656.distromirrorstatusONEHOURBEHIND,
1657.distromirrorstatusTWOHOURSBEHIND,1657.distromirrorstatusTWOHOURSBEHIND,
1658.distromirrorstatusFOURHOURSBEHIND,1658.distromirrorstatusFOURHOURSBEHIND,
1659.distromirrorstatusSIXHOURSBEHIND,1659.distromirrorstatusSIXHOURSBEHIND {
1660 color: green;
1661 }
1660.distromirrorstatusONEDAYBEHIND {1662.distromirrorstatusONEDAYBEHIND {
1661 color: #f60;1663 color: #f60;
1662 }1664 }
16631665
=== modified file 'lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py'
--- lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py 2009-09-21 16:21:50 +0000
+++ lib/canonical/launchpad/webapp/tests/test_breadcrumbs.py 2010-04-29 11:26:16 +0000
@@ -13,9 +13,8 @@
13from canonical.launchpad.webapp.interfaces import ICanonicalUrlData13from canonical.launchpad.webapp.interfaces import ICanonicalUrlData
14from canonical.launchpad.webapp.publisher import canonical_url14from canonical.launchpad.webapp.publisher import canonical_url
15from canonical.launchpad.webapp.servers import LaunchpadTestRequest15from canonical.launchpad.webapp.servers import LaunchpadTestRequest
16from canonical.launchpad.webapp.tests.breadcrumbs import (
17 BaseBreadcrumbTestCase)
18from lp.testing import login, TestCase16from lp.testing import login, TestCase
17from lp.testing.breadcrumbs import BaseBreadcrumbTestCase
1918
2019
21class Cookbook:20class Cookbook:
@@ -52,19 +51,17 @@
52 self.product_url = canonical_url(self.product)51 self.product_url = canonical_url(self.product)
5352
54 def test_default_page(self):53 def test_default_page(self):
55 urls = self._getBreadcrumbsURLs(54 self.assertBreadcrumbUrls([self.product_url], self.product)
56 self.product_url, [self.root, self.product])
57 self.assertEquals(urls, [self.product_url])
5855
59 def test_non_default_page(self):56 def test_non_default_page(self):
57 crumbs = self.getBreadcrumbsForObject(self.product, '+download')
60 downloads_url = "%s/+download" % self.product_url58 downloads_url = "%s/+download" % self.product_url
61 urls = self._getBreadcrumbsURLs(59 self.assertEquals(
62 downloads_url, [self.root, self.product])60 [self.product_url, downloads_url],
63 self.assertEquals(urls, [self.product_url, downloads_url])61 [crumb.url for crumb in crumbs])
64 texts = self._getBreadcrumbsTexts(62 self.assertEquals(
65 downloads_url, [self.root, self.product])63 '%s project files' % self.product.displayname,
66 self.assertEquals(texts[-1],64 crumbs[-1].text)
67 '%s project files' % self.product.displayname)
6865
69 def test_zope_i18n_Messages_are_interpolated(self):66 def test_zope_i18n_Messages_are_interpolated(self):
70 # Views can use zope.i18nmessageid.Message as their title when they67 # Views can use zope.i18nmessageid.Message as their title when they
@@ -108,31 +105,25 @@
108 self.package_bugtask_url = canonical_url(self.package_bugtask)105 self.package_bugtask_url = canonical_url(self.package_bugtask)
109106
110 def test_root_on_mainsite(self):107 def test_root_on_mainsite(self):
111 urls = self._getBreadcrumbsURLs('http://launchpad.dev/', [self.root])108 crumbs = self.getBreadcrumbsForUrl('http://launchpad.dev/')
112 self.assertEquals(urls, [])109 self.assertEquals(crumbs, [])
113110
114 def test_product_on_mainsite(self):111 def test_product_on_mainsite(self):
115 urls = self._getBreadcrumbsURLs(112 self.assertBreadcrumbUrls([self.product_url], self.product)
116 self.product_url, [self.root, self.product])
117 self.assertEquals(urls, [self.product_url])
118113
119 def test_root_on_vhost(self):114 def test_root_on_vhost(self):
120 urls = self._getBreadcrumbsURLs(115 crumbs = self.getBreadcrumbsForUrl('http://bugs.launchpad.dev/')
121 'http://bugs.launchpad.dev/', [self.root])116 self.assertEquals(crumbs, [])
122 self.assertEquals(urls, [])
123117
124 def test_product_on_vhost(self):118 def test_product_on_vhost(self):
125 urls = self._getBreadcrumbsURLs(119 self.assertBreadcrumbUrls(
126 self.product_bugs_url, [self.root, self.product])120 [self.product_url, self.product_bugs_url],
127 self.assertEquals(urls, [self.product_url, self.product_bugs_url])121 self.product, rootsite='bugs')
128122
129 def test_product_bugtask(self):123 def test_product_bugtask(self):
130 urls = self._getBreadcrumbsURLs(124 self.assertBreadcrumbUrls(
131 self.product_bugtask_url,125 [self.product_url, self.product_bugs_url, self.product_bugtask_url],
132 [self.root, self.product, self.product_bugtask])126 self.product_bugtask)
133 self.assertEquals(
134 urls, [self.product_url, self.product_bugs_url,
135 self.product_bugtask_url])
136127
137 def test_package_bugtask(self):128 def test_package_bugtask(self):
138 target = self.package_bugtask.target129 target = self.package_bugtask.target
@@ -140,14 +131,11 @@
140 distroseries_url = canonical_url(target.distroseries)131 distroseries_url = canonical_url(target.distroseries)
141 package_url = canonical_url(target)132 package_url = canonical_url(target)
142 package_bugs_url = canonical_url(target, rootsite='bugs')133 package_bugs_url = canonical_url(target, rootsite='bugs')
143 urls = self._getBreadcrumbsURLs(134
144 self.package_bugtask_url,135 self.assertBreadcrumbUrls(
145 [self.root, target.distribution, target.distroseries, target,
146 self.package_bugtask])
147 self.assertEquals(
148 urls,
149 [distro_url, distroseries_url, package_url, package_bugs_url,136 [distro_url, distroseries_url, package_url, package_bugs_url,
150 self.package_bugtask_url])137 self.package_bugtask_url],
138 self.package_bugtask)
151139
152140
153def test_suite():141def test_suite():
154142
=== modified file 'lib/lp/answers/browser/tests/test_breadcrumbs.py'
--- lib/lp/answers/browser/tests/test_breadcrumbs.py 2010-02-17 11:13:06 +0000
+++ lib/lp/answers/browser/tests/test_breadcrumbs.py 2010-04-29 11:26:16 +0000
@@ -6,10 +6,9 @@
6import unittest6import unittest
77
8from canonical.launchpad.webapp.publisher import canonical_url8from canonical.launchpad.webapp.publisher import canonical_url
9from canonical.launchpad.webapp.tests.breadcrumbs import (
10 BaseBreadcrumbTestCase)
119
12from lp.testing import login_person10from lp.testing import login_person
11from lp.testing.breadcrumbs import BaseBreadcrumbTestCase
1312
1413
15class TestQuestionTargetProjectAndPersonBreadcrumbOnAnswersVHost(14class TestQuestionTargetProjectAndPersonBreadcrumbOnAnswersVHost(
@@ -36,22 +35,19 @@
36 self.project, rootsite='answers')35 self.project, rootsite='answers')
3736
38 def test_product(self):37 def test_product(self):
39 crumbs = self._getBreadcrumbs(38 crumbs = self.getBreadcrumbsForObject(self.product, rootsite='answers')
40 self.product_questions_url, [self.root, self.product])
41 last_crumb = crumbs[-1]39 last_crumb = crumbs[-1]
42 self.assertEquals(last_crumb.url, self.product_questions_url)40 self.assertEquals(last_crumb.url, self.product_questions_url)
43 self.assertEquals(last_crumb.text, 'Questions')41 self.assertEquals(last_crumb.text, 'Questions')
4442
45 def test_project(self):43 def test_project(self):
46 crumbs = self._getBreadcrumbs(44 crumbs = self.getBreadcrumbsForObject(self.project, rootsite='answers')
47 self.project_questions_url, [self.root, self.project])
48 last_crumb = crumbs[-1]45 last_crumb = crumbs[-1]
49 self.assertEquals(last_crumb.url, self.project_questions_url)46 self.assertEquals(last_crumb.url, self.project_questions_url)
50 self.assertEquals(last_crumb.text, 'Questions')47 self.assertEquals(last_crumb.text, 'Questions')
5148
52 def test_person(self):49 def test_person(self):
53 crumbs = self._getBreadcrumbs(50 crumbs = self.getBreadcrumbsForObject(self.person, rootsite='answers')
54 self.person_questions_url, [self.root, self.person])
55 last_crumb = crumbs[-1]51 last_crumb = crumbs[-1]
56 self.assertEquals(last_crumb.url, self.person_questions_url)52 self.assertEquals(last_crumb.url, self.person_questions_url)
57 self.assertEquals(last_crumb.text, 'Questions')53 self.assertEquals(last_crumb.text, 'Questions')
@@ -69,16 +65,14 @@
69 self.question = self.factory.makeQuestion(65 self.question = self.factory.makeQuestion(
70 target=self.product, title='Seeds are hard to chew')66 target=self.product, title='Seeds are hard to chew')
71 self.question_url = canonical_url(self.question, rootsite='answers')67 self.question_url = canonical_url(self.question, rootsite='answers')
72 crumbs = self._getBreadcrumbs(68 crumbs = self.getBreadcrumbsForObject(self.question)
73 self.question_url, [self.root, self.product, self.question])
74 last_crumb = crumbs[-1]69 last_crumb = crumbs[-1]
75 self.assertEquals(last_crumb.text, 'Question #%d' % self.question.id)70 self.assertEquals(last_crumb.text, 'Question #%d' % self.question.id)
7671
77 def test_faq(self):72 def test_faq(self):
78 self.faq = self.factory.makeFAQ(target=self.product, title='Seedless')73 self.faq = self.factory.makeFAQ(target=self.product, title='Seedless')
79 self.faq_url = canonical_url(self.faq, rootsite='answers')74 self.faq_url = canonical_url(self.faq, rootsite='answers')
80 crumbs = self._getBreadcrumbs(75 crumbs = self.getBreadcrumbsForObject(self.faq)
81 self.faq_url, [self.root, self.product, self.faq])
82 last_crumb = crumbs[-1]76 last_crumb = crumbs[-1]
83 self.assertEquals(last_crumb.text, 'FAQ #%d' % self.faq.id)77 self.assertEquals(last_crumb.text, 'FAQ #%d' % self.faq.id)
8478
8579
=== modified file 'lib/lp/blueprints/browser/tests/test_breadcrumbs.py'
--- lib/lp/blueprints/browser/tests/test_breadcrumbs.py 2009-09-22 15:02:41 +0000
+++ lib/lp/blueprints/browser/tests/test_breadcrumbs.py 2010-04-29 11:26:16 +0000
@@ -6,8 +6,8 @@
6import unittest6import unittest
77
8from canonical.launchpad.webapp.publisher import canonical_url8from canonical.launchpad.webapp.publisher import canonical_url
9from canonical.launchpad.webapp.tests.breadcrumbs import (9
10 BaseBreadcrumbTestCase)10from lp.testing.breadcrumbs import BaseBreadcrumbTestCase
1111
1212
13class TestHasSpecificationsBreadcrumbOnBlueprintsVHost(13class TestHasSpecificationsBreadcrumbOnBlueprintsVHost(
@@ -25,15 +25,13 @@
25 self.product, rootsite='blueprints')25 self.product, rootsite='blueprints')
2626
27 def test_product(self):27 def test_product(self):
28 crumbs = self._getBreadcrumbs(28 crumbs = self.getBreadcrumbsForObject(self.product, rootsite='blueprints')
29 self.product_specs_url, [self.root, self.product])
30 last_crumb = crumbs[-1]29 last_crumb = crumbs[-1]
31 self.assertEquals(last_crumb.url, self.product_specs_url)30 self.assertEquals(last_crumb.url, self.product_specs_url)
32 self.assertEquals(last_crumb.text, 'Blueprints')31 self.assertEquals(last_crumb.text, 'Blueprints')
3332
34 def test_person(self):33 def test_person(self):
35 crumbs = self._getBreadcrumbs(34 crumbs = self.getBreadcrumbsForObject(self.person, rootsite='blueprints')
36 self.person_specs_url, [self.root, self.person])
37 last_crumb = crumbs[-1]35 last_crumb = crumbs[-1]
38 self.assertEquals(last_crumb.url, self.person_specs_url)36 self.assertEquals(last_crumb.url, self.person_specs_url)
39 self.assertEquals(last_crumb.text, 'Blueprints')37 self.assertEquals(last_crumb.text, 'Blueprints')
@@ -52,9 +50,7 @@
52 self.specification, rootsite='blueprints')50 self.specification, rootsite='blueprints')
5351
54 def test_specification(self):52 def test_specification(self):
55 crumbs = self._getBreadcrumbs(53 crumbs = self.getBreadcrumbsForObject(self.specification)
56 self.specification_url,
57 [self.root, self.product, self.specification])
58 last_crumb = crumbs[-1]54 last_crumb = crumbs[-1]
59 self.assertEquals(last_crumb.url, self.specification_url)55 self.assertEquals(last_crumb.url, self.specification_url)
60 self.assertEquals(56 self.assertEquals(
6157
=== modified file 'lib/lp/bugs/browser/configure.zcml'
--- lib/lp/bugs/browser/configure.zcml 2010-04-15 15:14:21 +0000
+++ lib/lp/bugs/browser/configure.zcml 2010-04-29 11:26:16 +0000
@@ -163,7 +163,8 @@
163 <browser:url163 <browser:url
164 for="canonical.launchpad.interfaces.IBugComment"164 for="canonical.launchpad.interfaces.IBugComment"
165 path_expression="string:comments/${index}"165 path_expression="string:comments/${index}"
166 attribute_to_parent="bugtask"/>166 attribute_to_parent="bugtask"
167 rootsite="bugs"/>
167 <browser:page168 <browser:page
168 for="canonical.launchpad.interfaces.IBugComment"169 for="canonical.launchpad.interfaces.IBugComment"
169 name="+index"170 name="+index"
170171
=== modified file 'lib/lp/bugs/browser/tests/test_breadcrumbs.py'
--- lib/lp/bugs/browser/tests/test_breadcrumbs.py 2010-03-22 18:39:24 +0000
+++ lib/lp/bugs/browser/tests/test_breadcrumbs.py 2010-04-29 11:26:16 +0000
@@ -8,10 +8,10 @@
8from zope.component import getUtility8from zope.component import getUtility
99
10from canonical.launchpad.webapp.publisher import canonical_url10from canonical.launchpad.webapp.publisher import canonical_url
11from canonical.launchpad.webapp.tests.breadcrumbs import (11
12 BaseBreadcrumbTestCase)
13from lp.bugs.interfaces.bugtracker import IBugTrackerSet12from lp.bugs.interfaces.bugtracker import IBugTrackerSet
14from lp.testing import ANONYMOUS, login13from lp.testing import login_person
14from lp.testing.breadcrumbs import BaseBreadcrumbTestCase
1515
1616
17class TestBugTaskBreadcrumb(BaseBreadcrumbTestCase):17class TestBugTaskBreadcrumb(BaseBreadcrumbTestCase):
@@ -21,54 +21,35 @@
21 product = self.factory.makeProduct(21 product = self.factory.makeProduct(
22 name='crumb-tester', displayname="Crumb Tester")22 name='crumb-tester', displayname="Crumb Tester")
23 self.bug = self.factory.makeBug(product=product)23 self.bug = self.factory.makeBug(product=product)
24 self.bugtask_url = canonical_url(24 self.bugtask = self.bug.default_bugtask
25 self.bug.default_bugtask, rootsite='bugs')25 self.bugtask_url = canonical_url(self.bugtask, rootsite='bugs')
26 self.traversed_objects = [
27 self.root, product, self.bug.default_bugtask]
2826
29 def test_bugtask(self):27 def test_bugtask(self):
30 urls = self._getBreadcrumbsURLs(28 crumbs = self.getBreadcrumbsForObject(self.bugtask)
31 self.bugtask_url, self.traversed_objects)29 last_crumb = crumbs[-1]
32 self.assertEquals(urls[-1], self.bugtask_url)30 self.assertEquals(self.bugtask_url, last_crumb.url)
33 texts = self._getBreadcrumbsTexts(31 self.assertEquals("Bug #%d" % self.bug.id, last_crumb.text)
34 self.bugtask_url, self.traversed_objects)
35 self.assertEquals(texts[-1], "Bug #%d" % self.bug.id)
3632
37 def test_bugtask_child(self):33 def test_bugtask_child(self):
38 url = canonical_url(34 crumbs = self.getBreadcrumbsForObject(self.bugtask, view_name='+activity')
39 self.bug.default_bugtask, rootsite='bugs', view_name='+activity')35 self.assertEquals(crumbs[-1].url, "%s/+activity" % self.bugtask_url)
40 urls = self._getBreadcrumbsURLs(url, self.traversed_objects)36 self.assertEquals(crumbs[-2].url, self.bugtask_url)
41 self.assertEquals(urls[-1], "%s/+activity" % self.bugtask_url)37 self.assertEquals(crumbs[-2].text, "Bug #%d" % self.bug.id)
42 self.assertEquals(urls[-2], self.bugtask_url)
43 texts = self._getBreadcrumbsTexts(url, self.traversed_objects)
44 self.assertEquals(texts[-2], "Bug #%d" % self.bug.id)
4538
46 def test_bugtask_comment(self):39 def test_bugtask_comment(self):
47 login('foo.bar@canonical.com')40 login_person(self.bug.owner)
48 comment = self.factory.makeBugComment(41 comment = self.factory.makeBugComment(
49 bug=self.bug, owner=self.bug.owner,42 bug=self.bug, owner=self.bug.owner,
50 subject="test comment subject", body="test comment body")43 subject="test comment subject", body="test comment body")
51 url = canonical_url(comment, rootsite='bugs')44 expected_breadcrumbs = [
52 urls = self._getBreadcrumbsURLs(url, self.traversed_objects)45 ('Crumb Tester', 'http://launchpad.dev/crumb-tester'),
53 texts = self._getBreadcrumbsTexts(url, self.traversed_objects)46 ('Bugs', 'http://bugs.launchpad.dev/crumb-tester'),
54 self.assertEquals(url, "%s/comments/1" % self.bugtask_url)47 ('Bug #%s' % self.bug.id,
55 self.assertEquals(urls[-1], "%s" % self.bugtask_url)48 'http://bugs.launchpad.dev/crumb-tester/+bug/%s' % self.bug.id),
56 self.assertEquals(texts[-1], "Bug #%d" % self.bug.id)49 ('Comment #1',
5750 'http://bugs.launchpad.dev/crumb-tester/+bug/%s/comments/1' % self.bug.id),
58 def test_bugtask_private_bug(self):51 ]
59 # A breadcrumb is not generated for a bug that the user does52 self.assertBreadcrumbs(expected_breadcrumbs, comment)
60 # not have permission to view.
61 login('foo.bar@canonical.com')
62 self.bug.setPrivate(True, self.bug.owner)
63 login(ANONYMOUS)
64 url = canonical_url(self.bug.default_bugtask, rootsite='bugs')
65 self.assertEquals(
66 ['http://launchpad.dev/crumb-tester',
67 'http://bugs.launchpad.dev/crumb-tester'],
68 self._getBreadcrumbsURLs(url, self.traversed_objects))
69 self.assertEquals(
70 ["Crumb Tester", "Bugs"],
71 self._getBreadcrumbsTexts(url, self.traversed_objects))
7253
7354
74class TestBugTrackerBreadcrumbs(BaseBreadcrumbTestCase):55class TestBugTrackerBreadcrumbs(BaseBreadcrumbTestCase):
@@ -84,28 +65,19 @@
8465
85 def test_bug_tracker_set(self):66 def test_bug_tracker_set(self):
86 # Check TestBugTrackerSetBreadcrumb.67 # Check TestBugTrackerSetBreadcrumb.
87 traversed_objects = [68 expected_breadcrumbs = [
88 self.root, self.bug_tracker_set]69 ('Bug trackers', self.bug_tracker_set_url),
89 urls = self._getBreadcrumbsURLs(70 ]
90 self.bug_tracker_set_url, traversed_objects)71 self.assertBreadcrumbs(expected_breadcrumbs, self.bug_tracker_set)
91 self.assertEquals(self.bug_tracker_set_url, urls[-1])
92 texts = self._getBreadcrumbsTexts(
93 self.bug_tracker_set_url, traversed_objects)
94 self.assertEquals("Bug trackers", texts[-1])
9572
96 def test_bug_tracker(self):73 def test_bug_tracker(self):
97 # Check TestBugTrackerBreadcrumb (and74 # Check TestBugTrackerBreadcrumb (and
98 # TestBugTrackerSetBreadcrumb).75 # TestBugTrackerSetBreadcrumb).
99 traversed_objects = [76 expected_breadcrumbs = [
100 self.root, self.bug_tracker_set, self.bug_tracker]77 ('Bug trackers', self.bug_tracker_set_url),
101 urls = self._getBreadcrumbsURLs(78 (self.bug_tracker.title, self.bug_tracker_url),
102 self.bug_tracker_url, traversed_objects)79 ]
103 self.assertEquals(self.bug_tracker_url, urls[-1])80 self.assertBreadcrumbs(expected_breadcrumbs, self.bug_tracker)
104 self.assertEquals(self.bug_tracker_set_url, urls[-2])
105 texts = self._getBreadcrumbsTexts(
106 self.bug_tracker_url, traversed_objects)
107 self.assertEquals(self.bug_tracker.title, texts[-1])
108 self.assertEquals("Bug trackers", texts[-2])
10981
11082
111def test_suite():83def test_suite():
11284
=== modified file 'lib/lp/code/browser/codeimportmachine.py'
--- lib/lp/code/browser/codeimportmachine.py 2009-08-24 20:28:33 +0000
+++ lib/lp/code/browser/codeimportmachine.py 2010-04-29 11:26:16 +0000
@@ -6,6 +6,7 @@
6__metaclass__ = type6__metaclass__ = type
77
8__all__ = [8__all__ = [
9 'CodeImportMachineBreadcrumb',
9 'CodeImportMachineSetBreadcrumb',10 'CodeImportMachineSetBreadcrumb',
10 'CodeImportMachineSetNavigation',11 'CodeImportMachineSetNavigation',
11 'CodeImportMachineSetView',12 'CodeImportMachineSetView',
@@ -30,6 +31,14 @@
30from lazr.delegates import delegates31from lazr.delegates import delegates
3132
3233
34class CodeImportMachineBreadcrumb(Breadcrumb):
35 """An `IBreadcrumb` that uses the machines hostname."""
36
37 @property
38 def text(self):
39 return self.context.hostname
40
41
33class CodeImportMachineSetNavigation(Navigation):42class CodeImportMachineSetNavigation(Navigation):
34 """Navigation methods for ICodeImportMachineSet."""43 """Navigation methods for ICodeImportMachineSet."""
35 usedfor = ICodeImportMachineSet44 usedfor = ICodeImportMachineSet
3645
=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml 2010-04-19 03:44:27 +0000
+++ lib/lp/code/browser/configure.zcml 2010-04-29 11:26:16 +0000
@@ -1055,6 +1055,19 @@
1055 for="lp.code.interfaces.branchmergeproposal.IBranchMergeProposal"1055 for="lp.code.interfaces.branchmergeproposal.IBranchMergeProposal"
1056 factory="lp.code.browser.branchmergeproposal.BranchMergeProposalBreadcrumb"1056 factory="lp.code.browser.branchmergeproposal.BranchMergeProposalBreadcrumb"
1057 permission="zope.Public"/>1057 permission="zope.Public"/>
1058
1059 <adapter
1060 provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
1061 for="lp.code.interfaces.codeimportmachine.ICodeImportMachine"
1062 factory="lp.code.browser.codeimportmachine.CodeImportMachineBreadcrumb"
1063 permission="zope.Public"/>
1064
1065 <adapter
1066 provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
1067 for="lp.code.interfaces.codeimportmachine.ICodeImportMachineSet"
1068 factory="lp.code.browser.codeimportmachine.CodeImportMachineSetBreadcrumb"
1069 permission="zope.Public"/>
1070
1058 <adapter1071 <adapter
1059 factory="lp.code.browser.branchmergeproposal.PreviewDiffHTMLRepresentation"1072 factory="lp.code.browser.branchmergeproposal.PreviewDiffHTMLRepresentation"
1060 name="lazr.restful.EntryResource"/>1073 name="lazr.restful.EntryResource"/>
10611074
=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
--- lib/lp/code/browser/sourcepackagerecipe.py 2010-04-23 03:30:30 +0000
+++ lib/lp/code/browser/sourcepackagerecipe.py 2010-04-29 11:26:16 +0000
@@ -45,6 +45,7 @@
45 IArchiveSet)45 IArchiveSet)
46from lp.registry.interfaces.distroseries import IDistroSeriesSet46from lp.registry.interfaces.distroseries import IDistroSeriesSet
47from lp.registry.interfaces.pocket import PackagePublishingPocket47from lp.registry.interfaces.pocket import PackagePublishingPocket
48from lp.services.job.interfaces.job import JobStatus
4849
4950
50class IRecipesForPerson(Interface):51class IRecipesForPerson(Interface):
@@ -52,8 +53,7 @@
5253
5354
54class RecipesForPersonBreadcrumb(Breadcrumb):55class RecipesForPersonBreadcrumb(Breadcrumb):
55 """A Breadcrumb that will handle the "Recipes" link for recipe breadcrumbs.56 """A Breadcrumb to handle the "Recipes" link for recipe breadcrumbs."""
56 """
5757
58 rootsite = 'code'58 rootsite = 'code'
59 text = 'Recipes'59 text = 'Recipes'
@@ -229,14 +229,26 @@
229229
230 @property230 @property
231 def eta(self):231 def eta(self):
232 """The datetime when the build job is estimated to begin."""232 """The datetime when the build job is estimated to complete.
233
234 This is the BuildQueue.estimated_duration plus the
235 Job.date_started or BuildQueue.getEstimatedJobStartTime.
236 """
233 if self.context.buildqueue_record is None:237 if self.context.buildqueue_record is None:
234 return None238 return None
235 return self.context.buildqueue_record.getEstimatedJobStartTime()239 queue_record = self.context.buildqueue_record
240 if queue_record.job.status == JobStatus.WAITING:
241 start_time = queue_record.getEstimatedJobStartTime()
242 if start_time is None:
243 return None
244 else:
245 start_time = queue_record.job.date_started
246 duration = queue_record.estimated_duration
247 return start_time + duration
236248
237 @property249 @property
238 def date(self):250 def date(self):
239 """The date when the build complete or will begin."""251 """The date when the build completed or is estimated to complete."""
240 if self.estimate:252 if self.estimate:
241 return self.eta253 return self.eta
242 return self.context.datebuilt254 return self.context.datebuilt
@@ -244,7 +256,9 @@
244 @property256 @property
245 def estimate(self):257 def estimate(self):
246 """If true, the date value is an estimate."""258 """If true, the date value is an estimate."""
247 return (self.context.datebuilt is None and self.eta is not None)259 if self.context.datebuilt is not None:
260 return False
261 return self.eta is not None
248262
249263
250class ISourcePackageAddEditSchema(Interface):264class ISourcePackageAddEditSchema(Interface):
@@ -272,7 +286,7 @@
272 def validate(self, data):286 def validate(self, data):
273 try:287 try:
274 parser = RecipeParser(data['recipe_text'])288 parser = RecipeParser(data['recipe_text'])
275 recipe_text = parser.parse()289 parser.parse()
276 except RecipeParseError:290 except RecipeParseError:
277 self.setFieldError(291 self.setFieldError(
278 'recipe_text',292 'recipe_text',
279293
=== added file 'lib/lp/code/browser/tests/test_breadcrumbs.py'
--- lib/lp/code/browser/tests/test_breadcrumbs.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/browser/tests/test_breadcrumbs.py 2010-04-29 11:26:16 +0000
@@ -0,0 +1,25 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5
6import unittest
7
8from lp.testing.breadcrumbs import BaseBreadcrumbTestCase
9
10
11class TestCodeImportMachineBreadcrumb(BaseBreadcrumbTestCase):
12 """Test breadcrumbs for an `ICodeImportMachine`."""
13
14 def test_machine(self):
15 machine = self.factory.makeCodeImportMachine(hostname='apollo')
16 expected = [
17 ('Code Import System', 'http://code.launchpad.dev/+code-imports'),
18 ('Machines', 'http://code.launchpad.dev/+code-imports/+machines'),
19 ('apollo',
20 'http://code.launchpad.dev/+code-imports/+machines/apollo')]
21 self.assertBreadcrumbs(expected, machine)
22
23
24def test_suite():
25 return unittest.TestLoader().loadTestsFromName(__name__)
026
=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-04-23 02:35:47 +0000
+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-04-29 11:26:16 +0000
@@ -19,9 +19,11 @@
19 extract_text, find_main_content, find_tags_by_class)19 extract_text, find_main_content, find_tags_by_class)
20from canonical.testing import DatabaseFunctionalLayer20from canonical.testing import DatabaseFunctionalLayer
21from lp.buildmaster.interfaces.buildbase import BuildStatus21from lp.buildmaster.interfaces.buildbase import BuildStatus
22from lp.code.browser.sourcepackagerecipe import SourcePackageRecipeView22from lp.code.browser.sourcepackagerecipe import (
23 SourcePackageRecipeView, SourcePackageRecipeBuildView
24)
23from lp.code.interfaces.sourcepackagerecipe import MINIMAL_RECIPE_TEXT25from lp.code.interfaces.sourcepackagerecipe import MINIMAL_RECIPE_TEXT
24from lp.testing import ANONYMOUS, BrowserTestCase, login26from lp.testing import ANONYMOUS, BrowserTestCase, login, TestCaseWithFactory
2527
2628
27class TestCaseForRecipe(BrowserTestCase):29class TestCaseForRecipe(BrowserTestCase):
@@ -330,6 +332,50 @@
330 self.assertEqual(['Secret Squirrel', 'Woody'], build_distros)332 self.assertEqual(['Secret Squirrel', 'Woody'], build_distros)
331333
332334
335class TestSourcePackageRecipeBuildView(TestCaseWithFactory):
336 """Test behaviour of SourcePackageReciptBuildView."""
337
338 layer = DatabaseFunctionalLayer
339
340 def test_estimate(self):
341 """Time should be estimated until the job is completed."""
342 build = self.factory.makeSourcePackageRecipeBuild()
343 queue_entry = self.factory.makeSourcePackageRecipeBuildJob(
344 recipe_build=build)
345 self.factory.makeBuilder()
346 view = SourcePackageRecipeBuildView(build, None)
347 self.assertTrue(view.estimate)
348 queue_entry.job.start()
349 self.assertTrue(view.estimate)
350 removeSecurityProxy(build).datebuilt = datetime.now(utc)
351 self.assertFalse(view.estimate)
352
353 def test_eta(self):
354 """ETA should be reasonable.
355
356 It should be None if there is no builder or queue entry.
357 It should be getEstimatedJobStartTime + estimated duration for jobs
358 that have not started.
359 It should be job.date_started + estimated duration for jobs that have
360 started.
361 """
362 build = self.factory.makeSourcePackageRecipeBuild()
363 view = SourcePackageRecipeBuildView(build, None)
364 self.assertIs(None, view.eta)
365 queue_entry = self.factory.makeSourcePackageRecipeBuildJob(
366 recipe_build=build)
367 queue_entry._now = lambda: datetime(1970, 1, 1, 0, 0, 0, 0, utc)
368 self.factory.makeBuilder()
369 self.assertIsNot(None, view.eta)
370 self.assertEqual(
371 queue_entry.getEstimatedJobStartTime() +
372 queue_entry.estimated_duration, view.eta)
373 queue_entry.job.start()
374 self.assertEqual(
375 queue_entry.job.date_started + queue_entry.estimated_duration,
376 view.eta)
377
378
333class TestSourcePackageRecipeDeleteView(TestCaseForRecipe):379class TestSourcePackageRecipeDeleteView(TestCaseForRecipe):
334380
335 layer = DatabaseFunctionalLayer381 layer = DatabaseFunctionalLayer
336382
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2010-04-23 22:48:25 +0000
+++ lib/lp/code/configure.zcml 2010-04-29 11:26:16 +0000
@@ -64,12 +64,6 @@
64 appropriate. -->64 appropriate. -->
65 </class>65 </class>
6666
67 <adapter
68 provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
69 for="lp.code.interfaces.codeimportmachine.ICodeImportMachineSet"
70 factory="lp.code.browser.codeimportmachine.CodeImportMachineSetBreadcrumb"
71 permission="zope.Public"/>
72
73 <!-- CodeImportMachineSet -->67 <!-- CodeImportMachineSet -->
7468
75 <securedutility69 <securedutility
7670
=== modified file 'lib/lp/code/model/tests/test_diff.py'
--- lib/lp/code/model/tests/test_diff.py 2010-04-26 00:24:26 +0000
+++ lib/lp/code/model/tests/test_diff.py 2010-04-29 11:26:16 +0000
@@ -263,7 +263,9 @@
263 last_oops_id = errorlog.globalErrorUtility.lastid263 last_oops_id = errorlog.globalErrorUtility.lastid
264 diff_bytes = "not a real diff"264 diff_bytes = "not a real diff"
265 diff = Diff.fromFile(StringIO(diff_bytes), len(diff_bytes))265 diff = Diff.fromFile(StringIO(diff_bytes), len(diff_bytes))
266 self.assertNotEqual(last_oops_id, errorlog.globalErrorUtility.lastid)266 # XXX MichaelHudson, 2010-04-29, bug=567257: This test fails
267 # intermittently when run close to midnight.
268 #self.assertNotEqual(last_oops_id, errorlog.globalErrorUtility.lastid)
267 self.assertIs(None, diff.diffstat)269 self.assertIs(None, diff.diffstat)
268 self.assertIs(None, diff.added_lines_count)270 self.assertIs(None, diff.added_lines_count)
269 self.assertIs(None, diff.removed_lines_count)271 self.assertIs(None, diff.removed_lines_count)
270272
=== modified file 'lib/lp/codehosting/codeimport/tests/test_worker.py'
--- lib/lp/codehosting/codeimport/tests/test_worker.py 2010-04-12 01:16:16 +0000
+++ lib/lp/codehosting/codeimport/tests/test_worker.py 2010-04-29 11:26:16 +0000
@@ -314,6 +314,21 @@
314 set([revid, revid1, revid2]),314 set([revid, revid1, revid2]),
315 set(retrieved_branch.repository.all_revision_ids()))315 set(retrieved_branch.repository.all_revision_ids()))
316316
317 def test_pull_doesnt_bring_backup_directories(self):
318 # If the branch has been upgraded in the branch store, `pull` does not
319 # copy the backup.bzr directory to `target_path`, just the .bzr
320 # directory.
321 store = self.makeBranchStore()
322 tree = create_branch_with_one_revision('original')
323 store.push(self.arbitrary_branch_id, tree.branch, default_format)
324 t = get_transport(store._getMirrorURL(self.arbitrary_branch_id))
325 t.mkdir('backup.bzr')
326 retrieved_branch = store.pull(
327 self.arbitrary_branch_id, 'pulled', default_format,
328 needs_tree=False)
329 self.assertEqual(
330 ['.bzr'], retrieved_branch.bzrdir.root_transport.list_dir('.'))
331
317332
318class TestImportDataStore(WorkerTest):333class TestImportDataStore(WorkerTest):
319 """Tests for `ImportDataStore`."""334 """Tests for `ImportDataStore`."""
320335
=== modified file 'lib/lp/codehosting/codeimport/worker.py'
--- lib/lp/codehosting/codeimport/worker.py 2010-04-15 02:27:31 +0000
+++ lib/lp/codehosting/codeimport/worker.py 2010-04-29 11:26:16 +0000
@@ -94,9 +94,12 @@
94 # revisions are in the ancestry of the tip of the remote branch, which94 # revisions are in the ancestry of the tip of the remote branch, which
95 # we strictly don't care about, so we just copy the whole thing down95 # we strictly don't care about, so we just copy the whole thing down
96 # at the vfs level.96 # at the vfs level.
97 control_dir = remote_bzr_dir.root_transport.relpath(
98 remote_bzr_dir.transport.abspath('.'))
97 target = get_transport(target_path)99 target = get_transport(target_path)
98 target.ensure_base()100 target_control = target.clone(control_dir)
99 remote_bzr_dir.root_transport.copy_tree_to_transport(target)101 target_control.create_prefix()
102 remote_bzr_dir.transport.copy_tree_to_transport(target_control)
100 local_bzr_dir = BzrDir.open_from_transport(target)103 local_bzr_dir = BzrDir.open_from_transport(target)
101 if needs_tree:104 if needs_tree:
102 local_bzr_dir.create_workingtree()105 local_bzr_dir.create_workingtree()
103106
=== modified file 'lib/lp/registry/browser/tests/test_breadcrumbs.py'
--- lib/lp/registry/browser/tests/test_breadcrumbs.py 2009-11-25 15:08:30 +0000
+++ lib/lp/registry/browser/tests/test_breadcrumbs.py 2010-04-29 11:26:16 +0000
@@ -9,8 +9,8 @@
99
10from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities10from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
11from canonical.launchpad.webapp.publisher import canonical_url11from canonical.launchpad.webapp.publisher import canonical_url
12from canonical.launchpad.webapp.tests.breadcrumbs import (12
13 BaseBreadcrumbTestCase)13from lp.testing.breadcrumbs import BaseBreadcrumbTestCase
1414
1515
16class TestDistroseriesBreadcrumb(BaseBreadcrumbTestCase):16class TestDistroseriesBreadcrumb(BaseBreadcrumbTestCase):
@@ -25,9 +25,7 @@
25 self.distroseries_url = canonical_url(self.distroseries)25 self.distroseries_url = canonical_url(self.distroseries)
2626
27 def test_distroseries(self):27 def test_distroseries(self):
28 crumbs = self._getBreadcrumbs(28 crumbs = self.getBreadcrumbsForObject(self.distroseries)
29 self.distroseries_url,
30 [self.root, self.distribution, self.distroseries])
31 last_crumb = crumbs[-1]29 last_crumb = crumbs[-1]
32 self.assertEqual(self.distroseries.named_version, last_crumb.text)30 self.assertEqual(self.distroseries.named_version, last_crumb.text)
3331
@@ -46,9 +44,7 @@
46 mirror = self.factory.makeMirror(44 mirror = self.factory.makeMirror(
47 distribution=self.distribution,45 distribution=self.distribution,
48 displayname=displayname)46 displayname=displayname)
49 crumbs = self._getBreadcrumbs(47 crumbs = self.getBreadcrumbsForObject(mirror)
50 canonical_url(mirror),
51 [self.root, self.distribution, mirror])
52 last_crumb = crumbs[-1]48 last_crumb = crumbs[-1]
53 self.assertEqual(displayname, last_crumb.text)49 self.assertEqual(displayname, last_crumb.text)
5450
@@ -60,9 +56,7 @@
60 distribution=self.distribution,56 distribution=self.distribution,
61 displayname=None,57 displayname=None,
62 http_url=http_url)58 http_url=http_url)
63 crumbs = self._getBreadcrumbs(59 crumbs = self.getBreadcrumbsForObject(mirror)
64 canonical_url(mirror),
65 [self.root, self.distribution, mirror])
66 last_crumb = crumbs[-1]60 last_crumb = crumbs[-1]
67 self.assertEqual("Example.com-archive", last_crumb.text)61 self.assertEqual("Example.com-archive", last_crumb.text)
6862
@@ -74,9 +68,7 @@
74 distribution=self.distribution,68 distribution=self.distribution,
75 displayname=None,69 displayname=None,
76 ftp_url=ftp_url)70 ftp_url=ftp_url)
77 crumbs = self._getBreadcrumbs(71 crumbs = self.getBreadcrumbsForObject(mirror)
78 canonical_url(mirror),
79 [self.root, self.distribution, mirror])
80 last_crumb = crumbs[-1]72 last_crumb = crumbs[-1]
81 self.assertEqual("Example.com-archive", last_crumb.text)73 self.assertEqual("Example.com-archive", last_crumb.text)
8274
@@ -93,15 +85,13 @@
93 self.milestone_url = canonical_url(self.milestone)85 self.milestone_url = canonical_url(self.milestone)
9486
95 def test_milestone_without_code_name(self):87 def test_milestone_without_code_name(self):
96 crumbs = self._getBreadcrumbs(88 crumbs = self.getBreadcrumbsForObject(self.milestone)
97 self.milestone_url, [self.root, self.project, self.milestone])
98 last_crumb = crumbs[-1]89 last_crumb = crumbs[-1]
99 self.assertEqual(self.milestone.name, last_crumb.text)90 self.assertEqual(self.milestone.name, last_crumb.text)
10091
101 def test_milestone_with_code_name(self):92 def test_milestone_with_code_name(self):
102 self.milestone.code_name = "duck"93 self.milestone.code_name = "duck"
103 crumbs = self._getBreadcrumbs(94 crumbs = self.getBreadcrumbsForObject(self.milestone)
104 self.milestone_url, [self.root, self.project, self.milestone])
105 last_crumb = crumbs[-1]95 last_crumb = crumbs[-1]
106 expected_text = '%s "%s"' % (96 expected_text = '%s "%s"' % (
107 self.milestone.name, self.milestone.code_name)97 self.milestone.name, self.milestone.code_name)
@@ -109,10 +99,7 @@
10999
110 def test_productrelease(self):100 def test_productrelease(self):
111 release = self.factory.makeProductRelease(milestone=self.milestone)101 release = self.factory.makeProductRelease(milestone=self.milestone)
112 self.release_url = canonical_url(release)102 crumbs = self.getBreadcrumbsForObject(release)
113 crumbs = self._getBreadcrumbs(
114 self.release_url,
115 [self.root, self.project, self.series, self.milestone])
116 last_crumb = crumbs[-1]103 last_crumb = crumbs[-1]
117 self.assertEqual(self.milestone.name, last_crumb.text)104 self.assertEqual(self.milestone.name, last_crumb.text)
118105
@@ -131,12 +118,9 @@
131 name=name,118 name=name,
132 title=title,119 title=title,
133 proposition=proposition)120 proposition=proposition)
134 self.poll_url = canonical_url(self.poll)
135121
136 def test_poll(self):122 def test_poll(self):
137 crumbs = self._getBreadcrumbs(123 crumbs = self.getBreadcrumbsForObject(self.poll)
138 self.poll_url,
139 [self.root, self.team, self.poll])
140 last_crumb = crumbs[-1]124 last_crumb = crumbs[-1]
141 self.assertEqual(self.poll.title, last_crumb.text)125 self.assertEqual(self.poll.title, last_crumb.text)
142126
143127
=== modified file 'lib/lp/services/mailman/doc/reactivate-lists.txt'
--- lib/lp/services/mailman/doc/reactivate-lists.txt 2009-09-24 18:51:29 +0000
+++ lib/lp/services/mailman/doc/reactivate-lists.txt 2010-04-29 11:26:16 +0000
@@ -50,7 +50,7 @@
5050
51Now the team owner reactivates the list.51Now the team owner reactivates the list.
5252
53 >>> browser.getLink('Create a mailing list').click()53 >>> browser.getLink('Configure mailing list').click()
54 >>> browser.getControl('Reactivate this Mailing List').click()54 >>> browser.getControl('Reactivate this Mailing List').click()
55 >>> xmlrpc_watcher.wait_for_reactivation('itest-one')55 >>> xmlrpc_watcher.wait_for_reactivation('itest-one')
5656
5757
=== modified file 'lib/lp/services/mailman/doc/staging.txt'
--- lib/lp/services/mailman/doc/staging.txt 2009-12-02 22:56:09 +0000
+++ lib/lp/services/mailman/doc/staging.txt 2010-04-29 11:26:16 +0000
@@ -22,7 +22,8 @@
2222
23 >>> login('admin@canonical.com')23 >>> login('admin@canonical.com')
2424
25 >>> owner = factory.makePerson()25 >>> from zope.security.proxy import removeSecurityProxy
26 >>> owner = removeSecurityProxy(factory.makePerson())
26 >>> team_one = factory.makeTeam(owner=owner, name='staging-one')27 >>> team_one = factory.makeTeam(owner=owner, name='staging-one')
27 >>> team_two = factory.makeTeam(owner=owner, name='staging-two')28 >>> team_two = factory.makeTeam(owner=owner, name='staging-two')
28 >>> transaction.commit()29 >>> transaction.commit()
2930
=== modified file 'lib/lp/soyuz/browser/archive.py'
--- lib/lp/soyuz/browser/archive.py 2010-04-16 05:03:03 +0000
+++ lib/lp/soyuz/browser/archive.py 2010-04-29 11:26:16 +0000
@@ -10,6 +10,7 @@
10 'ArchiveActivateView',10 'ArchiveActivateView',
11 'ArchiveBadges',11 'ArchiveBadges',
12 'ArchiveBuildsView',12 'ArchiveBuildsView',
13 'ArchiveDeleteView',
13 'ArchiveEditDependenciesView',14 'ArchiveEditDependenciesView',
14 'ArchiveEditView',15 'ArchiveEditView',
15 'ArchiveIndexActionsMenu',16 'ArchiveIndexActionsMenu',
@@ -59,8 +60,8 @@
59from lp.soyuz.adapters.archivesourcepublication import (60from lp.soyuz.adapters.archivesourcepublication import (
60 ArchiveSourcePublications)61 ArchiveSourcePublications)
61from lp.soyuz.interfaces.archive import (62from lp.soyuz.interfaces.archive import (
62 ArchivePurpose, CannotCopy, IArchive, IArchiveEditDependenciesForm,63 ArchivePurpose, ArchiveStatus, CannotCopy, IArchive,
63 IArchiveSet, IPPAActivateForm, NoSuchPPA)64 IArchiveEditDependenciesForm, IArchiveSet, IPPAActivateForm, NoSuchPPA)
64from lp.soyuz.interfaces.archivepermission import (65from lp.soyuz.interfaces.archivepermission import (
65 ArchivePermissionType, IArchivePermissionSet)66 ArchivePermissionType, IArchivePermissionSet)
66from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriberSet67from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriberSet
@@ -404,15 +405,24 @@
404 # This link should only be available for private archives:405 # This link should only be available for private archives:
405 view = self.context406 view = self.context
406 archive = view.context407 archive = view.context
407 if not archive.private:408 if not archive.private or not archive.is_active:
408 link.enabled = False409 link.enabled = False
409
410 return link410 return link
411411
412 @enabled_with_permission('launchpad.Edit')412 @enabled_with_permission('launchpad.Edit')
413 def edit(self):413 def edit(self):
414 text = 'Change details'414 text = 'Change details'
415 return Link('+edit', text, icon='edit')415 view = self.context
416 return Link(
417 '+edit', text, icon='edit', enabled=view.context.is_active)
418
419 @enabled_with_permission('launchpad.Edit')
420 def delete_ppa(self):
421 text = 'Delete PPA'
422 view = self.context
423 return Link(
424 '+delete', text, icon='trash-icon',
425 enabled=view.context.is_active)
416426
417 def builds(self):427 def builds(self):
418 text = 'View all builds'428 text = 'View all builds'
@@ -444,6 +454,9 @@
444 # archives without any sources.454 # archives without any sources.
445 if self.context.is_copy or not self.context.has_sources:455 if self.context.is_copy or not self.context.has_sources:
446 link.enabled = False456 link.enabled = False
457 view = self.context
458 if not view.context.is_active:
459 link.enabled = False
447 return link460 return link
448461
449 @enabled_with_permission('launchpad.AnyPerson')462 @enabled_with_permission('launchpad.AnyPerson')
@@ -460,7 +473,10 @@
460 @enabled_with_permission('launchpad.Edit')473 @enabled_with_permission('launchpad.Edit')
461 def edit_dependencies(self):474 def edit_dependencies(self):
462 text = 'Edit PPA dependencies'475 text = 'Edit PPA dependencies'
463 return Link('+edit-dependencies', text, icon='edit')476 view = self.context
477 return Link(
478 '+edit-dependencies', text, icon='edit',
479 enabled=view.context.is_active)
464480
465481
466class ArchiveNavigationMenu(NavigationMenu, ArchiveMenuMixin):482class ArchiveNavigationMenu(NavigationMenu, ArchiveMenuMixin):
@@ -468,9 +484,9 @@
468484
469 usedfor = IArchive485 usedfor = IArchive
470 facet = 'overview'486 facet = 'overview'
471 links = ['admin', 'builds', 'builds_building', 'builds_pending',487 links = ['admin', 'builds', 'builds_building',
472 'builds_successful', 'edit', 'edit_dependencies', 'packages',488 'builds_pending', 'builds_successful',
473 'ppa']489 'packages', 'ppa']
474490
475491
476class IArchiveIndexActionsMenu(Interface):492class IArchiveIndexActionsMenu(Interface):
@@ -481,8 +497,8 @@
481 """Archive index navigation menu."""497 """Archive index navigation menu."""
482 usedfor = IArchiveIndexActionsMenu498 usedfor = IArchiveIndexActionsMenu
483 facet = 'overview'499 facet = 'overview'
484 links = ['admin', 'edit', 'edit_dependencies', 'manage_subscribers',500 links = ['admin', 'edit', 'edit_dependencies',
485 'packages']501 'manage_subscribers', 'packages', 'delete_ppa']
486502
487503
488class IArchivePackagesActionMenu(Interface):504class IArchivePackagesActionMenu(Interface):
@@ -578,7 +594,7 @@
578 if self.context.is_ppa:594 if self.context.is_ppa:
579 return 'PPA'595 return 'PPA'
580 else:596 else:
581 return 'Archive'597 return 'archive'
582598
583 @cachedproperty599 @cachedproperty
584 def build_counters(self):600 def build_counters(self):
@@ -622,6 +638,19 @@
622 return list(copy_requests)638 return list(copy_requests)
623639
624640
641 @property
642 def disabled_warning_message(self):
643 """Return an appropriate message if the archive is disabled."""
644 if self.context.enabled:
645 return None
646
647 if self.context.status in (
648 ArchiveStatus.DELETED, ArchiveStatus.DELETING):
649 return "This %s has been deleted." % self.archive_label
650 else:
651 return "This %s has been disabled." % self.archive_label
652
653
625class ArchiveSeriesVocabularyFactory:654class ArchiveSeriesVocabularyFactory:
626 """A factory for generating vocabularies of an archive's series."""655 """A factory for generating vocabularies of an archive's series."""
627656
@@ -1916,3 +1945,44 @@
1916 :rtype: bool1945 :rtype: bool
1917 """1946 """
1918 return self.context.owner.visibility == PersonVisibility.PRIVATE1947 return self.context.owner.visibility == PersonVisibility.PRIVATE
1948
1949
1950class ArchiveDeleteView(LaunchpadFormView):
1951 """View class for deleting `IArchive`s"""
1952
1953 schema = Interface
1954
1955 @property
1956 def page_title(self):
1957 return smartquote('Delete "%s"' % self.context.displayname)
1958
1959 @property
1960 def label(self):
1961 return self.page_title
1962
1963 @property
1964 def can_be_deleted(self):
1965 return self.context.status not in (
1966 ArchiveStatus.DELETING, ArchiveStatus.DELETED)
1967
1968 @property
1969 def waiting_for_deletion(self):
1970 return self.context.status == ArchiveStatus.DELETING
1971
1972 @property
1973 def next_url(self):
1974 # We redirect back to the PPA owner's profile page on a
1975 # successful action.
1976 return canonical_url(self.context.owner)
1977
1978 @property
1979 def cancel_url(self):
1980 return canonical_url(self.context)
1981
1982 @action(_("Permanently delete PPA"), name="delete_ppa")
1983 def action_delete_ppa(self, action, data):
1984 self.context.delete(self.user)
1985 self.request.response.addInfoNotification(
1986 "Deletion of '%s' has been requested and the repository will be "
1987 "removed shortly." % self.context.title)
1988
19191989
=== modified file 'lib/lp/soyuz/browser/configure.zcml'
--- lib/lp/soyuz/browser/configure.zcml 2010-04-16 05:03:03 +0000
+++ lib/lp/soyuz/browser/configure.zcml 2010-04-29 11:26:16 +0000
@@ -276,6 +276,13 @@
276 permission="launchpad.Edit"276 permission="launchpad.Edit"
277 template="../templates/archive-edit.pt"/>277 template="../templates/archive-edit.pt"/>
278 <browser:page278 <browser:page
279 name="+delete"
280 facet="overview"
281 for="lp.soyuz.interfaces.archive.IArchive"
282 class="lp.soyuz.browser.archive.ArchiveDeleteView"
283 permission="launchpad.Edit"
284 template="../templates/archive-delete.pt"/>
285 <browser:page
279 name="+admin"286 name="+admin"
280 facet="overview"287 facet="overview"
281 for="lp.soyuz.interfaces.archive.IArchive"288 for="lp.soyuz.interfaces.archive.IArchive"
282289
=== modified file 'lib/lp/soyuz/browser/tests/archive-views.txt'
--- lib/lp/soyuz/browser/tests/archive-views.txt 2010-04-15 23:25:47 +0000
+++ lib/lp/soyuz/browser/tests/archive-views.txt 2010-04-29 11:26:16 +0000
@@ -38,13 +38,13 @@
38 None38 None
3939
40The ArchiveView includes an archive_label property that returns either40The ArchiveView includes an archive_label property that returns either
41the string 'PPA' or 'Archive' depending on whether the archive is a PPA41the string 'PPA' or 'archive' depending on whether the archive is a PPA
42(this is mainly for branding purposes):42(this is mainly for branding purposes):
4343
44 >>> print ppa_archive_view.archive_label44 >>> print ppa_archive_view.archive_label
45 PPA45 PPA
46 >>> print copy_archive_view.archive_label46 >>> print copy_archive_view.archive_label
47 Archive47 archive
4848
49The ArchiveView is provides the html for the inline description49The ArchiveView is provides the html for the inline description
50editing widget.50editing widget.
5151
=== modified file 'lib/lp/soyuz/browser/tests/test_breadcrumbs.py'
--- lib/lp/soyuz/browser/tests/test_breadcrumbs.py 2009-09-29 07:21:40 +0000
+++ lib/lp/soyuz/browser/tests/test_breadcrumbs.py 2010-04-29 11:26:16 +0000
@@ -8,11 +8,11 @@
8from zope.component import getUtility8from zope.component import getUtility
99
10from canonical.launchpad.webapp.publisher import canonical_url10from canonical.launchpad.webapp.publisher import canonical_url
11from canonical.launchpad.webapp.tests.breadcrumbs import (11
12 BaseBreadcrumbTestCase)
13from lp.registry.interfaces.distribution import IDistributionSet12from lp.registry.interfaces.distribution import IDistributionSet
14from lp.soyuz.browser.archivesubscription import PersonalArchiveSubscription13from lp.soyuz.browser.archivesubscription import PersonalArchiveSubscription
15from lp.testing import login, login_person14from lp.testing import login, login_person
15from lp.testing.breadcrumbs import BaseBreadcrumbTestCase
1616
1717
18class TestDistroArchSeriesBreadcrumb(BaseBreadcrumbTestCase):18class TestDistroArchSeriesBreadcrumb(BaseBreadcrumbTestCase):
@@ -22,39 +22,27 @@
22 self.ubuntu = getUtility(IDistributionSet).getByName('ubuntu')22 self.ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
23 self.hoary = self.ubuntu.getSeries('hoary')23 self.hoary = self.ubuntu.getSeries('hoary')
24 self.hoary_i386 = self.hoary['i386']24 self.hoary_i386 = self.hoary['i386']
25 self.traversed_objects = [
26 self.root, self.ubuntu, self.hoary, self.hoary_i386]
2725
28 def test_distroarchseries(self):26 def test_distroarchseries(self):
29 das_url = canonical_url(self.hoary_i386)27 das_url = canonical_url(self.hoary_i386)
30 urls = self._getBreadcrumbsURLs(das_url, self.traversed_objects)28 crumbs = self.getBreadcrumbsForObject(self.hoary_i386)
31 texts = self._getBreadcrumbsTexts(das_url, self.traversed_objects)29 self.assertEquals(crumbs[-1].url, das_url)
3230 self.assertEquals(crumbs[-1].text, "i386")
33 self.assertEquals(urls[-1], das_url)
34 self.assertEquals(texts[-1], "i386")
3531
36 def test_distroarchseriesbinarypackage(self):32 def test_distroarchseriesbinarypackage(self):
37 pmount_hoary_i386 = self.hoary_i386.getBinaryPackage("pmount")33 pmount_hoary_i386 = self.hoary_i386.getBinaryPackage("pmount")
38 self.traversed_objects.append(pmount_hoary_i386)
39 pmount_url = canonical_url(pmount_hoary_i386)34 pmount_url = canonical_url(pmount_hoary_i386)
40 urls = self._getBreadcrumbsURLs(pmount_url, self.traversed_objects)35 crumbs = self.getBreadcrumbsForObject(pmount_hoary_i386)
41 texts = self._getBreadcrumbsTexts(pmount_url, self.traversed_objects)36 self.assertEquals(crumbs[-1].url, pmount_url)
4237 self.assertEquals(crumbs[-1].text, "pmount")
43 self.assertEquals(urls[-1], pmount_url)
44 self.assertEquals(texts[-1], "pmount")
4538
46 def test_distroarchseriesbinarypackagerelease(self):39 def test_distroarchseriesbinarypackagerelease(self):
47 pmount_hoary_i386 = self.hoary_i386.getBinaryPackage("pmount")40 pmount_hoary_i386 = self.hoary_i386.getBinaryPackage("pmount")
48 pmount_release = pmount_hoary_i386['0.1-1']41 pmount_release = pmount_hoary_i386['0.1-1']
49 self.traversed_objects.extend([pmount_hoary_i386, pmount_release])
50 pmount_release_url = canonical_url(pmount_release)42 pmount_release_url = canonical_url(pmount_release)
51 urls = self._getBreadcrumbsURLs(43 crumbs = self.getBreadcrumbsForObject(pmount_release)
52 pmount_release_url, self.traversed_objects)44 self.assertEquals(crumbs[-1].url, pmount_release_url)
53 texts = self._getBreadcrumbsTexts(45 self.assertEquals(crumbs[-1].text, "0.1-1")
54 pmount_release_url, self.traversed_objects)
55
56 self.assertEquals(urls[-1], pmount_release_url)
57 self.assertEquals(texts[-1], "0.1-1")
5846
5947
60class TestArchiveSubscriptionBreadcrumb(BaseBreadcrumbTestCase):48class TestArchiveSubscriptionBreadcrumb(BaseBreadcrumbTestCase):
@@ -76,18 +64,10 @@
76 owner, self.ppa)64 owner, self.ppa)
7765
78 def test_personal_archive_subscription(self):66 def test_personal_archive_subscription(self):
79 self.traversed_objects = [
80 self.root, self.ppa.owner, self.personal_archive_subscription]
81 subscription_url = canonical_url(self.personal_archive_subscription)67 subscription_url = canonical_url(self.personal_archive_subscription)
8268 crumbs = self.getBreadcrumbsForObject(self.personal_archive_subscription)
83 urls = self._getBreadcrumbsURLs(69 self.assertEquals(subscription_url, crumbs[-1].url)
84 subscription_url, self.traversed_objects)70 self.assertEquals("Access to %s" % self.ppa.displayname, crumbs[-1].text)
85 texts = self._getBreadcrumbsTexts(
86 subscription_url, self.traversed_objects)
87
88 self.assertEquals(subscription_url, urls[-1])
89 self.assertEquals(
90 "Access to %s" % self.ppa.displayname, texts[-1])
9171
92def test_suite():72def test_suite():
93 return unittest.TestLoader().loadTestsFromName(__name__)73 return unittest.TestLoader().loadTestsFromName(__name__)
9474
=== added file 'lib/lp/soyuz/doc/archive-deletion.txt'
--- lib/lp/soyuz/doc/archive-deletion.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/doc/archive-deletion.txt 2010-04-29 11:26:16 +0000
@@ -0,0 +1,81 @@
1= Deleting an archive =
2
3When deleting an archive, the user calls IArchive.delete(), passing in
4the IPerson who is requesting the deletion.
5
6All of the publishing records will be marked as DELETED, the archive is
7disabled and the status is set to DELETING.
8
9This status tells the publisher to then delete the repository area. Once
10it completes that task it will set the status to DELETED.
11
12 >>> from lp.soyuz.interfaces.archive import (
13 ... ArchiveStatus, IArchiveSet, IArchive)
14 >>> from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
15 >>> login("admin@canonical.com")
16 >>> stp = SoyuzTestPublisher()
17 >>> stp.prepareBreezyAutotest()
18 >>> archive = factory.makeArchive()
19
20The archive is currently active:
21
22 >>> print archive.enabled
23 True
24
25 >>> print archive.status.name
26 ACTIVE
27
28We can create some packages in it using the test publisher:
29
30 >>> from lp.soyuz.interfaces.publishing import PackagePublishingStatus
31 >>> ignore = stp.getPubBinaries(
32 ... archive=archive, binaryname="foo-bin1",
33 ... status=PackagePublishingStatus.PENDING)
34 >>> ignore = stp.getPubBinaries(
35 ... archive=archive, binaryname="foo-bin2",
36 ... status=PackagePublishingStatus.PUBLISHED)
37 >>> from storm.store import Store
38 >>> Store.of(archive).flush()
39
40Calling delete() will now do the deletion. It is only callable by someone
41with launchpad.Edit permission on the archive. Here, "duderino" who is
42some random dude is refused:
43
44 >>> person = factory.makePerson(name="duderino")
45 >>> login_person(person)
46 >>> archive.delete(person)
47 Traceback (most recent call last):
48 ...
49 Unauthorized:...
50
51However we can delete it using the owner of the archive:
52
53 >>> login_person(archive.owner)
54 >>> archive.delete(archive.owner)
55
56The deletion code uses a store.execute() command to speed up the operation
57where many records need to be updated. Therefore we need to invalidate
58the cache to make Storm re-read the database objects.
59
60 >>> Store.of(archive).invalidate()
61
62Now, all the publications are DELETED, the archive is disabled and the
63status is DELETING to tell the publisher to remove the repository:
64
65 >>> publications = list(archive.getPublishedSources())
66 >>> publications.extend(list(archive.getAllPublishedBinaries()))
67 >>> for pub in publications:
68 ... print "%s, %s by %s" % (
69 ... pub.displayname, pub.status.name, pub.removed_by.name)
70 foo 666 in breezy-autotest, DELETED by person-name12
71 foo 666 in breezy-autotest, DELETED by person-name12
72 foo-bin1 666 in breezy-autotest i386, DELETED by person-name12
73 foo-bin1 666 in breezy-autotest hppa, DELETED by person-name12
74 foo-bin2 666 in breezy-autotest i386, DELETED by person-name12
75 foo-bin2 666 in breezy-autotest hppa, DELETED by person-name12
76
77 >>> print archive.enabled
78 False
79
80 >>> print archive.status.name
81 DELETING
082
=== modified file 'lib/lp/soyuz/doc/archive.txt'
--- lib/lp/soyuz/doc/archive.txt 2010-04-15 10:27:06 +0000
+++ lib/lp/soyuz/doc/archive.txt 2010-04-29 11:26:16 +0000
@@ -45,6 +45,8 @@
45 True45 True
46 >>> cprov_archive.is_main46 >>> cprov_archive.is_main
47 False47 False
48 >>> cprov_archive.is_active
49 True
48 >>> cprov_archive.distribution.main_archive.is_main50 >>> cprov_archive.distribution.main_archive.is_main
49 True51 True
50 >>> cprov_archive.total_count52 >>> cprov_archive.total_count
5153
=== modified file 'lib/lp/soyuz/doc/buildd-mass-retry.txt'
--- lib/lp/soyuz/doc/buildd-mass-retry.txt 2010-04-14 17:34:35 +0000
+++ lib/lp/soyuz/doc/buildd-mass-retry.txt 2010-04-29 11:26:16 +0000
@@ -47,6 +47,45 @@
47 INFO Dry-run.47 INFO Dry-run.
48 <BLANKLINE>48 <BLANKLINE>
4949
50Superseded builds won't be retried; buildd-manager will just skip the build
51and set it to SUPERSEDED.
52
53 >>> from zope.security.proxy import removeSecurityProxy
54 >>> from lp.soyuz.interfaces.binarypackagebuild import (
55 ... IBinaryPackageBuildSet)
56 >>> from lp.soyuz.interfaces.publishing import PackagePublishingStatus
57 >>> build = getUtility(IBinaryPackageBuildSet).getByBuildID(12)
58 >>> pub = removeSecurityProxy(build.current_source_publication)
59
60Let's mark the build from the previous run superseded.
61
62 >>> pub.status = PackagePublishingStatus.SUPERSEDED
63 >>> print build.current_source_publication
64 None
65 >>> transaction.commit()
66
67A new run doesn't pick it up.
68
69 >>> process = subprocess.Popen([sys.executable, script, "-v", "-NFDC",
70 ... "-s", "hoary"],
71 ... stdout=subprocess.PIPE,
72 ... stderr=subprocess.PIPE,)
73 >>> stdout, stderr = process.communicate()
74 >>> process.returncode
75 0
76 >>> print stderr
77 DEBUG Intitialising connection.
78 INFO Initialising Build Mass-Retry for 'The Hoary Hedgehog Release/RELEASE'
79 INFO Processing builds in 'Failed to build'
80 INFO Processing builds in 'Dependency wait'
81 DEBUG Skipping superseded i386 build of libstdc++ b8p in ubuntu hoary RELEASE (12)
82 INFO Processing builds in 'Chroot problem'
83 INFO Success.
84 INFO Dry-run.
85 <BLANKLINE>
86
87 >>> pub.status = PackagePublishingStatus.PUBLISHED
88 >>> transaction.commit()
5089
51Passing an architecture, which contains no failed build records,90Passing an architecture, which contains no failed build records,
52nothing is done:91nothing is done:
5392
=== modified file 'lib/lp/soyuz/interfaces/archive.py'
--- lib/lp/soyuz/interfaces/archive.py 2010-03-24 14:40:26 +0000
+++ lib/lp/soyuz/interfaces/archive.py 2010-04-29 11:26:16 +0000
@@ -222,6 +222,10 @@
222 is_main = Bool(222 is_main = Bool(
223 title=_("True if archive is a main archive type"), required=False)223 title=_("True if archive is a main archive type"), required=False)
224224
225 is_active = Bool(
226 title=_("True if the archive is in the active state"),
227 required=False, readonly=True)
228
225 series_with_sources = Attribute(229 series_with_sources = Attribute(
226 "DistroSeries to which this archive has published sources")230 "DistroSeries to which this archive has published sources")
227 number_of_sources = Attribute(231 number_of_sources = Attribute(
@@ -1151,6 +1155,19 @@
1151 def disable():1155 def disable():
1152 """Disable the archive."""1156 """Disable the archive."""
11531157
1158 def delete(deleted_by):
1159 """Delete this archive.
1160
1161 :param deleted_by: The `IPerson` requesting the deletion.
1162
1163 The ArchiveStatus will be set to DELETING and any published
1164 packages will be marked as DELETED by deleted_by.
1165
1166 The publisher is responsible for deleting the repository area
1167 when it sees the status change and sets it to DELETED once
1168 processed.
1169 """
1170
11541171
1155class IArchive(IArchivePublic, IArchiveAppend, IArchiveEdit, IArchiveView):1172class IArchive(IArchivePublic, IArchiveAppend, IArchiveEdit, IArchiveView):
1156 """Main Archive interface."""1173 """Main Archive interface."""
11571174
=== modified file 'lib/lp/soyuz/model/archive.py'
--- lib/lp/soyuz/model/archive.py 2010-04-15 10:27:06 +0000
+++ lib/lp/soyuz/model/archive.py 2010-04-29 11:26:16 +0000
@@ -150,7 +150,8 @@
150 dbName='purpose', unique=False, notNull=True, schema=ArchivePurpose)150 dbName='purpose', unique=False, notNull=True, schema=ArchivePurpose)
151151
152 status = EnumCol(152 status = EnumCol(
153 dbName="status", unique=False, notNull=True, schema=ArchiveStatus)153 dbName="status", unique=False, notNull=True, schema=ArchiveStatus,
154 default=ArchiveStatus.ACTIVE)
154155
155 _enabled = BoolCol(dbName='enabled', notNull=True, default=True)156 _enabled = BoolCol(dbName='enabled', notNull=True, default=True)
156 enabled = property(lambda x: x._enabled)157 enabled = property(lambda x: x._enabled)
@@ -269,6 +270,11 @@
269 return self.purpose in MAIN_ARCHIVE_PURPOSES270 return self.purpose in MAIN_ARCHIVE_PURPOSES
270271
271 @property272 @property
273 def is_active(self):
274 """See `IArchive`."""
275 return self.status == ArchiveStatus.ACTIVE
276
277 @property
272 def series_with_sources(self):278 def series_with_sources(self):
273 """See `IArchive`."""279 """See `IArchive`."""
274 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)280 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
@@ -1409,6 +1415,26 @@
1409 self._enabled = False1415 self._enabled = False
1410 self._setBuildStatuses(JobStatus.SUSPENDED)1416 self._setBuildStatuses(JobStatus.SUSPENDED)
14111417
1418 def delete(self, deleted_by):
1419 """See `IArchive`."""
1420 assert self.status not in (
1421 ArchiveStatus.DELETING, ArchiveStatus.DELETED,
1422 "This archive is already deleted.")
1423
1424 # Set all the publications to DELETED.
1425 statuses = (
1426 PackagePublishingStatus.PENDING,
1427 PackagePublishingStatus.PUBLISHED)
1428 sources = list(self.getPublishedSources(status=statuses))
1429 getUtility(IPublishingSet).requestDeletion(
1430 sources, removed_by=deleted_by,
1431 removal_comment="Removed when deleting archive")
1432
1433 # Mark the archive's status as DELETING so the repository can be
1434 # removed by the publisher.
1435 self.status = ArchiveStatus.DELETING
1436 self.disable()
1437
14121438
1413class ArchiveSet:1439class ArchiveSet:
1414 implements(IArchiveSet)1440 implements(IArchiveSet)
14151441
=== modified file 'lib/lp/soyuz/scripts/packagecopier.py'
--- lib/lp/soyuz/scripts/packagecopier.py 2010-04-14 09:53:04 +0000
+++ lib/lp/soyuz/scripts/packagecopier.py 2010-04-29 11:26:16 +0000
@@ -251,10 +251,6 @@
251251
252 inventory_conflicts = self.getConflicts(source)252 inventory_conflicts = self.getConflicts(source)
253253
254 if (destination_archive_conflicts.count() == 0 and
255 len(inventory_conflicts) == 0):
256 return
257
258 # Cache the conflicting publications because they will be iterated254 # Cache the conflicting publications because they will be iterated
259 # more than once.255 # more than once.
260 destination_archive_conflicts = list(destination_archive_conflicts)256 destination_archive_conflicts = list(destination_archive_conflicts)
261257
=== modified file 'lib/lp/soyuz/scripts/tests/test_copypackage.py'
--- lib/lp/soyuz/scripts/tests/test_copypackage.py 2010-04-14 09:53:04 +0000
+++ lib/lp/soyuz/scripts/tests/test_copypackage.py 2010-04-29 11:26:16 +0000
@@ -2456,10 +2456,10 @@
2456 """2456 """
2457 ubuntu = getUtility(IDistributionSet).getByName('ubuntu')2457 ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
2458 warty = ubuntu.getSeries('warty')2458 warty = ubuntu.getSeries('warty')
2459 hoary = ubuntu.getSeries('hoary')
2460 test_publisher = self.getTestPublisher(warty)2459 test_publisher = self.getTestPublisher(warty)
2461 test_publisher.addFakeChroots(warty)2460 test_publisher.addFakeChroots(warty)
24622461
2462 orig_tarball = 'test-source_1.0.orig.tar.gz'
2463 proposed_source = test_publisher.getPubSource(2463 proposed_source = test_publisher.getPubSource(
2464 sourcename='test-source', version='1.0-2',2464 sourcename='test-source', version='1.0-2',
2465 distroseries=warty, archive=warty.main_archive,2465 distroseries=warty, archive=warty.main_archive,
@@ -2467,7 +2467,7 @@
2467 status=PackagePublishingStatus.PUBLISHED,2467 status=PackagePublishingStatus.PUBLISHED,
2468 section='net')2468 section='net')
2469 proposed_tar = test_publisher.addMockFile(2469 proposed_tar = test_publisher.addMockFile(
2470 'test-source_1.0.orig.tar.gz', filecontent='aaabbbccc')2470 orig_tarball, filecontent='aaabbbccc')
2471 proposed_source.sourcepackagerelease.addFile(proposed_tar)2471 proposed_source.sourcepackagerelease.addFile(proposed_tar)
2472 updates_source = test_publisher.getPubSource(2472 updates_source = test_publisher.getPubSource(
2473 sourcename='test-source', version='1.0-1',2473 sourcename='test-source', version='1.0-1',
@@ -2476,7 +2476,7 @@
2476 status=PackagePublishingStatus.PUBLISHED,2476 status=PackagePublishingStatus.PUBLISHED,
2477 section='misc')2477 section='misc')
2478 updates_tar = test_publisher.addMockFile(2478 updates_tar = test_publisher.addMockFile(
2479 'test-source_1.0.orig.tar.gz', filecontent='zzzyyyxxx')2479 orig_tarball, filecontent='zzzyyyxxx')
2480 updates_source.sourcepackagerelease.addFile(updates_tar)2480 updates_source.sourcepackagerelease.addFile(updates_tar)
2481 # Commit to ensure librarian files are written.2481 # Commit to ensure librarian files are written.
2482 self.layer.txn.commit()2482 self.layer.txn.commit()
@@ -2489,6 +2489,51 @@
2489 checker.checkCopy, proposed_source, warty,2489 checker.checkCopy, proposed_source, warty,
2490 PackagePublishingPocket.UPDATES)2490 PackagePublishingPocket.UPDATES)
24912491
2492 def testCopySourceWithConflictingFilesInPPAs(self):
2493 """We can copy source if the source files match, both in name and
2494 contents. We can't if they don't.
2495 """
2496 joe = self.factory.makePerson(email='joe@example.com')
2497 ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
2498 warty = ubuntu.getSeries('warty')
2499 test_publisher = self.getTestPublisher(warty)
2500 test_publisher.addFakeChroots(warty)
2501 dest_ppa = self.factory.makeArchive(
2502 distribution=ubuntu, owner=joe, purpose=ArchivePurpose.PPA,
2503 name='test1')
2504 src_ppa = self.factory.makeArchive(
2505 distribution=ubuntu, owner=joe, purpose=ArchivePurpose.PPA,
2506 name='test2')
2507 test1_source = test_publisher.getPubSource(
2508 sourcename='test-source', version='1.0-1',
2509 distroseries=warty, archive=dest_ppa,
2510 pocket=PackagePublishingPocket.RELEASE,
2511 status=PackagePublishingStatus.PUBLISHED,
2512 section='misc')
2513 orig_tarball = 'test-source_1.0.orig.tar.gz'
2514 test1_tar = test_publisher.addMockFile(
2515 orig_tarball, filecontent='aaabbbccc')
2516 test1_source.sourcepackagerelease.addFile(test1_tar)
2517 test2_source = test_publisher.getPubSource(
2518 sourcename='test-source', version='1.0-2',
2519 distroseries=warty, archive=src_ppa,
2520 pocket=PackagePublishingPocket.RELEASE,
2521 status=PackagePublishingStatus.PUBLISHED,
2522 section='misc')
2523 test2_tar = test_publisher.addMockFile(
2524 orig_tarball, filecontent='zzzyyyxxx')
2525 test2_source.sourcepackagerelease.addFile(test2_tar)
2526 # Commit to ensure librarian files are written.
2527 self.layer.txn.commit()
2528
2529 checker = CopyChecker(dest_ppa, include_binaries=False)
2530 self.assertRaisesWithContent(
2531 CannotCopy,
2532 "test-source_1.0.orig.tar.gz already exists in destination "
2533 "archive with different contents.",
2534 checker.checkCopy, test2_source, warty,
2535 PackagePublishingPocket.RELEASE)
2536
24922537
2493def test_suite():2538def test_suite():
2494 return unittest.TestLoader().loadTestsFromName(__name__)2539 return unittest.TestLoader().loadTestsFromName(__name__)
24952540
=== modified file 'lib/lp/soyuz/stories/ppa/xx-ppa-workflow.txt'
--- lib/lp/soyuz/stories/ppa/xx-ppa-workflow.txt 2010-02-25 16:49:16 +0000
+++ lib/lp/soyuz/stories/ppa/xx-ppa-workflow.txt 2010-04-29 11:26:16 +0000
@@ -392,7 +392,7 @@
392392
393 >>> for msg in get_feedback_messages(admin_browser.contents):393 >>> for msg in get_feedback_messages(admin_browser.contents):
394 ... print msg394 ... print msg
395 This archive has been disabled.395 This PPA has been disabled.
396396
397We need go back to the "Administer archive" page to see the build score and397We need go back to the "Administer archive" page to see the build score and
398external dependencies changes that were made:398external dependencies changes that were made:
@@ -718,7 +718,8 @@
718718
719 >>> no_priv_browser = setupBrowser(719 >>> no_priv_browser = setupBrowser(
720 ... auth='Basic no-priv@canonical.com:test')720 ... auth='Basic no-priv@canonical.com:test')
721 >>> no_priv_browser.open("http://launchpad.dev/~no-priv/+archive/ppa/+edit")721 >>> no_priv_browser.open(
722 ... "http://launchpad.dev/~no-priv/+archive/ppa/+edit")
722723
723Initially, the PPA is enabled and publishes.724Initially, the PPA is enabled and publishes.
724725
@@ -734,11 +735,12 @@
734 >>> no_priv_browser.getControl('Save').click()735 >>> no_priv_browser.getControl('Save').click()
735 >>> print extract_text(736 >>> print extract_text(
736 ... first_tag_by_class(no_priv_browser.contents, 'warning message'))737 ... first_tag_by_class(no_priv_browser.contents, 'warning message'))
737 This archive has been disabled.738 This PPA has been disabled.
738739
739Going back to the edit page, we can see the publish flag was cleared:740Going back to the edit page, we can see the publish flag was cleared:
740741
741 >>> no_priv_browser.open("http://launchpad.dev/~no-priv/+archive/ppa/+edit")742 >>> no_priv_browser.open(
743 ... "http://launchpad.dev/~no-priv/+archive/ppa/+edit")
742 >>> print no_priv_browser.getControl(name='field.publish').value744 >>> print no_priv_browser.getControl(name='field.publish').value
743 False745 False
744746
@@ -754,3 +756,79 @@
754 True756 True
755757
756758
759== Deleting a PPA ==
760
761Users with launchpad.Edit permission see a "Delete PPA" link in the
762navigation menu.
763
764 >>> anon_browser.open("http://launchpad.dev/~no-priv/+archive/ppa")
765 >>> print anon_browser.getLink("Delete PPA")
766 Traceback (most recent call last):
767 ...
768 LinkNotFoundError
769
770 >>> no_priv_browser.open("http://launchpad.dev/~no-priv/+archive/ppa")
771 >>> no_priv_browser.getLink("Delete PPA").click()
772
773Clicking this link takes the user to a page that allows deletion of a PPA:
774
775 >>> print no_priv_browser.title
776 Delete “PPA for No Privileges Person” : PPA for No Privileges Person :
777 No Privileges Person
778
779The page contains a stern warning that this action is final and irreversible:
780
781 >>> print extract_text(find_main_content(no_priv_browser.contents))
782 Delete “PPA for No Privileges Person”
783 ...
784 Deleting a PPA will destroy all of its packages, files and the
785 repository area.
786 This deletion is PERMANENT and cannot be undone.
787 Are you sure ?
788 ...
789
790If the user changes his mind, he can click on the cancel link to go back
791a page:
792
793 >>> print no_priv_browser.getLink("Cancel").url
794 http://launchpad.dev/~no-priv/+archive/ppa
795
796Otherwise, he has a button to press to confirm the deletion.
797
798 >>> no_priv_browser.getControl("Permanently delete PPA").click()
799
800This action will redirect the user back to his profile page, which will
801contain a notification message that the deletion is in progress.
802
803 >>> print no_priv_browser.url
804 http://launchpad.dev/~no-priv
805
806 >>> for msg in get_feedback_messages(no_priv_browser.contents):
807 ... print msg
808 Deletion of 'PPA for No Privileges Person' has been requested and
809 the repository will be removed shortly.
810
811The deleted PPA is still available to browse via a link on the profile page
812so you can see its build history, etc.:
813
814 >>> no_priv_browser.getLink("PPA for No Privileges Person").click()
815
816However, most of the action links are removed for deleted PPAs, so you can
817no longer "Delete packages", "Edit PPA dependencies", or "Change details".
818
819 >>> print no_priv_browser.getLink("Change details")
820 Traceback (most recent call last):
821 ...
822 LinkNotFoundError
823
824 >>> print no_priv_browser.getLink("Edit PPA dependencies")
825 Traceback (most recent call last):
826 ...
827 LinkNotFoundError
828
829 >>> no_priv_browser.getLink("View package details").click()
830 >>> print no_priv_browser.getLink("Delete packages")
831 Traceback (most recent call last):
832 ...
833 LinkNotFoundError
834
757835
=== added file 'lib/lp/soyuz/templates/archive-delete.pt'
--- lib/lp/soyuz/templates/archive-delete.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/templates/archive-delete.pt 2010-04-29 11:26:16 +0000
@@ -0,0 +1,32 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad"
8>
9<body>
10
11 <div metal:fill-slot="main">
12
13 <div class="top-portlet"
14 tal:condition="view/can_be_deleted">
15 <p>Deleting a PPA will destroy all of its packages,
16 files and the repository area.</p>
17 <p>This deletion is PERMANENT and cannot be undone.</p>
18 <p>Are you sure ?</p>
19
20 <div metal:use-macro="context/@@launchpad_form/form" />
21 </div>
22
23 <div class="top-portlet"
24 tal:condition="view/waiting_for_deletion">
25 <p>This archive is marked for deletion and will be removed
26 shortly.</p>
27 </div>
28
29 </div> <!-- main -->
30
31</body>
32</html>
033
=== modified file 'lib/lp/soyuz/templates/archive-index.pt'
--- lib/lp/soyuz/templates/archive-index.pt 2010-02-25 10:47:50 +0000
+++ lib/lp/soyuz/templates/archive-index.pt 2010-04-29 11:26:16 +0000
@@ -26,7 +26,8 @@
2626
27 <div class="top-portlet" style="padding-top:0.5em;">27 <div class="top-portlet" style="padding-top:0.5em;">
28 <p tal:condition="not: context/enabled"28 <p tal:condition="not: context/enabled"
29 style="clear: right;" class="warning message">29 style="clear: right;" class="warning message"
30 tal:content="view/disabled_warning_message">
30 This archive has been disabled.31 This archive has been disabled.
31 </p>32 </p>
3233
3334
=== modified file 'lib/lp/soyuz/templates/archive-packages.pt'
--- lib/lp/soyuz/templates/archive-packages.pt 2009-12-03 18:33:22 +0000
+++ lib/lp/soyuz/templates/archive-packages.pt 2010-04-29 11:26:16 +0000
@@ -104,7 +104,8 @@
104 tal:content="structure view/@@+global-actions" />104 tal:content="structure view/@@+global-actions" />
105105
106 <p tal:condition="not: context/enabled"106 <p tal:condition="not: context/enabled"
107 style="clear: right;" class="warning message">107 style="clear: right;" class="warning message"
108 tal:content="view/disabled_warning_message">
108 This archive has been disabled.109 This archive has been disabled.
109 </p>110 </p>
110111
111112
=== renamed file 'lib/canonical/launchpad/webapp/tests/breadcrumbs.py' => 'lib/lp/testing/breadcrumbs.py'
--- lib/canonical/launchpad/webapp/tests/breadcrumbs.py 2009-09-17 18:05:29 +0000
+++ lib/lp/testing/breadcrumbs.py 2010-04-29 11:26:16 +0000
@@ -3,67 +3,51 @@
33
4__metaclass__ = type4__metaclass__ = type
55
6from zope.app import zapi6from canonical.launchpad.webapp.publisher import canonical_url
7from zope.component import ComponentLookupError, getMultiAdapter
8
9from canonical.lazr.testing.menus import make_fake_request
10from canonical.launchpad.layers import setFirstLayer
11from canonical.launchpad.webapp.publisher import RootObject
12from canonical.testing import DatabaseFunctionalLayer7from canonical.testing import DatabaseFunctionalLayer
8
13from lp.testing import TestCaseWithFactory9from lp.testing import TestCaseWithFactory
10from lp.testing.views import create_initialized_view
11from lp.testing.publication import test_traverse
1412
1513
16class BaseBreadcrumbTestCase(TestCaseWithFactory):14class BaseBreadcrumbTestCase(TestCaseWithFactory):
1715
18 layer = DatabaseFunctionalLayer16 layer = DatabaseFunctionalLayer
19 request_layer = None17
2018 def assertBreadcrumbs(self, expected, obj, view_name=None, rootsite=None):
21 def setUp(self):19 """Assert that the breadcrumbs for obj match the expected values.
22 super(BaseBreadcrumbTestCase, self).setUp()20
23 self.root = RootObject()21 :param expected: A list of tuples containing (text, url) pairs.
2422 """
25 def _getHierarchyView(self, url, traversed_objects):23 crumbs = self.getBreadcrumbsForObject(obj, view_name, rootsite)
26 request = self._make_request(url, traversed_objects)24 self.assertEqual(
27 return getMultiAdapter((self.root, request), name='+hierarchy')25 expected,
2826 [(crumb.text, crumb.url) for crumb in crumbs])
29 def _getBreadcrumbs(self, url, traversed_objects):27
30 view = self._getHierarchyView(url, traversed_objects)28 def assertBreadcrumbTexts(self, expected, obj, view_name=None,
29 rootsite=None):
30 """The text of the breadcrumbs for obj match the expected values."""
31 crumbs = self.getBreadcrumbsForObject(obj, view_name, rootsite)
32 self.assertEqual(expected, [crumb.text for crumb in crumbs])
33
34 def assertBreadcrumbUrls(self, expected, obj, view_name=None,
35 rootsite=None):
36 """The urls of the breadcrumbs for obj match the expected values."""
37 crumbs = self.getBreadcrumbsForObject(obj, view_name, rootsite)
38 self.assertEqual(expected, [crumb.url for crumb in crumbs])
39
40 def getBreadcrumbsForObject(self, obj, view_name=None, rootsite=None):
41 """Get the breadcrumbs for the specified object.
42
43 Traverse to the canonical_url of the object, and use the request from
44 that to feed into the initialized hierarchy view so we get the
45 traversed objects.
46 """
47 url = canonical_url(obj, view_name=view_name, rootsite=rootsite)
48 return self.getBreadcrumbsForUrl(url)
49
50 def getBreadcrumbsForUrl(self, url):
51 obj, view, request = test_traverse(url)
52 view = create_initialized_view(obj, '+hierarchy', request=request)
31 return view.items53 return view.items
32
33 def _getBreadcrumbsTexts(self, url, traversed_objects):
34 crumbs = self._getBreadcrumbs(url, traversed_objects)
35 return [crumb.text for crumb in crumbs]
36
37 def _getBreadcrumbsURLs(self, url, traversed_objects):
38 crumbs = self._getBreadcrumbs(url, traversed_objects)
39 return [crumb.url for crumb in crumbs]
40
41 def _make_request(self, url, traversed_objects):
42 """Create and return a LaunchpadTestRequest.
43
44 Set the given list of traversed objects as request.traversed_objects,
45 also appending the view that the given URL points to, to mimic how
46 request.traversed_objects behave in a real request.
47
48 XXX: salgado, bug=432025, 2009-09-17: Instead of setting
49 request.traversed_objects manually, we should duplicate parts of
50 zope.publisher.publish.publish here (or in make_fake_request()) so
51 that tests don't have to specify the list of traversed objects for us
52 to set here.
53 """
54 request = make_fake_request(url, traversed_objects=traversed_objects)
55 if self.request_layer is not None:
56 setFirstLayer(request, self.request_layer)
57 last_segment = request._traversed_names[-1]
58 if traversed_objects:
59 obj = traversed_objects[-1]
60 # Assume the last_segment is the name of the view on the last
61 # traversed object, and if we fail to find a view with that name,
62 # use the default view.
63 try:
64 view = getMultiAdapter((obj, request), name=last_segment)
65 except ComponentLookupError:
66 default_view_name = zapi.getDefaultViewName(obj, request)
67 view = getMultiAdapter((obj, request), name=default_view_name)
68 request.traversed_objects.append(view)
69 return request
7054
=== modified file 'lib/lp/testing/publication.py'
--- lib/lp/testing/publication.py 2009-07-17 02:25:09 +0000
+++ lib/lp/testing/publication.py 2010-04-29 11:26:16 +0000
@@ -7,14 +7,24 @@
7__all__ = [7__all__ = [
8 'get_request_and_publication',8 'get_request_and_publication',
9 'print_request_and_publication',9 'print_request_and_publication',
10 'test_traverse',
10 ]11 ]
1112
12from cStringIO import StringIO13from cStringIO import StringIO
1314
14# Z3 doesn't make this available as a utility.15# Z3 doesn't make this available as a utility.
16from zope.app import zapi
15from zope.app.publication.requestpublicationregistry import factoryRegistry17from zope.app.publication.requestpublicationregistry import factoryRegistry
18from zope.component import getUtility
19from zope.interface import providedBy
20from zope.publisher.interfaces.browser import IDefaultSkin
21
22from canonical.launchpad.interfaces.launchpad import IOpenLaunchBag
23import canonical.launchpad.layers as layers
24from canonical.launchpad.webapp import urlsplit
16from canonical.launchpad.webapp.servers import ProtocolErrorPublication25from canonical.launchpad.webapp.servers import ProtocolErrorPublication
1726
27
18# Defines an helper function that returns the appropriate28# Defines an helper function that returns the appropriate
19# IRequest and IPublication.29# IRequest and IPublication.
20def get_request_and_publication(host='localhost', port=None,30def get_request_and_publication(host='localhost', port=None,
@@ -39,6 +49,7 @@
39 publication = publication_factory(None)49 publication = publication_factory(None)
40 return request, publication50 return request, publication
4151
52
42def print_request_and_publication(host='localhost', port=None,53def print_request_and_publication(host='localhost', port=None,
43 method='GET',54 method='GET',
44 mime_type='text/html',55 mime_type='text/html',
@@ -56,3 +67,49 @@
56 print " %s: %s" % (name, value)67 print " %s: %s" % (name, value)
57 else:68 else:
58 print publication_classname69 print publication_classname
70
71
72def test_traverse(url):
73 """Traverse the url in the same way normal publishing occurs.
74
75 Returns a tuple of (object, view, request) where:
76 object is the last model object in the traversal chain
77 view is the defined view for the object at the specified url (if
78 the url didn't directly specify a view, then the view is the
79 default view for the object.
80 request is the request object resulting from the traversal. This
81 contains a populated traversed_objects list just as a browser
82 request would from a normal call into the app servers.
83
84 This call uses the currently logged in user, and does not start a new
85 transaction.
86 """
87 url_parts = urlsplit(url)
88 server_url = '://'.join(url_parts[0:2])
89 path_info = url_parts[2]
90 request, publication = get_request_and_publication(
91 host=url_parts[1], extra_environment={
92 'SERVER_URL': server_url,
93 'PATH_INFO': path_info})
94
95 request.setPublication(publication)
96 # We avoid calling publication.beforePublication because this starts a new
97 # transaction, which causes an abort of the existing transaction, and the
98 # removal of any created and uncommitted objects.
99
100 # Set the default layer.
101 adapters = zapi.getGlobalSiteManager().adapters
102 layer = adapters.lookup((providedBy(request),), IDefaultSkin, '')
103 if layer is not None:
104 layers.setAdditionalLayer(request, layer)
105
106 principal = publication.getPrincipal(request)
107 request.setPrincipal(principal)
108
109 getUtility(IOpenLaunchBag).clear()
110 app = publication.getApplication(request)
111 view = request.traverse(app)
112 # Since the last traversed object is the view, the second last should be
113 # the object that the view is on.
114 obj = request.traversed_objects[-2]
115 return obj, view, request
59116
=== modified file 'lib/lp/translations/browser/tests/test_breadcrumbs.py'
--- lib/lp/translations/browser/tests/test_breadcrumbs.py 2009-11-07 01:45:32 +0000
+++ lib/lp/translations/browser/tests/test_breadcrumbs.py 2010-04-29 11:26:16 +0000
@@ -7,12 +7,8 @@
77
8from canonical.lazr.utils import smartquote8from canonical.lazr.utils import smartquote
99
10from canonical.launchpad.layers import TranslationsLayer
11from canonical.launchpad.webapp.publisher import canonical_url
12from canonical.launchpad.webapp.tests.breadcrumbs import (
13 BaseBreadcrumbTestCase)
14
15from lp.services.worlddata.interfaces.language import ILanguageSet10from lp.services.worlddata.interfaces.language import ILanguageSet
11from lp.testing.breadcrumbs import BaseBreadcrumbTestCase
16from lp.translations.interfaces.distroserieslanguage import (12from lp.translations.interfaces.distroserieslanguage import (
17 IDistroSeriesLanguageSet)13 IDistroSeriesLanguageSet)
18from lp.translations.interfaces.productserieslanguage import (14from lp.translations.interfaces.productserieslanguage import (
@@ -20,111 +16,82 @@
20from lp.translations.interfaces.translationgroup import ITranslationGroupSet16from lp.translations.interfaces.translationgroup import ITranslationGroupSet
2117
2218
23class BaseTranslationsBreadcrumbTestCase(BaseBreadcrumbTestCase):19class TestTranslationsVHostBreadcrumb(BaseBreadcrumbTestCase):
24 request_layer = TranslationsLayer
25
26 def setUp(self):
27 super(BaseTranslationsBreadcrumbTestCase, self).setUp()
28 self.traversed_objects = [self.root]
29
30 def _testContextBreadcrumbs(self, traversal_list, links, texts, url=None):
31 self.traversed_objects.extend(traversal_list)
32 if url is None:
33 url = canonical_url(traversal_list[-1], rootsite='translations')
34
35 self.assertEquals(
36 links,
37 self._getBreadcrumbsURLs(url, self.traversed_objects))
38 self.assertEquals(
39 texts,
40 self._getBreadcrumbsTexts(url, self.traversed_objects))
41
42
43class TestTranslationsVHostBreadcrumb(BaseTranslationsBreadcrumbTestCase):
4420
45 def test_product(self):21 def test_product(self):
46 product = self.factory.makeProduct(22 product = self.factory.makeProduct(
47 name='crumb-tester', displayname="Crumb Tester")23 name='crumb-tester', displayname="Crumb Tester")
48 self._testContextBreadcrumbs(24 self.assertBreadcrumbs(
49 [product],25 [("Crumb Tester", 'http://launchpad.dev/crumb-tester'),
50 ['http://launchpad.dev/crumb-tester',26 ("Translations", 'http://translations.launchpad.dev/crumb-tester')],
51 'http://translations.launchpad.dev/crumb-tester'],27 product, rootsite='translations')
52 ["Crumb Tester", "Translations"])
5328
54 def test_productseries(self):29 def test_productseries(self):
55 product = self.factory.makeProduct(30 product = self.factory.makeProduct(
56 name='crumb-tester', displayname="Crumb Tester")31 name='crumb-tester', displayname="Crumb Tester")
57 series = self.factory.makeProductSeries(name="test", product=product)32 series = self.factory.makeProductSeries(name="test", product=product)
58 self._testContextBreadcrumbs(33 self.assertBreadcrumbs(
59 [product, series],34 [("Crumb Tester", 'http://launchpad.dev/crumb-tester'),
60 ['http://launchpad.dev/crumb-tester',35 ("Series test", 'http://launchpad.dev/crumb-tester/test'),
61 'http://launchpad.dev/crumb-tester/test',36 ("Translations", 'http://translations.launchpad.dev/crumb-tester/test')],
62 'http://translations.launchpad.dev/crumb-tester/test'],37 series, rootsite='translations')
63 ["Crumb Tester", "Series test", "Translations"])
6438
65 def test_distribution(self):39 def test_distribution(self):
66 distribution = self.factory.makeDistribution(40 distribution = self.factory.makeDistribution(
67 name='crumb-tester', displayname="Crumb Tester")41 name='crumb-tester', displayname="Crumb Tester")
68 self._testContextBreadcrumbs(42 self.assertBreadcrumbs(
69 [distribution],43 [("Crumb Tester", 'http://launchpad.dev/crumb-tester'),
70 ['http://launchpad.dev/crumb-tester',44 ("Translations", 'http://translations.launchpad.dev/crumb-tester')],
71 'http://translations.launchpad.dev/crumb-tester'],45 distribution, rootsite='translations')
72 ["Crumb Tester", "Translations"])
7346
74 def test_distroseries(self):47 def test_distroseries(self):
75 distribution = self.factory.makeDistribution(48 distribution = self.factory.makeDistribution(
76 name='crumb-tester', displayname="Crumb Tester")49 name='crumb-tester', displayname="Crumb Tester")
77 series = self.factory.makeDistroRelease(50 series = self.factory.makeDistroRelease(
78 name="test", version="1.0", distribution=distribution)51 name="test", version="1.0", distribution=distribution)
79 self._testContextBreadcrumbs(52 self.assertBreadcrumbs(
80 [distribution, series],53 [("Crumb Tester", 'http://launchpad.dev/crumb-tester'),
81 ['http://launchpad.dev/crumb-tester',54 ("Test (1.0)", 'http://launchpad.dev/crumb-tester/test'),
82 'http://launchpad.dev/crumb-tester/test',55 ("Translations", 'http://translations.launchpad.dev/crumb-tester/test')],
83 'http://translations.launchpad.dev/crumb-tester/test'],56 series, rootsite='translations')
84 ["Crumb Tester", "Test (1.0)", "Translations"])
8557
86 def test_project(self):58 def test_project(self):
87 project = self.factory.makeProject(59 project = self.factory.makeProject(
88 name='crumb-tester', displayname="Crumb Tester")60 name='crumb-tester', displayname="Crumb Tester")
89 self._testContextBreadcrumbs(61 self.assertBreadcrumbs(
90 [project],62 [("Crumb Tester", 'http://launchpad.dev/crumb-tester'),
91 ['http://launchpad.dev/crumb-tester',63 ("Translations", 'http://translations.launchpad.dev/crumb-tester')],
92 'http://translations.launchpad.dev/crumb-tester'],64 project, rootsite='translations')
93 ["Crumb Tester", "Translations"])
9465
95 def test_person(self):66 def test_person(self):
96 person = self.factory.makePerson(67 person = self.factory.makePerson(
97 name='crumb-tester', displayname="Crumb Tester")68 name='crumb-tester', displayname="Crumb Tester")
98 self._testContextBreadcrumbs(69 self.assertBreadcrumbs(
99 [person],70 [("Crumb Tester", 'http://launchpad.dev/~crumb-tester'),
100 ['http://launchpad.dev/~crumb-tester',71 ("Translations", 'http://translations.launchpad.dev/~crumb-tester')],
101 'http://translations.launchpad.dev/~crumb-tester'],72 person, rootsite='translations')
102 ["Crumb Tester", "Translations"])73
10374
10475class TestTranslationGroupsBreadcrumbs(BaseBreadcrumbTestCase):
105class TestTranslationGroupsBreadcrumbs(BaseTranslationsBreadcrumbTestCase):
10676
107 def test_translationgroupset(self):77 def test_translationgroupset(self):
108 group_set = getUtility(ITranslationGroupSet)78 group_set = getUtility(ITranslationGroupSet)
109 url = canonical_url(group_set, rootsite='translations')79 self.assertBreadcrumbs(
110 self._testContextBreadcrumbs(80 [("Translation groups", 'http://translations.launchpad.dev/+groups')],
111 [group_set],81 group_set, rootsite='translations')
112 ['http://translations.launchpad.dev/+groups'],
113 ['Translation groups'],
114 url=url)
11582
116 def test_translationgroup(self):83 def test_translationgroup(self):
117 group_set = getUtility(ITranslationGroupSet)
118 group = self.factory.makeTranslationGroup(84 group = self.factory.makeTranslationGroup(
119 name='test-translators', title='Test translators')85 name='test-translators', title='Test translators')
120 self._testContextBreadcrumbs(86 self.assertBreadcrumbs(
121 [group_set, group],87 [("Translation groups", 'http://translations.launchpad.dev/+groups'),
122 ["http://translations.launchpad.dev/+groups",88 ("Test translators",
123 "http://translations.launchpad.dev/+groups/test-translators"],89 'http://translations.launchpad.dev/+groups/test-translators')],
124 ["Translation groups", "Test translators"])90 group, rootsite='translations')
12591
12692
127class TestSeriesLanguageBreadcrumbs(BaseTranslationsBreadcrumbTestCase):93class TestSeriesLanguageBreadcrumbs(BaseBreadcrumbTestCase):
94
128 def setUp(self):95 def setUp(self):
129 super(TestSeriesLanguageBreadcrumbs, self).setUp()96 super(TestSeriesLanguageBreadcrumbs, self).setUp()
130 self.language = getUtility(ILanguageSet)['sr']97 self.language = getUtility(ILanguageSet)['sr']
@@ -134,15 +101,18 @@
134 name='crumb-tester', displayname="Crumb Tester")101 name='crumb-tester', displayname="Crumb Tester")
135 series = self.factory.makeDistroRelease(102 series = self.factory.makeDistroRelease(
136 name="test", version="1.0", distribution=distribution)103 name="test", version="1.0", distribution=distribution)
104 series.hide_all_translations = False
137 serieslanguage = getUtility(IDistroSeriesLanguageSet).getDummy(105 serieslanguage = getUtility(IDistroSeriesLanguageSet).getDummy(
138 series, self.language)106 series, self.language)
139 self._testContextBreadcrumbs(107
140 [distribution, series, serieslanguage],108 self.assertBreadcrumbs(
141 ["http://launchpad.dev/crumb-tester",109 [("Crumb Tester", "http://launchpad.dev/crumb-tester"),
142 "http://launchpad.dev/crumb-tester/test",110 ("Test (1.0)", "http://launchpad.dev/crumb-tester/test"),
143 "http://translations.launchpad.dev/crumb-tester/test",111 ("Translations",
144 "http://translations.launchpad.dev/crumb-tester/test/+lang/sr"],112 "http://translations.launchpad.dev/crumb-tester/test"),
145 ["Crumb Tester", "Test (1.0)", "Translations", "Serbian (sr)"])113 ("Serbian (sr)",
114 "http://translations.launchpad.dev/crumb-tester/test/+lang/sr")],
115 serieslanguage)
146116
147 def test_productserieslanguage(self):117 def test_productserieslanguage(self):
148 product = self.factory.makeProduct(118 product = self.factory.makeProduct(
@@ -151,57 +121,58 @@
151 name="test", product=product)121 name="test", product=product)
152 serieslanguage = getUtility(IProductSeriesLanguageSet).getDummy(122 serieslanguage = getUtility(IProductSeriesLanguageSet).getDummy(
153 series, self.language)123 series, self.language)
154 self._testContextBreadcrumbs(124
155 [product, series, serieslanguage],125 self.assertBreadcrumbs(
156 ["http://launchpad.dev/crumb-tester",126 [("Crumb Tester", "http://launchpad.dev/crumb-tester"),
157 "http://launchpad.dev/crumb-tester/test",127 ("Series test", "http://launchpad.dev/crumb-tester/test"),
158 "http://translations.launchpad.dev/crumb-tester/test",128 ("Translations",
159 "http://translations.launchpad.dev/crumb-tester/test/+lang/sr"],129 "http://translations.launchpad.dev/crumb-tester/test"),
160 ["Crumb Tester", "Series test", "Translations", "Serbian (sr)"])130 ("Serbian (sr)",
161131 "http://translations.launchpad.dev/crumb-tester/test/+lang/sr")],
162132 serieslanguage)
163class TestPOTemplateBreadcrumbs(BaseTranslationsBreadcrumbTestCase):133
134
135class TestPOTemplateBreadcrumbs(BaseBreadcrumbTestCase):
164 def test_potemplate(self):136 def test_potemplate(self):
165 product = self.factory.makeProduct(137 product = self.factory.makeProduct(
166 name='crumb-tester', displayname="Crumb Tester")138 name='crumb-tester', displayname="Crumb Tester",
139 official_rosetta=True)
167 series = self.factory.makeProductSeries(140 series = self.factory.makeProductSeries(
168 name="test", product=product)141 name="test", product=product)
169 potemplate = self.factory.makePOTemplate(name="template",142 potemplate = self.factory.makePOTemplate(
170 productseries=series)143 name="template", productseries=series)
171 self._testContextBreadcrumbs(144 self.assertBreadcrumbs(
172 [product, series, potemplate],145 [("Crumb Tester", "http://launchpad.dev/crumb-tester"),
173 ["http://launchpad.dev/crumb-tester",146 ("Series test", "http://launchpad.dev/crumb-tester/test"),
174 "http://launchpad.dev/crumb-tester/test",147 ("Translations",
175 "http://translations.launchpad.dev/crumb-tester/test",148 "http://translations.launchpad.dev/crumb-tester/test"),
176 "http://translations.launchpad.dev/crumb-tester/test"149 (smartquote('Template "template"'),
177 "/+pots/template"],150 "http://translations.launchpad.dev/crumb-tester/test/+pots/template")],
178 ["Crumb Tester", "Series test", "Translations",151 potemplate)
179 smartquote('Template "template"')])152
180153
181154class TestPOFileBreadcrumbs(BaseBreadcrumbTestCase):
182class TestPOFileBreadcrumbs(BaseTranslationsBreadcrumbTestCase):
183155
184 def setUp(self):156 def setUp(self):
185 super(TestPOFileBreadcrumbs, self).setUp()157 super(TestPOFileBreadcrumbs, self).setUp()
186 self.language = getUtility(ILanguageSet)['eo']
187 self.product = self.factory.makeProduct(
188 name='crumb-tester', displayname="Crumb Tester")
189 self.series = self.factory.makeProductSeries(
190 name="test", product=self.product)
191 self.potemplate = self.factory.makePOTemplate(self.series,
192 name="test-template")
193 self.pofile = self.factory.makePOFile('eo', self.potemplate)
194158
195 def test_pofiletranslate(self):159 def test_pofiletranslate(self):
196 self._testContextBreadcrumbs(160 product = self.factory.makeProduct(
197 [self.product, self.series, self.potemplate, self.pofile],161 name='crumb-tester', displayname="Crumb Tester",
198 ["http://launchpad.dev/crumb-tester",162 official_rosetta=True)
199 "http://launchpad.dev/crumb-tester/test",163 series = self.factory.makeProductSeries(name="test", product=product)
200 "http://translations.launchpad.dev/crumb-tester/test",164 potemplate = self.factory.makePOTemplate(series, name="test-template")
201 "http://translations.launchpad.dev/crumb-tester/test"165 pofile = self.factory.makePOFile('eo', potemplate)
202 "/+pots/test-template",166
203 "http://translations.launchpad.dev/crumb-tester/test"167 self.assertBreadcrumbs(
204 "/+pots/test-template/eo",168 [("Crumb Tester", "http://launchpad.dev/crumb-tester"),
205 ],169 ("Series test", "http://launchpad.dev/crumb-tester/test"),
206 ["Crumb Tester", "Series test", "Translations",170 ("Translations",
207 smartquote('Template "test-template"'), "Esperanto (eo)"])171 "http://translations.launchpad.dev/crumb-tester/test"),
172 (smartquote('Template "test-template"'),
173 "http://translations.launchpad.dev/crumb-tester/test"
174 "/+pots/test-template"),
175 ("Esperanto (eo)",
176 "http://translations.launchpad.dev/crumb-tester/test"
177 "/+pots/test-template/eo")],
178 pofile)
208179
=== modified file 'scripts/ftpmaster-tools/buildd-mass-retry.py'
--- scripts/ftpmaster-tools/buildd-mass-retry.py 2010-04-22 17:30:35 +0000
+++ scripts/ftpmaster-tools/buildd-mass-retry.py 2010-04-29 11:26:16 +0000
@@ -121,6 +121,12 @@
121 build_state=target_state, pocket=pocket)121 build_state=target_state, pocket=pocket)
122122
123 for build in target_builds:123 for build in target_builds:
124 # Skip builds for superseded sources; they won't ever
125 # actually build.
126 if not build.current_source_publication:
127 log.debug(
128 'Skipping superseded %s (%s)' % (build.title, build.id))
129 continue
124130
125 if not build.can_be_retried:131 if not build.can_be_retried:
126 log.warn('Can not retry %s (%s)' % (build.title, build.id))132 log.warn('Can not retry %s (%s)' % (build.title, build.id))
127133
=== modified file 'utilities/sourcedeps.conf'
--- utilities/sourcedeps.conf 2010-04-27 01:35:56 +0000
+++ utilities/sourcedeps.conf 2010-04-29 11:26:16 +0000
@@ -5,7 +5,12 @@
5bzr-svn lp:~launchpad-pqm/bzr-svn/devel;revno=27085bzr-svn lp:~launchpad-pqm/bzr-svn/devel;revno=2708
6cscvs lp:~launchpad-pqm/launchpad-cscvs/devel;revno=4326cscvs lp:~launchpad-pqm/launchpad-cscvs/devel;revno=432
7dulwich lp:~launchpad-pqm/dulwich/devel;revno=4187dulwich lp:~launchpad-pqm/dulwich/devel;revno=418
8<<<<<<< TREE
8loggerhead lp:~launchpad-pqm/loggerhead/devel;revno=1749loggerhead lp:~launchpad-pqm/loggerhead/devel;revno=174
10=======
11launchpad-loggerhead lp:~launchpad-pqm/launchpad-loggerhead/devel;revno=54
12loggerhead lp:~launchpad-pqm/loggerhead/devel;revno=176
13>>>>>>> MERGE-SOURCE
9lpreview lp:~launchpad-pqm/bzr-lpreview/devel;revno=2314lpreview lp:~launchpad-pqm/bzr-lpreview/devel;revno=23
10mailman lp:~launchpad-pqm/mailman/2.1;revno=97615mailman lp:~launchpad-pqm/mailman/2.1;revno=976
11old_xmlplus lp:~launchpad-pqm/dtdparser/trunk;revno=416old_xmlplus lp:~launchpad-pqm/dtdparser/trunk;revno=4

Subscribers

People subscribed via source and target branches

to status/vote changes: