Merge lp:~aaronp/rnr-server/get-usefulness-by-user into lp:rnr-server
- get-usefulness-by-user
- Merge into trunk
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 |
Related bugs: |
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.
Michael Nelson (michael.nelson) wrote : Posted in a previous version of this proposal | # |
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
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(
> @validate_
> @returns_json
> def get_usefulness(
> """Get a list of usefulness filtered by username/
> 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(
> scheme=
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(
> @validate_
> @returns_json
> def get_usefulness(
> """Get a list of usefulness filtered by username/
> 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(
> scheme=
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_
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
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
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(
< @validate_
< @returns_json
< def get_usefulness(
< """Get a list of usefulness filtered by username/
< 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(
< scheme=
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.
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/
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/
please remove the blank line at the end of src/reviewsapp/
please add another blank line before the definitions of classes ShowUsefulnessH
src/reviewsapp/
please add another blank line before def of _get_url_
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/
and trim line 456 to 80 chars or less
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/
> 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/
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/
> please remove the blank line at the end of src/reviewsapp/
> please add another blank line before the definitions of classes
> ShowUsefulnessH
> src/reviewsapp/
> please add another blank line before def of
> _get_url_
> ServerStatusHan
> ShowUsefulnessH
> 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/
> and trim line 456 to 80 chars or less
>
> --
>
> https:/
> You are reviewing the proposed merge of
> lp:~aaronp/rnr-server/get-usefulness-by-user into lp:rnr-server.
>
Preview Diff
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 | + |
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: 1.0/usefulness/ $ s/?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. 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.
* I wonder if it'd be worthwhile cutting the username out of the url:
r'^
and instead adding optional filtering via GET (/1.0/usefulnes
* 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/