Merge lp:~bac/launchpad/bug-652280-pg-trans into lp:launchpad

Proposed by Brad Crittenden
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: 11740
Proposed branch: lp:~bac/launchpad/bug-652280-pg-trans
Merge into: lp:launchpad
Diff against target: 320 lines (+191/-14)
7 files modified
lib/lp/registry/doc/projectgroup.txt (+11/-0)
lib/lp/registry/model/projectgroup.py (+8/-2)
lib/lp/testing/views.py (+4/-2)
lib/lp/translations/browser/project.py (+1/-0)
lib/lp/translations/browser/tests/test_noindex.py (+152/-0)
lib/lp/translations/templates/distroseries-translations.pt (+2/-5)
lib/lp/translations/templates/project-translations.pt (+13/-5)
To merge this branch: bzr merge lp:~bac/launchpad/bug-652280-pg-trans
Reviewer Review Type Date Requested Status
Robert Collins (community) Approve
Jeroen T. Vermeulen (community) code Approve
Curtis Hovey code Pending
Review via email: mp+38195@code.launchpad.net

Description of the change

= Summary =

The +translations page for project groups did not have the
"noindex,nofollow" meta data. It was added as was a new test that tests
all translations pages to ensure the meta data is included properly.

== Proposed fix ==

As above.

== Pre-implementation notes ==

Talks with Curtis and detailed work with Henning.

== Implementation details ==

It's pretty straightforward, as above. However, for distroseries
getting a view via 'create_initialized_view' and trying to render it
does not work, due, I think, to the way the pages are registered in the
ZCML with respect to layers and facets. The template uses the
navigation menus which are not adapted properly.

In the end I punted and used getUserBrowser in order to get the rendered
content of the pages which is a fine approach but leaves the mystery of
why getting the view directly and rendering it does not work. Is there
a hidden bug that I've discovered but didn't crack?

== Tests ==

bin/test -vvm lp.translations -t test_noindex

== Demo and Q/A ==

Go to +translations and inspect the HTML for the anti-robots meta
instructions.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/translations/templates/distroseries-translations.pt
  lib/lp/translations/browser/project.py
  lib/lp/translations/browser/tests/test_noindex.py
  lib/lp/testing/views.py
  lib/lp/translations/templates/project-translations.pt

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Hi Brad,

Nice to see a branch with a solid cover letter again.

You found some interesting bugs in Storm's SQLObject compatibility layer while working on this: is_empty returns the inverse of what it should, and then __nonzero__ compounds that sin by inverting it again. I hope you'll file a bug about this.

I have no objections to the code overall. There are a few things in the test that I'd like to see fixed, but nothing to stop an approval:

 * You use lower_case_with_underscores for their helper methods, whereas our coding standard mandates sayItWithCapitals for methods (but methods only). The requirement may be going away though; I haven't kept track.

 * You pointed out that there's a redundant get_rendered_contents in the products test. To be removed.

 * You also pointed out that there's already a has_translatables on your context object, so no need to duplicate it in the view.

 * Why do you bother making "target" and "naked_translatable" properties instead of attributes that you initialize in setUp? I do appreciate the docstring but a comment would be fine as far as I'm concerned. You could scratch self.product, self.projectgroup, self.distro, and self.distroseries to make up for it; they would become unnecessary.

 * I like the orthogonal test setup, with a mixin for the test scenarios and leaf classes for the different alternate setups. But I find the inheritance graph a little confusing. I think it'd be more manageable if the actual test cases were all leaf classes, without test cases inheriting from other classes that will also run as separate test cases. I'd also put the layer specifications in those leaf classes, so that there's a clear separation between the classes that "go into" a test case and the individual test cases.

 * You have your own verification methods (verify_robots_are_blocked, verify_robots_are_not_blocked) that invoke other assertion methods. I normally avoid this myself because it hides the "where did it go wrong" information deeper in the traceback, but looking at this I quite like the way the naming documents the intent of the assertion.

 * Isn't testing for exactly "noindex,nofollow" more brittle than needed? Would "nofollow,noindex" be equally valid? If so, it'd be more robust to assertContentEqual on robots['content'].split(','). I'll admit it's far-fetched, but brittle tests have given us enough headaches to make us worry about these things.

 * Out of interest, why do you pass rootsite to create_initialized_view for distroseries but not for productseries?

Jeroen

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) :
review: Approve (code)
Revision history for this message
Brad Crittenden (bac) wrote :
Download full text (3.6 KiB)

On Oct 12, 2010, at 18:34 , Jeroen T. Vermeulen wrote:

> Review: Approve
> Hi Brad,

Thanks for the review Jeroen.

>
> Nice to see a branch with a solid cover letter again.

'bzr lp-send'

It's so easy I'm not sure why everyone doesn't do it.

>
> You found some interesting bugs in Storm's SQLObject compatibility layer while working on this: is_empty returns the inverse of what it should, and then __nonzero__ compounds that sin by inverting it again. I hope you'll file a bug about this.

Filed and fixed by you in record time! Thanks for the follow through.

>
> * You use lower_case_with_underscores for their helper methods, whereas our coding standard mandates sayItWithCapitals for methods (but methods only). The requirement may be going away though; I haven't kept track.

No, I was just not paying attention. Fixed.

>
> * You pointed out that there's a redundant get_rendered_contents in the products test. To be removed.

Gone.

>
> * You also pointed out that there's already a has_translatables on your context object, so no need to duplicate it in the view.

Gone along with the view tests. Added missing tests to doc/projectgroup.txt. (Renamed from project.txt.)

>
> * Why do you bother making "target" and "naked_translatable" properties instead of attributes that you initialize in setUp? I do appreciate the docstring but a comment would be fine as far as I'm concerned. You could scratch self.product, self.projectgroup, self.distro, and self.distroseries to make up for it; they would become unnecessary.

I did that before I converted the base class to a mixin to force the base to throw a NotImplementedError for those as I thought it was more explicit than a comment stating the contract. Overkill? Probably, so I have removed it.

>
> * I like the orthogonal test setup, with a mixin for the test scenarios and leaf classes for the different alternate setups. But I find the inheritance graph a little confusing. I think it'd be more manageable if the actual test cases were all leaf classes, without test cases inheriting from other classes that will also run as separate test cases. I'd also put the layer specifications in those leaf classes, so that there's a clear separation between the classes that "go into" a test case and the individual test cases.

OK, changed to all extend BrowserTestCase and the mixin directly. I disagree with setting the layer on each one, though, as it is the same. That just seems redundant and silly.

>
> * You have your own verification methods (verify_robots_are_blocked, verify_robots_are_not_blocked) that invoke other assertion methods. I normally avoid this myself because it hides the "where did it go wrong" information deeper in the traceback, but looking at this I quite like the way the naming documents the intent of the assertion.
>
> * Isn't testing for exactly "noindex,nofollow" more brittle than needed? Would "nofollow,noindex" be equally valid? If so, it'd be more robust to assertContentEqual on robots['content'].split(','). I'll admit it's far-fetched, but brittle tests have given us enough headaches to make us worry about these things.

Yes, that probably is too brittle. I'll ...

Read more...

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

This looks just fine to me. Nice to see how much boilerplate you were able to strip off the tests. The layers thing was just a suggestion; I'm fine with your choice.

Revision history for this message
Brad Crittenden (bac) wrote :

Hi Curtis,

This branch includes a lot of good stuff...but it only addresses one part of the original bug. There was disagreement between you and Danilo about what UI changes needed to be done. What are your current thoughts?

Revision history for this message
Curtis Hovey (sinzui) wrote :

The Translations team is also changing these pages. There is not need to change something that the team will remove or alter in a few weeks anyway.

Revision history for this message
Robert Collins (lifeless) wrote :

I'm very happy with one test:

169 + def test_service_status_controls_robots(self):
170 + self.verifyRobotsAreBlocked(ServiceUsage.UNKNOWN)
173 + self.verifyRobotsAreBlocked(ServiceUsage.EXTERNAL)
176 + self.verifyRobotsAreBlocked(ServiceUsage.NOT_APPLICABLE)
179 + self.verifyRobotsNotBlocked(ServiceUsage.LAUNCHPAD)

or however you wish to spell it; using cachedproperty should be ok if you want to, but be sure to do browser.open() after you change the service usage. How many seconds difference does the cachedproperty make (perhaps we need a bug on the performance of canonical_url?)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== renamed file 'lib/lp/registry/doc/project.txt' => 'lib/lp/registry/doc/projectgroup.txt'
--- lib/lp/registry/doc/project.txt 2010-10-09 16:36:22 +0000
+++ lib/lp/registry/doc/projectgroup.txt 2010-10-18 02:57:48 +0000
@@ -331,10 +331,21 @@
331 >>> import transaction331 >>> import transaction
332 >>> transaction.abort()332 >>> transaction.abort()
333333
334A project group with no translatable products is shown by
335'has_translatables' being false.
336
337 >>> product = factory.makeProduct()
338 >>> project_group = factory.makeProject()
339 >>> product.project = project_group
340 >>> project_group.has_translatable()
341 False
342
334GNOME Project is a good example that has translations.343GNOME Project is a good example that has translations.
335It has one translatable product.344It has one translatable product.
336345
337 >>> gnome = getUtility(IProjectGroupSet)['gnome']346 >>> gnome = getUtility(IProjectGroupSet)['gnome']
347 >>> gnome.has_translatable()
348 True
338 >>> translatables = gnome.translatables()349 >>> translatables = gnome.translatables()
339 >>> translatables.count()350 >>> translatables.count()
340 1351 1
341352
=== modified file 'lib/lp/registry/model/projectgroup.py'
--- lib/lp/registry/model/projectgroup.py 2010-10-07 13:26:53 +0000
+++ lib/lp/registry/model/projectgroup.py 2010-10-18 02:57:48 +0000
@@ -206,9 +206,11 @@
206206
207 def translatables(self):207 def translatables(self):
208 """See `IProjectGroup`."""208 """See `IProjectGroup`."""
209 # XXX j.c.sackett 2010-08-30 bug=627631 Once data migration has209 # XXX j.c.sackett 2010-08-30 bug=627631: Once data migration has
210 # happened for the usage enums, this sql needs to be updated to210 # happened for the usage enums, this sql needs to be updated to
211 # check for the translations_usage, not official_rosetta.211 # check for the translations_usage, not official_rosetta. At that
212 # time it should also be converted to a Storm query and the issue with
213 # has_translatables resolved.
212 return Product.select('''214 return Product.select('''
213 Product.project = %s AND215 Product.project = %s AND
214 Product.official_rosetta = TRUE AND216 Product.official_rosetta = TRUE AND
@@ -220,6 +222,10 @@
220222
221 def has_translatable(self):223 def has_translatable(self):
222 """See `IProjectGroup`."""224 """See `IProjectGroup`."""
225 # XXX: BradCrittenden 2010-10-12 bug=659078: The test should be
226 # converted to use is_empty but the implementation in storm's
227 # sqlobject wrapper is broken.
228 # return not self.translatables().is_empty()
223 return self.translatables().count() != 0229 return self.translatables().count() != 0
224230
225 def _getBaseQueryAndClauseTablesForQueryingSprints(self):231 def _getBaseQueryAndClauseTablesForQueryingSprints(self):
226232
=== modified file 'lib/lp/testing/views.py'
--- lib/lp/testing/views.py 2010-09-19 03:09:49 +0000
+++ lib/lp/testing/views.py 2010-10-18 02:57:48 +0000
@@ -85,7 +85,8 @@
85def create_initialized_view(context, name, form=None, layer=None,85def create_initialized_view(context, name, form=None, layer=None,
86 server_url=None, method=None, principal=None,86 server_url=None, method=None, principal=None,
87 query_string=None, cookie=None, request=None,87 query_string=None, cookie=None, request=None,
88 path_info='/', rootsite=None):88 path_info='/', rootsite=None,
89 current_request=False):
89 """Return a view that has already been initialized."""90 """Return a view that has already been initialized."""
90 if method is None:91 if method is None:
91 if form is None:92 if form is None:
@@ -94,7 +95,8 @@
94 method = 'POST'95 method = 'POST'
95 view = create_view(96 view = create_view(
96 context, name, form, layer, server_url, method, principal,97 context, name, form, layer, server_url, method, principal,
97 query_string, cookie, request, path_info, rootsite=rootsite)98 query_string, cookie, request, path_info, rootsite=rootsite,
99 current_request=current_request)
98 view.initialize()100 view.initialize()
99 return view101 return view
100102
101103
=== modified file 'lib/lp/translations/browser/project.py'
--- lib/lp/translations/browser/project.py 2010-08-20 20:31:18 +0000
+++ lib/lp/translations/browser/project.py 2010-10-18 02:57:48 +0000
@@ -21,6 +21,7 @@
21from canonical.launchpad.webapp.menu import NavigationMenu21from canonical.launchpad.webapp.menu import NavigationMenu
22from lp.registry.browser.project import ProjectEditView22from lp.registry.browser.project import ProjectEditView
23from lp.registry.interfaces.projectgroup import IProjectGroup23from lp.registry.interfaces.projectgroup import IProjectGroup
24from lp.services.propertycache import cachedproperty
24from lp.translations.browser.translations import TranslationsMixin25from lp.translations.browser.translations import TranslationsMixin
2526
2627
2728
=== added file 'lib/lp/translations/browser/tests/test_noindex.py'
--- lib/lp/translations/browser/tests/test_noindex.py 1970-01-01 00:00:00 +0000
+++ lib/lp/translations/browser/tests/test_noindex.py 2010-10-18 02:57:48 +0000
@@ -0,0 +1,152 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5
6
7from BeautifulSoup import BeautifulSoup
8from zope.security.proxy import removeSecurityProxy
9
10from canonical.launchpad.webapp import canonical_url
11from canonical.testing.layers import DatabaseFunctionalLayer
12from lp.app.enums import ServiceUsage
13from lp.testing import (
14 BrowserTestCase,
15 login_person,
16 )
17from lp.services.propertycache import cachedproperty
18
19
20class TestRobotsMixin:
21 """Test the inclusion of the meta "noindex,nofollow" directives.
22
23 Subclasses using this mixin must set the following attributes:
24 target - the pillar under test
25 naked_translatable - the translatable object for the test, with the
26 security proxy removed. It may be the target or a subordinate object.
27 (For example when testing a ProjectGroup the translatable is one of the
28 products in the project group.)
29 """
30
31 layer = DatabaseFunctionalLayer
32
33 def setUsage(self, usage):
34 self.naked_translatable.translations_usage = usage
35
36 @cachedproperty
37 def url(self):
38 return canonical_url(self.target, rootsite='translations')
39
40 @cachedproperty
41 def user_browser(self):
42 return self.getUserBrowser(user=self.user)
43
44 def getRenderedContents(self):
45 """Return an initialized view's rendered contents."""
46 # Using create_initialized_view for distroseries causes an error when
47 # rendering the view due to the way the view is registered and menus
48 # are adapted. Getting the contents via a browser does work.
49 self.user_browser.open(self.url)
50 return self.user_browser.contents
51
52 def getRobotsDirective(self):
53 contents = self.getRenderedContents()
54 soup = BeautifulSoup(contents)
55 return soup.find('meta', attrs={'name': 'robots'})
56
57 def verifyRobotsAreBlocked(self, usage):
58 self.setUsage(usage)
59 robots = self.getRobotsDirective()
60 self.assertIsNot(None, robots,
61 "Robot blocking meta information not present.")
62 self.assertEqual('noindex,nofollow', robots['content'])
63 expected = ('noindex', 'nofollow')
64 actual = robots['content'].split(',')
65 self.assertContentEqual(expected, actual)
66
67 def verifyRobotsNotBlocked(self, usage):
68 self.setUsage(usage)
69 robots = self.getRobotsDirective()
70 self.assertIs(
71 None, robots,
72 "Robot blocking metadata present when it should not be.")
73
74 def test_robots(self):
75 # Robots are blocked for usage that is not Launchpad.
76 self.verifyRobotsAreBlocked(ServiceUsage.UNKNOWN)
77 self.verifyRobotsAreBlocked(ServiceUsage.EXTERNAL)
78 self.verifyRobotsAreBlocked(ServiceUsage.NOT_APPLICABLE)
79 # Robots are not blocked for Launchpad usage.
80 self.verifyRobotsNotBlocked(ServiceUsage.LAUNCHPAD)
81
82
83class TestRobotsProduct(BrowserTestCase, TestRobotsMixin):
84 """Test noindex,nofollow for products."""
85
86 def setUp(self):
87 super(TestRobotsProduct, self).setUp()
88 self.target = self.factory.makeProduct()
89 self.factory.makePOTemplate(
90 productseries=self.target.development_focus)
91 self.naked_translatable = removeSecurityProxy(self.target)
92
93
94class TestRobotsProjectGroup(BrowserTestCase, TestRobotsMixin):
95 """Test noindex,nofollow for project groups."""
96
97 def setUp(self):
98 super(TestRobotsProjectGroup, self).setUp()
99 self.target = self.factory.makeProject()
100 self.product = self.factory.makeProduct()
101 self.factory.makePOTemplate(
102 productseries=self.product.development_focus)
103 self.naked_translatable = removeSecurityProxy(self.product)
104 self.naked_translatable.project = self.target
105
106
107class TestRobotsProductSeries(BrowserTestCase, TestRobotsMixin):
108 """Test noindex,nofollow for product series."""
109
110 def setUp(self):
111 super(TestRobotsProductSeries, self).setUp()
112 self.product = self.factory.makeProduct()
113 self.target = self.product.development_focus
114 self.factory.makePOTemplate(
115 productseries=self.target)
116 self.naked_translatable = removeSecurityProxy(self.product)
117
118
119class TestRobotsDistroSeries(BrowserTestCase, TestRobotsMixin):
120 """Test noindex,nofollow for distro series."""
121
122 def setUp(self):
123 super(TestRobotsDistroSeries, self).setUp()
124 login_person(self.user)
125 self.distro = self.factory.makeDistribution(
126 name="whobuntu", owner=self.user)
127 self.target = self.factory.makeDistroSeries(
128 name="zephyr", distribution=self.distro)
129 self.target.hide_all_translations = False
130 new_sourcepackagename = self.factory.makeSourcePackageName()
131 self.factory.makePOTemplate(
132 distroseries=self.target,
133 sourcepackagename=new_sourcepackagename)
134 self.naked_translatable = removeSecurityProxy(self.distro)
135
136
137class TestRobotsDistro(BrowserTestCase, TestRobotsMixin):
138 """Test noindex,nofollow for distro."""
139
140 def setUp(self):
141 super(TestRobotsDistro, self).setUp()
142 login_person(self.user)
143 self.target = self.factory.makeDistribution(
144 name="whobuntu", owner=self.user)
145 self.distroseries = self.factory.makeDistroSeries(
146 name="zephyr", distribution=self.target)
147 self.distroseries.hide_all_translations = False
148 new_sourcepackagename = self.factory.makeSourcePackageName()
149 self.factory.makePOTemplate(
150 distroseries=self.distroseries,
151 sourcepackagename=new_sourcepackagename)
152 self.naked_translatable = removeSecurityProxy(self.target)
0153
=== modified file 'lib/lp/translations/templates/distroseries-translations.pt'
--- lib/lp/translations/templates/distroseries-translations.pt 2010-09-24 15:30:10 +0000
+++ lib/lp/translations/templates/distroseries-translations.pt 2010-10-18 02:57:48 +0000
@@ -10,10 +10,8 @@
10 use-macro="context/@@+translations-macros/translations-js" />10 use-macro="context/@@+translations-macros/translations-js" />
11 <metal:languages-table-js11 <metal:languages-table-js
12 use-macro="context/@@+translations-macros/languages-table-js" />12 use-macro="context/@@+translations-macros/languages-table-js" />
13 <tal:robots13 <meta tal:condition="not:context/translations_usage/enumvalue:LAUNCHPAD"
14 condition="not:context/translations_usage/enumvalue:LAUNCHPAD">14 name="robots" content="noindex,nofollow" />
15 <meta name="robots" content="noindex,nofollow" />
16 </tal:robots>
17 </div>15 </div>
18 </head>16 </head>
19 <body>17 <body>
@@ -156,4 +154,3 @@
156 </div>154 </div>
157 </body>155 </body>
158</html>156</html>
159
160157
=== modified file 'lib/lp/translations/templates/project-translations.pt'
--- lib/lp/translations/templates/project-translations.pt 2010-04-19 08:11:52 +0000
+++ lib/lp/translations/templates/project-translations.pt 2010-10-18 02:57:48 +0000
@@ -4,16 +4,26 @@
4 xmlns:metal="http://xml.zope.org/namespaces/metal"4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 metal:use-macro="view/macro:page/main_only"5 metal:use-macro="view/macro:page/main_only"
6>6>
7
8<head>
9 <tal:head_epilogue metal:fill-slot="head_epilogue">
10 <meta tal:condition="not:context/has_translatable"
11 name="robots" content="noindex,nofollow" />
12 </tal:head_epilogue>
13</head>
14
7<body>15<body>
8 <div metal:fill-slot="main">16 <div metal:fill-slot="main">
917
10 <div class="top-portlet">18 <div class="top-portlet">
11 <p tal:condition="context/translatables">19 <p tal:condition="context/has_translatable">
12 Select a project you want to translate into your own language.20 Select a project you want to translate into your own language.
13 </p>21 </p>
14 <p tal:condition="not:context/translatables">22 <p tal:condition="not:context/has_translatable">
23 <strong>
15 There are no translatable projects in24 There are no translatable projects in
16 <span tal:content="context/displayname">Project Group</span>.25 <span tal:content="context/displayname">Project Group</span>.
26 </strong>
17 </p>27 </p>
18 <p tal:condition="not:view/required:launchpad.Edit">28 <p tal:condition="not:view/required:launchpad.Edit">
19 If a project for <span tal:content="context/displayname">Project Group29 If a project for <span tal:content="context/displayname">Project Group
@@ -34,7 +44,7 @@
34 <div class="yui-u first">44 <div class="yui-u first">
35 <div class="portlet" id="translatable-projects">45 <div class="portlet" id="translatable-projects">
36 <h3>Translatable projects</h3>46 <h3>Translatable projects</h3>
37 <dl tal:condition="context/translatables">47 <dl tal:condition="context/has_translatable">
38 <tal:project repeat="product context/translatables">48 <tal:project repeat="product context/translatables">
39 <dt>49 <dt>
40 <a tal:replace="structure product/fmt:link/+translations">50 <a tal:replace="structure product/fmt:link/+translations">
@@ -80,8 +90,6 @@
80 <a class="add sprite" href="+newproduct">Add another project</a>90 <a class="add sprite" href="+newproduct">Add another project</a>
81 </p>91 </p>
82 </div>92 </div>
83
84
85 </div><!--main-->93 </div><!--main-->
86</body>94</body>
87</html>95</html>