Merge lp:~aaronp/rnr-server/get-usefulness-by-user into lp:rnr-server

Proposed by Aaron Peachey
Status: Merged
Approved by: Michael Nelson
Approved revision: 153
Merged at revision: 154
Proposed branch: lp:~aaronp/rnr-server/get-usefulness-by-user
Merge into: lp:rnr-server
Diff against target: 289 lines (+166/-0)
6 files modified
src/reviewsapp/api/handlers.py (+31/-0)
src/reviewsapp/api/urls.py (+13/-0)
src/reviewsapp/models/reviews.py (+7/-0)
src/reviewsapp/tests/factory.py (+21/-0)
src/reviewsapp/tests/test_handlers.py (+79/-0)
src/reviewsapp/tests/test_rnrclient.py (+15/-0)
To merge this branch: bzr merge lp:~aaronp/rnr-server/get-usefulness-by-user
Reviewer Review Type Date Requested Status
Danny Tamez (community) Needs Fixing
Michael Nelson (community) Approve
Review via email: mp+53580@code.launchpad.net

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

Commit message

[r=noodles] Adds handler for retrieval of usefulness records per review or user (aaronp)

Description of the change

New handler/url to enable retrieval of all usefulness records that have been voted by a particular user and associated tests. Can be used on client to determine which reviews a user has submitted usefulness for without the need to send all usefulness vote details back with every single review.

To post a comment you must log in.
Revision history for this message
Michael Nelson (michael.nelson) wrote : Posted in a previous version of this proposal

Hey Aaron! Thanks for the branch.. it's looking great!

There's a small typo in your makeUsefulness() factory method ('review = self.makeUser()'), but other than that, it looks ready to land :)

If you've got time, here are a few optional things that you may (or may not) want to think about:
 * I wonder if it'd be worthwhile cutting the username out of the url:
    r'^1.0/usefulness/$
   and instead adding optional filtering via GET (/1.0/usefulness/?username=aaronp)? To make it easier, we could just require the username GET param at the moment, but it means we can extend the API later without adding more urls? See what you think.
 * A few long lines there... if you get a chance, just snip them to 78 chars (no problem if you don't...).
 * Great tests! I assume we'll also want to add this call to the rnr client? If so, just patch your local rnr client (parts/required-isd-branches/rnrclient/rnrclient.py) and add a basic test to test_rnrclient() that exercises the new method. Update the description of this MP with a link to the patch for rnrclient and we can make sure it lands at the same time.

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

Hi Michael,

Thanks for the feedback. I'll take a look and resubmit.

Thanks
Aaron

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

Changes made as per feedback. Also added ability to filter by review_id as it was trivial to add.

Diff for rnrclient below:

34c34
<
---
>
149a150,166
> @validate('review_id', int, required=False)
> @validate_pattern('username', r'[^\n]+', required=False)
> @returns_json
> def get_usefulness(self, review_id=None, username=None):
> """Get a list of usefulness filtered by username/review_id"""
> if not username and not review_id:
> return None
> get_string = "?"
> if username:
> get_string = '%susername=%s' % (get_string, username)
> if review_id:
> get_string = "%s&review_id=%s" % (get_string, str(review_id))
> else:
> get_string = '%sreview_id=%s' % (get_string, review_id)
>
> return self._get('/usefulness/%s' % get_string,
> scheme=PUBLIC_API_SCHEME)

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

I'm re-thinking the patch now. Is this a better way?

> @validate('review_id', int, required=False)
> @validate_pattern('username', r'[^\n]+', required=False)
> @returns_json
> def get_usefulness(self, review_id=None, username=None):
> """Get a list of usefulness filtered by username/review_id"""
> if not username and not review_id:
> return None
>
> data = {}
> if username:
> data ['username'] = username
> if review_id:
> data ['review_id'] = str(review_id)
>
> return self._get('/usefulness/%s' % get_string, data=data,
> scheme=PUBLIC_API_SCHEME)

Revision history for this message
Michael Nelson (michael.nelson) wrote : Posted in a previous version of this proposal

Hey again Aaron!

On Tue, Mar 15, 2011 at 1:47 PM, Aaron Peachey <email address hidden> wrote:

> I'm re-thinking the patch now. Is this a better way?
>

Yeah, this one is nicer... thanks, although your test should show (I think?)
that you've left get_string in there - but I'll remove that when running it
before landing.

With the changes to the actual branch, can you pls revert the changes to
config/main.cfg . A great way for your next branch (/me hopes :) ), is to
add a django_project/local.cfg with just the customisations you need - this
will overwrite anything in main.cfg, and won't get committed.

The rest looks great! Thanks for being the first community contributor :)
Once you revert the main.cfg, I'll land the patch to rnrclient and then this
branch.

-Michael

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

Thanks for the feedback again Michael. I've made the changes to revert main.cfg and resubmitted.
Let me know if anything else.
cheers
Aaron

Revision history for this message
Aaron Peachey (aaronp) wrote :

btw - correct, re the get_String left in. I had issues with the computer that my server was set up on so was feeling my way around for the second patch. Running the test just now confirmed that it was incorrect, and also that data=data was incorrect.

Here's the correct diff, 3rd time lucky:

34c34
<
---
>
150,166d149
< @validate('review_id', int, required=False)
< @validate_pattern('username', r'[^\n]+', required=False)
< @returns_json
< def get_usefulness(self, review_id=None, username=None):
< """Get a list of usefulness filtered by username/review_id"""
< if not username and not review_id:
< return None
<
< data = {}
<
< if username:
< data['username'] = username
< if review_id:
< data['review_id'] = str(review_id)
<
< return self._get('/usefulness', args=data,
< scheme=PUBLIC_API_SCHEME)

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

Great, thanks again Aaron. I've landed the patch for rnrclient and will set this branch to land now too.

review: Approve
Revision history for this message
Danny Tamez (zematynnad) wrote :

Aaron - thanks for this. Everything looks good except for some minor issues.
Could you make the following changes?

Please change line 112 of src/reviewsapp/api/urls.py so that it uses parameterized urls for each variation rather than one url with a query string. We like to avoid query strings to be able to use caching.

Also (as long as you're resubmitting) - not required but it would be nice to fix some PEP8 issues:
please remove the extra blank line at 339 of src/reviewsapp/models/reviews.py
please remove the blank line at the end of src/reviewsapp/api/handlers.py
please add another blank line before the definitions of classes ShowUsefulnessHandler and ShowReviewsHandler.
src/reviewsapp/tests/test_handlers.py
please add another blank line before def of _get_url_kwargs_dict_for_review, before def of class ServerStatusHandlerTestCase, before def of class ShowUsefulnessHandlerTesetCase
please add spaces around '=' when they aren't keyword assignments (lines 70, 72,
remove space before ":" line 181 and after "{" at 534
add space after ":" 205, 206, 208
remove blank line at line 233
please remove blank line at end of file in src/reviewsapp/tests/test_rnrclient.py
and trim line 456 to 80 chars or less

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

On Wed, Mar 16, 2011 at 4:53 PM, Danny Tamez <email address hidden>wrote:

> Review: Needs Fixing
> Aaron - thanks for this. Everything looks good except for some minor
> issues.
> Could you make the following changes?
>

Hey Danny - this branch has already been approved and landed, but just on
your points below:

>
> Please change line 112 of src/reviewsapp/api/urls.py so that it uses
> parameterized urls for each variation rather than one url with a query
> string. We like to avoid query strings to be able to use caching.
>

The original MP that Aaron submitted was using a url like:

r'^1.0/usefulness/(?P<username>\w+)/$'

and I asked him to change this to use GET parms so that this API call could
be more general (filter not just by username, but also by reviewid etc.) in
the future (and aaron added a filter by reviewid just because it was easy),
based on my (possibly wrong?) assumption that caching was not so relevant
for this api call (I don't see 1000 requests for the usefulness objects for
the one user).

If you disagree, then we'll need to fix it as a separate branch since this
one has landed.

> Also (as long as you're resubmitting) - not required but it would be nice
> to fix some PEP8 issues:
>

Same - one of us can submit a branch fixing these. Similar to you, I didn't
want to require those kind of changes from community contributors. I think
when we have a make target (or auto-lint check) for this kind of stuff it
will be much easier for community contributors to check, but until then, I'm
happy to merge branches locally and then do the lint/PEP8 fixes as a
separate commit before landing it (I didn't do this this morning). Let me
know what you think.

-Michael

> please remove the extra blank line at 339 of
> src/reviewsapp/models/reviews.py
> please remove the blank line at the end of src/reviewsapp/api/handlers.py
> please add another blank line before the definitions of classes
> ShowUsefulnessHandler and ShowReviewsHandler.
> src/reviewsapp/tests/test_handlers.py
> please add another blank line before def of
> _get_url_kwargs_dict_for_review, before def of class
> ServerStatusHandlerTestCase, before def of class
> ShowUsefulnessHandlerTesetCase
> please add spaces around '=' when they aren't keyword assignments (lines
> 70, 72,
> remove space before ":" line 181 and after "{" at 534
> add space after ":" 205, 206, 208
> remove blank line at line 233
> please remove blank line at end of file in
> src/reviewsapp/tests/test_rnrclient.py
> and trim line 456 to 80 chars or less
>
> --
>
> https://code.launchpad.net/~aaronp/rnr-server/get-usefulness-by-user/+merge/53580
> You are reviewing the proposed merge of
> lp:~aaronp/rnr-server/get-usefulness-by-user into lp:rnr-server.
>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/reviewsapp/api/handlers.py'
2--- src/reviewsapp/api/handlers.py 2011-03-11 15:02:54 +0000
3+++ src/reviewsapp/api/handlers.py 2011-03-16 08:45:50 +0000
4@@ -26,6 +26,7 @@
5 'ServerStatusHandler',
6 'ShowReviewsHandler',
7 'SubmitReviewHandler',
8+ 'ShowUsefulnessHandler',
9 ]
10
11 import urllib
12@@ -88,6 +89,33 @@
13 except Review.DoesNotExist:
14 return HttpResponseNotFound('No matching review found')
15
16+class ShowUsefulnessHandler(BaseHandler):
17+ model = Usefulness
18+ fields = (
19+ 'review_id',
20+ 'username',
21+ 'useful',
22+ )
23+ allowed_methods = ('GET',)
24+
25+ def read(self, request):
26+ username = request.GET.get('username', None)
27+ review_id = request.GET.get('review_id', None)
28+
29+ if not username and not review_id:
30+ return HttpResponseBadRequest('No valid parameters passed.'
31+ + ' Please pass at least username or review_id')
32+
33+ if username:
34+ response = Usefulness.objects.filter(user__username=username)
35+ if review_id:
36+ response = response.filter(
37+ review__id=review_id)
38+ else:
39+ response = Usefulness.objects.filter(
40+ review__id=review_id)
41+
42+ return response
43
44 class ShowReviewsHandler(BaseHandler):
45 model = Review
46@@ -228,3 +256,6 @@
47 return HttpResponse('"Updated"', status=200)
48 else:
49 return HttpResponse('"Not modified"', status=200)
50+
51+
52+
53
54=== modified file 'src/reviewsapp/api/urls.py'
55--- src/reviewsapp/api/urls.py 2011-03-08 16:04:05 +0000
56+++ src/reviewsapp/api/urls.py 2011-03-16 08:45:50 +0000
57@@ -30,6 +30,7 @@
58 SubmitReviewHandler,
59 SubmitUsefulnessHandler,
60 RetrieveReviewHandler,
61+ ShowUsefulnessHandler
62 )
63 from reviewsapp.auth import SSOOAuthAuthentication
64 from reviewsapp.api.decorators import dont_vary
65@@ -58,6 +59,11 @@
66 retrieve_review_resource.__name__ = "Resource"
67 retrieve_review_resource = dont_vary(retrieve_review_resource)
68
69+# The show usefulness resource is cached.
70+show_usefulness_resource = Resource(handler=ShowUsefulnessHandler)
71+show_usefulness_resource.__name__ = "Resource"
72+show_usefulness_resource = dont_vary(show_usefulness_resource)
73+
74
75 # The following POST handlers have POST data and will not be cached.
76 submit_review_resource = Resource(handler=SubmitReviewHandler,
77@@ -99,6 +105,13 @@
78 url(r'^1.0/reviews/(?P<review_id>\d+)/recommendations/$',
79 submit_usefulness_resource, name='rnr-api-recommendations'),
80
81+ # get the filtered usefulness records using parameters for filtering
82+ # GET /1.0/reviews/usefulness/?username=aaronp
83+ # GET /1.0/reviews/usefulness/?review_id=100
84+ # GET /1.0/reviews/usefulness/?username=aaronp&review_id=100
85+ url(r'^1.0/usefulness/$',
86+ show_usefulness_resource, name='rnr-api-usefulness-filter'),
87+
88 # gets a single review for a given review id
89 # GET /1.0/reviews/100/
90 url(r'^1.0/reviews/(?P<review_id>\d+)/$',
91
92=== modified file 'src/reviewsapp/models/reviews.py'
93--- src/reviewsapp/models/reviews.py 2011-03-15 14:19:57 +0000
94+++ src/reviewsapp/models/reviews.py 2011-03-16 08:45:50 +0000
95@@ -367,10 +367,17 @@
96 user = models.ForeignKey(User)
97 useful = models.BooleanField(db_index=True)
98
99+ def username(self):
100+ return self.user.username
101+
102+ def review_id(self):
103+ return self.review.id
104+
105 class Meta:
106 app_label = 'reviewsapp'
107
108
109+
110 class RNRSettings(models.Model):
111 ACTIVE_MODE_FLAG_SUMMARY = 'Automatically Flagged'
112 ACTIVE_MODE_FLAG_DESCRIPTION = ('This review was automatically flagged '
113
114=== modified file 'src/reviewsapp/tests/factory.py'
115--- src/reviewsapp/tests/factory.py 2011-03-15 14:19:57 +0000
116+++ src/reviewsapp/tests/factory.py 2011-03-16 08:45:50 +0000
117@@ -44,10 +44,20 @@
118 SoftwareItemInRepository,
119 Token,
120 Consumer,
121+ Usefulness
122 )
123 from reviewsapp.utilities import full_claimed_id
124
125
126+class RequestTestObject(object):
127+ """Test object for emulating GET requests"""
128+ def __init__(self,fakeGetDict=None):
129+ if fakeGetDict:
130+ self.GET = fakeGetDict
131+ else:
132+ self.GET = {}
133+
134+
135 class ReviewsObjectFactory(object):
136 """A factory for creating model objects for testing."""
137
138@@ -136,6 +146,17 @@
139 return Repository.objects.create(
140 origin=origin, distroseries=distro_series)
141
142+ def makeUsefulness(self, review=None, user=None, useful=True):
143+ if review is None:
144+ review = self.makeReview()
145+ if user is None:
146+ user = self.makeUser()
147+
148+ usefulness = Usefulness.objects.create(review=review, user=user,
149+ useful=useful)
150+
151+ return usefulness
152+
153 def makeReview(self, reviewer=None, software_item=None, repository=None,
154 version=None, rating="5", summary=None, review_text=None,
155 date_created=None, language=None, hide=False, num_flags=0,
156
157=== modified file 'src/reviewsapp/tests/test_handlers.py'
158--- src/reviewsapp/tests/test_handlers.py 2011-03-15 14:19:57 +0000
159+++ src/reviewsapp/tests/test_handlers.py 2011-03-16 08:45:50 +0000
160@@ -27,6 +27,7 @@
161 'ShowReviewsHandlerTestCase',
162 'SubmitReviewHandlerTestCase',
163 'UsefulnessHandlerTestCase',
164+ 'ShowUsefulnessHandlerTestCase',
165 ]
166
167
168@@ -49,6 +50,7 @@
169 ServerStatusHandler,
170 ShowReviewsHandler,
171 SubmitUsefulnessHandler,
172+ ShowUsefulnessHandler,
173 )
174 from reviewsapp.models import (
175 Repository,
176@@ -57,6 +59,7 @@
177 SoftwareItem,
178 )
179 from reviewsapp.tests.factory import TestCaseWithFactory
180+from reviewsapp.tests.factory import RequestTestObject
181 from reviewsapp.tests.helpers import patch_settings
182 from reviewsapp.tests.matchers import (
183 ContainsStatsForPackage,
184@@ -164,6 +167,82 @@
185 NoReverseMatch, reverse, 'rnr-api-reviews-stats',
186 kwargs={'days': days})
187
188+class ShowUsefulnessHandlerTestCase(TestCaseWithFactory):
189+ def test_read_for_single_parameter(self):
190+ '''test that a single usefulness vote by a user is returned when
191+ searching for that user'''
192+ review = self.factory.makeReview(
193+ software_item=self.factory.makeSoftwareItem('package_bar'))
194+ user = self.factory.makeUser()
195+ usefulness = self.factory.makeUsefulness(review, user)
196+
197+ handler = ShowUsefulnessHandler()
198+
199+ request1 = RequestTestObject({'username': user.username})
200+ request2 = RequestTestObject({'review_id' : review.id})
201+
202+ results1 = handler.read(request1)
203+ results2 = handler.read(request2)
204+
205+ self.assertEqual(1, len(results1))
206+ self.assertEqual(usefulness, results1[0])
207+ self.assertEqual(usefulness, results2[0])
208+
209+ def test_read_filtering(self):
210+ '''test that a passed parameters results in all relevant items
211+ being returned, but no unwanted items'''
212+ review1 = self.factory.makeReview(
213+ software_item=self.factory.makeSoftwareItem('package_bar'))
214+ review2 = self.factory.makeReview(
215+ software_item=self.factory.makeSoftwareItem('foo_bar'))
216+ user1 = self.factory.makeUser()
217+ user2 = self.factory.makeUser()
218+ usefulness1 = self.factory.makeUsefulness(review1, user1)
219+ usefulness2 = self.factory.makeUsefulness(review1, user2)
220+ usefulness3 = self.factory.makeUsefulness(review2, user1)
221+
222+ handler = ShowUsefulnessHandler()
223+
224+ request1 = RequestTestObject({'username':user1.username})
225+ request2 = RequestTestObject({'review_id':review1.id})
226+ request3 = RequestTestObject(
227+ {'username':user1.username, 'review_id':review2.id}
228+ )
229+
230+ results1 = handler.read(request1)
231+ results2 = handler.read(request2)
232+ results3 = handler.read(request3)
233+
234+ #test username filter results
235+ self.assertEqual(2, len(results1))
236+ self.assertIn(usefulness1, results1)
237+ self.assertIn(usefulness3, results1)
238+ self.assertNotIn(usefulness2, results1)
239+
240+ #test review_id filter results
241+ self.assertEqual(2, len(results2))
242+ self.assertIn(usefulness1, results2)
243+ self.assertIn(usefulness2, results2)
244+ self.assertNotIn(usefulness3, results2)
245+
246+ #test usefulness AND review_id filter results
247+ self.assertEqual(1, len(results3))
248+ self.assertNotIn(usefulness1, results3)
249+ self.assertNotIn(usefulness2, results3)
250+ self.assertIn(usefulness3, results3)
251+
252+
253+ def test_read_for_no_parameters_gives_400_error(self):
254+ '''test that a http 400 error is returned if no valid parameter
255+ is passed'''
256+ handler = ShowUsefulnessHandler()
257+ request1 = RequestTestObject()
258+ request2 = RequestTestObject({'something':'nothing'})
259+ results1 = handler.read(request1)
260+ results2 = handler.read(request2)
261+ self.assertEqual(httplib.BAD_REQUEST, results1.status_code)
262+ self.assertEqual(httplib.BAD_REQUEST, results2.status_code)
263+
264
265 class ShowReviewsHandlerTestCase(TestCaseWithFactory):
266
267
268=== modified file 'src/reviewsapp/tests/test_rnrclient.py'
269--- src/reviewsapp/tests/test_rnrclient.py 2011-03-11 15:13:18 +0000
270+++ src/reviewsapp/tests/test_rnrclient.py 2011-03-16 08:45:50 +0000
271@@ -451,3 +451,18 @@
272 count = Usefulness.objects.filter(review=review).count()
273 self.assertEqual(1, count)
274
275+ def test_get_usefulness(self):
276+ api = RatingsAndReviewsAPI()
277+ software_item = self.factory.makeSoftwareItem(app_name='something2')
278+ review = self.factory.makeReview(software_item=software_item)
279+ user = self.factory.makeUser()
280+ usefulness = self.factory.makeUsefulness(review=review, user=user)
281+
282+ response = api.get_usefulness(review_id=review.id, username=user.username)
283+
284+ self.assertEqual(1, len(response))
285+ self.assertEqual(user.username, response[0].get('username'))
286+ self.assertEqual(review.id, response[0].get('review_id'))
287+ self.assertEqual(usefulness.useful, response[0].get('useful'))
288+
289+

Subscribers

People subscribed via source and target branches