Merge lp:~aaronp/rnr-server/delete-review into lp:rnr-server

Proposed by Aaron Peachey
Status: Merged
Approved by: Michael Nelson
Approved revision: 187
Merged at revision: 183
Proposed branch: lp:~aaronp/rnr-server/delete-review
Merge into: lp:rnr-server
Diff against target: 502 lines (+294/-8)
10 files modified
src/reviewsapp/api/handlers.py (+9/-0)
src/reviewsapp/api/urls.py (+9/-1)
src/reviewsapp/forms.py (+3/-1)
src/reviewsapp/migrations/0010_add_review_date_deleted.py (+157/-0)
src/reviewsapp/models/reviews.py (+9/-1)
src/reviewsapp/tests/factory.py (+9/-4)
src/reviewsapp/tests/test_handlers.py (+58/-0)
src/reviewsapp/tests/test_rnrclient.py (+26/-0)
src/reviewsapp/tests/test_views.py (+12/-0)
src/reviewsapp/views/moderation.py (+2/-1)
To merge this branch: bzr merge lp:~aaronp/rnr-server/delete-review
Reviewer Review Type Date Requested Status
Michael Nelson (community) Approve
Aaron Peachey (community) Needs Resubmitting
Review via email: mp+59658@code.launchpad.net

Commit message

[r=noodles][bug=778301] Enable rnrclient method for user to delete their own review.

Description of the change

add's functionality for a user to self-delete a review they have submitted.
Adds a new field in the Review model to hold a date/time of deletion (where null = not deleted) and uses existing hide flag.

To post a comment you must log in.
Revision history for this message
Michael Nelson (michael.nelson) wrote :
Download full text (10.7 KiB)

On Mon, May 2, 2011 at 3:21 PM, Aaron Peachey <email address hidden> wrote:
Aaron Peachey has proposed merging lp:~aaronp/rnr-server/delete-review into lp:rnr-server.

Requested reviews:
 Ratings and Reviews Developers (rnr-developers)

Hey Aaron! Thanks for the branch!

For more details, see:
https://code.launchpad.net/~aaronp/rnr-server/delete-review/+merge/59658

add's functionality for a user to self-delete a review they have submitted.

Great!

Adds a new field in the Review model to hold a date/time of deletion (where null = not deleted) and uses existing hide flag.

A thought - can the review be unhidden by some other process? ie., something like:

1) users creates review,
2) it gets flagged (and is hidden)
3) user deletes review (deletion tiime set, already hidden), but still in moderation queue
4) moderator marks the review as OK (and it is unhidden, even though deletion timestamp is set).

I guess we just need to ensure that these reviews don't appear for moderation? What do you think?

Also, whenever we change the database models, we need to create a new migration so that the production databases can be updated. Normally this is as simple as `fab manage:'migrate reviewsapp add_deletion_timestamp --auto'`. But if it's a hassle, I can add that on to your branch afterwards.

Some other thoughts below.

--

https://code.launchpad.net/~aaronp/rnr-server/delete-review/+merge/59658
Your team Ratings and Reviews Developers is requested to review the proposed merge of lp:~aaronp/rnr-server/delete-review into lp:rnr-server.

=== modified file 'src/reviewsapp/api/handlers.py'
--- src/reviewsapp/api/handlers.py 2011-04-12 10:43:30 +0000
+++ src/reviewsapp/api/handlers.py 2011-05-02 13:21:35 +0000
@@ -27,6 +27,7 @@
    'ShowReviewsHandler',
    'SubmitReviewHandler',
    'ShowUsefulnessHandler',
+ 'DeleteReviewHandler',
 ]

 import urllib
@@ -37,6 +38,7 @@
    HttpResponse,
    HttpResponseBadRequest,
    HttpResponseNotFound,
+ HttpResponseForbidden,
    )
 from django.shortcuts import get_object_or_404
 from django.utils import simplejson
@@ -236,6 +238,23 @@

        return review

+class DeleteReviewHandler(BaseHandler):
+ allowed_methods = ('POST',)

Unrelated to your branch (as we already seem to have these multiple handlers), but we should have been able to have a single handler resource that takes different methods... ie, rather than creating a new handler here and using a POST, we could use I've not used the functionality yet, but we should really be able to have a siingle ReviewHandler that uses create (POST), read (GET), update (PUT), etc.

Depending how you feel about it, I might even have a go at renaming the SubmitReviewHandler to do this when you've finished, or let me know if it's something that interests you too.

+
+ def create(self, request, review_id):
+ try:
+ review = Review.objects.get(id=review_id)
+ except Review.DoesNotExist:
+ return HttpResponseNotFound('No matching review found')

We should be able to simplify the above with a single:

get_object_or_404(Review, id=review_id)

+
+ try:
+ deleted = review.delete(request.user)
+ ...

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

Actually, I was just chatting with achuni about this:

{{{
14:25 <achuni> we need to add a bit of doc or a comment to urls.py
14:26 <achuni> so that people consider the http/https issue when adding a new url
14:26 <achuni> while you're there...
14:27 <achuni> could you ask him to change it to /reviews/delete/<id>/?
14:27 <achuni> I know it's nicer to be able to say /reviews/42 -> brings the review, and /reviews/42/delete -> deletes it
14:28 <achuni> but /reviews/42 is a public (http) request, and /reviews/42/delete needs to be authenticated
14:28 <noodles> oh,
14:29 <noodles> that rules out my suggestion, which was GET /reviews/42 to get, and PUT /reviews/42 to update it (for eg., setting date_deleted)
14:29 <achuni> as a rule of thumb, it's best to leave all the variable part of the url pattern at the end, and make sure it has a unique non-variable prefix
14:29 <achuni> ah that would be extremely hard to do also :-/
14:29 <noodles> yep.
14:29 <achuni> well, unless we don't care about doing https for everybody
14:29 <achuni> but it screws with caching
14:30 <achuni> (I understand retrieved reviews aren't cached if they're fetched over https)
14:30 <noodles> Is it not possible to do GET as http, and PUT/POST/DELETE etc. as https? or is it url only?
14:30 <achuni> (piston-mini-client should fix that really... sucky pos)
14:31 <achuni> atm piston-mini-client distinguishes them by url
14:31 <achuni> ...and Apache's redirect rules do so too I think?
14:31 <achuni> or can Apache say "this url, if it's a GET request, redirect to http"?
14:31 <achuni> I'd bet it can do that, yeah
14:32 <achuni> but we'd still need to realize (on the server) that one url would sometimes need to be authenticated, others not
14:32 <noodles> http://stackoverflow.com/questions/2838728/rewrite-url-for-put-request
}}}

So for the moment, it might be best just to modify the url and not worry about the handler changes I mentioned as possibilities. If we find we can do per method apache redirects as above, then I'll do a separate branch updating the handlers.

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

Hi Michael,
Thanks alot for the speedy review. You can probably tell I'm just feeling my way around django for now so I appreciate the comprehensive feedback.

I'm happy to make the suggested changes from you and achuni and resubmit shortly.

Just a couple of things:
1. Re the risk of a deleted review being unhidden. Good point! Should I just modify the moderation stuff to prevent a review being queued for moderation (or deleted from the queue if it's there already) if it has a deleted date or add a new boolean field ' deleted' (would still need to modify the moderation chide). I'm thinking the latter is cleaner, especially if we want to treat hidden reviews differently to deleted reviews later e.g. Allowing a user to resubmit for that app.

2. Re get_object_or_404 - the reason I differentiated between the reasons is to return more useful feedback to the client, as i've written some code in usc that uses the body of the response to give the user a clue as to why it failed. Would I still be able to do this using thie get_object_or_404 method? I.e. Saying the review does not exist is technically only correct if it can't be found using its id - what do you think?

Cheers
Aaron

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

> Hi Michael,
> Thanks alot for the speedy review. You can probably tell I'm just feeling my
> way around django for now so I appreciate the comprehensive feedback.

No problem! It's great that you're getting in there from your desktop background :) I hope you're enjoying django!

>
> I'm happy to make the suggested changes from you and achuni and resubmit
> shortly.
>
> Just a couple of things:
> 1. Re the risk of a deleted review being unhidden. Good point! Should I just
> modify the moderation stuff to prevent a review being queued for moderation
> (or deleted from the queue if it's there already) if it has a deleted date or
> add a new boolean field ' deleted' (would still need to modify the moderation
> chide). I'm thinking the latter is cleaner, especially if we want to treat
> hidden reviews differently to deleted reviews later e.g. Allowing a user to
> resubmit for that app.

It should be as simple as updating the query in views.reviewmoderation_list from:
    review_moderations = ReviewModeration.objects.all()
to:
    review_moderations = ReviewModeration.objects.all(review__date_deleted=None)

(and adding a test to ensure they're not included of course). I don't think we need another deleted field given that you've already got date_deleted?

Actually, something else... currently people can't create more than one view for an app (in a given series). See reviewsapp.forms.ReviewForm.clean(), we'll just need a similar update to the filter there used to determine whether the user already has a review, so that they can submit a new one if they delete the old one (?). Does that make sense?

>
> 2. Re get_object_or_404 - the reason I differentiated between the reasons is
> to return more useful feedback to the client, as i've written some code in usc
> that uses the body of the response to give the user a clue as to why it
> failed. Would I still be able to do this using thie get_object_or_404 method?

No - that makes sense if you want to do that, but two points:
 1) I'd be very careful depending on the body of the response (it could be modified, translated etc. in sca without us knowing that it'll affect the client. If you are depending on the body text, I'd check for it in the test and add a comment so it's not removed from the test.
 2) Is it really necessary? Are they not options that should never be presented in the client UI anyway? (A button to delete someone else's review, or a review of your own that you've already deleted?)

> I.e. Saying the review does not exist is technically only correct if it can't
> be found using its id - what do you think?

Right - but I'm not sure how likely the other two are in the client UI, but you're in a better position to know that.

Thanks!
>
> Cheers
> Aaron

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

Yeah, that makes sense, technically in the client the user should not be allowed to delete the review if it's already deleted or if it's not theirs, so it may be overkill to cater for it here. I'll implement using the get_object_or_404 and we'll see how it goes. If it's coded well enough in client (which it might not be - remember I wrote the code!!) then it shouldn't be an issue.

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

OK all the changes are made.
I wasn't able to do the migration, as it's complaining that the --auto option doesn't exist (??). Anything I can do there?
I felt my way around the tests a bit and had to make some changes to the factory to make it work. Assuming with default values it won't impact any other tests but not 100% sure.

Thanks again!
Aaron

review: Needs Resubmitting
lp:~aaronp/rnr-server/delete-review updated
184. By Aaron Peachey

* delete review functionality:
- modify to simplify handler in case of error
- ensure deleted reviews don't get moderated
- ensure user can submit review if one has been deleted
- update order of url parts
- add/modify test cases for above changes

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

On Thu, May 5, 2011 at 2:54 PM, Aaron Peachey <email address hidden> wrote:

> Review: Resubmit
> OK all the changes are made.
> I wasn't able to do the migration, as it's complaining that the --auto
> option doesn't exist (??). Anything I can do there?
>

Yeah, sorry, I should have checked the command I gave you. It's:

fab manage:'startmigration reviewsapp add_review_date_deleted --auto'

It'll tell you that a bunch of other fields have changed too,, but most are
just because some model fields have utcnow as the default value, and so can
be deleted from the migration. It should just contain the forward and
backward to add/remove your new column, as well as all the auto-generated
orm data. Either check one of the other simple migrations, or it's fine just
to leave it and I'll add to your branch before landing.

I'll try to get to your other changes asap. Thanks!

> I felt my way around the tests a bit and had to make some changes to the
> factory to make it work. Assuming with default values it won't impact any
> other tests but not 100% sure.
>
> Thanks again!
> Aaron
> --
> https://code.launchpad.net/~aaronp/rnr-server/delete-review/+merge/59658
> You are reviewing the proposed merge of lp:~aaronp/rnr-server/delete-review
> into lp:rnr-server.
>

--

Michael Nelson

Canonical Ltd.

+49 176 491 53481 (mob)

<email address hidden>

IRC nick: noodles (noodles775 on Freenode)

Ubuntu - Linux for human beings | www.ubuntu.com | www.canonical.com

lp:~aaronp/rnr-server/delete-review updated
185. By Aaron Peachey

add migration to bring in new date_deleted field to Review

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

Easy, thanks Michael. I've pushed the migration now too.

lp:~aaronp/rnr-server/delete-review updated
186. By Aaron Peachey

- test_rnrclient.py: add test for delete review in rnrclient
- api/handlers.py: return date_deleted in review response

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

One more thing for now. Updated and tested rnrclient (diff below).
Added a test for the rnrclient change and added date_deleted to the fields in the show review handler so it gets returned when a review is successfully deleted (I don't know why this worked, but I took a guess and it did!)

=== modified file 'rnrclient.py'
--- rnrclient.py 2011-03-17 09:17:26 +0000
+++ rnrclient.py 2011-05-05 14:10:04 +0000
@@ -167,3 +167,10 @@

         return self._get('usefulness/', args=data,
             scheme=PUBLIC_API_SCHEME)
+
+ @validate('review_id', int)
+ @returns_json
+ def delete_review(self, review_id):
+ """Delete a review"""
+ return self._post('/reviews/delete/%s/' % review_id, data = {},
+ scheme=AUTHENTICATED_API_SCHEME)

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

Wow, great work Aaron. AFAICS, there are only a few small tiny issues (again, I'm happy to do them if you're getting sick of me, but if you're keen, go for it :) ).

1) Test ensuring reviews stats updated after delete (you've added the code already). You can either mock the review.softwareitem_in_repository to ensure update_stats is called, or setup the test to check the actual stats afterwards.
2) AFAICS, the two exceptions you created are no longer necessary? (ReviewAlreadyDeleted and ReviewNotUsersOwn)
3) test_delete_review currently just checks that the same review is returned... it should also be checking that hide is false and date_deleted is not none right, like you've done on the api test? (it's tested in part implicitly with test_already_deleted)
4) With test_wrong_user, the first var (reviewer) is never used and unnecessary.

Also, is there a reason to catch the exceptions in your _request_delete_id helper? It hides the fact that an exception is occurring in the test itself, whereas using assertRaises() would make it obvious and explicit.

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

> Wow, great work Aaron. AFAICS, there are only a few small tiny issues (again,
> I'm happy to do them if you're getting sick of me, but if you're keen, go for
> it :) ).

Thanks. Happy to.

> 1) Test ensuring reviews stats updated after delete (you've added the code
> already). You can either mock the review.softwareitem_in_repository to ensure
> update_stats is called, or setup the test to check the actual stats
> afterwards.
OK, thanks.

> 2) AFAICS, the two exceptions you created are no longer necessary?
> (ReviewAlreadyDeleted and ReviewNotUsersOwn)
Yep, forgot to delete them!

> 3) test_delete_review currently just checks that the same review is
> returned... it should also be checking that hide is false and date_deleted is
> not none right, like you've done on the api test? (it's tested in part
> implicitly with test_already_deleted)
Yep, will change that.

> 4) With test_wrong_user, the first var (reviewer) is never used and
> unnecessary.
Thanks.

>
> Also, is there a reason to catch the exceptions in your _request_delete_id
> helper? It hides the fact that an exception is occurring in the test itself,
> whereas using assertRaises() would make it obvious and explicit.
Thanks for that. It took me a while to figure out how to resolve that (turns out I've used a hack!!) and I didn't know assertRaises existed. Will fix that too.

Cheers. Will resubmit soon.

thanks
Aaron

lp:~aaronp/rnr-server/delete-review updated
187. By Aaron Peachey

- models/reviews.py: remove unused exceptions
- tests/test_handlers.py: tweak tests and add test
    to check stats are updated on delete

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

All changes made. (Re-)Re-submitted.
Thanks

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

Excellent, thanks Aaron! I'll land the rnrclient change first then this one.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/reviewsapp/api/handlers.py'
--- src/reviewsapp/api/handlers.py 2011-04-12 10:43:30 +0000
+++ src/reviewsapp/api/handlers.py 2011-05-06 01:20:55 +0000
@@ -27,6 +27,7 @@
27 'ShowReviewsHandler',27 'ShowReviewsHandler',
28 'SubmitReviewHandler',28 'SubmitReviewHandler',
29 'ShowUsefulnessHandler',29 'ShowUsefulnessHandler',
30 'DeleteReviewHandler',
30]31]
3132
32import urllib33import urllib
@@ -37,6 +38,7 @@
37 HttpResponse,38 HttpResponse,
38 HttpResponseBadRequest,39 HttpResponseBadRequest,
39 HttpResponseNotFound,40 HttpResponseNotFound,
41 HttpResponseForbidden,
40 )42 )
41from django.shortcuts import get_object_or_40443from django.shortcuts import get_object_or_404
42from django.utils import simplejson44from django.utils import simplejson
@@ -174,6 +176,7 @@
174 'summary',176 'summary',
175 'review_text',177 'review_text',
176 'hide',178 'hide',
179 'date_deleted',
177 'version',180 'version',
178 'usefulness_total',181 'usefulness_total',
179 'usefulness_favorable',182 'usefulness_favorable',
@@ -236,6 +239,12 @@
236239
237 return review240 return review
238241
242class DeleteReviewHandler(BaseHandler):
243 allowed_methods = ('POST',)
244
245 def create(self, request, review_id):
246 review = get_object_or_404(Review, id=review_id, reviewer=request.user, date_deleted=None)
247 return review.delete()
239248
240class FlagReviewHandler(BaseHandler):249class FlagReviewHandler(BaseHandler):
241 allowed_methods = ('POST',)250 allowed_methods = ('POST',)
242251
=== modified file 'src/reviewsapp/api/urls.py'
--- src/reviewsapp/api/urls.py 2011-03-23 16:02:43 +0000
+++ src/reviewsapp/api/urls.py 2011-05-06 01:20:55 +0000
@@ -30,7 +30,8 @@
30 SubmitReviewHandler,30 SubmitReviewHandler,
31 SubmitUsefulnessHandler,31 SubmitUsefulnessHandler,
32 RetrieveReviewHandler,32 RetrieveReviewHandler,
33 ShowUsefulnessHandler33 ShowUsefulnessHandler,
34 DeleteReviewHandler
34)35)
35from reviewsapp.auth import SSOOAuthAuthentication36from reviewsapp.auth import SSOOAuthAuthentication
36from reviewsapp.api.decorators import dont_vary37from reviewsapp.api.decorators import dont_vary
@@ -72,6 +73,8 @@
72 authentication=auth)73 authentication=auth)
73submit_usefulness_resource = Resource(handler=SubmitUsefulnessHandler,74submit_usefulness_resource = Resource(handler=SubmitUsefulnessHandler,
74 authentication=auth)75 authentication=auth)
76delete_review_resource = Resource(handler=DeleteReviewHandler,
77 authentication=auth)
7578
76urlpatterns = patterns('',79urlpatterns = patterns('',
77 # get status of the service (usually just "ok", might be "read-only")80 # get status of the service (usually just "ok", might be "read-only")
@@ -135,4 +138,9 @@
135 # POST /1.0/reviews/1/+report-review/138 # POST /1.0/reviews/1/+report-review/
136 url(r'^1.0/reviews/(?P<review_id>\d+)/flags/$',139 url(r'^1.0/reviews/(?P<review_id>\d+)/flags/$',
137 flag_review_resource),140 flag_review_resource),
141
142 # delete own reviews
143 # POST /1.0/reviews/delete/100
144 url(r'^1.0/reviews/delete/(?P<review_id>\d+)/$',
145 delete_review_resource),
138)146)
139147
=== modified file 'src/reviewsapp/forms.py'
--- src/reviewsapp/forms.py 2011-03-18 17:07:30 +0000
+++ src/reviewsapp/forms.py 2011-05-06 01:20:55 +0000
@@ -92,10 +92,12 @@
9292
93 if not settings.ALLOW_MULTIPLE_REVIEWS_FOR_TESTING:93 if not settings.ALLOW_MULTIPLE_REVIEWS_FOR_TESTING:
94 # Ensure a user cannot submit a second review for the same app94 # Ensure a user cannot submit a second review for the same app
95 # unless their first one has been deleted by them
95 previous_reviews = Review.objects.filter(96 previous_reviews = Review.objects.filter(
96 reviewer=self.instance.reviewer,97 reviewer=self.instance.reviewer,
97 softwareitem=self.cleaned_data['software_item'],98 softwareitem=self.cleaned_data['software_item'],
98 repository=self.cleaned_data['repository'])99 repository=self.cleaned_data['repository'],
100 date_deleted=None)
99 if previous_reviews:101 if previous_reviews:
100 raise forms.ValidationError(102 raise forms.ValidationError(
101 "A user cannot create multiple reviews for an app.")103 "A user cannot create multiple reviews for an app.")
102104
=== added file 'src/reviewsapp/migrations/0010_add_review_date_deleted.py'
--- src/reviewsapp/migrations/0010_add_review_date_deleted.py 1970-01-01 00:00:00 +0000
+++ src/reviewsapp/migrations/0010_add_review_date_deleted.py 2011-05-06 01:20:55 +0000
@@ -0,0 +1,157 @@
1
2from south.db import db
3from django.db import models
4from reviewsapp.models import *
5
6class Migration:
7
8 def forwards(self, orm):
9 # Adding field 'Review.date_deleted'
10 db.add_column('reviewsapp_review', 'date_deleted', orm['reviewsapp.review:date_deleted'])
11
12 def backwards(self, orm):
13 # Deleting field 'Review.date_deleted'
14 db.delete_column('reviewsapp_review', 'date_deleted')
15
16 models = {
17 'auth.group': {
18 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
19 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
20 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
21 },
22 'auth.permission': {
23 'Meta': {'unique_together': "(('content_type', 'codename'),)"},
24 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
25 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
26 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
27 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
28 },
29 'auth.user': {
30 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
31 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
32 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
33 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
34 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
35 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
36 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
37 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
38 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
39 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
40 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
41 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
42 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
43 },
44 'contenttypes.contenttype': {
45 'Meta': {'unique_together': "(('app_label', 'model'),)", 'db_table': "'django_content_type'"},
46 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
47 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
48 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
49 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
50 },
51 'reviewsapp.architecture': {
52 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
53 'tag': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '16'})
54 },
55 'reviewsapp.consumer': {
56 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
57 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
58 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
59 'secret': ('django.db.models.fields.CharField', [], {'default': "'QQcOHuhpGcauNucDPiRTuWKsVbFWrr'", 'max_length': '255', 'blank': 'True'}),
60 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
61 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'oauth_consumer'", 'unique': 'True', 'to': "orm['auth.User']"})
62 },
63 'reviewsapp.nonce': {
64 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reviewsapp.Consumer']"}),
65 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
66 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
67 'nonce': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
68 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reviewsapp.Token']"})
69 },
70 'reviewsapp.repository': {
71 'distroseries': ('django.db.models.fields.SlugField', [], {'max_length': '25', 'db_index': 'True'}),
72 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
73 'origin': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'db_index': 'True'})
74 },
75 'reviewsapp.review': {
76 'app_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}),
77 'architecture': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reviewsapp.Architecture']"}),
78 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2011, 5, 5, 13, 29, 1, 431193)'}),
79 'date_deleted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
80 'hide': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True', 'blank': 'True'}),
81 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
82 'language': ('django.db.models.fields.SlugField', [], {'max_length': '10', 'db_index': 'True'}),
83 'rating': ('django.db.models.fields.IntegerField', [], {}),
84 'repository': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reviewsapp.Repository']"}),
85 'review_text': ('django.db.models.fields.CharField', [], {'max_length': '5000'}),
86 'reviewer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
87 'softwareitem': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reviewsapp.SoftwareItem']"}),
88 'summary': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
89 'usefulness_favorable': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
90 'usefulness_percentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
91 'usefulness_total': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
92 'version': ('django.db.models.fields.CharField', [], {'max_length': '100'})
93 },
94 'reviewsapp.reviewmoderation': {
95 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2011, 5, 5, 13, 29, 1, 491649)'}),
96 'date_moderated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
97 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
98 'moderation_text': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
99 'moderator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
100 'review': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reviewsapp.Review']"}),
101 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'})
102 },
103 'reviewsapp.reviewmoderationflag': {
104 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2011, 5, 5, 13, 29, 1, 630446)'}),
105 'description': ('django.db.models.fields.TextField', [], {}),
106 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
107 'review_moderation': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reviewsapp.ReviewModeration']"}),
108 'summary': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
109 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'})
110 },
111 'reviewsapp.rnrsettings': {
112 'blacklist_words': ('django.db.models.fields.TextField', [], {'default': "''"}),
113 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
114 'moderation_mode': ('django.db.models.fields.CharField', [], {'default': "'passive'", 'max_length': '8'})
115 },
116 'reviewsapp.softwareitem': {
117 'date_ratings_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2011, 5, 5, 13, 28, 59, 374540)'}),
118 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
119 'package_name': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'db_index': 'True'}),
120 'ratings_average': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '3', 'decimal_places': '2'}),
121 'ratings_total': ('django.db.models.fields.IntegerField', [], {'default': '0'})
122 },
123 'reviewsapp.softwareiteminorigin': {
124 'Meta': {'unique_together': "(('softwareitem', 'origin'),)"},
125 'date_ratings_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2011, 5, 5, 13, 28, 59, 374540)'}),
126 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
127 'origin': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'db_index': 'True'}),
128 'ratings_average': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '3', 'decimal_places': '2'}),
129 'ratings_total': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
130 'softwareitem': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reviewsapp.SoftwareItem']"})
131 },
132 'reviewsapp.softwareiteminrepository': {
133 'Meta': {'unique_together': "(('softwareitem', 'repository'),)"},
134 'date_ratings_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2011, 5, 5, 13, 28, 59, 374540)'}),
135 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
136 'ratings_average': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '3', 'decimal_places': '2'}),
137 'ratings_total': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
138 'repository': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reviewsapp.Repository']"}),
139 'softwareitem': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reviewsapp.SoftwareItem']"})
140 },
141 'reviewsapp.token': {
142 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reviewsapp.Consumer']"}),
143 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
144 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
145 'token': ('django.db.models.fields.CharField', [], {'default': "'xFzxdTdlozOgtDGjUHucpzkQBIAUakPHtqfwhEgXLrCjQYvfqm'", 'max_length': '50', 'primary_key': 'True'}),
146 'token_secret': ('django.db.models.fields.CharField', [], {'default': "'brHDYrMqjlzNiHVQdWSWDIqrtflQLWGVcSwDZCGszrsqLuApYN'", 'max_length': '50'}),
147 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
148 },
149 'reviewsapp.usefulness': {
150 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
151 'review': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reviewsapp.Review']"}),
152 'useful': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True', 'blank': 'True'}),
153 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
154 }
155 }
156
157 complete_apps = ['reviewsapp']
0158
=== modified file 'src/reviewsapp/models/reviews.py'
--- src/reviewsapp/models/reviews.py 2011-03-22 15:43:40 +0000
+++ src/reviewsapp/models/reviews.py 2011-05-06 01:20:55 +0000
@@ -298,6 +298,7 @@
298 language = models.SlugField(max_length=10)298 language = models.SlugField(max_length=10)
299 hide = models.BooleanField(299 hide = models.BooleanField(
300 'Hide review from the UI', db_index=True, default=False)300 'Hide review from the UI', db_index=True, default=False)
301 date_deleted=models.DateTimeField(blank=True, null=True)
301 # The following get updated when a usefulness vote is added302 # The following get updated when a usefulness vote is added
302 usefulness_total = models.IntegerField(default=0)303 usefulness_total = models.IntegerField(default=0)
303 usefulness_favorable = models.IntegerField(default=0)304 usefulness_favorable = models.IntegerField(default=0)
@@ -340,6 +341,14 @@
340341
341 def reviewer_displayname(self):342 def reviewer_displayname(self):
342 return self.reviewer.get_full_name()343 return self.reviewer.get_full_name()
344
345 def delete(self):
346 """Delete a review submitted by the user"""
347 self.hide = True
348 self.date_deleted = datetime.utcnow()
349 self.save()
350 self.softwareitem_in_repository.update_stats()
351 return self
343352
344 def flag(self, user, summary, description):353 def flag(self, user, summary, description):
345 """Flag an object as possibly requiring moderation.354 """Flag an object as possibly requiring moderation.
@@ -378,7 +387,6 @@
378 class Meta:387 class Meta:
379 app_label = 'reviewsapp'388 app_label = 'reviewsapp'
380389
381
382class Usefulness(models.Model):390class Usefulness(models.Model):
383 review = models.ForeignKey(Review, db_index=True)391 review = models.ForeignKey(Review, db_index=True)
384 user = models.ForeignKey(User)392 user = models.ForeignKey(User)
385393
=== modified file 'src/reviewsapp/tests/factory.py'
--- src/reviewsapp/tests/factory.py 2011-03-22 16:28:23 +0000
+++ src/reviewsapp/tests/factory.py 2011-05-06 01:20:55 +0000
@@ -163,7 +163,7 @@
163 def makeReview(self, reviewer=None, software_item=None, repository=None,163 def makeReview(self, reviewer=None, software_item=None, repository=None,
164 version=None, rating="5", summary=None, review_text=None,164 version=None, rating="5", summary=None, review_text=None,
165 date_created=None, language=None, hide=False, num_flags=0,165 date_created=None, language=None, hide=False, num_flags=0,
166 architecture='i386', app_name=''):166 architecture='i386', app_name='', deleted=False):
167 if reviewer is None:167 if reviewer is None:
168 reviewer = self.makeUser()168 reviewer = self.makeUser()
169 if software_item is None:169 if software_item is None:
@@ -180,6 +180,11 @@
180 date_created = datetime.utcnow()180 date_created = datetime.utcnow()
181 if language is None:181 if language is None:
182 language = self.getUniqueString(prefix='lang')182 language = self.getUniqueString(prefix='lang')
183 if deleted:
184 hide = True
185 date_deleted = datetime.utcnow()
186 else:
187 date_deleted = None
183188
184 architecture, created = Architecture.objects.get_or_create(189 architecture, created = Architecture.objects.get_or_create(
185 tag=architecture)190 tag=architecture)
@@ -189,7 +194,7 @@
189 version=version, rating=rating, summary=summary,194 version=version, rating=rating, summary=summary,
190 review_text=review_text, language=language, hide=hide,195 review_text=review_text, language=language, hide=hide,
191 date_created=date_created, architecture=architecture,196 date_created=date_created, architecture=architecture,
192 app_name=app_name)197 app_name=app_name, date_deleted=date_deleted)
193 review.softwareitem_in_repository.update_stats()198 review.softwareitem_in_repository.update_stats()
194 for flag_count in range(num_flags):199 for flag_count in range(num_flags):
195 review.flag(200 review.flag(
@@ -200,9 +205,9 @@
200205
201 def makeReviewModeration(self, num_flags=1,206 def makeReviewModeration(self, num_flags=1,
202 status=ReviewModeration.PENDING_STATUS,207 status=ReviewModeration.PENDING_STATUS,
203 language=None, distro_series=None):208 language=None, distro_series=None, deleted_review=False):
204 repository = self.makeRepository(distro_series=distro_series)209 repository = self.makeRepository(distro_series=distro_series)
205 review = self.makeReview(language=language, repository=repository)210 review = self.makeReview(language=language, repository=repository, deleted=deleted_review)
206 review_moderation = ReviewModeration.objects.create(review=review)211 review_moderation = ReviewModeration.objects.create(review=review)
207 for flag_count in range(num_flags):212 for flag_count in range(num_flags):
208 review.flag(213 review.flag(
209214
=== modified file 'src/reviewsapp/tests/test_handlers.py'
--- src/reviewsapp/tests/test_handlers.py 2011-04-12 07:44:43 +0000
+++ src/reviewsapp/tests/test_handlers.py 2011-05-06 01:20:55 +0000
@@ -28,10 +28,12 @@
28 'SubmitReviewHandlerTestCase',28 'SubmitReviewHandlerTestCase',
29 'UsefulnessHandlerTestCase',29 'UsefulnessHandlerTestCase',
30 'ShowUsefulnessHandlerTestCase',30 'ShowUsefulnessHandlerTestCase',
31 'DeleteReviewHandlerTestCase',
31 ]32 ]
3233
3334
34import httplib35import httplib
36from django.http import Http404
35from datetime import datetime, timedelta37from datetime import datetime, timedelta
36from urllib import quote_plus38from urllib import quote_plus
3739
@@ -51,6 +53,7 @@
51 ShowReviewsHandler,53 ShowReviewsHandler,
52 SubmitUsefulnessHandler,54 SubmitUsefulnessHandler,
53 ShowUsefulnessHandler,55 ShowUsefulnessHandler,
56 DeleteReviewHandler
54)57)
55from reviewsapp.models import (58from reviewsapp.models import (
56 Repository,59 Repository,
@@ -837,6 +840,61 @@
837 self.assertEqual(httplib.OK, first.status_code)840 self.assertEqual(httplib.OK, first.status_code)
838 self.assertEqual(httplib.OK, second.status_code)841 self.assertEqual(httplib.OK, second.status_code)
839842
843class DeleteReviewHandlerTestCase(TestCaseWithFactory):
844 def _delete_review_id(self, review_id, user=None):
845 if user is None:
846 user = self.factory.makeUser()
847 request = Mock()
848 request.POST = None
849 request.user = user
850 handler = DeleteReviewHandler()
851 return handler.create(request, review_id)
852
853 def test_delete_review(self):
854 review = self.factory.makeReview()
855 response = self._delete_review_id(review.id, review.reviewer)
856 self.assertEqual(review.id, response.id)
857 self.assertEqual(True,response.hide)
858 self.assertNotEqual(None, response.date_deleted)
859
860 def test_non_existent_review(self):
861 review = self.factory.makeReview()
862 self.assertRaises(Http404, self._delete_review_id, review.id+1,
863 review.reviewer)
864
865 def test_already_deleted(self):
866 review = self.factory.makeReview()
867
868 response_good = self._delete_review_id(review.id, review.reviewer)
869 self.assertEqual(review, response_good)
870 #try to delete a second time and check it raises a 404 error
871 self.assertRaises(Http404, self._delete_review_id, review.id,
872 review.reviewer)
873
874 def test_wrong_user(self):
875 review = self.factory.makeReview()
876 wrong_user = self.factory.makeUser(username='deleter')
877 self.assertRaises(Http404, self._delete_review_id, review.id,
878 wrong_user)
879
880 def test_delete_updates_stats(self):
881 software_item = self.factory.makeSoftwareItem(package_name='compiz-core',
882 ratings_total=0, ratings_average=0)
883 review = self.factory.makeReview(software_item=software_item, rating=5)
884 review2 = self.factory.makeReview(software_item=software_item, rating=3)
885 software_item = SoftwareItem.objects.get(id=software_item.id)
886
887 # 2 reviews with ratings of 5 + 3 = 8 (average of 4)
888 self.assertEqual(2, software_item.ratings_total)
889 self.assertEqual(4, software_item.ratings_average)
890
891 self._delete_review_id(review2.id, review2.reviewer)
892 software_item = SoftwareItem.objects.get(id=software_item.id)
893
894 # 1 review with rating 5 = average of 5
895 self.assertEqual(1, software_item.ratings_total)
896 self.assertEqual(5, software_item.ratings_average)
897
840898
841class FlagReviewHandlerTestCase(TestCaseWithFactory):899class FlagReviewHandlerTestCase(TestCaseWithFactory):
842 def _flag_review_id(self, review_id, user=None, data=None):900 def _flag_review_id(self, review_id, user=None, data=None):
843901
=== modified file 'src/reviewsapp/tests/test_rnrclient.py'
--- src/reviewsapp/tests/test_rnrclient.py 2011-04-19 12:32:23 +0000
+++ src/reviewsapp/tests/test_rnrclient.py 2011-05-06 01:20:55 +0000
@@ -29,6 +29,7 @@
29 'RnRReviewStatsTestCase',29 'RnRReviewStatsTestCase',
30 'RnRServerStatusTestCase',30 'RnRServerStatusTestCase',
31 'RnRSubmitReviewTestCase',31 'RnRSubmitReviewTestCase',
32 'RnRDeleteReviewTestCase',
32 'UsefulnessAPITestCase',33 'UsefulnessAPITestCase',
33 ]34 ]
3435
@@ -246,6 +247,31 @@
246 self.assertEqual(review_detail.distroseries, review.distroseries())247 self.assertEqual(review_detail.distroseries, review.distroseries())
247 invalidate_paginated_reviews_for(review)248 invalidate_paginated_reviews_for(review)
248249
250class RnRDeleteReviewTestCase(RnRTxBaseTestCase):
251
252 def test_delete_review(self):
253 user = self.factory.makeUser()
254 review = self.factory.makeReview(reviewer=user)
255 token, consumer = self.factory.makeOAuthTokenAndConsumer(user=user)
256
257 auth = OAuthAuthorizer(token.token, token.token_secret, consumer.key,
258 consumer.secret)
259
260 lp_verify_packagename_method = (
261 'reviewsapp.utilities.WebServices.'
262 'lp_verify_packagename_in_repository')
263
264 mock_verify_package = Mock()
265 mock_verify_package.return_value = True
266
267 api = RatingsAndReviewsAPI(auth=auth)
268
269 with patch(lp_verify_packagename_method, mock_verify_package):
270 response = api.delete_review(review_id=review.id)
271
272 self.assertEqual(review.id, response['id'])
273 self.assertNotEqual(None, response['date_deleted'])
274
249275
250class RnRSubmitReviewTestCase(RnRTxBaseTestCase):276class RnRSubmitReviewTestCase(RnRTxBaseTestCase):
251277
252278
=== modified file 'src/reviewsapp/tests/test_views.py'
--- src/reviewsapp/tests/test_views.py 2011-04-20 16:47:48 +0000
+++ src/reviewsapp/tests/test_views.py 2011-05-06 01:20:55 +0000
@@ -80,6 +80,18 @@
8080
81 self.assertEqual(302, response.status_code)81 self.assertEqual(302, response.status_code)
82 self.assertTrue('/openid/login' in response['Location'])82 self.assertTrue('/openid/login' in response['Location'])
83
84 def test_moderation_excludes_deleted(self):
85 review_moderation = self.factory.makeReviewModeration()
86 review_moderation_deleted = self.factory.makeReviewModeration(deleted_review=True)
87
88 response = self._request_url_as_moderator()
89
90 review_moderations = response.context['review_moderations']
91
92 self.assertEqual(1, len(review_moderations))
93 self.assertEqual(review_moderation, review_moderations[0])
94 self.assertNotEqual(review_moderation_deleted, review_moderations[0])
8395
84 def _request_url_as_moderator(self, data=None, user=None):96 def _request_url_as_moderator(self, data=None, user=None):
85 # Helper to request the url as a moderator.97 # Helper to request the url as a moderator.
8698
=== modified file 'src/reviewsapp/views/moderation.py'
--- src/reviewsapp/views/moderation.py 2011-03-31 11:09:37 +0000
+++ src/reviewsapp/views/moderation.py 2011-05-06 01:20:55 +0000
@@ -58,7 +58,8 @@
58def reviewmoderation_list(request):58def reviewmoderation_list(request):
59 """A filterable list of review moderations with a useful default."""59 """A filterable list of review moderations with a useful default."""
6060
61 review_moderations = ReviewModeration.objects.all()61 #don't present reviews for moderation if they've been deleted
62 review_moderations = ReviewModeration.objects.filter(review__date_deleted=None)
62 next_url = reverse('rnr-reviewmoderation-list')63 next_url = reverse('rnr-reviewmoderation-list')
6364
64 # By default we'll be displaying pending moderations.65 # By default we'll be displaying pending moderations.

Subscribers

People subscribed via source and target branches