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

Subscribers

People subscribed via source and target branches

to status/vote changes: