Skip to content
This repository has been archived by the owner on Jul 17, 2018. It is now read-only.

Commit

Permalink
Added RATINGS_VOTES_PER_IP setting, defaults to 3. All instance looku…
Browse files Browse the repository at this point in the history
…ps are now pk rather than id. Added some initial unit tests.
  • Loading branch information
David Cramer authored and David Cramer committed Dec 27, 2009
1 parent 492075c commit 8c06cd1
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 12 deletions.
5 changes: 4 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
2 changes: 1 addition & 1 deletion djangoratings/__init__.py
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
5 changes: 5 additions & 0 deletions djangoratings/default_settings.py
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions djangoratings/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
class InvalidRating(ValueError): pass
class AuthRequired(TypeError): pass
class CannotChangeVote(Exception): pass
class IPLimitReached(Exception): pass
55 changes: 46 additions & 9 deletions djangoratings/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
)

Expand All @@ -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."""
Expand All @@ -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,
)
Expand All @@ -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

Expand All @@ -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(
Expand All @@ -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,
)

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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))
Expand Down
20 changes: 20 additions & 0 deletions djangoratings/helpers.py
Original file line number Diff line number Diff line change
@@ -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()
46 changes: 46 additions & 0 deletions djangoratings/tests.py
Original file line number Diff line number Diff line change
@@ -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')
12 changes: 11 additions & 1 deletion djangoratings/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand All @@ -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
Expand Down

0 comments on commit 8c06cd1

Please sign in to comment.