Permalink
Browse files

Added RATINGS_VOTES_PER_IP setting, defaults to 3. All instance looku…

…ps are now pk rather than id. Added some initial unit tests.
  • Loading branch information...
1 parent 492075c commit 8c06cd138f125f2bdbbe8fa120a7d19a98e76720 David Cramer committed Dec 27, 2009
View
@@ -102,7 +102,6 @@ The best way to use the generic views is by extending it, or calling it within y
Another example, on Nibbits we use a basic API interface, and we simply call the ``AddRatingView`` within our own view::
-
from djangoratings.views import AddRatingView
# For the sake of this actually looking like documentation:
@@ -118,3 +117,7 @@ Another example, on Nibbits we use a basic API interface, and we simply call the
request.user.add_xp(settings.XP_BONUSES['submit-rating'])
return {'message': response.content, 'score': params['score']}
return {'error': 9, 'message': response.content}
+
+*New in 0.3.5*: There is now a setting, ``RATINGS_VOTES_PER_IP``, to limit the number of unique IPs per object/rating-field combination. This is useful if you have issues with users registering multiple accounts to vote on a single object::
+
+ RATINGS_VOTES_PER_IP = 3
@@ -1,7 +1,7 @@
import os.path
import warnings
-__version__ = (0, 3, 4)
+__version__ = (0, 3, 5)
def _get_git_revision(path):
revision_file = os.path.join(path, 'refs', 'heads', 'master')
@@ -0,0 +1,5 @@
+from django.conf import settings
+
+# Used to limit the number of unique IPs that can vote on a single object+field.
+# useful if you're getting rating spam by users registering multiple accounts
+RATINGS_VOTES_PER_IP = 3
@@ -1,3 +1,4 @@
class InvalidRating(ValueError): pass
class AuthRequired(TypeError): pass
class CannotChangeVote(Exception): pass
+class IPLimitReached(Exception): pass
View
@@ -5,7 +5,7 @@
import itertools
from models import Vote, Score
-
+from default_settings import RATINGS_VOTES_PER_IP
from exceptions import *
if 'django.contrib.contenttypes' not in settings.INSTALLED_APPS:
@@ -57,7 +57,7 @@ def get_ratings(self):
"""get_ratings()
Returns a Vote QuerySet for this rating field."""
- return Vote.objects.filter(content_type=self.get_content_type(), object_id=self.instance.id, key=self.field.key)
+ return Vote.objects.filter(content_type=self.get_content_type(), object_id=self.instance.pk, key=self.field.key)
def get_rating(self):
"""get_rating()
@@ -87,7 +87,7 @@ def get_rating_for_user(self, user, ip_address):
Returns the rating for a user or anonymous IP."""
kwargs = dict(
content_type = self.get_content_type(),
- object_id = self.instance.id,
+ object_id = self.instance.pk,
key = self.field.key,
)
@@ -103,7 +103,7 @@ def get_rating_for_user(self, user, ip_address):
pass
return
- def add(self, score, user, ip_address):
+ def add(self, score, user, ip_address, commit=True):
"""add(score, user, ip_address)
Used to add a rating to an object."""
@@ -121,15 +121,15 @@ def add(self, score, user, ip_address):
if is_anonymous:
user = None
-
+
defaults = dict(
score = score,
ip_address = ip_address,
)
kwargs = dict(
content_type = self.get_content_type(),
- object_id = self.instance.id,
+ object_id = self.instance.pk,
key = self.field.key,
user = user,
)
@@ -139,6 +139,15 @@ def add(self, score, user, ip_address):
try:
rating, created = Vote.objects.get(**kwargs), False
except Vote.DoesNotExist:
+ if getattr(settings, 'RATINGS_VOTES_PER_IP', RATINGS_VOTES_PER_IP):
+ num_votes = Vote.objects.filter(
+ content_type=kwargs['content_type'],
+ object_id=kwargs['object_id'],
+ key=kwargs['key'],
+ ip_address=ip_address,
+ ).count()
+ if num_votes >= getattr(settings, 'RATINGS_VOTES_PER_IP', RATINGS_VOTES_PER_IP):
+ raise IPLimitReached()
kwargs.update(defaults)
rating, created = Vote.objects.create(**kwargs), True
@@ -156,7 +165,8 @@ def add(self, score, user, ip_address):
self.votes += 1
if has_changed:
self.score += rating.score
- self.instance.save()
+ if commit:
+ self.instance.save()
#setattr(self.instance, self.field.name, Rating(score=self.score, votes=self.votes))
defaults = dict(
@@ -166,7 +176,7 @@ def add(self, score, user, ip_address):
kwargs = dict(
content_type = self.get_content_type(),
- object_id = self.instance.id,
+ object_id = self.instance.pk,
key = self.field.key,
)
@@ -200,6 +210,34 @@ def get_content_type(self):
if self.content_type is None:
self.content_type = ContentType.objects.get_for_model(self.instance)
return self.content_type
+
+ def _update(self, commit=False):
+ """Forces an update of this rating (useful for when Vote objects are removed)."""
+ votes = Vote.objects.filter(
+ content_type = self.get_content_type(),
+ object_id = self.instance.pk,
+ key = self.field.key,
+ )
+ obj_score = sum([v.score for v in votes])
+ obj_votes = len(votes)
+
+ score, created = Score.objects.get_or_create(
+ content_type = self.get_content_type(),
+ object_id = self.instance.pk,
+ key = self.field.key,
+ defaults = dict(
+ score = obj_score,
+ votes = obj_votes,
+ )
+ )
+ if not created:
+ score.score = obj_score
+ score.votes = obj_votes
+ score.save()
+ self.score = obj_score
+ self.votes = obj_votes
+ if commit:
+ self.instance.save()
class RatingCreator(object):
def __init__(self, field):
@@ -251,7 +289,6 @@ def contribute_to_class(self, cls, name):
editable=False, default=0, blank=True)
cls.add_to_class("%s_score" % (self.name,), self.score_field)
-
self.key = md5_hexdigest(self.name)
setattr(cls, name, RatingCreator(self))
View
@@ -0,0 +1,20 @@
+from models import Vote
+from django.contrib.contenttypes.models import ContentType
+from fields import RatingField
+
+def delete_from_ip_address(ip_address):
+ qs = Vote.objects.filter(ip_address=ip_address)
+
+ to_update = []
+ for content_type, objects in itertools.groupby(qs.distinct().values('content_type_id', 'object_id').order_by('content_type_id'), key=lambda x: x[0]):
+ ct = ContentType.objects.get_for_model(pk=content_type)
+ to_update.extend(ct.get_object_for_this_type(pk__in=objects))
+
+ qs.delete()
+
+ # TODO: this could be improved
+ for obj in to_update:
+ for field in obj._meta.fields:
+ if isinstance(field, RatingField):
+ getattr(obj, field.name)._update()
+ obj.save()
View
@@ -0,0 +1,46 @@
+import unittest
+from django.db import models
+from django.contrib.auth.models import User
+from django.conf import settings
+
+from exceptions import *
+from fields import AnonymousRatingField, RatingField
+
+settings.RATINGS_VOTES_PER_IP = 1
+
+class RatingTestModel(models.Model):
+ rating = AnonymousRatingField(range=2, can_change_vote=True)
+ rating2 = RatingField(range=2, can_change_vote=False)
+
+class RatingTestCase(unittest.TestCase):
+ def testRatings(self):
+ instance = RatingTestModel.objects.create()
+
+ # Test adding votes
+ instance.rating.add(score=1, user=None, ip_address='127.0.0.1')
+ self.assertEquals(instance.rating.score, 1)
+ self.assertEquals(instance.rating.votes, 1)
+
+ # Test adding votes
+ instance.rating.add(score=2, user=None, ip_address='127.0.0.2')
+ self.assertEquals(instance.rating.score, 3)
+ self.assertEquals(instance.rating.votes, 2)
+
+ # Test changing of votes
+ instance.rating.add(score=2, user=None, ip_address='127.0.0.1')
+ self.assertEquals(instance.rating.score, 4)
+ self.assertEquals(instance.rating.votes, 2)
+
+ # Test users
+ user = User.objects.create(username='django-ratings')
+ user2 = User.objects.create(username='django-ratings2')
+
+ instance.rating.add(score=2, user=user, ip_address='127.0.0.3')
+ self.assertEquals(instance.rating.score, 6)
+ self.assertEquals(instance.rating.votes, 3)
+
+ instance.rating2.add(score=2, user=user, ip_address='127.0.0.3')
+ self.assertEquals(instance.rating2.score, 2)
+ self.assertEquals(instance.rating2.votes, 1)
+
+ self.assertRaises(IPLimitReached, instance.rating2.add, score=2, user=user2, ip_address='127.0.0.3')
View
@@ -2,7 +2,9 @@
from django.http import HttpResponse, Http404
from exceptions import *
-
+from django.conf import settings
+from default_settings import RATINGS_VOTES_PER_IP
+
class AddRatingView(object):
def __call__(self, request, content_type_id, object_id, field_name, score):
"""__call__(request, content_type_id, object_id, field_name, score)
@@ -25,8 +27,12 @@ def __call__(self, request, content_type_id, object_id, field_name, score):
had_voted = bool(field.get_rating_for_user(request.user, request.META['REMOTE_ADDR']))
+ context['had_voted'] = had_voted
+
try:
field.add(score, request.user, request.META.get('REMOTE_ADDR'))
+ except IPLimitReached:
+ return self.too_many_votes_from_ip_response(request, context)
except AuthRequired:
return self.authentication_required_response(request, context)
except InvalidRating:
@@ -43,6 +49,10 @@ def get_context(self, request, context={}):
def render_to_response(self, template, context, request):
raise NotImplementedError
+ def too_many_votes_from_ip_response(self, request, context):
+ response = HttpResponse('Too many votes from this IP address for this object.')
+ return response
+
def rating_changed_response(self, request, context):
response = HttpResponse('Vote changed.')
return response

0 comments on commit 8c06cd1

Please sign in to comment.