Merge lp:~jcsackett/launchpad/set-question-message-visibility into lp:launchpad

Proposed by j.c.sackett
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 12765
Proposed branch: lp:~jcsackett/launchpad/set-question-message-visibility
Merge into: lp:launchpad
Diff against target: 256 lines (+137/-1)
9 files modified
lib/canonical/launchpad/browser/message.py (+1/-0)
lib/canonical/launchpad/security.py (+9/-0)
lib/lp/answers/configure.zcml (+4/-0)
lib/lp/answers/interfaces/question.py (+19/-0)
lib/lp/answers/interfaces/questionmessage.py (+5/-0)
lib/lp/answers/model/question.py (+5/-0)
lib/lp/answers/model/questionmessage.py (+5/-0)
lib/lp/answers/templates/question-index.pt (+2/-1)
lib/lp/answers/tests/test_question_webservice.py (+87/-0)
To merge this branch: bzr merge lp:~jcsackett/launchpad/set-question-message-visibility
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Launchpad code reviewers Pending
Review via email: mp+56588@code.launchpad.net

This proposal supersedes a proposal from 2011-03-23.

Commit message

[r=sinzui][bug=45419] Adds a method via the API to hide question comments.

Description of the change

NB: This is a resubmit to re-land this branch on devel; previously it was landed against db-devel, but that branch was not qa'ed in time for deployment. The branch that this previously depended on was deployed successfully, so this can now be easily landed on devel. See previous MP for discussion and approval for landing on db-devel.

Summary
=======

As part of ongoing Maintenance and spam work, this branch adds a means of hiding comments on questions to silence question spam via the API.

There may be some noise because of the dependent branch including some bits not included in this branch. To get around the noise of those bits, you should look at http://paste.ubuntu.com/583961/ instead of the generated diff.

Implementation
==============

I've added a new method to Question, setCommentVisibility, that's a pretty close copy of the same method on Bug. This method is also exported over the webservice.

It's security is set to launchpad.Moderate, as set in the configure.zcml, with an adapter in the main security.py file.

Tests
=====

bin/test -t answers.*webservice.*Visibililty

QA
==

Hide a comment using the API; it shouldn't be visible on the answers page. Setting visible back to True should show it on the answers page.

Lint
====

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/canonical/launchpad/security.py
  lib/canonical/launchpad/browser/message.py
  lib/lp/answers/configure.zcml
  lib/lp/answers/interfaces/question.py
  lib/lp/answers/interfaces/webservice.py
  lib/lp/answers/model/question.py
  lib/lp/answers/tests/test_question_webservice.py

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote : Posted in a previous version of this proposal

Hi JC.

I am very excited to see Questions over the API. I have a few trivial requests and a question.

The setCommentVisibility method's docstring does not conform to PEP 257. I expect to see a single line with an optional paragraph of further information.

IQuestion.priority is unused. We do not want this exported.

I do not see a security checker set for IQuestionMessage in security.py. In the past, this prevented anonymous users from getting the object. That is to say question.messages is always an empty list. Is this true when you access the list as anon? The fix to security.py is pretty trivial:

{{{
class ViewQuestionMessage(AnonymousAuthorization):
    """Anyone can view an IQustionMessage."""
    usedfor = IQuestionMessage
}}}

However, I think this will show visible and hidden the comments over the API. I am unsure if this is an issue. You may want to override the checkUnauthenticated() method.

review: Needs Information (code)
Revision history for this message
Aaron Bentley (abentley) wrote : Posted in a previous version of this proposal

So apparently the provided diff doesn't accurately represent the changes. Please provide an accurate diff, preferably by fully merging your prerequisite branch.

In addition to Curtis' comments, I recommend using WebServiceTestCase as the basis of TestSetCommentVisibility.

review: Needs Fixing
Revision history for this message
j.c.sackett (jcsackett) wrote : Posted in a previous version of this proposal

> Hi JC.
> The setCommentVisibility method's docstring does not conform to PEP 257. I
> expect to see a single line with an optional paragraph of further information.

Done.

> IQuestion.priority is unused. We do not want this exported.

Also done.

> I do not see a security checker set for IQuestionMessage in security.py. In
> the past, this prevented anonymous users from getting the object. That is to
> say question.messages is always an empty list. Is this true when you access
> the list as anon? The fix to security.py is pretty trivial:
>
> {{{
> class ViewQuestionMessage(AnonymousAuthorization):
> """Anyone can view an IQustionMessage."""
> usedfor = IQuestionMessage
> }}}
>
> However, I think this will show visible and hidden the comments over the API.
> I am unsure if this is an issue. You may want to override the
> checkUnauthenticated() method.

Per your comment in IRC and the bug you showed, I think the correct option is to just not expose messages for now; the comment hiding code still works as messages don't need to be exposes for a method on question to operate on them.

Revision history for this message
j.c.sackett (jcsackett) wrote : Posted in a previous version of this proposal

> So apparently the provided diff doesn't accurately represent the changes.
> Please provide an accurate diff, preferably by fully merging your prerequisite
> branch.

Diff is now accurate. The pre-req is completely merged in.

> In addition to Curtis' comments, I recommend using WebServiceTestCase as the
> basis of TestSetCommentVisibility.

I looked into that, but WebServiceTestCase can't provide an anonymous connection to the API, which is needed to check that anon connections can't alter comment visibility. It may be worth it to update the TestCase class, but I feel that's out of scope for this branch. As it stands, the current test case does provide an instance of launchpadlib to work with, with a logged in user or anonymous connection as needed.

Thanks for pointing me to the WebServiceTestCase though--that'll be very useful in subsequent work.

Revision history for this message
Aaron Bentley (abentley) wrote : Posted in a previous version of this proposal

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 11-03-23 03:03 PM, j.c.sackett wrote:
> I looked into that, but WebServiceTestCase can't provide an anonymous connection to the API, which is needed to check that anon connections can't alter comment visibility. It may be worth it to update the TestCase class, but I feel that's out of scope for this branch. As it stands, the current test case does provide an instance of launchpadlib to work with, with a logged in user or anonymous connection as needed.

In that case, I recommend using testing.ws_object in place of
hand-crafting a url etc.

Also, launchpadlib_for(.., user=None) appears to be equivalent to
launchpadlib_for(...), so I think your conditional is redundant.

Aaron
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.10 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk2KRqcACgkQ0F+nu1YWqI3j0wCfWRnNe5FyixB9oY50Elefx+qK
pRcAn28hzJFLbGNtMwh/sKiuWsrEq3dV
=whBr
-----END PGP SIGNATURE-----

Revision history for this message
Aaron Bentley (abentley) wrote : Posted in a previous version of this proposal

Looking at the diff, I see you are un-exporting messages. Is that deliberate?

Also, I've never seen endInteraction used before. What does it do?

Revision history for this message
j.c.sackett (jcsackett) wrote : Posted in a previous version of this proposal

> Looking at the diff, I see you are un-exporting messages. Is that deliberate?

Yes, per my comment to Curtis in this MP. There are some security concerns when exposing the various message objects and it's unnecessary to export them in order for the setCommentVisibility method to work.

> Also, I've never seen endInteraction used before. What does it do?

It kills the existing _thread.interaction data so you can start a new one. Without it, calling up new lplib instances in the test can run into an error about establishing _thread.interaction while the previous one is present. This is particularly common with the anonymous login case. I believe Salgado was the one who pointed it out to me for some other, similar test cases a few months ago.

Revision history for this message
Aaron Bentley (abentley) wrote : Posted in a previous version of this proposal

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 11-03-23 03:46 PM, j.c.sackett wrote:
>> Looking at the diff, I see you are un-exporting messages. Is that deliberate?
>
> Yes, per my comment to Curtis in this MP. There are some security concerns when exposing the various message objects and it's unnecessary to export them in order for the setCommentVisibility method to work.
>
>> Also, I've never seen endInteraction used before. What does it do?
>
> It kills the existing _thread.interaction data so you can start a new one. Without it, calling up new lplib instances in the test can run into an error about establishing _thread.interaction while the previous one is present. This is particularly common with the anonymous login case. I believe Salgado was the one who pointed it out to me for some other, similar test cases a few months ago.

It appears to be the underlying implementation of logout. logout is
much more familiar to everyone. Is there a reason you're not using it?

Aaron
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.10 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk2KUYcACgkQ0F+nu1YWqI3q9QCfZOnXzryg25OlfKSqLCDaijdK
ilIAnRNr12aZtxTKxglRKdykIqydJbQd
=WNC7
-----END PGP SIGNATURE-----

Revision history for this message
j.c.sackett (jcsackett) wrote : Posted in a previous version of this proposal

> It appears to be the underlying implementation of logout. logout is
> much more familiar to everyone. Is there a reason you're not using it?

Only because in a similar situation when I was starting on LP I was shown endInteraction. logout works, and I've pushed that up as a change.

Revision history for this message
Aaron Bentley (abentley) : Posted in a previous version of this proposal
review: Approve
Revision history for this message
Curtis Hovey (sinzui) wrote :

This is good to land in devel now that the schema supports it.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/browser/message.py'
2--- lib/canonical/launchpad/browser/message.py 2011-03-27 15:30:05 +0000
3+++ lib/canonical/launchpad/browser/message.py 2011-04-06 15:38:21 +0000
4@@ -10,6 +10,7 @@
5 from canonical.launchpad.interfaces.message import IIndexedMessage
6 from canonical.launchpad.webapp.interfaces import ICanonicalUrlData
7
8+
9 class QuestionMessageCanonicalUrlData:
10 """Question messages have a canonical_url within the question."""
11 implements(ICanonicalUrlData)
12
13=== modified file 'lib/canonical/launchpad/security.py'
14--- lib/canonical/launchpad/security.py 2011-03-30 10:46:24 +0000
15+++ lib/canonical/launchpad/security.py 2011-04-06 15:38:21 +0000
16@@ -1656,6 +1656,15 @@
17 checkUnauthenticated = _checkAccess
18
19
20+class SetQuestionCommentVisibility(AuthorizationBase):
21+ permission = 'launchpad.Moderate'
22+ usedfor = IQuestion
23+
24+ def checkAuthenticated(self, user):
25+ """Admins and registry admins can set bug comment visibility."""
26+ return (user.in_admin or user.in_registry_experts)
27+
28+
29 class AdminQuestion(AdminByAdminsTeam):
30 permission = 'launchpad.Admin'
31 usedfor = IQuestion
32
33=== modified file 'lib/lp/answers/configure.zcml'
34--- lib/lp/answers/configure.zcml 2011-03-21 19:58:39 +0000
35+++ lib/lp/answers/configure.zcml 2011-04-06 15:38:21 +0000
36@@ -108,6 +108,10 @@
37 set_attributes="assignee"
38 />
39 <require
40+ permission="launchpad.Moderate"
41+ attributes="setCommentVisibility"
42+ />
43+ <require
44 permission="launchpad.Admin"
45 attributes="setStatus"
46 set_attributes="priority whiteboard"
47
48=== modified file 'lib/lp/answers/interfaces/question.py'
49--- lib/lp/answers/interfaces/question.py 2011-03-27 15:30:33 +0000
50+++ lib/lp/answers/interfaces/question.py 2011-04-06 15:38:21 +0000
51@@ -20,6 +20,10 @@
52 call_with,
53 export_as_webservice_entry,
54 exported,
55+ export_write_operation,
56+ operation_for_version,
57+ operation_parameters,
58+ REQUEST_USER,
59 )
60 from lazr.restful.fields import (
61 CollectionField,
62@@ -475,6 +479,21 @@
63 notify along the rationale for doing so.
64 """
65
66+ @operation_parameters(
67+ comment_number=Int(
68+ title=_('The number of the comment in the list of messages.'),
69+ required=True),
70+ visible=Bool(title=_('Show this comment?'), required=True))
71+ @call_with(user=REQUEST_USER)
72+ @export_write_operation()
73+ @operation_for_version('devel')
74+ def setCommentVisibility(user, comment_number, visible):
75+ """Set the visible attribute on a question message.
76+
77+ This is restricted to Launchpad admins and registry members, and will
78+ return a HTTP Error 401: Unauthorized error for non-admin callers.
79+ """
80+
81
82 # These schemas are only used by browser/question.py and should really live
83 # there. See Bug #66950.
84
85=== modified file 'lib/lp/answers/interfaces/questionmessage.py'
86--- lib/lp/answers/interfaces/questionmessage.py 2010-08-20 20:31:18 +0000
87+++ lib/lp/answers/interfaces/questionmessage.py 2011-04-06 15:38:21 +0000
88@@ -12,6 +12,7 @@
89 ]
90
91 from zope.schema import (
92+ Bool,
93 Choice,
94 Field,
95 )
96@@ -47,4 +48,8 @@
97 "related the action operated by this message."), required=True,
98 readonly=True, default=QuestionStatus.OPEN,
99 vocabulary=QuestionStatus)
100+ visible = Bool(
101+ title=_("Message visibility."),
102+ description=_("Whether or not the message is visible."),
103+ readonly=True)
104
105
106=== modified file 'lib/lp/answers/model/question.py'
107--- lib/lp/answers/model/question.py 2011-01-29 16:52:18 +0000
108+++ lib/lp/answers/model/question.py 2011-04-06 15:38:21 +0000
109@@ -646,6 +646,11 @@
110 """See BugLinkTargetMixin."""
111 return QuestionBug(question=self, bug=bug)
112
113+ def setCommentVisibility(self, user, comment_number, visible):
114+ """See `IQuestion`."""
115+ message = self.messages[comment_number].message
116+ message.visible = visible
117+
118
119 class QuestionSet:
120 """The set of questions in the Answer Tracker."""
121
122=== modified file 'lib/lp/answers/model/questionmessage.py'
123--- lib/lp/answers/model/questionmessage.py 2010-10-03 15:30:06 +0000
124+++ lib/lp/answers/model/questionmessage.py 2011-04-06 15:38:21 +0000
125@@ -48,3 +48,8 @@
126 """See IMessage."""
127 # Delegates do not proxy __ methods, because of the name mangling.
128 return iter(self.chunks)
129+
130+ @property
131+ def visible(self):
132+ """See `IQuestionMessage.`"""
133+ return self.message.visible
134
135=== modified file 'lib/lp/answers/templates/question-index.pt'
136--- lib/lp/answers/templates/question-index.pt 2011-03-04 15:16:53 +0000
137+++ lib/lp/answers/templates/question-index.pt 2011-04-06 15:38:21 +0000
138@@ -92,7 +92,8 @@
139
140
141 <tal:message repeat="message context/messages">
142- <div tal:replace="structure message/@@+display" />
143+ <div tal:condition="message/visible"
144+ tal:replace="structure message/@@+display" />
145 </tal:message>
146
147 <div id="question"
148
149=== modified file 'lib/lp/answers/tests/test_question_webservice.py'
150--- lib/lp/answers/tests/test_question_webservice.py 2011-03-24 22:49:57 +0000
151+++ lib/lp/answers/tests/test_question_webservice.py 2011-04-06 15:38:21 +0000
152@@ -6,13 +6,21 @@
153 __metaclass__ = type
154
155 from BeautifulSoup import BeautifulSoup
156+from lazr.restfulclient.errors import HTTPError
157 from simplejson import dumps
158+import transaction
159+from zope.component import getUtility
160
161 from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
162 from canonical.testing.layers import DatabaseFunctionalLayer
163+from lp.registry.interfaces.person import IPersonSet
164 from lp.testing import (
165 TestCaseWithFactory,
166 celebrity_logged_in,
167+ launchpadlib_for,
168+ logout,
169+ person_logged_in,
170+ ws_object
171 )
172
173
174@@ -67,3 +75,82 @@
175 self.assertEqual(
176 self.findQuestionTitle(response),
177 "<p>No, this is a question</p>")
178+
179+
180+class TestSetCommentVisibility(TestCaseWithFactory):
181+ """Tests who can successfully set comment visibility."""
182+
183+ layer = DatabaseFunctionalLayer
184+
185+ def setUp(self):
186+ super(TestSetCommentVisibility, self).setUp()
187+ self.person_set = getUtility(IPersonSet)
188+ admins = self.person_set.getByName('admins')
189+ self.admin = admins.teamowner
190+ with person_logged_in(self.admin):
191+ self.question = self.factory.makeQuestion()
192+ self.message = self.question.addComment(
193+ self.admin, 'Some comment')
194+ transaction.commit()
195+
196+ def _get_question_for_user(self, user=None):
197+ """Convenience function to get the api question reference."""
198+ # End any open lplib instance.
199+ logout()
200+ if user is not None:
201+ lp = launchpadlib_for("test", user)
202+ else:
203+ lp = launchpadlib_for("test")
204+
205+ question_entry = lp.load(
206+ '/%s/+question/%d/' % (
207+ self.question.target.name, self.question.id))
208+ lp = launchpadlib_for("test", user)
209+ return ws_object(lp, self.question)
210+
211+ def _set_visibility(self, question):
212+ """Method to set visibility; needed for assertRaises."""
213+ question.setCommentVisibility(
214+ comment_number=0,
215+ visible=False)
216+
217+ def test_random_user_cannot_set_visible(self):
218+ # Logged in users without privs can't set question comment
219+ # visibility.
220+ nopriv = self.person_set.getByName('no-priv')
221+ question = self._get_question_for_user(nopriv)
222+ self.assertRaises(
223+ HTTPError,
224+ self._set_visibility,
225+ question)
226+
227+ def test_anon_cannot_set_visible(self):
228+ # Anonymous users can't set question comment
229+ # visibility.
230+ question = self._get_question_for_user()
231+ self.assertRaises(
232+ HTTPError,
233+ self._set_visibility,
234+ question)
235+
236+ def test_registry_admin_can_set_visible(self):
237+ # Members of registry experts can set question comment
238+ # visibility.
239+ registry = self.person_set.getByName('registry')
240+ person = self.factory.makePerson()
241+ with person_logged_in(registry.teamowner):
242+ registry.addMember(person, registry.teamowner)
243+ question = self._get_question_for_user(person)
244+ self._set_visibility(question)
245+ self.assertFalse(self.message.visible)
246+
247+ def test_admin_can_set_visible(self):
248+ # Admins can set question comment
249+ # visibility.
250+ admins = self.person_set.getByName('admins')
251+ person = self.factory.makePerson()
252+ with person_logged_in(admins.teamowner):
253+ admins.addMember(person, admins.teamowner)
254+ question = self._get_question_for_user(person)
255+ self._set_visibility(question)
256+ self.assertFalse(self.message.visible)