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
=== modified file 'lib/canonical/launchpad/icing/style-3-0.css'
--- lib/canonical/launchpad/icing/style-3-0.css 2009-12-05 03:11:48 +0000
+++ lib/canonical/launchpad/icing/style-3-0.css 2009-12-08 11:40:29 +0000
@@ -289,13 +289,33 @@
289 display: inline;289 display: inline;
290 margin: 0 0.25em 0.75em 0;290 margin: 0 0.25em 0.75em 0;
291 }291 }
292.three-column-list dl {
293 width: 31%;
294 float: left;
295 display: inline;
296 margin: 0 0.25em 0.75em 0;
297 }
292.two-column-list li {298.two-column-list li {
293 width: 48%;299 width: 48%;
294 float: left;300 float: left;
295 display: inline;301 display: inline;
296 margin: 0 0.25em 0 0;302 margin: 0 0.25em 0 0;
297 }303 }
298.two-column-list:after {304.three-column-list li {
305 width: 31%;
306 float: left;
307 display: inline;
308 margin: 0 0.25em 0 0;
309 }
310/* Keep the abilty to hide list entries. */
311.two-column-list dl.unseen,
312.two-column-list li.unseen,
313.three-column-list dl.unseen,
314.three-column-list li.unseen {
315 display: none;
316 }
317.two-column-list:after,
318.three-column-list:after {
299 content: ".";319 content: ".";
300 display: block;320 display: block;
301 height: 0;321 height: 0;
302322
=== added symlink 'lib/canonical/launchpad/javascript/worlddata'
=== target is u'../../../lp/services/worlddata/javascript'
=== modified file 'lib/canonical/testing/layers.py'
--- lib/canonical/testing/layers.py 2009-10-31 12:07:16 +0000
+++ lib/canonical/testing/layers.py 2009-12-08 11:40:29 +0000
@@ -475,7 +475,7 @@
475 "Librarian has been killed or has hung."475 "Librarian has been killed or has hung."
476 "Tests should use LibrarianLayer.hide() and "476 "Tests should use LibrarianLayer.hide() and "
477 "LibrarianLayer.reveal() where possible, and ensure "477 "LibrarianLayer.reveal() where possible, and ensure "
478 "the Librarian is restarted if it absolutetly must be "478 "the Librarian is restarted if it absolutely must be "
479 "shutdown: " + str(e)479 "shutdown: " + str(e)
480 )480 )
481 if LibrarianLayer._reset_between_tests:481 if LibrarianLayer._reset_between_tests:
482482
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2009-12-08 04:58:31 +0000
+++ lib/lp/registry/browser/person.py 2009-12-08 11:40:29 +0000
@@ -2447,7 +2447,8 @@
2447 def getRedirectionURL(self):2447 def getRedirectionURL(self):
2448 request = self.request2448 request = self.request
2449 referrer = request.getHeader('referer')2449 referrer = request.getHeader('referer')
2450 if referrer and referrer.startswith(request.getApplicationURL()):2450 if referrer and (referrer.startswith(request.getApplicationURL()) or
2451 referrer.find('+languages') != -1):
2451 return referrer2452 return referrer
2452 else:2453 else:
2453 return ''2454 return ''
@@ -2459,10 +2460,20 @@
24592460
2460 @property2461 @property
2461 def next_url(self):2462 def next_url(self):
2462 """Redirect to this url after successfully processing the form."""2463 """Redirect back to the +languages page if request originated there."""
2463 return canonical_url(self.context)2464 redirection_url = self.request.get('redirection_url')
24642465 if redirection_url:
2465 cancel_url = next_url2466 return redirection_url
2467 return canonical_url(self.context)
2468
2469 @property
2470 def cancel_url(self):
2471 """Redirect back to the +languages page if request originated there."""
2472 redirection_url = self.getRedirectionURL()
2473 if redirection_url:
2474 return redirection_url
2475 return canonical_url(self.context)
2476
24662477
2467 @action(_("Save"), name="save")2478 @action(_("Save"), name="save")
2468 def submitLanguages(self, action, data):2479 def submitLanguages(self, action, data):
@@ -2502,9 +2513,6 @@
2502 if len(messages) > 0:2513 if len(messages) > 0:
2503 message = structured('<br />'.join(messages))2514 message = structured('<br />'.join(messages))
2504 self.request.response.addInfoNotification(message)2515 self.request.response.addInfoNotification(message)
2505 redirection_url = self.request.get('redirection_url')
2506 if redirection_url:
2507 self.request.response.redirect(redirection_url)
25082516
2509 @property2517 @property
2510 def answers_url(self):2518 def answers_url(self):
25112519
=== added directory 'lib/lp/services/worlddata/javascript'
=== added file 'lib/lp/services/worlddata/javascript/languages.js'
--- lib/lp/services/worlddata/javascript/languages.js 1970-01-01 00:00:00 +0000
+++ lib/lp/services/worlddata/javascript/languages.js 2009-12-08 11:40:29 +0000
@@ -0,0 +1,66 @@
1/* Copyright 2009 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 *
4 * @module Languages
5 * @requires oop, event, node
6 */
7
8YUI.add('languages', function(Y) {
9
10var languages = Y.namespace('languages');
11
12/* Prefilled in initialize_language_page. */
13var all_languages;
14
15var hide_and_show = function(searchstring) {
16 searchstring = searchstring.toLowerCase();
17 var count_matches = 0;
18 all_languages.each(function(element, index, list) {
19 var href = element.get('href');
20 var code = href.substr(href.lastIndexOf("/")+1);
21 var english_name = element.get('text').toLowerCase();
22 var comment_start = english_name.indexOf(' (');
23 if(comment_start != -1) {
24 english_name = english_name.substring(0, comment_start);
25 }
26 if(code.indexOf(searchstring) == -1 &&
27 english_name.indexOf(searchstring) == -1) {
28 element.ancestor('li').addClass('unseen');
29 }
30 else {
31 count_matches = count_matches +1;
32 element.ancestor('li').removeClass('unseen');
33 }
34 });
35 var no_filter_matches = Y.get('#no_filter_matches');
36 if(count_matches == 0) {
37 no_filter_matches.removeClass('unseen');
38 }
39 else {
40 no_filter_matches.addClass('unseen');
41 }
42};
43
44var init_filter_form = function() {
45 var heading = Y.get('.searchform h2');
46 heading.setContent('Filter languages in Launchpad');
47 var button = Y.get('.searchform input.submit');
48 var inputfind = Y.get('.searchform input.textType');
49 button.set('value', 'Filter languages');
50 all_languages = Y.all('#all-languages li a');
51 button.on('click', function(e){
52 e.preventDefault();
53 hide_and_show(inputfind.get('value'));
54 });
55};
56
57
58languages.initialize_languages_page = function(Y) {
59 init_filter_form();
60
61};
62
63
64// "oop" and "event" are required to fix known bugs in YUI, which
65// are apparently fixed in a later version.
66}, "0.1", {"requires": ["oop", "event", "node"]});
067
=== modified file 'lib/lp/soyuz/doc/publishing.txt'
--- lib/lp/soyuz/doc/publishing.txt 2009-11-18 23:56:26 +0000
+++ lib/lp/soyuz/doc/publishing.txt 2009-12-08 11:40:29 +0000
@@ -1055,22 +1055,61 @@
1055each build found.1055each build found.
10561056
1057 >>> cprov_builds.count()1057 >>> cprov_builds.count()
1058 71058 8
10591059
1060The `ResultSet` is ordered by ascending1060The `ResultSet` is ordered by ascending
1061`SourcePackagePublishingHistory.id` and ascending1061`SourcePackagePublishingHistory.id` and ascending
1062`DistroArchseries.architecturetag` in this order.1062`DistroArchseries.architecturetag` in this order.
10631063
1064 >>> source_pub, build, arch = cprov_builds.last()1064 # The easiest thing we can do here (without printing ids)
10651065 # is to show that sorting a list of the resulting ids+tags does not
1066 >>> print source_pub.displayname1066 # modify the list.
1067 foo 666 in breezy-autotest1067 >>> ids_and_tags = [(pub.id, arch.architecturetag)
10681068 ... for pub, build, arch in cprov_builds]
1069 >>> print build.title1069 >>> ids_and_tags == sorted(ids_and_tags)
1070 i386 build of foo 666 in ubuntutest breezy-autotest RELEASE1070 True
10711071
1072 >>> print arch.displayname1072If a source package is copied from another archive (including the
1073 ubuntutest Breezy Badger Autotest i3861073binaries), then the related builds for that source package will
1074also be retrievable via the copied source publication.
1075For example, if a package is built in a private security PPA, and then
1076later copied out into the primary archive, the builds will then
1077be available when looking at the copied source package in the primary
1078archive.
1079
1080 # Create a new PPA and publish a source with some builds
1081 # and binaries.
1082 >>> other_ppa = factory.makeArchive(name="otherppa")
1083 >>> binaries = test_publisher.getPubBinaries(archive=other_ppa)
1084
1085The associated builds and binaries will be created in the context of the
1086other PPA.
1087
1088 >>> build = binaries[0].binarypackagerelease.build
1089 >>> source_pub = build.sourcepackagerelease.publishings[0]
1090 >>> print build.archive.name
1091 otherppa
1092
1093 # Copy the source into Celso's PPA, ensuring that the binaries
1094 # are alse published there.
1095 >>> source_pub_cprov = source_pub.copyTo(
1096 ... source_pub.distroseries, source_pub.pocket,
1097 ... cprov.archive)
1098 >>> binaries_cprov = test_publisher.publishBinaryInArchive(
1099 ... binaries[0].binarypackagerelease, cprov.archive)
1100
1101Now we will see an extra source in Celso's PPA as well as an extra
1102build - even though the build's context is not Celso's PPA. Previously
1103there were 8 sources and builds.
1104
1105 >>> cprov_sources_new = cprov.archive.getPublishedSources()
1106 >>> cprov_sources_new.count()
1107 9
1108
1109 >>> cprov_builds_new = publishing_set.getBuildsForSources(
1110 ... cprov_sources_new)
1111 >>> cprov_builds_new.count()
1112 9
10741113
1075Next we'll create two sources with two builds each (the SoyuzTestPublisher1114Next we'll create two sources with two builds each (the SoyuzTestPublisher
1076default) and show that the number of unpublished builds for these sources1115default) and show that the number of unpublished builds for these sources
10771116
=== modified file 'lib/lp/soyuz/interfaces/publishing.py'
--- lib/lp/soyuz/interfaces/publishing.py 2009-11-18 23:56:26 +0000
+++ lib/lp/soyuz/interfaces/publishing.py 2009-12-08 11:40:29 +0000
@@ -511,7 +511,7 @@
511 "Return an IDistributionSourcePackageRelease meta object "511 "Return an IDistributionSourcePackageRelease meta object "
512 "correspondent to the sourcepackagerelease attribute")512 "correspondent to the sourcepackagerelease attribute")
513 meta_supersededby = Attribute(513 meta_supersededby = Attribute(
514 "Return an IDistribuitionSourcePackageRelease meta object "514 "Return an IDistributionSourcePackageRelease meta object "
515 "correspondent to the supersededby attribute. if supersededby "515 "correspondent to the supersededby attribute. if supersededby "
516 "is None return None.")516 "is None return None.")
517 meta_distroseriessourcepackagerelease = Attribute(517 meta_distroseriessourcepackagerelease = Attribute(
518518
=== modified file 'lib/lp/soyuz/model/publishing.py'
--- lib/lp/soyuz/model/publishing.py 2009-11-19 00:26:13 +0000
+++ lib/lp/soyuz/model/publishing.py 2009-12-08 11:40:29 +0000
@@ -40,6 +40,7 @@
40from canonical.database.enumcol import EnumCol40from canonical.database.enumcol import EnumCol
41from lp.registry.interfaces.pocket import PackagePublishingPocket41from lp.registry.interfaces.pocket import PackagePublishingPocket
42from lp.soyuz.model.binarypackagename import BinaryPackageName42from lp.soyuz.model.binarypackagename import BinaryPackageName
43from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease
43from lp.soyuz.model.files import (44from lp.soyuz.model.files import (
44 BinaryPackageFile, SourcePackageReleaseFile)45 BinaryPackageFile, SourcePackageReleaseFile)
45from canonical.launchpad.database.librarian import (46from canonical.launchpad.database.librarian import (
@@ -593,7 +594,7 @@
593 # not blow up because of bad data.594 # not blow up because of bad data.
594 return None595 return None
595 source, packageupload, spr, changesfile, lfc = result596 source, packageupload, spr, changesfile, lfc = result
596 597
597 # Return a webapp-proxied LibraryFileAlias so that restricted598 # Return a webapp-proxied LibraryFileAlias so that restricted
598 # librarian files are accessible. Non-restricted files will get599 # librarian files are accessible. Non-restricted files will get
599 # a 302 so that webapp threads are not tied up.600 # a 302 so that webapp threads are not tied up.
@@ -1262,23 +1263,64 @@
1262 Build.buildstate.is_in(build_states))1263 Build.buildstate.is_in(build_states))
12631264
1264 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)1265 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
1265 result_set = store.find(1266
1266 (SourcePackagePublishingHistory, Build, DistroArchSeries),1267 # We'll be looking for builds in the same distroseries as the
1268 # SPPH for the same release.
1269 builds_for_distroseries_expr = (
1267 Build.distroarchseriesID == DistroArchSeries.id,1270 Build.distroarchseriesID == DistroArchSeries.id,
1271 SourcePackagePublishingHistory.distroseriesID ==
1272 DistroArchSeries.distroseriesID,
1273 SourcePackagePublishingHistory.sourcepackagereleaseID ==
1274 Build.sourcepackagereleaseID,
1275 In(SourcePackagePublishingHistory.id, source_publication_ids)
1276 )
1277
1278 # First, we'll find the builds that were built in the same
1279 # archive context as the published sources.
1280 builds_in_same_archive = store.find(
1281 Build,
1282 builds_for_distroseries_expr,
1268 SourcePackagePublishingHistory.archiveID == Build.archiveID,1283 SourcePackagePublishingHistory.archiveID == Build.archiveID,
1269 SourcePackagePublishingHistory.distroseriesID ==1284 *extra_exprs)
1270 DistroArchSeries.distroseriesID,1285
1271 SourcePackagePublishingHistory.sourcepackagereleaseID ==1286 # Next get all the builds that have a binary published in the
1272 Build.sourcepackagereleaseID,1287 # same archive... even though the build was not built in
1273 In(SourcePackagePublishingHistory.id, source_publication_ids),1288 # the same context archive.
1274 *extra_exprs)1289 builds_copied_into_archive = store.find(
12751290 Build,
1276 result_set.order_by(1291 builds_for_distroseries_expr,
1292 SourcePackagePublishingHistory.archiveID != Build.archiveID,
1293 BinaryPackagePublishingHistory.archive ==
1294 SourcePackagePublishingHistory.archiveID,
1295 BinaryPackagePublishingHistory.binarypackagerelease ==
1296 BinaryPackageRelease.id,
1297 BinaryPackageRelease.build == Build.id,
1298 *extra_exprs)
1299
1300 builds_union = builds_copied_into_archive.union(
1301 builds_in_same_archive).config(distinct=True)
1302
1303 # Now that we have a result_set of all the builds, we'll use it
1304 # as a subquery to get the required publishing and arch to do
1305 # the ordering. We do this in this round-about way because we
1306 # can't sort on SourcePackagePublishingHistory.id after the
1307 # union. See bug 443353 for details.
1308 find_spec = (
1309 SourcePackagePublishingHistory, Build, DistroArchSeries)
1310
1311 # Storm doesn't let us do builds_union.values('id') -
1312 # ('Union' object has no attribute 'columns'). So instead
1313 # we have to instantiate the objects just to get the id.
1314 build_ids = [build.id for build in builds_union]
1315
1316 result_set = store.find(
1317 find_spec, builds_for_distroseries_expr,
1318 Build.id.is_in(build_ids))
1319
1320 return result_set.order_by(
1277 SourcePackagePublishingHistory.id,1321 SourcePackagePublishingHistory.id,
1278 DistroArchSeries.architecturetag)1322 DistroArchSeries.architecturetag)
12791323
1280 return result_set
1281
1282 def getByIdAndArchive(self, id, archive, source=True):1324 def getByIdAndArchive(self, id, archive, source=True):
1283 """See `IPublishingSet`."""1325 """See `IPublishingSet`."""
1284 if source:1326 if source:
@@ -1317,12 +1359,10 @@
1317 def _getSourceBinaryJoinForSources(self, source_publication_ids,1359 def _getSourceBinaryJoinForSources(self, source_publication_ids,
1318 active_binaries_only=True):1360 active_binaries_only=True):
1319 """Return the join linking sources with binaries."""1361 """Return the join linking sources with binaries."""
1320 # Import Build, BinaryPackageRelease and DistroArchSeries locally1362 # Import Build and DistroArchSeries locally
1321 # to avoid circular imports, since Build uses1363 # to avoid circular imports, since Build uses
1322 # SourcePackagePublishingHistory, BinaryPackageRelease uses Build1364 # SourcePackagePublishingHistory, BinaryPackageRelease uses Build
1323 # and DistroArchSeries uses BinaryPackagePublishingHistory.1365 # and DistroArchSeries uses BinaryPackagePublishingHistory.
1324 from lp.soyuz.model.binarypackagerelease import (
1325 BinaryPackageRelease)
1326 from lp.soyuz.model.build import Build1366 from lp.soyuz.model.build import Build
1327 from lp.soyuz.model.distroarchseries import (1367 from lp.soyuz.model.distroarchseries import (
1328 DistroArchSeries)1368 DistroArchSeries)
@@ -1397,12 +1437,8 @@
13971437
1398 def getBinaryFilesForSources(self, one_or_more_source_publications):1438 def getBinaryFilesForSources(self, one_or_more_source_publications):
1399 """See `IPublishingSet`."""1439 """See `IPublishingSet`."""
1400 # Import Build and BinaryPackageRelease locally to avoid circular1440 # Import Build locally to avoid circular imports, since that
1401 # imports, since that Build already imports1441 # Build already imports SourcePackagePublishingHistory.
1402 # SourcePackagePublishingHistory and BinaryPackageRelease imports
1403 # Build.
1404 from lp.soyuz.model.binarypackagerelease import (
1405 BinaryPackageRelease)
1406 from lp.soyuz.model.build import Build1442 from lp.soyuz.model.build import Build
14071443
1408 source_publication_ids = self._extractIDs(1444 source_publication_ids = self._extractIDs(
14091445
=== modified file 'lib/lp/soyuz/scripts/packagecopier.py'
--- lib/lp/soyuz/scripts/packagecopier.py 2009-11-10 13:09:26 +0000
+++ lib/lp/soyuz/scripts/packagecopier.py 2009-12-08 11:40:29 +0000
@@ -352,10 +352,10 @@
352 :raise CannotCopy when a copy is not allowed to be performed352 :raise CannotCopy when a copy is not allowed to be performed
353 containing the reason of the error.353 containing the reason of the error.
354 """354 """
355 if source.distroseries.distribution != self.archive.distribution:355 if series not in self.archive.distribution.series:
356 raise CannotCopy(356 raise CannotCopy(
357 "Cannot copy to an unsupported distribution: %s." %357 "No such distro series %s in distribution %s." %
358 source.distroseries.distribution.name)358 (series.name, source.distroseries.distribution.name))
359359
360 format = SourcePackageFormat.getTermByToken(360 format = SourcePackageFormat.getTermByToken(
361 source.sourcepackagerelease.dsc_format).value361 source.sourcepackagerelease.dsc_format).value
362362
=== modified file 'lib/lp/soyuz/scripts/tests/test_copypackage.py'
--- lib/lp/soyuz/scripts/tests/test_copypackage.py 2009-11-17 21:38:28 +0000
+++ lib/lp/soyuz/scripts/tests/test_copypackage.py 2009-12-08 11:40:29 +0000
@@ -696,23 +696,45 @@
696 'source has expired binaries',696 'source has expired binaries',
697 copy_checker.checkCopy, source, series, pocket)697 copy_checker.checkCopy, source, series, pocket)
698698
699 def test_checkCopy_forbids_copies_from_other_distributions(self):699 def test_checkCopy_allows_copies_from_other_distributions(self):
700 # It is possible to copy packages between distributions,
701 # as long as the target distroseries exists for the target
702 # distribution.
703
704 # Create a testing source in ubuntu.
705 ubuntu = getUtility(IDistributionSet).getByName('debian')
706 sid = ubuntu.getSeries('sid')
707 source = self.test_publisher.getPubSource(distroseries=sid)
708
709 # Create a fresh PPA for ubuntutest, which will be the copy
710 # destination.
711 archive = self.factory.makeArchive(
712 distribution=self.test_publisher.ubuntutest,
713 purpose=ArchivePurpose.PPA)
714 series = self.test_publisher.ubuntutest.getSeries('hoary-test')
715 pocket = source.pocket
716
717 # Copy of sources to series in another distribution can be
718 # performed.
719 copy_checker = CopyChecker(archive, include_binaries=False)
720 copy_checker.checkCopy(source, series, pocket)
721
722 def test_checkCopy_forbids_copies_to_unknown_distroseries(self):
700 # We currently deny copies to series that are not for the Archive723 # We currently deny copies to series that are not for the Archive
701 # distribution, because they will never be published. And abandoned724 # distribution, because they will never be published. And abandoned
702 # copies like these keep triggering the PPA publication spending725 # copies like these keep triggering the PPA publication spending
703 # resources.726 # resources.
704727
705 # Create a testing source in ubuntu.728 # Create a testing source in ubuntu.
706 ubuntu = getUtility(IDistributionSet).getByName('ubuntu')729 ubuntu = getUtility(IDistributionSet).getByName('debian')
707 hoary = ubuntu.getSeries('hoary')730 sid = ubuntu.getSeries('sid')
708 source = self.test_publisher.getPubSource(distroseries=hoary)731 source = self.test_publisher.getPubSource(distroseries=sid)
709732
710 # Create a fresh PPA for ubuntutest, which will be the copy733 # Create a fresh PPA for ubuntutest, which will be the copy
711 # destination.734 # destination.
712 archive = self.factory.makeArchive(735 archive = self.factory.makeArchive(
713 distribution=self.test_publisher.ubuntutest,736 distribution=self.test_publisher.ubuntutest,
714 purpose=ArchivePurpose.PPA)737 purpose=ArchivePurpose.PPA)
715 series = source.distroseries
716 pocket = source.pocket738 pocket = source.pocket
717739
718 # Copy of sources to series in another distribution, cannot be740 # Copy of sources to series in another distribution, cannot be
@@ -720,8 +742,8 @@
720 copy_checker = CopyChecker(archive, include_binaries=False)742 copy_checker = CopyChecker(archive, include_binaries=False)
721 self.assertRaisesWithContent(743 self.assertRaisesWithContent(
722 CannotCopy,744 CannotCopy,
723 'Cannot copy to an unsupported distribution: ubuntu.',745 'No such distro series sid in distribution debian.',
724 copy_checker.checkCopy, source, series, pocket)746 copy_checker.checkCopy, source, sid, pocket)
725747
726 def test_checkCopy_respects_sourceformatselection(self):748 def test_checkCopy_respects_sourceformatselection(self):
727 # A source copy should be denied if the source's dsc_format is749 # A source copy should be denied if the source's dsc_format is
@@ -974,7 +996,8 @@
974 # The returned object has a more descriptive 'displayname'996 # The returned object has a more descriptive 'displayname'
975 # attribute than plain `IPackageUpload` instances.997 # attribute than plain `IPackageUpload` instances.
976 self.assertEquals(998 self.assertEquals(
977 'Delayed copy of foocomm - 1.0-2 (source, i386, raw-dist-upgrader)',999 'Delayed copy of foocomm - '
1000 '1.0-2 (source, i386, raw-dist-upgrader)',
978 delayed_copy.displayname)1001 delayed_copy.displayname)
9791002
980 # It is targeted to the right publishing context.1003 # It is targeted to the right publishing context.
@@ -1368,6 +1391,21 @@
1368 target_archive = copy_helper.destination.archive1391 target_archive = copy_helper.destination.archive
1369 self.checkCopies(copied, target_archive, 3)1392 self.checkCopies(copied, target_archive, 3)
13701393
1394 # The second copy will fail explicitly because the new BPPH
1395 # records are not yet published.
1396 nothing_copied = copy_helper.mainTask()
1397 self.assertEqual(len(nothing_copied), 0)
1398 self.assertEqual(
1399 copy_helper.logger.buffer.getvalue().splitlines()[-1],
1400 'ERROR: foo 666 in hoary (same version has unpublished binaries '
1401 'in the destination archive for Hoary, please wait for them to '
1402 'be published before copying)')
1403
1404 # If we ensure that the copied binaries are published, the
1405 # copy won't fail but will simply not copy anything.
1406 for bin_pub in copied[1:3]:
1407 bin_pub.secure_record.setPublished()
1408
1371 nothing_copied = copy_helper.mainTask()1409 nothing_copied = copy_helper.mainTask()
1372 self.assertEqual(len(nothing_copied), 0)1410 self.assertEqual(len(nothing_copied), 0)
1373 self.assertEqual(1411 self.assertEqual(
@@ -1504,7 +1542,7 @@
1504 name='boing')1542 name='boing')
1505 self.assertEqual(copied_source.displayname, 'boing 1.0 in hoary')1543 self.assertEqual(copied_source.displayname, 'boing 1.0 in hoary')
1506 self.assertEqual(len(copied_source.getPublishedBinaries()), 2)1544 self.assertEqual(len(copied_source.getPublishedBinaries()), 2)
1507 self.assertEqual(len(copied_source.getBuilds()), 0)1545 self.assertEqual(len(copied_source.getBuilds()), 1)
15081546
1509 def _setupArchitectureGrowingScenario(self, architecturehintlist="all"):1547 def _setupArchitectureGrowingScenario(self, architecturehintlist="all"):
1510 """Prepare distroseries with different sets of architectures.1548 """Prepare distroseries with different sets of architectures.
15111549
=== modified file 'lib/lp/soyuz/stories/ppa/xx-copy-packages.txt'
--- lib/lp/soyuz/stories/ppa/xx-copy-packages.txt 2009-10-13 10:05:58 +0000
+++ lib/lp/soyuz/stories/ppa/xx-copy-packages.txt 2009-12-08 11:40:29 +0000
@@ -1062,7 +1062,7 @@
1062 >>> print_ppa_packages(jblack_browser.contents)1062 >>> print_ppa_packages(jblack_browser.contents)
1063 Source Published Status Series Section Build1063 Source Published Status Series Section Build
1064 Status1064 Status
1065 foo - 2.0 (changesfile) Pending Hoary Base1065 foo - 2.0 (changesfile) Pending Hoary Base i386
1066 foo - 1.1 (changesfile) Pending Warty Base1066 foo - 1.1 (changesfile) Pending Warty Base
1067 pmount - 0.1-1 Pending Hoary Editors1067 pmount - 0.1-1 Pending Hoary Editors
1068 pmount - 0.1-1 Pending Warty Editors1068 pmount - 0.1-1 Pending Warty Editors
10691069
=== modified file 'lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt'
--- lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt 2009-11-05 10:51:36 +0000
+++ lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt 2009-12-08 11:40:29 +0000
@@ -129,29 +129,51 @@
129If a the binaries for a package are fully built, but have not yet been129If a the binaries for a package are fully built, but have not yet been
130published, this will be indicated to the viewer:130published, this will be indicated to the viewer:
131131
132 >>> anon_browser.open(132 # First, we'll update the binary publishing history for the i386
133 ... "http://launchpad.dev/~cprov/+archive/ppa/+packages")133 # record so that it is pending publication.
134 >>> expander_url = anon_browser.getLink(id='pub28-expander').url134 >>> login('foo.bar@canonical.com')
135 >>> from zope.component import getUtility
136 >>> from lp.registry.interfaces.person import IPersonSet
137 >>> cprov_ppa = getUtility(IPersonSet).getByName('cprov').archive
138 >>> pmount_i386_pub = cprov_ppa.getAllPublishedBinaries(
139 ... name='pmount', version='0.1-1')[1]
140 >>> print pmount_i386_pub.displayname
141 pmount 0.1-1 in warty i386
142 >>> from lp.soyuz.interfaces.publishing import PackagePublishingStatus
143 >>> pmount_i386_pub.secure_record.status = PackagePublishingStatus.PENDING
144 >>> pmount_i386_pub.secure_record.datepublished = None
145 >>> transaction.commit()
146 >>> logout()
147
148 # Now, to re-display the pmount expanded section:
135 >>> anon_browser.open(expander_url)149 >>> anon_browser.open(expander_url)
136 >>> print extract_text(anon_browser.contents)150 >>> print extract_text(anon_browser.contents)
137 Note: Some binary packages for this source are not yet published in the151 Note: Some binary packages for this source are not yet published in the
138 repository.152 repository.
139 Publishing details153 Publishing details
140 Published on 2007-07-09154 Published on 2007-07-09
141 Copied from ubuntu warty in PPA for Mark Shuttleworth155 Copied from ubuntu hoary in Primary Archive for Ubuntu Linux
142 Changelog156 Changelog
157 pmount (0.1-1) hoary; urgency=low
158 * Fix description (Malone #1)
159 * Fix debian (Debian #2000)
160 * Fix warty (Warty Ubuntu #1)
161 -- Sample Person...
143 Builds162 Builds
144 i386 - Pending publication163 i386 - Pending publication
145 Built packages164 Built packages
146 mozilla-firefox ff from iceweasel165 pmount
166 pmount shortdesc
147 Package files167 Package files
148 firefox_0.9.2.orig.tar.gz (9.5 MiB)168 No files published for this package.
149 iceweasel-1.0.dsc (123 bytes)
150 mozilla-firefox_0.9_i386.deb (3 bytes)
151169
152The package was copied from a PPA. The archive title will hence link170When the package is copied from a PPA, the archive title will link
153back to the source PPA.171back to the source PPA.
154172
173 >>> anon_browser.open(
174 ... "http://launchpad.dev/~cprov/+archive/ppa/+packages")
175 >>> expander_url = anon_browser.getLink(id='pub28-expander').url
176 >>> anon_browser.open(expander_url)
155 >>> anon_browser.getLink("PPA for Mark Shuttleworth").url177 >>> anon_browser.getLink("PPA for Mark Shuttleworth").url
156 'http://launchpad.dev/~mark/+archive/ppa'178 'http://launchpad.dev/~mark/+archive/ppa'
157179
@@ -164,7 +186,7 @@
164 >>> admin_browser.getControl(name="field.buildd_secret").value = "secret"186 >>> admin_browser.getControl(name="field.buildd_secret").value = "secret"
165 >>> admin_browser.getControl("Save").click()187 >>> admin_browser.getControl("Save").click()
166188
167 >>> anon_browser.open("http://launchpad.dev/~cprov/+archive/ppa")189 >>> anon_browser.open(expander_url)
168 >>> anon_browser.getLink("PPA for Mark Shuttleworth")190 >>> anon_browser.getLink("PPA for Mark Shuttleworth")
169 Traceback (most recent call last):191 Traceback (most recent call last):
170 ...192 ...
171193
=== added file 'lib/lp/soyuz/stories/soyuz/xx-packagepublishinghistory.txt'
--- lib/lp/soyuz/stories/soyuz/xx-packagepublishinghistory.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/stories/soyuz/xx-packagepublishinghistory.txt 2009-12-08 11:40:29 +0000
@@ -0,0 +1,49 @@
1= Publishing History Page =
2
3The Publishing History page hangs off a distribution source page and
4shows the complete history of a package in all series.
5
6 >>> from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
7 >>> from lp.soyuz.interfaces.publishing import (
8 ... PackagePublishingStatus)
9 >>> stp = SoyuzTestPublisher()
10 >>> login('foo.bar@canonical.com')
11 >>> stp.prepareBreezyAutotest()
12 >>> source_pub = stp.getPubSource(
13 ... "test-history", status=PackagePublishingStatus.PUBLISHED)
14 >>> logout()
15
16 >>> anon_browser.open(
17 ... 'http://launchpad.dev/ubuntutest/+source/test-history/'
18 ... '+publishinghistory')
19
20 >>> print extract_text(
21 ... find_tag_by_id(anon_browser.contents, 'publishing-summary'))
22 Date Status Target Pocket Component Section Version
23 ... UTC Published Breezy ... release main base 666
24 Published ... ago
25
26A publishing record will be shown as deleted in the publishing history after a
27request for deletion by a user.
28
29 >>> login('foo.bar@canonical.com')
30 >>> unused = source_pub.requestDeletion(stp.factory.makePerson(), "fix bug 1")
31 >>> logout()
32
33 >>> anon_browser.open(
34 ... 'http://launchpad.dev/ubuntutest/+source/test-history/'
35 ... '+publishinghistory')
36
37 >>> table = find_tag_by_id(anon_browser.contents, 'publishing-summary')
38 >>> print extract_text(table)
39 Date Status Target Pocket Component Section Version
40 Deleted Breezy ... release main base 666
41 Deleted ... ago by ... fix bug 1
42 Published ... ago
43
44Links to bug reports are added for bugs mentioned in the removal comment.
45
46 >>> print anon_browser.getLink("bug 1").url
47 http://launchpad.dev/bugs/1
48
49
050
=== modified file 'lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt'
--- lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt 2009-11-18 23:56:26 +0000
+++ lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt 2009-12-08 11:40:29 +0000
@@ -207,18 +207,20 @@
207======================207======================
208208
209The source publication object has a custom operation called 'getBuilds' and209The source publication object has a custom operation called 'getBuilds' and
210it returns the build records in the context of that publication.210it returns the build records for builds that were built in the same context
211archive as the publication, or builds from other archives but where the
212binaries have been copied and published in the same context archive.
211213
212 >>> pubs = webservice.named_get(214 >>> pubs = webservice.named_get(
213 ... cprov_archive['self_link'], 'getPublishedSources',215 ... cprov_archive['self_link'], 'getPublishedSources',
214 ... source_name="iceweasel", version="1.0",216 ... source_name="pmount", version="0.1-1",
215 ... exact_match=True).jsonBody()217 ... exact_match=True).jsonBody()
216 >>> source_pub = pubs['entries'][0]218 >>> source_pub = pubs['entries'][0]
217 >>> builds = webservice.named_get(219 >>> builds = webservice.named_get(
218 ... source_pub['self_link'], 'getBuilds').jsonBody()220 ... source_pub['self_link'], 'getBuilds').jsonBody()
219 >>> for entry in sorted(builds['entries']):221 >>> for entry in sorted(builds['entries']):
220 ... print entry['title']222 ... print entry['title']
221 i386 build of iceweasel 1.0 in ubuntu warty RELEASE223 i386 build of pmount 0.1-1 in ubuntu warty RELEASE
222224
223225
224Finding related Binary publications226Finding related Binary publications
225227
=== modified file 'lib/lp/soyuz/templates/packagepublishing-details.pt'
--- lib/lp/soyuz/templates/packagepublishing-details.pt 2009-07-17 17:59:07 +0000
+++ lib/lp/soyuz/templates/packagepublishing-details.pt 2009-12-08 11:40:29 +0000
@@ -20,7 +20,7 @@
20 tal:content="context/datesuperseded/fmt:displaydate" />20 tal:content="context/datesuperseded/fmt:displaydate" />
21 by <a tal:replace="structure context/removed_by/fmt:link"/>21 by <a tal:replace="structure context/removed_by/fmt:link"/>
22 <div tal:condition="context/removal_comment"22 <div tal:condition="context/removal_comment"
23 tal:content="context/removal_comment" />23 tal:content="structure context/removal_comment/fmt:text-to-html" />
24 </li>24 </li>
25 <li tal:condition="view/wasSuperseded">25 <li tal:condition="view/wasSuperseded">
26 <strong>Superseded</strong>26 <strong>Superseded</strong>
2727
=== modified file 'lib/lp/translations/browser/language.py'
--- lib/lp/translations/browser/language.py 2009-11-27 14:18:05 +0000
+++ lib/lp/translations/browser/language.py 2009-12-08 11:40:29 +0000
@@ -17,17 +17,22 @@
17from zope.lifecycleevent import ObjectCreatedEvent17from zope.lifecycleevent import ObjectCreatedEvent
18from zope.component import getUtility18from zope.component import getUtility
19from zope.event import notify19from zope.event import notify
20from zope.app.form.browser import TextWidget
21from zope.interface import Interface
22from zope.schema import TextLine
2023
21from canonical.cachedproperty import cachedproperty24from canonical.cachedproperty import cachedproperty
25from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
26from canonical.launchpad.webapp import (
27 action, canonical_url, ContextMenu, custom_widget,
28 enabled_with_permission, GetitemNavigation, LaunchpadEditFormView,
29 LaunchpadFormView, LaunchpadView, Link, NavigationMenu)
22from canonical.launchpad.webapp.breadcrumb import Breadcrumb30from canonical.launchpad.webapp.breadcrumb import Breadcrumb
31from canonical.launchpad.webapp.tales import LanguageFormatterAPI
23from lp.services.worlddata.interfaces.language import ILanguage, ILanguageSet32from lp.services.worlddata.interfaces.language import ILanguage, ILanguageSet
24from lp.translations.interfaces.translationsperson import (33from lp.translations.interfaces.translationsperson import (
25 ITranslationsPerson)34 ITranslationsPerson)
26from lp.translations.browser.translations import TranslationsMixin35from lp.translations.browser.translations import TranslationsMixin
27from canonical.launchpad.webapp import (
28 action, canonical_url, ContextMenu, custom_widget,
29 enabled_with_permission, GetitemNavigation, LaunchpadEditFormView,
30 LaunchpadFormView, LaunchpadView, Link, NavigationMenu)
31from lp.translations.utilities.pluralforms import make_friendly_plural_forms36from lp.translations.utilities.pluralforms import make_friendly_plural_forms
32from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities37from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
3338
@@ -80,16 +85,37 @@
80 return Link('+admin', text, icon='edit')85 return Link('+admin', text, icon='edit')
8186
8287
83class LanguageSetView:88def _format_language(language):
89 """Format a language as a link."""
90 return LanguageFormatterAPI(language).link(None)
91
92
93class ILanguageSetSearch(Interface):
94 """The collection of languages."""
95
96 search_lang = TextLine(
97 title=u'Name of the language to search for.',
98 required=True)
99
100class LanguageSetView(LaunchpadFormView):
84 """View class to render main ILanguageSet page."""101 """View class to render main ILanguageSet page."""
85 label = "Languages in Launchpad"102 label = "Languages in Launchpad"
86 page_title = "Languages"103 page_title = "Languages"
87104
88 def __init__(self, context, request):105 schema = ILanguageSetSearch
89 self.context = context106
90 self.request = request107 custom_widget('search_lang', TextWidget, displayWidth=30)
91 form = self.request.form108
92 self.language_search = form.get('find')109 def initialize(self):
110 """See `LaunchpadFormView`."""
111 LaunchpadFormView.initialize(self)
112
113 self.language_search = None
114
115 search_lang_widget = self.widgets.get('search_lang')
116 if (search_lang_widget is not None and
117 search_lang_widget.hasValidInput()):
118 self.language_search = search_lang_widget.getInputValue()
93 self.search_requested = self.language_search is not None119 self.search_requested = self.language_search is not None
94120
95 @cachedproperty121 @cachedproperty
@@ -103,6 +129,14 @@
103 else:129 else:
104 return 0130 return 0
105131
132 @cachedproperty
133 def user_languages(self):
134 """The user's preferred languages, or English if none are set."""
135 languages = list(self.user.languages)
136 if len(languages) == 0:
137 languages = [getUtility(ILaunchpadCelebrities).english]
138 return ", ".join(map(_format_language, languages))
139
106140
107# There is no easy way to remove an ILanguage from the database due all the141# There is no easy way to remove an ILanguage from the database due all the
108# dependencies that ILanguage would have. That's the reason why we don't have142# dependencies that ILanguage would have. That's the reason why we don't have
109143
=== modified file 'lib/lp/translations/browser/tests/language-views.txt'
--- lib/lp/translations/browser/tests/language-views.txt 2009-09-17 21:03:25 +0000
+++ lib/lp/translations/browser/tests/language-views.txt 2009-12-08 11:40:29 +0000
@@ -95,4 +95,29 @@
95 1 : 2, 3, 4, 22, 23, 24...95 1 : 2, 3, 4, 22, 23, 24...
96 2 : 5, 6, 7, 8, 9, 10...96 2 : 5, 6, 7, 8, 9, 10...
9797
98View LanguageSet
99------------------
100
101 >>> login('carlos@canonical.com')
102 >>> languageset_view = create_initialized_view(language_set, '+index',
103 ... layer=TranslationsLayer)
104
105The user_languages property contains a list of the current user's preferred
106languages formated as links.
107
108 >>> print languageset_view.user_languages
109 <a href=".../ca" ...>Catalan</a>,
110 <a href=".../en" ...>English</a>,
111 <a href=".../es" ...>Spanish</a>
112
113For a user without any preferred languages, English will be returned.
114
115 >>> person = factory.makePerson()
116 >>> print person.languages
117 []
118 >>> login_person(person)
119 >>> languageset_view = create_initialized_view(language_set, '+index',
120 ... layer=TranslationsLayer)
121 >>> print languageset_view.user_languages
122 <a href=".../en" ...>English</a>
98123
99124
=== modified file 'lib/lp/translations/stories/standalone/xx-language.txt'
--- lib/lp/translations/stories/standalone/xx-language.txt 2009-11-27 14:18:05 +0000
+++ lib/lp/translations/stories/standalone/xx-language.txt 2009-12-08 11:40:29 +0000
@@ -95,11 +95,11 @@
95 >>> print browser.url95 >>> print browser.url
96 http://translations.launchpad.dev/+languages96 http://translations.launchpad.dev/+languages
9797
98 >>> text_search = browser.getControl(name='find')98 >>> text_search = browser.getControl(name='field.search_lang')
99 >>> text_search.value = 'Spanish'99 >>> text_search.value = 'Spanish'
100 >>> browser.getControl('Find language', index=0).click()100 >>> browser.getControl('Find language', index=0).click()
101 >>> print browser.url101 >>> print browser.url
102 http://translations.launchpad.dev/+languages/+index?find=Spanish102 http://translations.launchpad.dev/+languages/+index?field.search_lang=Spanish
103103
104104
105Read language information105Read language information
106106
=== modified file 'lib/lp/translations/templates/languageset-index.pt'
--- lib/lp/translations/templates/languageset-index.pt 2009-10-31 11:06:44 +0000
+++ lib/lp/translations/templates/languageset-index.pt 2009-12-08 11:40:29 +0000
@@ -7,6 +7,20 @@
7>7>
88
9 <body>9 <body>
10 <div metal:fill-slot="head_epilogue">
11 <script
12 type="text/javascript"
13 tal:condition="devmode"
14 tal:attributes="src string:${icingroot}/build/worlddata/languages.js">
15 </script>
16 <script type="text/javascript">
17 YUI().use('languages', 'event', function(Y) {
18 Y.on('domready', function(e) {
19 Y.languages.initialize_languages_page(Y);
20 });
21 });
22 </script>
23 </div>
10 <div metal:fill-slot="main">24 <div metal:fill-slot="main">
11 <div class="yui-b top-portlet">25 <div class="yui-b top-portlet">
12 <p>26 <p>
@@ -24,8 +38,17 @@
24 </a>38 </a>
25 FAQ entry.39 FAQ entry.
26 </p>40 </p>
2741 <dl id="preferred_languages"
28 <div class="portlet">42 tal:condition="view/user">
43 <dt>Your preferred languages:
44 <a tal:attributes="href string:${view/user/fmt:url}/+editlanguages"
45 class="edit sprite"></a>
46 </dt>
47 <dd tal:content="structure view/user_languages">
48 English
49 </dd>
50 </dl>
51 <div class="portlet searchform">
29 <h2>Find a language in Launchpad</h2>52 <h2>Find a language in Launchpad</h2>
30 <form method="get">53 <form method="get">
31 <p>54 <p>
@@ -33,49 +56,67 @@
33 Language name/code contains:56 Language name/code contains:
34 </label>57 </label>
35 <input58 <input
36 name="find" size="30"59 size="30"
37 tal:attributes="value view/language_search" />60 tal:replace="structure view/widgets/search_lang"
61 />
38 <input62 <input
63 class="submit"
39 type="submit"64 type="submit"
40 value="Find language"65 value="Find language"
41 />66 />
42 </p>67 </p>
68 <tal:none condition="not: view/search_matches">
69 <script type="text/javascript"
70 tal:define="script view/focusedElementScript"
71 tal:condition="script"
72 tal:content="structure script" />
73 </tal:none>
43 </form>74 </form>
44 <div tal:condition="context/required:launchpad.Admin">75 </div>
76 <div tal:condition="not:view/search_requested"
77 class="portlet">
78 <h2>Available languages in Launchpad</h2>
79 <p tal:condition="context/required:launchpad.Admin">
45 <a tal:attributes="href context/fmt:url/+add"80 <a tal:attributes="href context/fmt:url/+add"
46 class="add sprite">Add new language</a>81 class="add sprite">Add new language</a>
47 </div>82 </p>
83 <p id="no_filter_matches" class="unseen">
84 No languages are matching your filter.
85 </p>
86 <ul id="all-languages" class="three-column-list">
87 <li tal:repeat="language context/common_languages">
88 <a tal:replace="structure language/fmt:link">English</a>
89 </li>
90 </ul>
48 </div>91 </div>
49 </div>92 <div tal:condition="view/search_requested" class="yui-b portlet">
50 <div tal:condition="view/search_requested" class="yui-b portlet">93 <tal:block tal:define="results view/search_results">
51 <tal:block tal:define="results view/search_results">94 <tal:none condition="not: view/search_matches">
52 <tal:none condition="not: view/search_matches">95 <h2>No matching languages</h2>
53 <h2>No matching languages</h2>96 <p>No languages matching
54 <p>No languages matching97 &#8220;<span tal:replace="view/language_search">
55 &#8220;<span tal:replace="view/language_search">98 Foo
56 Foo99 </span>&#8221; were found.
57 </span>&#8221; were found.100 </p>
58 </p>101 </tal:none>
59 </tal:none>102 <tal:one condition="python: view.search_matches == 1">
60 <tal:one condition="python: view.search_matches == 1">103 <h2>One matching language</h2>
61 <h2>One matching language</h2>104 </tal:one>
62 </tal:one>105 <tal:more condition="python: view.search_matches > 1">
63 <tal:more condition="python: view.search_matches > 1">106 <h2>
64 <h2>107 <tal:count replace="view/search_matches">3</tal:count>
65 <tal:count replace="view/search_matches">3</tal:count>108 matching languages
66 matching languages109 </h2>
67 </h2>110 </tal:more>
68 </tal:more>111 <ul condition="view/search_matches" class="three-column-list">
69 <ul condition="view/search_matches" class="languages">112 <li language tal:repeat="language results">
70 <tal:language repeat="language results">
71 <li>
72 <a tal:replace="structure language/fmt:link">113 <a tal:replace="structure language/fmt:link">
73 Serbian (sr)114 Serbian (sr)
74 </a>115 </a>
75 </li>116 </li>
76 </tal:language>117 </ul>
77 </ul>118 </tal:block>
78 </tal:block>119 </div>
79 </div>120 </div>
80 </div>121 </div>
81 </body>122 </body>
82123
=== added file 'lib/lp/translations/windmill/tests/test_languages.py'
--- lib/lp/translations/windmill/tests/test_languages.py 1970-01-01 00:00:00 +0000
+++ lib/lp/translations/windmill/tests/test_languages.py 2009-12-08 11:40:29 +0000
@@ -0,0 +1,107 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test for languages listing and filtering behaviour."""
5
6__metaclass__ = type
7__all__ = []
8
9import transaction
10
11from windmill.authoring import WindmillTestClient
12from zope.component import getUtility
13
14from canonical.launchpad.windmill.testing.constants import (
15 FOR_ELEMENT, PAGE_LOAD, SLEEP)
16from lp.translations.windmill.testing import TranslationsWindmillLayer
17from lp.testing import TestCaseWithFactory
18
19INPUT_FIELD=(u"//div[contains(@class,'searchform')]"+
20 u"//input[@id='field.search_lang']")
21FILTER_BUTTON=(u"//div[contains(@class,'searchform')]"+
22 u"//input[@value='Filter languages']")
23LANGUAGE=u"//a[contains(@class, 'language') and text()='%s']/parent::li"
24UNSEEN_VALIDATOR='className|unseen'
25
26
27class LanguagesFilterTest(TestCaseWithFactory):
28 """Test that filtering on the +languages page works."""
29
30 layer = TranslationsWindmillLayer
31
32 def _enter_filter_string(self, filterstring):
33 self.client.type(xpath=INPUT_FIELD, text=filterstring)
34 self.client.click(xpath=FILTER_BUTTON)
35 self.client.waits.sleep(milliseconds=SLEEP)
36
37 def _assert_languages_visible(self, languages):
38 for language, visibility in languages.items():
39 xpath = LANGUAGE % language
40 if visibility:
41 self.client.asserts.assertNotProperty(
42 xpath=xpath, validator=UNSEEN_VALIDATOR)
43 else:
44 self.client.asserts.assertProperty(
45 xpath=xpath, validator=UNSEEN_VALIDATOR)
46
47 def test_filter_languages(self):
48 """Test that filtering on the +languages page works.
49
50 The test cannot fully cover all languages on the page and so just
51 tests three with a search string of 'de':
52 German, because it's language code is 'de' but the names does not,
53 Mende, because it contains a 'de' but the language code does not,
54 French, because neither its name nor language code contain 'de'.
55 """
56 self.client = WindmillTestClient('Languages filter')
57 start_url = 'http://translations.launchpad.dev:8085/+languages'
58 # Go to the languages page
59 self.client.open(url=start_url)
60 self.client.waits.forPageLoad(timeout=PAGE_LOAD)
61
62 # "Not-matching" message is hidden and languages are visible.
63 self.client.asserts.assertProperty(
64 id=u'no_filter_matches',
65 validator='className|unseen')
66 self._assert_languages_visible({
67 u'German': True,
68 u'Mende': True,
69 u'French': True,
70 })
71
72 # Enter search string, search and wait.
73 self._enter_filter_string(u"de")
74 # "Not-matching" message and French are hidden now.
75 self.client.asserts.assertProperty(
76 id=u'no_filter_matches',
77 validator='className|unseen')
78 self._assert_languages_visible({
79 u'German': True,
80 u'Mende': True,
81 u'French': False,
82 })
83
84 # Enter not matching search string, search and wait.
85 self._enter_filter_string(u"xxxxxx")
86 # "Not-matching" message is shown, all languages are hidden.
87 self.client.asserts.assertNotProperty(
88 id=u'no_filter_matches',
89 validator='className|unseen')
90 self._assert_languages_visible({
91 u'German': False,
92 u'Mende': False,
93 u'French': False,
94 })
95
96 # Enter empty search string, search and wait.
97 self._enter_filter_string(u"")
98 # "Not-matching" message is hidden, all languages are visible again.
99 self.client.asserts.assertProperty(
100 id=u'no_filter_matches',
101 validator='className|unseen')
102 self._assert_languages_visible({
103 u'German': True,
104 u'Mende': True,
105 u'French': True,
106 })
107
0108
=== removed directory 'lib/lp/translations/windmill/tests/test_translations'

Subscribers

People subscribed via source and target branches

to status/vote changes: