Merge lp:~henninge/launchpad/bug-425583-languages into lp:launchpad/db-devel
- bug-425583-languages
- Merge into db-devel
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Muharem Hrnjadovic (community) | code | Approve | |
Michael Nelson (community) | ui | Approve | |
Review via email: mp+15594@code.launchpad.net |
Commit message
Description of the change
Henning Eggers (henninge) wrote : | # |
Michael Nelson (michael.nelson) wrote : | # |
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:/
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://
<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/
<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 ...
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.
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 :)
Henning Eggers (henninge) wrote : | # |
= Windmill test =
bin/test --layer=
Muharem Hrnjadovic (al-maisan) wrote : | # |
Looks good!
Preview Diff
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 | - “<span tal:replace="view/language_search"> |
935 | - Foo |
936 | - </span>” 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 | + “<span tal:replace="view/language_search"> |
957 | + Foo |
958 | + </span>” 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' |
= 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.