Merge lp:~fgallina/rnr-server/edit-click-review-handler into lp:rnr-server

Proposed by Fabián Ezequiel Gallina
Status: Merged
Approved by: Fabián Ezequiel Gallina
Approved revision: 296
Merged at revision: 295
Proposed branch: lp:~fgallina/rnr-server/edit-click-review-handler
Merge into: lp:rnr-server
Diff against target: 343 lines (+173/-15)
7 files modified
src/clickreviews/api/handlers.py (+17/-1)
src/clickreviews/api/urls.py (+3/-0)
src/clickreviews/forms.py (+22/-0)
src/clickreviews/models.py (+8/-2)
src/clickreviews/tests/test_forms.py (+60/-0)
src/clickreviews/tests/test_handlers.py (+45/-12)
src/clickreviews/tests/test_models.py (+18/-0)
To merge this branch: bzr merge lp:~fgallina/rnr-server/edit-click-review-handler
Reviewer Review Type Date Requested Status
Natalia Bidart (community) Approve
Review via email: mp+242540@code.launchpad.net

Commit message

clickreviews: Added review edit endpoint

PUT click/api/1.0/reviews/<review_id>/ with summary, review_text and
rating in request.data will modify a review provided it has no
moderations pending and it was created within the
`MODIFY_WINDOW_MINUTES` window.

To post a comment you must log in.
294. By Fabián Ezequiel Gallina

Merged trunk into edit-click-review-handler.

Revision history for this message
Natalia Bidart (nataliabidart) :
295. By Fabián Ezequiel Gallina

Fixes per review

296. By Fabián Ezequiel Gallina

Small cleanup

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Looks good!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/clickreviews/api/handlers.py'
2--- src/clickreviews/api/handlers.py 2015-02-04 19:54:12 +0000
3+++ src/clickreviews/api/handlers.py 2015-03-10 16:48:49 +0000
4@@ -18,6 +18,7 @@
5 ClickPackageReviewFlagForm,
6 ClickPackageReviewForm,
7 ClickPackageReviewModerationStatusForm,
8+ ClickPackageReviewUpdateForm,
9 )
10 from core.api.pagination import make_paginated_response
11 from core.models import BaseModeration
12@@ -65,7 +66,7 @@
13
14
15 class ClickReviewHandler(BaseHandler):
16- allowed_methods = ('POST',)
17+ allowed_methods = ('POST', 'PUT')
18 anonymous = ClickReviewAnonymousHandler
19 fields = REVIEW_FIELDS
20
21@@ -81,6 +82,21 @@
22
23 return review
24
25+ def update(self, request, review_id):
26+ """Use PUT verb to modify an existing review."""
27+ review = get_object_or_404(
28+ ClickPackageReview, id=review_id, reviewer=request.user)
29+
30+ form = ClickPackageReviewUpdateForm(request.data, instance=review)
31+ if form.is_valid():
32+ review = form.save()
33+ # XXX: Refactor this to be triggered on review save/delete.
34+ review.click_package.update_stats()
35+ response = review
36+ else:
37+ response = HttpResponseBadRequest(form.errors_json())
38+ return response
39+
40
41 class ClickReviewsStatsHandler(BaseHandler):
42 allowed_methods = ('GET',)
43
44=== modified file 'src/clickreviews/api/urls.py'
45--- src/clickreviews/api/urls.py 2014-10-07 19:04:20 +0000
46+++ src/clickreviews/api/urls.py 2015-03-10 16:48:49 +0000
47@@ -27,6 +27,7 @@
48
49 review_resource = CSRFExemptResource(handler=ClickReviewHandler,
50 authentication=auth)
51+review_resource.callmap['PATCH'] = 'patch'
52 review_resource = never_cache(review_resource)
53
54 # Although the review stats resource will be cached by the cache
55@@ -76,6 +77,8 @@
56 urlpatterns = patterns(
57 '',
58 url(r'^1.0/reviews/$', review_resource, name='click-api-reviews'),
59+ url(r'^1.0/reviews/(?P<review_id>\d+)/$',
60+ review_resource, name='click-api-reviews'),
61 url(r'^1.0/review-stats/$', reviews_stats_resource,
62 name='click-api-reviews-stats'),
63 url(r'^1.0/review-stats/(?P<package_name>[^/]+)/$',
64
65=== modified file 'src/clickreviews/forms.py'
66--- src/clickreviews/forms.py 2014-10-07 19:04:20 +0000
67+++ src/clickreviews/forms.py 2015-03-10 16:48:49 +0000
68@@ -1,6 +1,7 @@
69 from django import forms
70 from django.conf import settings
71 from django.core import validators
72+from django.utils.timezone import now
73 from django.utils.translation import ugettext as _
74
75 from clickreviews.models import (
76@@ -86,6 +87,27 @@
77 package_name))
78
79
80+class ClickPackageReviewUpdateForm(forms.ModelForm, JSONErrorMixin):
81+
82+ can_modify = forms.BooleanField(required=False, widget=forms.HiddenInput)
83+
84+ def clean_can_modify(self):
85+ review = self.instance
86+ time_diff = (now() - review.date_created).seconds
87+ time_limit = settings.MODIFY_WINDOW_MINUTES * 60
88+ if time_diff > time_limit:
89+ raise forms.ValidationError(
90+ "This review cannot be edited (edit window expired).")
91+ elif review.is_awaiting_moderation():
92+ raise forms.ValidationError(
93+ "This review cannot be edited (pending moderation).")
94+ return True
95+
96+ class Meta:
97+ model = ClickPackageReview
98+ fields = ('summary', 'review_text', 'rating')
99+
100+
101 class ClickPackageReviewFilterForm(forms.Form, JSONErrorMixin):
102 package_name = forms.CharField(max_length=255)
103
104
105=== modified file 'src/clickreviews/models.py'
106--- src/clickreviews/models.py 2014-10-09 13:27:06 +0000
107+++ src/clickreviews/models.py 2015-03-10 16:48:49 +0000
108@@ -70,6 +70,10 @@
109 def reviewer_displayname(self):
110 return self.reviewer.get_full_name()
111
112+ def is_awaiting_moderation(self):
113+ """Return true if the review has any pending moderations."""
114+ return self.moderation_set.pending().exists()
115+
116
117 # Q) So, what's this tonguetwister all about? A) In click packages,
118 # not only reviews can be flagged for moderation but also the click
119@@ -79,7 +83,8 @@
120 class ClickPackageModeration(BaseModeration):
121 """Track moderation of Click Packages."""
122
123- package = models.ForeignKey(ClickPackage)
124+ package = models.ForeignKey(
125+ ClickPackage, related_name='moderation_set')
126
127 objects = ModerationManager()
128
129@@ -122,7 +127,8 @@
130 class ClickPackageReviewModeration(BaseModeration):
131 """Track moderation of Click Package reviews."""
132
133- review = models.ForeignKey(ClickPackageReview)
134+ review = models.ForeignKey(
135+ ClickPackageReview, related_name='moderation_set')
136
137 objects = ModerationManager()
138
139
140=== modified file 'src/clickreviews/tests/test_forms.py'
141--- src/clickreviews/tests/test_forms.py 2014-10-07 19:04:20 +0000
142+++ src/clickreviews/tests/test_forms.py 2015-03-10 16:48:49 +0000
143@@ -1,11 +1,16 @@
144+import datetime
145 import mock
146
147+from django.conf import settings
148+from django.utils.timezone import now
149+
150 from clickreviews.forms import (
151 ClickPackageFlagForm,
152 ClickPackageModerationStatusForm,
153 ClickPackageReviewFlagForm,
154 ClickPackageReviewForm,
155 ClickPackageReviewModerationStatusForm,
156+ ClickPackageReviewUpdateForm,
157 )
158 from clickreviews.models import (
159 ClickPackage,
160@@ -94,6 +99,61 @@
161 self.assertEqual(True, form.is_valid())
162
163
164+class ClickPackageReviewUpdateFormTestCase(TestCaseWithFactory):
165+
166+ def setUp(self):
167+ super(ClickPackageReviewUpdateFormTestCase, self).setUp()
168+ self.valid_data = {
169+ 'summary': 'New summary',
170+ 'review_text': 'New review body',
171+ 'rating': 5}
172+
173+ def test_valid(self):
174+ review = self.factory.makeClickPackageReview(rating=3)
175+ form = ClickPackageReviewUpdateForm(self.valid_data, instance=review)
176+ self.assertTrue(form.is_valid())
177+ review = form.save()
178+ for key, value in self.valid_data.items():
179+ self.assertEqual(getattr(review, key), self.valid_data[key])
180+
181+ def assert_field_required(self, field_name):
182+ del self.valid_data[field_name]
183+ review = self.factory.makeClickPackageReview()
184+ form = ClickPackageReviewUpdateForm(self.valid_data, instance=review)
185+ self.assertFalse(form.is_valid())
186+ self.assertIn(field_name, form.errors)
187+ self.assertIn('This field is required.', form.errors[field_name])
188+
189+ def test_summary_required(self):
190+ self.assert_field_required('summary')
191+
192+ def test_review_text_required(self):
193+ self.assert_field_required('review_text')
194+
195+ def test_rating_required(self):
196+ self.assert_field_required('rating')
197+
198+ def test_modify_window_expired(self):
199+ created = now() - datetime.timedelta(
200+ minutes=settings.MODIFY_WINDOW_MINUTES, seconds=1)
201+ review = self.factory.makeClickPackageReview(date_created=created)
202+ form = ClickPackageReviewUpdateForm(self.valid_data, instance=review)
203+ self.assertFalse(form.is_valid())
204+ self.assertIn('can_modify', form.errors)
205+ self.assertIn("This review cannot be edited (edit window expired).",
206+ form.errors['can_modify'])
207+
208+ def test_is_awaiting_moderation(self):
209+ review = self.factory.makeClickPackageReview()
210+ self.factory.makeClickPackageReviewModeration(review=review)
211+ self.assertTrue(review.is_awaiting_moderation())
212+ form = ClickPackageReviewUpdateForm(self.valid_data, instance=review)
213+ self.assertFalse(form.is_valid())
214+ self.assertIn('can_modify', form.errors)
215+ self.assertIn("This review cannot be edited (pending moderation).",
216+ form.errors['can_modify'])
217+
218+
219 class BaseClickPackageFlagFormTestCase(TestCaseWithFactory):
220
221 form = None
222
223=== modified file 'src/clickreviews/tests/test_handlers.py'
224--- src/clickreviews/tests/test_handlers.py 2015-02-04 19:54:12 +0000
225+++ src/clickreviews/tests/test_handlers.py 2015-03-10 16:48:49 +0000
226@@ -26,17 +26,18 @@
227 from core.tests.helpers import SSOTestCaseMixin
228
229
230-class ClickReviewsHandlerPostTestCase(TestCaseWithFactory):
231+class ClickReviewsHandlerTestCase(TestCaseWithFactory):
232
233 def setUp(self):
234- super(ClickReviewsHandlerPostTestCase, self).setUp()
235+ super(ClickReviewsHandlerTestCase, self).setUp()
236
237 # Ensure we don't hit the network for our tests.
238 is_authed_fn = ('core.api.auth.SSOOAuthAuthentication.'
239 'is_authenticated')
240- patcher = mock.patch(is_authed_fn, new_callable=mock.Mock)
241+ patcher = mock.patch(
242+ is_authed_fn, new_callable=mock.Mock, return_value=True)
243 self.addCleanup(patcher.stop)
244- patcher.start().return_value = True
245+ self.mock_is_authenticated = patcher.start()
246
247 click_verify_fn = ('clickreviews.forms.'
248 'verify_click_package_published')
249@@ -132,19 +133,13 @@
250 self.assertEqual(1, click_package.ratings_total)
251 self.assertEqual(5, click_package.ratings_average)
252
253-
254-class ClickReviewsHandlerGetTestCase(TestCaseWithFactory):
255-
256 def _get_reviews(self, data=None):
257 url = reverse('click-api-reviews')
258 kwargs = {}
259 if data is not None:
260 kwargs['data'] = data
261- is_authenticated_method = (
262- 'core.api.auth.SSOOAuthAuthentication.is_authenticated')
263- with mock.patch(is_authenticated_method) as mock_is_authenticated:
264- mock_is_authenticated.return_value = False
265- return self.client.get(url, **kwargs)
266+ self.mock_is_authenticated.return_value = False
267+ return self.client.get(url, **kwargs)
268
269 def test_get_multiple_reviews_for_package(self):
270 package = self.factory.makeClickPackage()
271@@ -252,6 +247,44 @@
272 'package_name': package.package_name,
273 })
274
275+ def _update_review(self, review, data, user=None):
276+ # Post a review as an authenticated user.
277+ url = reverse('click-api-reviews', args=(review.pk,))
278+ if user is None:
279+ user = review.reviewer
280+ self.client.login(username=user.username, password='test')
281+ response = self.client.put(
282+ url, data=json.dumps(data), content_type='application/json')
283+ return response
284+
285+ def test_update_review(self):
286+ review = self.factory.makeClickPackageReview()
287+ data = {'summary': 'New summary',
288+ 'review_text': 'Review text',
289+ 'rating': 5}
290+ response = self._update_review(review, data)
291+ self.assertEqual(response.status_code, httplib.OK)
292+ review = json.loads(response.content)
293+ for key, value in data.items():
294+ self.assertEqual(review[key], data[key])
295+
296+ def test_update_review_missing_data(self):
297+ review = self.factory.makeClickPackageReview()
298+ response = self._update_review(review, {})
299+ self.assertEqual(response.status_code, httplib.BAD_REQUEST)
300+ errors = json.loads(response.content)
301+ expected = {'errors':
302+ {'rating': ['This field is required.'],
303+ 'review_text': ['This field is required.'],
304+ 'summary': ['This field is required.']}}
305+ self.assertEqual(errors, expected)
306+
307+ def test_update_review_wrong_user(self):
308+ review = self.factory.makeClickPackageReview()
309+ user = self.factory.makeUser()
310+ response = self._update_review(review, {}, user)
311+ self.assertEqual(response.status_code, httplib.NOT_FOUND)
312+
313
314 class ClickReviewsStatsHandlerTestCase(TestCaseWithFactory):
315
316
317=== modified file 'src/clickreviews/tests/test_models.py'
318--- src/clickreviews/tests/test_models.py 2014-10-07 19:04:20 +0000
319+++ src/clickreviews/tests/test_models.py 2015-03-10 16:48:49 +0000
320@@ -101,6 +101,24 @@
321 self.assertFalse(form.is_valid(), 'form should fail validation')
322 self.assertTrue(field in form.errors)
323
324+ def test_is_awaiting_moderation_no_moderations(self):
325+ review = self.factory.makeClickPackageReview()
326+ review.moderation_set.all().delete()
327+ self.assertFalse(review.is_awaiting_moderation())
328+
329+ def test_is_awaiting_moderation_with_pending_moderations(self):
330+ review = self.factory.makeClickPackageReview()
331+ self.factory.makeClickPackageReviewModeration(review=review)
332+ self.assertTrue(review.is_awaiting_moderation())
333+
334+ def test_is_awaiting_moderation_with_no_pending_moderations(self):
335+ review = self.factory.makeClickPackageReview()
336+ self.factory.makeClickPackageReviewModeration(
337+ review=review, status=ClickPackageReviewModeration.APPROVED_STATUS)
338+ self.factory.makeClickPackageReviewModeration(
339+ review=review, status=ClickPackageReviewModeration.REJECTED_STATUS)
340+ self.assertFalse(review.is_awaiting_moderation())
341+
342
343 class BaseClickPackageModerationTestCase(TestCaseWithFactory):
344

Subscribers

People subscribed via source and target branches