Merge lp:~henninge/launchpad/bug-425583-languages into lp:launchpad/db-devel

Proposed by Henning Eggers
Status: Merged
Approved by: Henning Eggers
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~henninge/launchpad/bug-425583-languages
Merge into: lp:launchpad/db-devel
Diff against target: 1098 lines (+603/-116)
19 files modified
lib/canonical/launchpad/icing/style-3-0.css (+21/-1)
lib/canonical/testing/layers.py (+1/-1)
lib/lp/registry/browser/person.py (+16/-8)
lib/lp/services/worlddata/javascript/languages.js (+66/-0)
lib/lp/soyuz/doc/publishing.txt (+50/-11)
lib/lp/soyuz/interfaces/publishing.py (+1/-1)
lib/lp/soyuz/model/publishing.py (+58/-22)
lib/lp/soyuz/scripts/packagecopier.py (+3/-3)
lib/lp/soyuz/scripts/tests/test_copypackage.py (+47/-9)
lib/lp/soyuz/stories/ppa/xx-copy-packages.txt (+1/-1)
lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt (+32/-10)
lib/lp/soyuz/stories/soyuz/xx-packagepublishinghistory.txt (+49/-0)
lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt (+5/-3)
lib/lp/soyuz/templates/packagepublishing-details.pt (+1/-1)
lib/lp/translations/browser/language.py (+44/-10)
lib/lp/translations/browser/tests/language-views.txt (+25/-0)
lib/lp/translations/stories/standalone/xx-language.txt (+2/-2)
lib/lp/translations/templates/languageset-index.pt (+74/-33)
lib/lp/translations/windmill/tests/test_languages.py (+107/-0)
To merge this branch: bzr merge lp:~henninge/launchpad/bug-425583-languages
Reviewer Review Type Date Requested Status
Muharem Hrnjadovic (community) code Approve
Michael Nelson (community) ui Approve
Review via email: mp+15594@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Henning Eggers (henninge) wrote :

= Bug 425593 =

This branch adds the UI elements for the user's preferred languages and the full list of languages in Launchpad as described in the bug report. Adding the breadcrumb (as mentioned in the bug) was already done by some other branch.

As discussed in the bug report, the page changes the search form to a filter form and filters the dispĺayed languages on the client side.

== Implementation notes ==

Although the preferred languages display is copied from what is on the person index page (person-portlet-contact-details.pt) it does not share code. I wanted the languages to be links, too. Also, constructing a comma-speparated list is not easily done in TAL and can get ugly, so I am generating the HTML snippet in the view code. I *could* use the TAL formatter here but I think the icons might be confusing when used in such a list.

Displaying the language listing was a breeze, as there is a TAL link formatter for ILanguage. Also I had found out about the "two-column-list" css class earlier and could easily construct a "three-column-list".

For the JS code I created a new file in the lib/lp hierarchy and linked to it from lib/canonical/launchpad. I assumed that this is the new way of doing things and that l/c/l will go away eventually, won't it?

The diff for the template got quite long as I put everything into the top-portlet and had to adapt the indention.

== Tests ==

bin/test -vvt language-views.txt

Windmill tests to follow.

== Demo/QA ==

Got to https://translations.launchpad.dev/+languages as user carlos and see the three preferred languages he has. Also have fun filtering languages. ;)

== Launchpad Lint ==

I merged db-devel to make use of yui3final, so "make lint" is confused now, trying to diff against devel. So, no useful lint output available, sorry.

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

Henning, this is great! The responsiveness of your JS solution is so much better than a page-load. I'm only marking it as needs fixing because of where you end up if you edit preferred languages.

UI-wise, at first I wasn't sure about having the language icon next to each language (as there is nothing to differentiate visually here), but then looking at other pages, such as:

https://edge.launchpad.net/~launchpad/+members

we display person icons for every member (although there are a few team icons thrown in there to justify it). What are your thought? I'm tending towards leaving it (even if we don't need to differentiate visually between the languages, the icon looks nice and is consistent with the rest of LP).

If I click on the link to edit my preferred languages, and update them, I expected to land back at this page, but it looks as though the preferred languages page always redirects back to the user profile page. That should be easy enough to fix (a next_url in the view which defaults to profile?).

Also, it should only be a few extra lines to call hide_and_show on a key press event for the input box (and hide the button). Do you want to give it a go, or did you decide against it for other reasons?

IRC Notes:
<henninge> noodles775: Guten Morgen! ;)
<noodles775> Hi hennige!
* noodles775 has changed the topic to: on-call: noodles775 || reviewing: - || queue [] || This channel is logged: http://irclogs.ubuntu.com || https://code.edge.launchpad.net/launchpad/+activereviews
<noodles775> :-D
<henninge> noodles775: ah, you are the ocr! :)
<noodles775> Yeah, myself and al-maisan are around today.
* al-maisan will be around after preparing his cappuccino <wink> :)
<henninge> noodles775: cool, let me prepare an mp for a final ui review. I'd like to have your input on the languages page.
<noodles775> Wow - that was fast?
<henninge> noodles775: well, I did not go for using and improving the lazr-js widget, I am sorry.
<henninge> noodles775: but I quite like what's come out of it and I only had yesterday to do it. ;)
<noodles775> henninge: no, I didn't expect you to do that as part of the initial branch (it's not necessary for the improved behavior, it'd just be a nice bling later).
<henninge> noodles775: It *does* have the client-side filter, just not as-you-type. You need to click.
<henninge> oh, and windmill test ist still missing ...
<noodles775> Aha.
* adeuring (<email address hidden>) has joined #launchpad-reviews
<henninge> noodles775: ok, mp is done.
<henninge> noodles775: I'd ask you to please branch it and have a look at the page.
<noodles775> Great, yep, I always do :-)
* noodles775 has changed the topic to: on-call: noodles775 || reviewing: - || queue [henninge/ui,henninge/code] || This channel is logged: http://irclogs.ubuntu.com || https://code.edge.launchpad.net/launchpad/+activereviews
<henninge> noodles775: also, I am not sure if I am writing good js/yui code.
<noodles775> OK, I'll check it out too.
<henninge> noodles775: fyi, just pushed a new version.
<noodles775> henninge: is this dependent on other changes currently in db-devel only? Or why are you targeting db-devel?
<henninge> noodles775: well, I just don't ...

Read more...

review: Needs Fixing (ui)
Revision history for this message
Henning Eggers (henninge) wrote :

> Henning, this is great! The responsiveness of your JS solution is so much
> better than a page-load.

Thank you, I like it a lot, too. ;)

> UI-wise, at first I wasn't sure about having the language icon next to each
> language (as there is nothing to differentiate visually here), but then
> looking at other pages, such as:

Yes, you are right. It actually makes for clearer view code, too, using the tal formatter. Thanks for prodding me to do it, I was just to lazy to remember how to use a tal formatter in view code ...

> If I click on the link to edit my preferred languages, and update them, I
> expected to land back at this page, but it looks as though the preferred
> languages page always redirects back to the user profile page. That should be
> easy enough to fix (a next_url in the view which defaults to profile?).

I did that because I don't like things like that happening to me, either. But it was a bit more work because the view code was not handling the redirection correctly in the first place.

> Also, it should only be a few extra lines to call hide_and_show on a key press
> event for the input box (and hide the button). Do you want to give it a go, or
> did you decide against it for other reasons?

I tried it out now and it actually saves a few lines of js code but it is not very responsive. I'd have to do the hide_and_show asynchronously but I don't want to put that much work into it since searching for languages is not a daily task anybody would do (as apposed to say adding tags to a bug).

>
> IRC Notes:

I got the "unseen" class thing sorted out, it was shadowed by a more specific setting of display. I added an even more specific one. ;)

Thanks for the review, I think this really looks good now.

Revision history for this message
Michael Nelson (michael.nelson) wrote :

> > If I click on the link to edit my preferred languages, and update them, I
> > expected to land back at this page, but it looks as though the preferred
> > languages page always redirects back to the user profile page. That should
> be
> > easy enough to fix (a next_url in the view which defaults to profile?).
>
> I did that because I don't like things like that happening to me, either. But
> it was a bit more work because the view code was not handling the redirection
> correctly in the first place.

Thanks for fixing it even though it was more work than expected!

>
> > Also, it should only be a few extra lines to call hide_and_show on a key
> press
> > event for the input box (and hide the button). Do you want to give it a go,
> or
> > did you decide against it for other reasons?
>
> I tried it out now and it actually saves a few lines of js code but it is not
> very responsive. I'd have to do the hide_and_show asynchronously but I don't
> want to put that much work into it since searching for languages is not a
> daily task anybody would do (as apposed to say adding tags to a bug).

Yes, good point. Another thought (for the future) is that we could only call hide_and_show 1 second *after* the last keypress. But as you say, it's not so important. It works really well the way you've got it.

>
> >
> > IRC Notes:
>
> I got the "unseen" class thing sorted out, it was shadowed by a more specific
> setting of display. I added an even more specific one. ;)
>
> Thanks for the review, I think this really looks good now.

np! I learned lots :)

review: Approve (ui)
Revision history for this message
Henning Eggers (henninge) wrote :

= Windmill test =

bin/test --layer=TranslationsWindmillLayer --test=filter_languages

Revision history for this message
Muharem Hrnjadovic (al-maisan) wrote :

Looks good!

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/icing/style-3-0.css'
2--- lib/canonical/launchpad/icing/style-3-0.css 2009-12-05 03:11:48 +0000
3+++ lib/canonical/launchpad/icing/style-3-0.css 2009-12-08 11:40:29 +0000
4@@ -289,13 +289,33 @@
5 display: inline;
6 margin: 0 0.25em 0.75em 0;
7 }
8+.three-column-list dl {
9+ width: 31%;
10+ float: left;
11+ display: inline;
12+ margin: 0 0.25em 0.75em 0;
13+ }
14 .two-column-list li {
15 width: 48%;
16 float: left;
17 display: inline;
18 margin: 0 0.25em 0 0;
19 }
20-.two-column-list:after {
21+.three-column-list li {
22+ width: 31%;
23+ float: left;
24+ display: inline;
25+ margin: 0 0.25em 0 0;
26+ }
27+/* Keep the abilty to hide list entries. */
28+.two-column-list dl.unseen,
29+.two-column-list li.unseen,
30+.three-column-list dl.unseen,
31+.three-column-list li.unseen {
32+ display: none;
33+ }
34+.two-column-list:after,
35+.three-column-list:after {
36 content: ".";
37 display: block;
38 height: 0;
39
40=== added symlink 'lib/canonical/launchpad/javascript/worlddata'
41=== target is u'../../../lp/services/worlddata/javascript'
42=== modified file 'lib/canonical/testing/layers.py'
43--- lib/canonical/testing/layers.py 2009-10-31 12:07:16 +0000
44+++ lib/canonical/testing/layers.py 2009-12-08 11:40:29 +0000
45@@ -475,7 +475,7 @@
46 "Librarian has been killed or has hung."
47 "Tests should use LibrarianLayer.hide() and "
48 "LibrarianLayer.reveal() where possible, and ensure "
49- "the Librarian is restarted if it absolutetly must be "
50+ "the Librarian is restarted if it absolutely must be "
51 "shutdown: " + str(e)
52 )
53 if LibrarianLayer._reset_between_tests:
54
55=== modified file 'lib/lp/registry/browser/person.py'
56--- lib/lp/registry/browser/person.py 2009-12-08 04:58:31 +0000
57+++ lib/lp/registry/browser/person.py 2009-12-08 11:40:29 +0000
58@@ -2447,7 +2447,8 @@
59 def getRedirectionURL(self):
60 request = self.request
61 referrer = request.getHeader('referer')
62- if referrer and referrer.startswith(request.getApplicationURL()):
63+ if referrer and (referrer.startswith(request.getApplicationURL()) or
64+ referrer.find('+languages') != -1):
65 return referrer
66 else:
67 return ''
68@@ -2459,10 +2460,20 @@
69
70 @property
71 def next_url(self):
72- """Redirect to this url after successfully processing the form."""
73- return canonical_url(self.context)
74-
75- cancel_url = next_url
76+ """Redirect back to the +languages page if request originated there."""
77+ redirection_url = self.request.get('redirection_url')
78+ if redirection_url:
79+ return redirection_url
80+ return canonical_url(self.context)
81+
82+ @property
83+ def cancel_url(self):
84+ """Redirect back to the +languages page if request originated there."""
85+ redirection_url = self.getRedirectionURL()
86+ if redirection_url:
87+ return redirection_url
88+ return canonical_url(self.context)
89+
90
91 @action(_("Save"), name="save")
92 def submitLanguages(self, action, data):
93@@ -2502,9 +2513,6 @@
94 if len(messages) > 0:
95 message = structured('<br />'.join(messages))
96 self.request.response.addInfoNotification(message)
97- redirection_url = self.request.get('redirection_url')
98- if redirection_url:
99- self.request.response.redirect(redirection_url)
100
101 @property
102 def answers_url(self):
103
104=== added directory 'lib/lp/services/worlddata/javascript'
105=== added file 'lib/lp/services/worlddata/javascript/languages.js'
106--- lib/lp/services/worlddata/javascript/languages.js 1970-01-01 00:00:00 +0000
107+++ lib/lp/services/worlddata/javascript/languages.js 2009-12-08 11:40:29 +0000
108@@ -0,0 +1,66 @@
109+/* Copyright 2009 Canonical Ltd. This software is licensed under the
110+ * GNU Affero General Public License version 3 (see the file LICENSE).
111+ *
112+ * @module Languages
113+ * @requires oop, event, node
114+ */
115+
116+YUI.add('languages', function(Y) {
117+
118+var languages = Y.namespace('languages');
119+
120+/* Prefilled in initialize_language_page. */
121+var all_languages;
122+
123+var hide_and_show = function(searchstring) {
124+ searchstring = searchstring.toLowerCase();
125+ var count_matches = 0;
126+ all_languages.each(function(element, index, list) {
127+ var href = element.get('href');
128+ var code = href.substr(href.lastIndexOf("/")+1);
129+ var english_name = element.get('text').toLowerCase();
130+ var comment_start = english_name.indexOf(' (');
131+ if(comment_start != -1) {
132+ english_name = english_name.substring(0, comment_start);
133+ }
134+ if(code.indexOf(searchstring) == -1 &&
135+ english_name.indexOf(searchstring) == -1) {
136+ element.ancestor('li').addClass('unseen');
137+ }
138+ else {
139+ count_matches = count_matches +1;
140+ element.ancestor('li').removeClass('unseen');
141+ }
142+ });
143+ var no_filter_matches = Y.get('#no_filter_matches');
144+ if(count_matches == 0) {
145+ no_filter_matches.removeClass('unseen');
146+ }
147+ else {
148+ no_filter_matches.addClass('unseen');
149+ }
150+};
151+
152+var init_filter_form = function() {
153+ var heading = Y.get('.searchform h2');
154+ heading.setContent('Filter languages in Launchpad');
155+ var button = Y.get('.searchform input.submit');
156+ var inputfind = Y.get('.searchform input.textType');
157+ button.set('value', 'Filter languages');
158+ all_languages = Y.all('#all-languages li a');
159+ button.on('click', function(e){
160+ e.preventDefault();
161+ hide_and_show(inputfind.get('value'));
162+ });
163+};
164+
165+
166+languages.initialize_languages_page = function(Y) {
167+ init_filter_form();
168+
169+};
170+
171+
172+// "oop" and "event" are required to fix known bugs in YUI, which
173+// are apparently fixed in a later version.
174+}, "0.1", {"requires": ["oop", "event", "node"]});
175
176=== modified file 'lib/lp/soyuz/doc/publishing.txt'
177--- lib/lp/soyuz/doc/publishing.txt 2009-11-18 23:56:26 +0000
178+++ lib/lp/soyuz/doc/publishing.txt 2009-12-08 11:40:29 +0000
179@@ -1055,22 +1055,61 @@
180 each build found.
181
182 >>> cprov_builds.count()
183- 7
184+ 8
185
186 The `ResultSet` is ordered by ascending
187 `SourcePackagePublishingHistory.id` and ascending
188 `DistroArchseries.architecturetag` in this order.
189
190- >>> source_pub, build, arch = cprov_builds.last()
191-
192- >>> print source_pub.displayname
193- foo 666 in breezy-autotest
194-
195- >>> print build.title
196- i386 build of foo 666 in ubuntutest breezy-autotest RELEASE
197-
198- >>> print arch.displayname
199- ubuntutest Breezy Badger Autotest i386
200+ # The easiest thing we can do here (without printing ids)
201+ # is to show that sorting a list of the resulting ids+tags does not
202+ # modify the list.
203+ >>> ids_and_tags = [(pub.id, arch.architecturetag)
204+ ... for pub, build, arch in cprov_builds]
205+ >>> ids_and_tags == sorted(ids_and_tags)
206+ True
207+
208+If a source package is copied from another archive (including the
209+binaries), then the related builds for that source package will
210+also be retrievable via the copied source publication.
211+For example, if a package is built in a private security PPA, and then
212+later copied out into the primary archive, the builds will then
213+be available when looking at the copied source package in the primary
214+archive.
215+
216+ # Create a new PPA and publish a source with some builds
217+ # and binaries.
218+ >>> other_ppa = factory.makeArchive(name="otherppa")
219+ >>> binaries = test_publisher.getPubBinaries(archive=other_ppa)
220+
221+The associated builds and binaries will be created in the context of the
222+other PPA.
223+
224+ >>> build = binaries[0].binarypackagerelease.build
225+ >>> source_pub = build.sourcepackagerelease.publishings[0]
226+ >>> print build.archive.name
227+ otherppa
228+
229+ # Copy the source into Celso's PPA, ensuring that the binaries
230+ # are alse published there.
231+ >>> source_pub_cprov = source_pub.copyTo(
232+ ... source_pub.distroseries, source_pub.pocket,
233+ ... cprov.archive)
234+ >>> binaries_cprov = test_publisher.publishBinaryInArchive(
235+ ... binaries[0].binarypackagerelease, cprov.archive)
236+
237+Now we will see an extra source in Celso's PPA as well as an extra
238+build - even though the build's context is not Celso's PPA. Previously
239+there were 8 sources and builds.
240+
241+ >>> cprov_sources_new = cprov.archive.getPublishedSources()
242+ >>> cprov_sources_new.count()
243+ 9
244+
245+ >>> cprov_builds_new = publishing_set.getBuildsForSources(
246+ ... cprov_sources_new)
247+ >>> cprov_builds_new.count()
248+ 9
249
250 Next we'll create two sources with two builds each (the SoyuzTestPublisher
251 default) and show that the number of unpublished builds for these sources
252
253=== modified file 'lib/lp/soyuz/interfaces/publishing.py'
254--- lib/lp/soyuz/interfaces/publishing.py 2009-11-18 23:56:26 +0000
255+++ lib/lp/soyuz/interfaces/publishing.py 2009-12-08 11:40:29 +0000
256@@ -511,7 +511,7 @@
257 "Return an IDistributionSourcePackageRelease meta object "
258 "correspondent to the sourcepackagerelease attribute")
259 meta_supersededby = Attribute(
260- "Return an IDistribuitionSourcePackageRelease meta object "
261+ "Return an IDistributionSourcePackageRelease meta object "
262 "correspondent to the supersededby attribute. if supersededby "
263 "is None return None.")
264 meta_distroseriessourcepackagerelease = Attribute(
265
266=== modified file 'lib/lp/soyuz/model/publishing.py'
267--- lib/lp/soyuz/model/publishing.py 2009-11-19 00:26:13 +0000
268+++ lib/lp/soyuz/model/publishing.py 2009-12-08 11:40:29 +0000
269@@ -40,6 +40,7 @@
270 from canonical.database.enumcol import EnumCol
271 from lp.registry.interfaces.pocket import PackagePublishingPocket
272 from lp.soyuz.model.binarypackagename import BinaryPackageName
273+from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease
274 from lp.soyuz.model.files import (
275 BinaryPackageFile, SourcePackageReleaseFile)
276 from canonical.launchpad.database.librarian import (
277@@ -593,7 +594,7 @@
278 # not blow up because of bad data.
279 return None
280 source, packageupload, spr, changesfile, lfc = result
281-
282+
283 # Return a webapp-proxied LibraryFileAlias so that restricted
284 # librarian files are accessible. Non-restricted files will get
285 # a 302 so that webapp threads are not tied up.
286@@ -1262,23 +1263,64 @@
287 Build.buildstate.is_in(build_states))
288
289 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
290- result_set = store.find(
291- (SourcePackagePublishingHistory, Build, DistroArchSeries),
292+
293+ # We'll be looking for builds in the same distroseries as the
294+ # SPPH for the same release.
295+ builds_for_distroseries_expr = (
296 Build.distroarchseriesID == DistroArchSeries.id,
297+ SourcePackagePublishingHistory.distroseriesID ==
298+ DistroArchSeries.distroseriesID,
299+ SourcePackagePublishingHistory.sourcepackagereleaseID ==
300+ Build.sourcepackagereleaseID,
301+ In(SourcePackagePublishingHistory.id, source_publication_ids)
302+ )
303+
304+ # First, we'll find the builds that were built in the same
305+ # archive context as the published sources.
306+ builds_in_same_archive = store.find(
307+ Build,
308+ builds_for_distroseries_expr,
309 SourcePackagePublishingHistory.archiveID == Build.archiveID,
310- SourcePackagePublishingHistory.distroseriesID ==
311- DistroArchSeries.distroseriesID,
312- SourcePackagePublishingHistory.sourcepackagereleaseID ==
313- Build.sourcepackagereleaseID,
314- In(SourcePackagePublishingHistory.id, source_publication_ids),
315- *extra_exprs)
316-
317- result_set.order_by(
318+ *extra_exprs)
319+
320+ # Next get all the builds that have a binary published in the
321+ # same archive... even though the build was not built in
322+ # the same context archive.
323+ builds_copied_into_archive = store.find(
324+ Build,
325+ builds_for_distroseries_expr,
326+ SourcePackagePublishingHistory.archiveID != Build.archiveID,
327+ BinaryPackagePublishingHistory.archive ==
328+ SourcePackagePublishingHistory.archiveID,
329+ BinaryPackagePublishingHistory.binarypackagerelease ==
330+ BinaryPackageRelease.id,
331+ BinaryPackageRelease.build == Build.id,
332+ *extra_exprs)
333+
334+ builds_union = builds_copied_into_archive.union(
335+ builds_in_same_archive).config(distinct=True)
336+
337+ # Now that we have a result_set of all the builds, we'll use it
338+ # as a subquery to get the required publishing and arch to do
339+ # the ordering. We do this in this round-about way because we
340+ # can't sort on SourcePackagePublishingHistory.id after the
341+ # union. See bug 443353 for details.
342+ find_spec = (
343+ SourcePackagePublishingHistory, Build, DistroArchSeries)
344+
345+ # Storm doesn't let us do builds_union.values('id') -
346+ # ('Union' object has no attribute 'columns'). So instead
347+ # we have to instantiate the objects just to get the id.
348+ build_ids = [build.id for build in builds_union]
349+
350+ result_set = store.find(
351+ find_spec, builds_for_distroseries_expr,
352+ Build.id.is_in(build_ids))
353+
354+ return result_set.order_by(
355 SourcePackagePublishingHistory.id,
356 DistroArchSeries.architecturetag)
357
358- return result_set
359-
360 def getByIdAndArchive(self, id, archive, source=True):
361 """See `IPublishingSet`."""
362 if source:
363@@ -1317,12 +1359,10 @@
364 def _getSourceBinaryJoinForSources(self, source_publication_ids,
365 active_binaries_only=True):
366 """Return the join linking sources with binaries."""
367- # Import Build, BinaryPackageRelease and DistroArchSeries locally
368+ # Import Build and DistroArchSeries locally
369 # to avoid circular imports, since Build uses
370 # SourcePackagePublishingHistory, BinaryPackageRelease uses Build
371 # and DistroArchSeries uses BinaryPackagePublishingHistory.
372- from lp.soyuz.model.binarypackagerelease import (
373- BinaryPackageRelease)
374 from lp.soyuz.model.build import Build
375 from lp.soyuz.model.distroarchseries import (
376 DistroArchSeries)
377@@ -1397,12 +1437,8 @@
378
379 def getBinaryFilesForSources(self, one_or_more_source_publications):
380 """See `IPublishingSet`."""
381- # Import Build and BinaryPackageRelease locally to avoid circular
382- # imports, since that Build already imports
383- # SourcePackagePublishingHistory and BinaryPackageRelease imports
384- # Build.
385- from lp.soyuz.model.binarypackagerelease import (
386- BinaryPackageRelease)
387+ # Import Build locally to avoid circular imports, since that
388+ # Build already imports SourcePackagePublishingHistory.
389 from lp.soyuz.model.build import Build
390
391 source_publication_ids = self._extractIDs(
392
393=== modified file 'lib/lp/soyuz/scripts/packagecopier.py'
394--- lib/lp/soyuz/scripts/packagecopier.py 2009-11-10 13:09:26 +0000
395+++ lib/lp/soyuz/scripts/packagecopier.py 2009-12-08 11:40:29 +0000
396@@ -352,10 +352,10 @@
397 :raise CannotCopy when a copy is not allowed to be performed
398 containing the reason of the error.
399 """
400- if source.distroseries.distribution != self.archive.distribution:
401+ if series not in self.archive.distribution.series:
402 raise CannotCopy(
403- "Cannot copy to an unsupported distribution: %s." %
404- source.distroseries.distribution.name)
405+ "No such distro series %s in distribution %s." %
406+ (series.name, source.distroseries.distribution.name))
407
408 format = SourcePackageFormat.getTermByToken(
409 source.sourcepackagerelease.dsc_format).value
410
411=== modified file 'lib/lp/soyuz/scripts/tests/test_copypackage.py'
412--- lib/lp/soyuz/scripts/tests/test_copypackage.py 2009-11-17 21:38:28 +0000
413+++ lib/lp/soyuz/scripts/tests/test_copypackage.py 2009-12-08 11:40:29 +0000
414@@ -696,23 +696,45 @@
415 'source has expired binaries',
416 copy_checker.checkCopy, source, series, pocket)
417
418- def test_checkCopy_forbids_copies_from_other_distributions(self):
419+ def test_checkCopy_allows_copies_from_other_distributions(self):
420+ # It is possible to copy packages between distributions,
421+ # as long as the target distroseries exists for the target
422+ # distribution.
423+
424+ # Create a testing source in ubuntu.
425+ ubuntu = getUtility(IDistributionSet).getByName('debian')
426+ sid = ubuntu.getSeries('sid')
427+ source = self.test_publisher.getPubSource(distroseries=sid)
428+
429+ # Create a fresh PPA for ubuntutest, which will be the copy
430+ # destination.
431+ archive = self.factory.makeArchive(
432+ distribution=self.test_publisher.ubuntutest,
433+ purpose=ArchivePurpose.PPA)
434+ series = self.test_publisher.ubuntutest.getSeries('hoary-test')
435+ pocket = source.pocket
436+
437+ # Copy of sources to series in another distribution can be
438+ # performed.
439+ copy_checker = CopyChecker(archive, include_binaries=False)
440+ copy_checker.checkCopy(source, series, pocket)
441+
442+ def test_checkCopy_forbids_copies_to_unknown_distroseries(self):
443 # We currently deny copies to series that are not for the Archive
444 # distribution, because they will never be published. And abandoned
445 # copies like these keep triggering the PPA publication spending
446 # resources.
447
448 # Create a testing source in ubuntu.
449- ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
450- hoary = ubuntu.getSeries('hoary')
451- source = self.test_publisher.getPubSource(distroseries=hoary)
452+ ubuntu = getUtility(IDistributionSet).getByName('debian')
453+ sid = ubuntu.getSeries('sid')
454+ source = self.test_publisher.getPubSource(distroseries=sid)
455
456 # Create a fresh PPA for ubuntutest, which will be the copy
457 # destination.
458 archive = self.factory.makeArchive(
459 distribution=self.test_publisher.ubuntutest,
460 purpose=ArchivePurpose.PPA)
461- series = source.distroseries
462 pocket = source.pocket
463
464 # Copy of sources to series in another distribution, cannot be
465@@ -720,8 +742,8 @@
466 copy_checker = CopyChecker(archive, include_binaries=False)
467 self.assertRaisesWithContent(
468 CannotCopy,
469- 'Cannot copy to an unsupported distribution: ubuntu.',
470- copy_checker.checkCopy, source, series, pocket)
471+ 'No such distro series sid in distribution debian.',
472+ copy_checker.checkCopy, source, sid, pocket)
473
474 def test_checkCopy_respects_sourceformatselection(self):
475 # A source copy should be denied if the source's dsc_format is
476@@ -974,7 +996,8 @@
477 # The returned object has a more descriptive 'displayname'
478 # attribute than plain `IPackageUpload` instances.
479 self.assertEquals(
480- 'Delayed copy of foocomm - 1.0-2 (source, i386, raw-dist-upgrader)',
481+ 'Delayed copy of foocomm - '
482+ '1.0-2 (source, i386, raw-dist-upgrader)',
483 delayed_copy.displayname)
484
485 # It is targeted to the right publishing context.
486@@ -1368,6 +1391,21 @@
487 target_archive = copy_helper.destination.archive
488 self.checkCopies(copied, target_archive, 3)
489
490+ # The second copy will fail explicitly because the new BPPH
491+ # records are not yet published.
492+ nothing_copied = copy_helper.mainTask()
493+ self.assertEqual(len(nothing_copied), 0)
494+ self.assertEqual(
495+ copy_helper.logger.buffer.getvalue().splitlines()[-1],
496+ 'ERROR: foo 666 in hoary (same version has unpublished binaries '
497+ 'in the destination archive for Hoary, please wait for them to '
498+ 'be published before copying)')
499+
500+ # If we ensure that the copied binaries are published, the
501+ # copy won't fail but will simply not copy anything.
502+ for bin_pub in copied[1:3]:
503+ bin_pub.secure_record.setPublished()
504+
505 nothing_copied = copy_helper.mainTask()
506 self.assertEqual(len(nothing_copied), 0)
507 self.assertEqual(
508@@ -1504,7 +1542,7 @@
509 name='boing')
510 self.assertEqual(copied_source.displayname, 'boing 1.0 in hoary')
511 self.assertEqual(len(copied_source.getPublishedBinaries()), 2)
512- self.assertEqual(len(copied_source.getBuilds()), 0)
513+ self.assertEqual(len(copied_source.getBuilds()), 1)
514
515 def _setupArchitectureGrowingScenario(self, architecturehintlist="all"):
516 """Prepare distroseries with different sets of architectures.
517
518=== modified file 'lib/lp/soyuz/stories/ppa/xx-copy-packages.txt'
519--- lib/lp/soyuz/stories/ppa/xx-copy-packages.txt 2009-10-13 10:05:58 +0000
520+++ lib/lp/soyuz/stories/ppa/xx-copy-packages.txt 2009-12-08 11:40:29 +0000
521@@ -1062,7 +1062,7 @@
522 >>> print_ppa_packages(jblack_browser.contents)
523 Source Published Status Series Section Build
524 Status
525- foo - 2.0 (changesfile) Pending Hoary Base
526+ foo - 2.0 (changesfile) Pending Hoary Base i386
527 foo - 1.1 (changesfile) Pending Warty Base
528 pmount - 0.1-1 Pending Hoary Editors
529 pmount - 0.1-1 Pending Warty Editors
530
531=== modified file 'lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt'
532--- lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt 2009-11-05 10:51:36 +0000
533+++ lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt 2009-12-08 11:40:29 +0000
534@@ -129,29 +129,51 @@
535 If a the binaries for a package are fully built, but have not yet been
536 published, this will be indicated to the viewer:
537
538- >>> anon_browser.open(
539- ... "http://launchpad.dev/~cprov/+archive/ppa/+packages")
540- >>> expander_url = anon_browser.getLink(id='pub28-expander').url
541+ # First, we'll update the binary publishing history for the i386
542+ # record so that it is pending publication.
543+ >>> login('foo.bar@canonical.com')
544+ >>> from zope.component import getUtility
545+ >>> from lp.registry.interfaces.person import IPersonSet
546+ >>> cprov_ppa = getUtility(IPersonSet).getByName('cprov').archive
547+ >>> pmount_i386_pub = cprov_ppa.getAllPublishedBinaries(
548+ ... name='pmount', version='0.1-1')[1]
549+ >>> print pmount_i386_pub.displayname
550+ pmount 0.1-1 in warty i386
551+ >>> from lp.soyuz.interfaces.publishing import PackagePublishingStatus
552+ >>> pmount_i386_pub.secure_record.status = PackagePublishingStatus.PENDING
553+ >>> pmount_i386_pub.secure_record.datepublished = None
554+ >>> transaction.commit()
555+ >>> logout()
556+
557+ # Now, to re-display the pmount expanded section:
558 >>> anon_browser.open(expander_url)
559 >>> print extract_text(anon_browser.contents)
560 Note: Some binary packages for this source are not yet published in the
561 repository.
562 Publishing details
563 Published on 2007-07-09
564- Copied from ubuntu warty in PPA for Mark Shuttleworth
565+ Copied from ubuntu hoary in Primary Archive for Ubuntu Linux
566 Changelog
567+ pmount (0.1-1) hoary; urgency=low
568+ * Fix description (Malone #1)
569+ * Fix debian (Debian #2000)
570+ * Fix warty (Warty Ubuntu #1)
571+ -- Sample Person...
572 Builds
573 i386 - Pending publication
574 Built packages
575- mozilla-firefox ff from iceweasel
576+ pmount
577+ pmount shortdesc
578 Package files
579- firefox_0.9.2.orig.tar.gz (9.5 MiB)
580- iceweasel-1.0.dsc (123 bytes)
581- mozilla-firefox_0.9_i386.deb (3 bytes)
582+ No files published for this package.
583
584-The package was copied from a PPA. The archive title will hence link
585+When the package is copied from a PPA, the archive title will link
586 back to the source PPA.
587
588+ >>> anon_browser.open(
589+ ... "http://launchpad.dev/~cprov/+archive/ppa/+packages")
590+ >>> expander_url = anon_browser.getLink(id='pub28-expander').url
591+ >>> anon_browser.open(expander_url)
592 >>> anon_browser.getLink("PPA for Mark Shuttleworth").url
593 'http://launchpad.dev/~mark/+archive/ppa'
594
595@@ -164,7 +186,7 @@
596 >>> admin_browser.getControl(name="field.buildd_secret").value = "secret"
597 >>> admin_browser.getControl("Save").click()
598
599- >>> anon_browser.open("http://launchpad.dev/~cprov/+archive/ppa")
600+ >>> anon_browser.open(expander_url)
601 >>> anon_browser.getLink("PPA for Mark Shuttleworth")
602 Traceback (most recent call last):
603 ...
604
605=== added file 'lib/lp/soyuz/stories/soyuz/xx-packagepublishinghistory.txt'
606--- lib/lp/soyuz/stories/soyuz/xx-packagepublishinghistory.txt 1970-01-01 00:00:00 +0000
607+++ lib/lp/soyuz/stories/soyuz/xx-packagepublishinghistory.txt 2009-12-08 11:40:29 +0000
608@@ -0,0 +1,49 @@
609+= Publishing History Page =
610+
611+The Publishing History page hangs off a distribution source page and
612+shows the complete history of a package in all series.
613+
614+ >>> from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
615+ >>> from lp.soyuz.interfaces.publishing import (
616+ ... PackagePublishingStatus)
617+ >>> stp = SoyuzTestPublisher()
618+ >>> login('foo.bar@canonical.com')
619+ >>> stp.prepareBreezyAutotest()
620+ >>> source_pub = stp.getPubSource(
621+ ... "test-history", status=PackagePublishingStatus.PUBLISHED)
622+ >>> logout()
623+
624+ >>> anon_browser.open(
625+ ... 'http://launchpad.dev/ubuntutest/+source/test-history/'
626+ ... '+publishinghistory')
627+
628+ >>> print extract_text(
629+ ... find_tag_by_id(anon_browser.contents, 'publishing-summary'))
630+ Date Status Target Pocket Component Section Version
631+ ... UTC Published Breezy ... release main base 666
632+ Published ... ago
633+
634+A publishing record will be shown as deleted in the publishing history after a
635+request for deletion by a user.
636+
637+ >>> login('foo.bar@canonical.com')
638+ >>> unused = source_pub.requestDeletion(stp.factory.makePerson(), "fix bug 1")
639+ >>> logout()
640+
641+ >>> anon_browser.open(
642+ ... 'http://launchpad.dev/ubuntutest/+source/test-history/'
643+ ... '+publishinghistory')
644+
645+ >>> table = find_tag_by_id(anon_browser.contents, 'publishing-summary')
646+ >>> print extract_text(table)
647+ Date Status Target Pocket Component Section Version
648+ Deleted Breezy ... release main base 666
649+ Deleted ... ago by ... fix bug 1
650+ Published ... ago
651+
652+Links to bug reports are added for bugs mentioned in the removal comment.
653+
654+ >>> print anon_browser.getLink("bug 1").url
655+ http://launchpad.dev/bugs/1
656+
657+
658
659=== modified file 'lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt'
660--- lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt 2009-11-18 23:56:26 +0000
661+++ lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt 2009-12-08 11:40:29 +0000
662@@ -207,18 +207,20 @@
663 ======================
664
665 The source publication object has a custom operation called 'getBuilds' and
666-it returns the build records in the context of that publication.
667+it returns the build records for builds that were built in the same context
668+archive as the publication, or builds from other archives but where the
669+binaries have been copied and published in the same context archive.
670
671 >>> pubs = webservice.named_get(
672 ... cprov_archive['self_link'], 'getPublishedSources',
673- ... source_name="iceweasel", version="1.0",
674+ ... source_name="pmount", version="0.1-1",
675 ... exact_match=True).jsonBody()
676 >>> source_pub = pubs['entries'][0]
677 >>> builds = webservice.named_get(
678 ... source_pub['self_link'], 'getBuilds').jsonBody()
679 >>> for entry in sorted(builds['entries']):
680 ... print entry['title']
681- i386 build of iceweasel 1.0 in ubuntu warty RELEASE
682+ i386 build of pmount 0.1-1 in ubuntu warty RELEASE
683
684
685 Finding related Binary publications
686
687=== modified file 'lib/lp/soyuz/templates/packagepublishing-details.pt'
688--- lib/lp/soyuz/templates/packagepublishing-details.pt 2009-07-17 17:59:07 +0000
689+++ lib/lp/soyuz/templates/packagepublishing-details.pt 2009-12-08 11:40:29 +0000
690@@ -20,7 +20,7 @@
691 tal:content="context/datesuperseded/fmt:displaydate" />
692 by <a tal:replace="structure context/removed_by/fmt:link"/>
693 <div tal:condition="context/removal_comment"
694- tal:content="context/removal_comment" />
695+ tal:content="structure context/removal_comment/fmt:text-to-html" />
696 </li>
697 <li tal:condition="view/wasSuperseded">
698 <strong>Superseded</strong>
699
700=== modified file 'lib/lp/translations/browser/language.py'
701--- lib/lp/translations/browser/language.py 2009-11-27 14:18:05 +0000
702+++ lib/lp/translations/browser/language.py 2009-12-08 11:40:29 +0000
703@@ -17,17 +17,22 @@
704 from zope.lifecycleevent import ObjectCreatedEvent
705 from zope.component import getUtility
706 from zope.event import notify
707+from zope.app.form.browser import TextWidget
708+from zope.interface import Interface
709+from zope.schema import TextLine
710
711 from canonical.cachedproperty import cachedproperty
712+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
713+from canonical.launchpad.webapp import (
714+ action, canonical_url, ContextMenu, custom_widget,
715+ enabled_with_permission, GetitemNavigation, LaunchpadEditFormView,
716+ LaunchpadFormView, LaunchpadView, Link, NavigationMenu)
717 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
718+from canonical.launchpad.webapp.tales import LanguageFormatterAPI
719 from lp.services.worlddata.interfaces.language import ILanguage, ILanguageSet
720 from lp.translations.interfaces.translationsperson import (
721 ITranslationsPerson)
722 from lp.translations.browser.translations import TranslationsMixin
723-from canonical.launchpad.webapp import (
724- action, canonical_url, ContextMenu, custom_widget,
725- enabled_with_permission, GetitemNavigation, LaunchpadEditFormView,
726- LaunchpadFormView, LaunchpadView, Link, NavigationMenu)
727 from lp.translations.utilities.pluralforms import make_friendly_plural_forms
728 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
729
730@@ -80,16 +85,37 @@
731 return Link('+admin', text, icon='edit')
732
733
734-class LanguageSetView:
735+def _format_language(language):
736+ """Format a language as a link."""
737+ return LanguageFormatterAPI(language).link(None)
738+
739+
740+class ILanguageSetSearch(Interface):
741+ """The collection of languages."""
742+
743+ search_lang = TextLine(
744+ title=u'Name of the language to search for.',
745+ required=True)
746+
747+class LanguageSetView(LaunchpadFormView):
748 """View class to render main ILanguageSet page."""
749 label = "Languages in Launchpad"
750 page_title = "Languages"
751
752- def __init__(self, context, request):
753- self.context = context
754- self.request = request
755- form = self.request.form
756- self.language_search = form.get('find')
757+ schema = ILanguageSetSearch
758+
759+ custom_widget('search_lang', TextWidget, displayWidth=30)
760+
761+ def initialize(self):
762+ """See `LaunchpadFormView`."""
763+ LaunchpadFormView.initialize(self)
764+
765+ self.language_search = None
766+
767+ search_lang_widget = self.widgets.get('search_lang')
768+ if (search_lang_widget is not None and
769+ search_lang_widget.hasValidInput()):
770+ self.language_search = search_lang_widget.getInputValue()
771 self.search_requested = self.language_search is not None
772
773 @cachedproperty
774@@ -103,6 +129,14 @@
775 else:
776 return 0
777
778+ @cachedproperty
779+ def user_languages(self):
780+ """The user's preferred languages, or English if none are set."""
781+ languages = list(self.user.languages)
782+ if len(languages) == 0:
783+ languages = [getUtility(ILaunchpadCelebrities).english]
784+ return ", ".join(map(_format_language, languages))
785+
786
787 # There is no easy way to remove an ILanguage from the database due all the
788 # dependencies that ILanguage would have. That's the reason why we don't have
789
790=== modified file 'lib/lp/translations/browser/tests/language-views.txt'
791--- lib/lp/translations/browser/tests/language-views.txt 2009-09-17 21:03:25 +0000
792+++ lib/lp/translations/browser/tests/language-views.txt 2009-12-08 11:40:29 +0000
793@@ -95,4 +95,29 @@
794 1 : 2, 3, 4, 22, 23, 24...
795 2 : 5, 6, 7, 8, 9, 10...
796
797+View LanguageSet
798+------------------
799+
800+ >>> login('carlos@canonical.com')
801+ >>> languageset_view = create_initialized_view(language_set, '+index',
802+ ... layer=TranslationsLayer)
803+
804+The user_languages property contains a list of the current user's preferred
805+languages formated as links.
806+
807+ >>> print languageset_view.user_languages
808+ <a href=".../ca" ...>Catalan</a>,
809+ <a href=".../en" ...>English</a>,
810+ <a href=".../es" ...>Spanish</a>
811+
812+For a user without any preferred languages, English will be returned.
813+
814+ >>> person = factory.makePerson()
815+ >>> print person.languages
816+ []
817+ >>> login_person(person)
818+ >>> languageset_view = create_initialized_view(language_set, '+index',
819+ ... layer=TranslationsLayer)
820+ >>> print languageset_view.user_languages
821+ <a href=".../en" ...>English</a>
822
823
824=== modified file 'lib/lp/translations/stories/standalone/xx-language.txt'
825--- lib/lp/translations/stories/standalone/xx-language.txt 2009-11-27 14:18:05 +0000
826+++ lib/lp/translations/stories/standalone/xx-language.txt 2009-12-08 11:40:29 +0000
827@@ -95,11 +95,11 @@
828 >>> print browser.url
829 http://translations.launchpad.dev/+languages
830
831- >>> text_search = browser.getControl(name='find')
832+ >>> text_search = browser.getControl(name='field.search_lang')
833 >>> text_search.value = 'Spanish'
834 >>> browser.getControl('Find language', index=0).click()
835 >>> print browser.url
836- http://translations.launchpad.dev/+languages/+index?find=Spanish
837+ http://translations.launchpad.dev/+languages/+index?field.search_lang=Spanish
838
839
840 Read language information
841
842=== modified file 'lib/lp/translations/templates/languageset-index.pt'
843--- lib/lp/translations/templates/languageset-index.pt 2009-10-31 11:06:44 +0000
844+++ lib/lp/translations/templates/languageset-index.pt 2009-12-08 11:40:29 +0000
845@@ -7,6 +7,20 @@
846 >
847
848 <body>
849+ <div metal:fill-slot="head_epilogue">
850+ <script
851+ type="text/javascript"
852+ tal:condition="devmode"
853+ tal:attributes="src string:${icingroot}/build/worlddata/languages.js">
854+ </script>
855+ <script type="text/javascript">
856+ YUI().use('languages', 'event', function(Y) {
857+ Y.on('domready', function(e) {
858+ Y.languages.initialize_languages_page(Y);
859+ });
860+ });
861+ </script>
862+ </div>
863 <div metal:fill-slot="main">
864 <div class="yui-b top-portlet">
865 <p>
866@@ -24,8 +38,17 @@
867 </a>
868 FAQ entry.
869 </p>
870-
871- <div class="portlet">
872+ <dl id="preferred_languages"
873+ tal:condition="view/user">
874+ <dt>Your preferred languages:
875+ <a tal:attributes="href string:${view/user/fmt:url}/+editlanguages"
876+ class="edit sprite"></a>
877+ </dt>
878+ <dd tal:content="structure view/user_languages">
879+ English
880+ </dd>
881+ </dl>
882+ <div class="portlet searchform">
883 <h2>Find a language in Launchpad</h2>
884 <form method="get">
885 <p>
886@@ -33,49 +56,67 @@
887 Language name/code contains:
888 </label>
889 <input
890- name="find" size="30"
891- tal:attributes="value view/language_search" />
892+ size="30"
893+ tal:replace="structure view/widgets/search_lang"
894+ />
895 <input
896+ class="submit"
897 type="submit"
898 value="Find language"
899 />
900 </p>
901+ <tal:none condition="not: view/search_matches">
902+ <script type="text/javascript"
903+ tal:define="script view/focusedElementScript"
904+ tal:condition="script"
905+ tal:content="structure script" />
906+ </tal:none>
907 </form>
908- <div tal:condition="context/required:launchpad.Admin">
909+ </div>
910+ <div tal:condition="not:view/search_requested"
911+ class="portlet">
912+ <h2>Available languages in Launchpad</h2>
913+ <p tal:condition="context/required:launchpad.Admin">
914 <a tal:attributes="href context/fmt:url/+add"
915- class="add sprite">Add new language</a>
916- </div>
917+ class="add sprite">Add new language</a>
918+ </p>
919+ <p id="no_filter_matches" class="unseen">
920+ No languages are matching your filter.
921+ </p>
922+ <ul id="all-languages" class="three-column-list">
923+ <li tal:repeat="language context/common_languages">
924+ <a tal:replace="structure language/fmt:link">English</a>
925+ </li>
926+ </ul>
927 </div>
928- </div>
929- <div tal:condition="view/search_requested" class="yui-b portlet">
930- <tal:block tal:define="results view/search_results">
931- <tal:none condition="not: view/search_matches">
932- <h2>No matching languages</h2>
933- <p>No languages matching
934- &#8220;<span tal:replace="view/language_search">
935- Foo
936- </span>&#8221; were found.
937- </p>
938- </tal:none>
939- <tal:one condition="python: view.search_matches == 1">
940- <h2>One matching language</h2>
941- </tal:one>
942- <tal:more condition="python: view.search_matches > 1">
943- <h2>
944- <tal:count replace="view/search_matches">3</tal:count>
945- matching languages
946- </h2>
947- </tal:more>
948- <ul condition="view/search_matches" class="languages">
949- <tal:language repeat="language results">
950- <li>
951+ <div tal:condition="view/search_requested" class="yui-b portlet">
952+ <tal:block tal:define="results view/search_results">
953+ <tal:none condition="not: view/search_matches">
954+ <h2>No matching languages</h2>
955+ <p>No languages matching
956+ &#8220;<span tal:replace="view/language_search">
957+ Foo
958+ </span>&#8221; were found.
959+ </p>
960+ </tal:none>
961+ <tal:one condition="python: view.search_matches == 1">
962+ <h2>One matching language</h2>
963+ </tal:one>
964+ <tal:more condition="python: view.search_matches > 1">
965+ <h2>
966+ <tal:count replace="view/search_matches">3</tal:count>
967+ matching languages
968+ </h2>
969+ </tal:more>
970+ <ul condition="view/search_matches" class="three-column-list">
971+ <li language tal:repeat="language results">
972 <a tal:replace="structure language/fmt:link">
973 Serbian (sr)
974 </a>
975 </li>
976- </tal:language>
977- </ul>
978- </tal:block>
979+ </ul>
980+ </tal:block>
981+ </div>
982 </div>
983 </div>
984 </body>
985
986=== added file 'lib/lp/translations/windmill/tests/test_languages.py'
987--- lib/lp/translations/windmill/tests/test_languages.py 1970-01-01 00:00:00 +0000
988+++ lib/lp/translations/windmill/tests/test_languages.py 2009-12-08 11:40:29 +0000
989@@ -0,0 +1,107 @@
990+# Copyright 2009 Canonical Ltd. This software is licensed under the
991+# GNU Affero General Public License version 3 (see the file LICENSE).
992+
993+"""Test for languages listing and filtering behaviour."""
994+
995+__metaclass__ = type
996+__all__ = []
997+
998+import transaction
999+
1000+from windmill.authoring import WindmillTestClient
1001+from zope.component import getUtility
1002+
1003+from canonical.launchpad.windmill.testing.constants import (
1004+ FOR_ELEMENT, PAGE_LOAD, SLEEP)
1005+from lp.translations.windmill.testing import TranslationsWindmillLayer
1006+from lp.testing import TestCaseWithFactory
1007+
1008+INPUT_FIELD=(u"//div[contains(@class,'searchform')]"+
1009+ u"//input[@id='field.search_lang']")
1010+FILTER_BUTTON=(u"//div[contains(@class,'searchform')]"+
1011+ u"//input[@value='Filter languages']")
1012+LANGUAGE=u"//a[contains(@class, 'language') and text()='%s']/parent::li"
1013+UNSEEN_VALIDATOR='className|unseen'
1014+
1015+
1016+class LanguagesFilterTest(TestCaseWithFactory):
1017+ """Test that filtering on the +languages page works."""
1018+
1019+ layer = TranslationsWindmillLayer
1020+
1021+ def _enter_filter_string(self, filterstring):
1022+ self.client.type(xpath=INPUT_FIELD, text=filterstring)
1023+ self.client.click(xpath=FILTER_BUTTON)
1024+ self.client.waits.sleep(milliseconds=SLEEP)
1025+
1026+ def _assert_languages_visible(self, languages):
1027+ for language, visibility in languages.items():
1028+ xpath = LANGUAGE % language
1029+ if visibility:
1030+ self.client.asserts.assertNotProperty(
1031+ xpath=xpath, validator=UNSEEN_VALIDATOR)
1032+ else:
1033+ self.client.asserts.assertProperty(
1034+ xpath=xpath, validator=UNSEEN_VALIDATOR)
1035+
1036+ def test_filter_languages(self):
1037+ """Test that filtering on the +languages page works.
1038+
1039+ The test cannot fully cover all languages on the page and so just
1040+ tests three with a search string of 'de':
1041+ German, because it's language code is 'de' but the names does not,
1042+ Mende, because it contains a 'de' but the language code does not,
1043+ French, because neither its name nor language code contain 'de'.
1044+ """
1045+ self.client = WindmillTestClient('Languages filter')
1046+ start_url = 'http://translations.launchpad.dev:8085/+languages'
1047+ # Go to the languages page
1048+ self.client.open(url=start_url)
1049+ self.client.waits.forPageLoad(timeout=PAGE_LOAD)
1050+
1051+ # "Not-matching" message is hidden and languages are visible.
1052+ self.client.asserts.assertProperty(
1053+ id=u'no_filter_matches',
1054+ validator='className|unseen')
1055+ self._assert_languages_visible({
1056+ u'German': True,
1057+ u'Mende': True,
1058+ u'French': True,
1059+ })
1060+
1061+ # Enter search string, search and wait.
1062+ self._enter_filter_string(u"de")
1063+ # "Not-matching" message and French are hidden now.
1064+ self.client.asserts.assertProperty(
1065+ id=u'no_filter_matches',
1066+ validator='className|unseen')
1067+ self._assert_languages_visible({
1068+ u'German': True,
1069+ u'Mende': True,
1070+ u'French': False,
1071+ })
1072+
1073+ # Enter not matching search string, search and wait.
1074+ self._enter_filter_string(u"xxxxxx")
1075+ # "Not-matching" message is shown, all languages are hidden.
1076+ self.client.asserts.assertNotProperty(
1077+ id=u'no_filter_matches',
1078+ validator='className|unseen')
1079+ self._assert_languages_visible({
1080+ u'German': False,
1081+ u'Mende': False,
1082+ u'French': False,
1083+ })
1084+
1085+ # Enter empty search string, search and wait.
1086+ self._enter_filter_string(u"")
1087+ # "Not-matching" message is hidden, all languages are visible again.
1088+ self.client.asserts.assertProperty(
1089+ id=u'no_filter_matches',
1090+ validator='className|unseen')
1091+ self._assert_languages_visible({
1092+ u'German': True,
1093+ u'Mende': True,
1094+ u'French': True,
1095+ })
1096+
1097
1098=== removed directory 'lib/lp/translations/windmill/tests/test_translations'

Subscribers

People subscribed via source and target branches

to status/vote changes: