diff --git a/README.rst b/README.rst index f2de874..d4c4c15 100644 --- a/README.rst +++ b/README.rst @@ -1,193 +1,5 @@ -############## -django-ratings -############## +Currently pushing to -**This project is no longer maintained** +https://github.com/bung87/django-ratings -A generic ratings module. The field itself appends two additional fields on the model, for optimization reasons. It adds ``_score``, and ``_votes`` fields, which are both integer fields. - -============ -Installation -============ - -You will need to add ``djangoratings`` to your ``INSTALLED_APPS``:: - - INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'djangoratings', - ) - -Finally, run ``python manage.py syncdb`` in your application's directory to create the tables. - -================= -Setup your models -================= - -The way django-ratings is built requires you to attach a RatingField to your models. This field will create two columns, a votes column, and a score column. They will both be prefixed with your field name:: - - from djangoratings.fields import RatingField - - class MyModel(models.Model): - rating = RatingField(range=5) # 5 possible rating values, 1-5 - -Alternatively you could do something like:: - - from djangoratings.fields import AnonymousRatingField - - class MyModel(models.Model): - rating = AnonymousRatingField(range=10) - -If you'd like to use the built-in weighting methods, to make it appear more difficult for an object -to obtain a higher rating, you can use the ``weight`` kwarg:: - - class MyModel(models.Model): - rating = RatingField(range=10, weight=10) - -``RatingField`` allows the following options: - -* ``range = 2`` - The range in which values are accepted. For example, a range of 2, says there are 2 possible vote scores. -* ``can_change_vote = False`` - Allow the modification of votes that have already been made. -* ``allow_delete = False`` - Allow the deletion of existent votes. Works only if ``can_change_vote = True`` -* ``allow_anonymous = False`` - Whether to allow anonymous votes. -* ``use_cookies = False`` - Use COOKIES to authenticate user votes. Works only if ``allow_anonymous = True``. - -=================== -Using the model API -=================== - -And adding votes is also simple:: - - myinstance.rating.add(score=1, user=request.user, ip_address=request.META['REMOTE_ADDR'], request.COOKIES) # last param is optional - only if you use COOKIES-auth - -Retrieving votes is just as easy:: - - myinstance.rating.get_rating_for_user(request.user, request.META['REMOTE_ADDR'], request.COOKIES) # last param is optional - only if you use COOKIES-auth - -*New* You're also able to delete existent votes (if deletion enabled):: - - myinstance.rating.delete(request.user, request.META['REMOTE_ADDR'], request.COOKIES) # last param is optional - only if you use COOKIES-auth - -Accessing information about the rating of an object is also easy:: - - # these do not hit the database - myinstance.rating.votes - myinstance.rating.score - -How you can order by top-rated using an algorithm (example from Nibbits.com source):: - - # In this example, ``rating`` is the attribute name for your ``RatingField`` - qs = qs.extra(select={ - 'rating': '((100/%s*rating_score/(rating_votes+%s))+100)/2' % (MyModel.rating.range, MyModel.rating.weight) - }) - qs = qs.order_by('-rating') - -Get overall rating for your instance on a scale [0-range]:: - - myinstance.rating.get_rating() - -Get recent ratings for your instance:: - - # This returns ``Vote`` instances. - myinstance.rating.get_ratings()[0:5] - -Get the percent of voters approval:: - - myinstance.rating.get_percent() - -Get that same percentage, but excluding your ``weight``:: - - myinstance.rating.get_real_percent() - -=============================== -Generic Views: Processing Votes -=============================== - -The best way to use the generic views is by extending it, or calling it within your own code:: - - from djangoratings.views import AddRatingFromModel - - urlpatterns = patterns('', - url(r'rate-my-post/(?P\d+)/(?P\d+)/', AddRatingFromModel(), { - 'app_label': 'blogs', - 'model': 'post', - 'field_name': 'rating', - }), - ) - -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: - params = { - 'content_type_id': 23, - 'object_id': 34, - 'field_name': 'ratings', # this should match the field name defined in your model - 'score': 1, # the score value they're sending - } - response = AddRatingView()(request, **params) - if response.status_code == 200: - if response.content == 'Vote recorded.': - request.user.add_xp(settings.XP_BONUSES['submit-rating']) - return {'message': response.content, 'score': params['score']} - return {'error': 9, 'message': response.content} - -========================== -COOKIE format -========================== - -*New*: For now COOKIE name has fixed format: "vote-{{ content_type.id }}.{{ object.id }}.{{ rating_field.key }}[:6]" and COOKIE value is simple datetime-stamp. - -Example: vote-15.56.2c5504=20101213101523456000 - -And this COOKIE lives in user's browser for 1 year (this period is also fixed for now) - -*This feature may change in the future* - -========================== -Limit Votes Per IP Address -========================== -*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 - -============= -Template Tags -============= - -Right now django-ratings has limited support for template tags, and only for Django. -Load a ratings template tag set. ```{% load ratings %}```. - ------------------ -rating_by_request ------------------ - -Retrieves the ``Vote`` cast by a user on a particular object and -stores it in a context variable. If the user has not voted, the -context variable will be 0:: - - {% rating_by_request request on instance.field as vote %} - -If you are using Coffin, a better approach might be:: - - {% with instance.field_name.get_rating_for_user(request.user, request.META['REMOTE_ADDR'], request.COOKIES) as vote %} - Do some magic with {{ vote }} - {% endwith %} - -To use the ``request`` context variable you will need to add ``django.core.context_processors.request`` to the ``TEMPLATE_CONTEXT_PROCESSORS`` setting. - --------------- -rating_by_user --------------- - -It is recommended that you use rating_by_request as you will gain full support -for anonymous users if they are enabled - -Retrieves the ``Vote`` cast by a user on a particular object and -stores it in a context variable. If the user has not voted, the -context variable will be 0:: - - {% rating_by_user user on instance.field as vote %} +Use that instead diff --git a/djangoratings/_migrations/0001_initial.py b/djangoratings/_migrations/0001_initial.py new file mode 100644 index 0000000..9eda4fe --- /dev/null +++ b/djangoratings/_migrations/0001_initial.py @@ -0,0 +1,117 @@ + +from south.db import db +from django.db import models +from djangoratings.models import * + +class Migration: + + def forwards(self, orm): + + # Adding model 'Score' + db.create_table('djangoratings_score', ( + ('id', orm['djangoratings.Score:id']), + ('content_type', orm['djangoratings.Score:content_type']), + ('object_id', orm['djangoratings.Score:object_id']), + ('key', orm['djangoratings.Score:key']), + ('score', orm['djangoratings.Score:score']), + ('votes', orm['djangoratings.Score:votes']), + )) + db.send_create_signal('djangoratings', ['Score']) + + # Adding model 'Vote' + db.create_table('djangoratings_vote', ( + ('id', orm['djangoratings.Vote:id']), + ('content_type', orm['djangoratings.Vote:content_type']), + ('object_id', orm['djangoratings.Vote:object_id']), + ('key', orm['djangoratings.Vote:key']), + ('score', orm['djangoratings.Vote:score']), + ('user', orm['djangoratings.Vote:user']), + ('ip_address', orm['djangoratings.Vote:ip_address']), + ('date_added', orm['djangoratings.Vote:date_added']), + ('date_changed', orm['djangoratings.Vote:date_changed']), + )) + db.send_create_signal('djangoratings', ['Vote']) + + # Creating unique_together for [content_type, object_id, key, user, ip_address] on Vote. + db.create_unique('djangoratings_vote', ['content_type_id', 'object_id', 'key', 'user_id', 'ip_address']) + + # Creating unique_together for [content_type, object_id, key] on Score. + db.create_unique('djangoratings_score', ['content_type_id', 'object_id', 'key']) + + + + def backwards(self, orm): + + # Deleting unique_together for [content_type, object_id, key] on Score. + db.delete_unique('djangoratings_score', ['content_type_id', 'object_id', 'key']) + + # Deleting unique_together for [content_type, object_id, key, user, ip_address] on Vote. + db.delete_unique('djangoratings_vote', ['content_type_id', 'object_id', 'key', 'user_id', 'ip_address']) + + # Deleting model 'Score' + db.delete_table('djangoratings_score') + + # Deleting model 'Vote' + db.delete_table('djangoratings_vote') + + + + models = { + 'auth.group': { + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'unique_together': "(('content_type', 'codename'),)"}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'unique_together': "(('app_label', 'model'),)", 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'djangoratings.score': { + 'Meta': {'unique_together': "(('content_type', 'object_id', 'key'),)"}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'score': ('django.db.models.fields.IntegerField', [], {}), + 'votes': ('django.db.models.fields.PositiveIntegerField', [], {}) + }, + 'djangoratings.vote': { + 'Meta': {'unique_together': "(('content_type', 'object_id', 'key', 'user', 'ip_address'),)"}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['contenttypes.ContentType']"}), + 'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'date_changed': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'score': ('django.db.models.fields.IntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'votes'", 'null': 'True', 'to': "orm['auth.User']"}) + } + } + + complete_apps = ['djangoratings'] diff --git a/djangoratings/migrations/0002_add_mean_and_stddev.py b/djangoratings/_migrations/0002_add_mean_and_stddev.py similarity index 100% rename from djangoratings/migrations/0002_add_mean_and_stddev.py rename to djangoratings/_migrations/0002_add_mean_and_stddev.py diff --git a/djangoratings/migrations/0003_add_correlations.py b/djangoratings/_migrations/0003_add_correlations.py similarity index 100% rename from djangoratings/migrations/0003_add_correlations.py rename to djangoratings/_migrations/0003_add_correlations.py diff --git a/djangoratings/migrations/0004_rethink_recommendations.py b/djangoratings/_migrations/0004_rethink_recommendations.py similarity index 100% rename from djangoratings/migrations/0004_rethink_recommendations.py rename to djangoratings/_migrations/0004_rethink_recommendations.py diff --git a/djangoratings/migrations/0005_add_exclusions.py b/djangoratings/_migrations/0005_add_exclusions.py similarity index 100% rename from djangoratings/migrations/0005_add_exclusions.py rename to djangoratings/_migrations/0005_add_exclusions.py diff --git a/djangoratings/migrations/0006_add_cookies.py b/djangoratings/_migrations/0006_add_cookies.py similarity index 100% rename from djangoratings/migrations/0006_add_cookies.py rename to djangoratings/_migrations/0006_add_cookies.py diff --git a/djangoratings/_migrations/__init__.py b/djangoratings/_migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/djangoratings/fields.py b/djangoratings/fields.py index 2eb76f8..41d0a9b 100644 --- a/djangoratings/fields.py +++ b/djangoratings/fields.py @@ -2,12 +2,11 @@ from django.conf import settings import forms -import itertools from datetime import datetime from models import Vote, Score from default_settings import RATINGS_VOTES_PER_IP -from exceptions import * +import exceptions as e if 'django.contrib.contenttypes' not in settings.INSTALLED_APPS: raise ImportError("djangoratings requires django.contrib.contenttypes in your INSTALLED_APPS") @@ -26,75 +25,78 @@ except ImportError: now = datetime.now + def md5_hexdigest(value): return md5(value).hexdigest() + class Rating(object): def __init__(self, score, votes): self.score = score self.votes = votes + class RatingManager(object): def __init__(self, instance, field): self.content_type = None self.instance = instance self.field = field - + self.votes_field_name = "%s_votes" % (self.field.name,) self.score_field_name = "%s_score" % (self.field.name,) - + def get_percent(self): """get_percent() - + Returns the weighted percentage of the score from min-max values""" if not (self.votes and self.score): return 0 return 100 * (self.get_rating() / self.field.range) - + def get_real_percent(self): """get_real_percent() - + Returns the unmodified percentage of the score based on a 0-point scale.""" if not (self.votes and self.score): return 0 return 100 * (self.get_real_rating() / self.field.range) - + 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.pk, key=self.field.key) - + def get_rating(self): """get_rating() - + Returns the weighted average rating.""" if not (self.votes and self.score): return 0 return float(self.score)/(self.votes+self.field.weight) - + def get_opinion_percent(self): """get_opinion_percent() - + Returns a neutral-based percentage.""" return (self.get_percent()+100)/2 def get_real_rating(self): """get_rating() - + Returns the unmodified average rating.""" if not (self.votes and self.score): return 0 return float(self.score)/self.votes - + def get_rating_for_user(self, user, ip_address=None, cookies={}): """get_rating_for_user(user, ip_address=None, cookie=None) - + Returns the rating for a user or anonymous IP.""" kwargs = dict( - content_type = self.get_content_type(), - object_id = self.instance.pk, - key = self.field.key, + content_type=self.get_content_type(), + object_id=self.instance.pk, + key=self.field.key, ) if not (user and user.is_authenticated()): @@ -104,17 +106,17 @@ def get_rating_for_user(self, user, ip_address=None, cookies={}): kwargs['ip_address'] = ip_address else: kwargs['user'] = user - + use_cookies = (self.field.allow_anonymous and self.field.use_cookies) if use_cookies: # TODO: move 'vote-%d.%d.%s' to settings or something cookie_name = 'vote-%d.%d.%s' % (kwargs['content_type'].pk, kwargs['object_id'], kwargs['key'][:6],) # -> md5_hexdigest? cookie = cookies.get(cookie_name) - if cookie: + if cookie: kwargs['cookie'] = cookie else: kwargs['cookie__isnull'] = True - + try: rating = Vote.objects.get(**kwargs) return rating.score @@ -123,48 +125,48 @@ def get_rating_for_user(self, user, ip_address=None, cookies={}): except Vote.DoesNotExist: pass return - + def get_iterable_range(self): - return range(1, self.field.range) #started from 1, because 0 is equal to delete - + return range(1, self.field.range) # started from 1, because 0 is equal to delete + def add(self, score, user, ip_address, cookies={}, commit=True): """add(score, user, ip_address) - + Used to add a rating to an object.""" try: score = int(score) except (ValueError, TypeError): - raise InvalidRating("%s is not a valid choice for %s" % (score, self.field.name)) - + raise e.InvalidRating("%s is not a valid choice for %s" % (score, self.field.name)) + delete = (score == 0) if delete and not self.field.allow_delete: - raise CannotDeleteVote("you are not allowed to delete votes for %s" % (self.field.name,)) + raise e.CannotDeleteVote("you are not allowed to delete votes for %s" % (self.field.name,)) # ... you're also can't delete your vote if you haven't permissions to change it. I leave this case for CannotChangeVote - + if score < 0 or score > self.field.range: - raise InvalidRating("%s is not a valid choice for %s" % (score, self.field.name)) + raise e.InvalidRating("%s is not a valid choice for %s" % (score, self.field.name)) is_anonymous = (user is None or not user.is_authenticated()) if is_anonymous and not self.field.allow_anonymous: - raise AuthRequired("user must be a user, not '%r'" % (user,)) - + raise e.AuthRequired("user must be a user, not '%r'" % (user,)) + if is_anonymous: user = None - + defaults = dict( - score = score, - ip_address = ip_address, + score=score, + ip_address=ip_address, ) - + kwargs = dict( - content_type = self.get_content_type(), - object_id = self.instance.pk, - key = self.field.key, - user = user, + content_type=self.get_content_type(), + object_id=self.instance.pk, + key=self.field.key, + user=user, ) if not user: kwargs['ip_address'] = ip_address - + use_cookies = (self.field.allow_anonymous and self.field.use_cookies) if use_cookies: defaults['cookie'] = now().strftime('%Y%m%d%H%M%S%f') # -> md5_hexdigest? @@ -179,7 +181,7 @@ def add(self, score, user, ip_address, cookies={}, commit=True): rating, created = Vote.objects.get(**kwargs), False except Vote.DoesNotExist: if delete: - raise CannotDeleteVote("attempt to find and delete your vote for %s is failed" % (self.field.name,)) + raise e.CannotDeleteVote("attempt to find and delete your vote for %s is failed" % (self.field.name,)) if getattr(settings, 'RATINGS_VOTES_PER_IP', RATINGS_VOTES_PER_IP): num_votes = Vote.objects.filter( content_type=kwargs['content_type'], @@ -188,14 +190,14 @@ def add(self, score, user, ip_address, cookies={}, commit=True): ip_address=ip_address, ).count() if num_votes >= getattr(settings, 'RATINGS_VOTES_PER_IP', RATINGS_VOTES_PER_IP): - raise IPLimitReached() + raise e.IPLimitReached() kwargs.update(defaults) if use_cookies: # record with specified cookie was not found ... - cookie = defaults['cookie'] # ... thus we need to replace old cookie (if presented) with new one - kwargs.pop('cookie__isnull', '') # ... and remove 'cookie__isnull' (if presented) from .create()'s **kwargs + cookie = defaults['cookie'] # ... thus we need to replace old cookie (if presented) with new one + kwargs.pop('cookie__isnull', '') # ... and remove 'cookie__isnull' (if presented) from .create()'s **kwargs rating, created = Vote.objects.create(**kwargs), True - + has_changed = False if not created: if self.field.can_change_vote: @@ -209,7 +211,7 @@ def add(self, score, user, ip_address, cookies={}, commit=True): self.votes -= 1 rating.delete() else: - raise CannotChangeVote() + raise e.CannotChangeVote() else: has_changed = True self.votes += 1 @@ -217,30 +219,30 @@ def add(self, score, user, ip_address, cookies={}, commit=True): if not delete: self.score += rating.score if commit: - self.instance.save() - #setattr(self.instance, self.field.name, Rating(score=self.score, votes=self.votes)) - + self.instance.save(update_fields=['rating_score', 'rating_votes']) + # setattr(self.instance, self.field.name, Rating(score=self.score, votes=self.votes)) + defaults = dict( - score = self.score, - votes = self.votes, + score=self.score, + votes=self.votes, ) - + kwargs = dict( - content_type = self.get_content_type(), - object_id = self.instance.pk, - key = self.field.key, + content_type=self.get_content_type(), + object_id=self.instance.pk, + key=self.field.key, ) - + try: score, created = Score.objects.get(**kwargs), False except Score.DoesNotExist: kwargs.update(defaults) score, created = Score.objects.create(**kwargs), True - + if not created: score.__dict__.update(defaults) score.save() - + # return value adds = {} if use_cookies: @@ -252,45 +254,45 @@ def add(self, score, user, ip_address, cookies={}, commit=True): def delete(self, user, ip_address, cookies={}, commit=True): return self.add(0, user, ip_address, cookies, commit) - + def _get_votes(self, default=None): return getattr(self.instance, self.votes_field_name, default) - + def _set_votes(self, value): return setattr(self.instance, self.votes_field_name, value) - + votes = property(_get_votes, _set_votes) def _get_score(self, default=None): return getattr(self.instance, self.score_field_name, default) - + def _set_score(self, value): return setattr(self.instance, self.score_field_name, value) - + score = property(_get_score, _set_score) 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, + 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, + 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: @@ -302,6 +304,7 @@ def _update(self, commit=False): if commit: self.instance.save() + class RatingCreator(object): def __init__(self, field): self.field = field @@ -311,7 +314,7 @@ def __init__(self, field): def __get__(self, instance, type=None): if instance is None: return self.field - #raise AttributeError('Can only be accessed via an instance.') + # raise AttributeError('Can only be accessed via an instance.') return RatingManager(instance, self.field) def __set__(self, instance, value): @@ -321,6 +324,7 @@ def __set__(self, instance, value): else: raise TypeError("%s value must be a Rating instance, not '%r'" % (self.field.name, value)) + class RatingField(IntegerField): """ A rating field contributes two columns to the model instead of the standard single column. @@ -338,7 +342,7 @@ def __init__(self, *args, **kwargs): kwargs['default'] = 0 kwargs['blank'] = True super(RatingField, self).__init__(*args, **kwargs) - + def contribute_to_class(self, cls, name): self.name = name @@ -371,7 +375,7 @@ def get_db_prep_lookup(self, lookup_type, value): # TODO: order_by on this field should use the weighted algorithm raise NotImplementedError(self.get_db_prep_lookup) # if lookup_type in ('score', 'votes'): - # lookup_type = + # lookup_type = # return self.score_field.get_db_prep_lookup() if lookup_type == 'exact': return [self.get_db_prep_save(value)] diff --git a/djangoratings/migrations/0001_initial.py b/djangoratings/migrations/0001_initial.py index 9eda4fe..867726b 100644 --- a/djangoratings/migrations/0001_initial.py +++ b/djangoratings/migrations/0001_initial.py @@ -1,117 +1,83 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-08-11 19:37 +from __future__ import unicode_literals -from south.db import db -from django.db import models -from djangoratings.models import * +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone -class Migration: - - def forwards(self, orm): - - # Adding model 'Score' - db.create_table('djangoratings_score', ( - ('id', orm['djangoratings.Score:id']), - ('content_type', orm['djangoratings.Score:content_type']), - ('object_id', orm['djangoratings.Score:object_id']), - ('key', orm['djangoratings.Score:key']), - ('score', orm['djangoratings.Score:score']), - ('votes', orm['djangoratings.Score:votes']), - )) - db.send_create_signal('djangoratings', ['Score']) - - # Adding model 'Vote' - db.create_table('djangoratings_vote', ( - ('id', orm['djangoratings.Vote:id']), - ('content_type', orm['djangoratings.Vote:content_type']), - ('object_id', orm['djangoratings.Vote:object_id']), - ('key', orm['djangoratings.Vote:key']), - ('score', orm['djangoratings.Vote:score']), - ('user', orm['djangoratings.Vote:user']), - ('ip_address', orm['djangoratings.Vote:ip_address']), - ('date_added', orm['djangoratings.Vote:date_added']), - ('date_changed', orm['djangoratings.Vote:date_changed']), - )) - db.send_create_signal('djangoratings', ['Vote']) - - # Creating unique_together for [content_type, object_id, key, user, ip_address] on Vote. - db.create_unique('djangoratings_vote', ['content_type_id', 'object_id', 'key', 'user_id', 'ip_address']) - - # Creating unique_together for [content_type, object_id, key] on Score. - db.create_unique('djangoratings_score', ['content_type_id', 'object_id', 'key']) - - - - def backwards(self, orm): - - # Deleting unique_together for [content_type, object_id, key] on Score. - db.delete_unique('djangoratings_score', ['content_type_id', 'object_id', 'key']) - - # Deleting unique_together for [content_type, object_id, key, user, ip_address] on Vote. - db.delete_unique('djangoratings_vote', ['content_type_id', 'object_id', 'key', 'user_id', 'ip_address']) - - # Deleting model 'Score' - db.delete_table('djangoratings_score') - - # Deleting model 'Vote' - db.delete_table('djangoratings_vote') - - - - models = { - 'auth.group': { - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'unique_together': "(('content_type', 'codename'),)"}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - 'auth.user': { - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True', 'null': 'True', 'blank': 'True'}) - }, - 'contenttypes.contenttype': { - 'Meta': {'unique_together': "(('app_label', 'model'),)", 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - 'djangoratings.score': { - 'Meta': {'unique_together': "(('content_type', 'object_id', 'key'),)"}, - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), - 'score': ('django.db.models.fields.IntegerField', [], {}), - 'votes': ('django.db.models.fields.PositiveIntegerField', [], {}) - }, - 'djangoratings.vote': { - 'Meta': {'unique_together': "(('content_type', 'object_id', 'key', 'user', 'ip_address'),)"}, - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['contenttypes.ContentType']"}), - 'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'date_changed': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}), - 'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), - 'score': ('django.db.models.fields.IntegerField', [], {}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'votes'", 'null': 'True', 'to': "orm['auth.User']"}) - } - } - - complete_apps = ['djangoratings'] + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='IgnoredObject', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Score', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('key', models.CharField(max_length=32)), + ('score', models.IntegerField()), + ('votes', models.PositiveIntegerField()), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ], + ), + migrations.CreateModel( + name='SimilarUser', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('agrees', models.PositiveIntegerField(default=0)), + ('disagrees', models.PositiveIntegerField(default=0)), + ('exclude', models.BooleanField(default=False)), + ('from_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='similar_users', to=settings.AUTH_USER_MODEL)), + ('to_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='similar_users_from', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Vote', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('key', models.CharField(max_length=32)), + ('score', models.IntegerField()), + ('ip_address', models.GenericIPAddressField()), + ('cookie', models.CharField(blank=True, max_length=32, null=True)), + ('date_added', models.DateTimeField(default=django.utils.timezone.now, editable=False)), + ('date_changed', models.DateTimeField(default=django.utils.timezone.now, editable=False)), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='contenttypes.ContentType')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='votes', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AlterUniqueTogether( + name='vote', + unique_together=set([('content_type', 'object_id', 'key', 'user', 'ip_address', 'cookie')]), + ), + migrations.AlterUniqueTogether( + name='similaruser', + unique_together=set([('from_user', 'to_user')]), + ), + migrations.AlterUniqueTogether( + name='score', + unique_together=set([('content_type', 'object_id', 'key')]), + ), + migrations.AlterUniqueTogether( + name='ignoredobject', + unique_together=set([('content_type', 'object_id')]), + ), + ] diff --git a/djangoratings/models.py b/djangoratings/models.py index 6e78d29..a0aaa57 100644 --- a/djangoratings/models.py +++ b/djangoratings/models.py @@ -2,7 +2,7 @@ from django.db import models from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes import generic +from django.contrib.contenttypes import fields from django.contrib.auth.models import User try: @@ -12,20 +12,20 @@ from managers import VoteManager, SimilarUserManager -class Vote(models.Model): - content_type = models.ForeignKey(ContentType, related_name="votes") - object_id = models.PositiveIntegerField() - key = models.CharField(max_length=32) - score = models.IntegerField() - user = models.ForeignKey(User, blank=True, null=True, related_name="votes") - ip_address = models.IPAddressField() - cookie = models.CharField(max_length=32, blank=True, null=True) - date_added = models.DateTimeField(default=now, editable=False) - date_changed = models.DateTimeField(default=now, editable=False) - - objects = VoteManager() - content_object = generic.GenericForeignKey() +class Vote(models.Model): + content_type = models.ForeignKey(ContentType, related_name="votes") + object_id = models.PositiveIntegerField() + key = models.CharField(max_length=32) + score = models.IntegerField() + user = models.ForeignKey(User, blank=True, null=True, related_name="votes") + ip_address = models.GenericIPAddressField() + ip_address = models.GenericIPAddressField() if hasattr(models, "GenericIPAddressField") else models.IPAddressField() + cookie = models.CharField(max_length=32, blank=True, null=True) + date_added = models.DateTimeField(default=now, editable=False) + date_changed = models.DateTimeField(default=now, editable=False) + objects = VoteManager() + content_object = fields.GenericForeignKey() class Meta: unique_together = (('content_type', 'object_id', 'key', 'user', 'ip_address', 'cookie')) @@ -50,44 +50,44 @@ def partial_ip_address(self): partial_ip_address = property(partial_ip_address) class Score(models.Model): - content_type = models.ForeignKey(ContentType) - object_id = models.PositiveIntegerField() - key = models.CharField(max_length=32) - score = models.IntegerField() - votes = models.PositiveIntegerField() + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + key = models.CharField(max_length=32) + score = models.IntegerField() + votes = models.PositiveIntegerField() - content_object = generic.GenericForeignKey() + content_object = fields.GenericForeignKey() class Meta: - unique_together = (('content_type', 'object_id', 'key'),) + unique_together = (('content_type', 'object_id', 'key'), ) def __unicode__(self): return u"%s scored %s with %s votes" % (self.content_object, self.score, self.votes) + class SimilarUser(models.Model): - from_user = models.ForeignKey(User, related_name="similar_users") - to_user = models.ForeignKey(User, related_name="similar_users_from") - agrees = models.PositiveIntegerField(default=0) - disagrees = models.PositiveIntegerField(default=0) - exclude = models.BooleanField(default=False) - - objects = SimilarUserManager() - + from_user = models.ForeignKey(User, related_name="similar_users") + to_user = models.ForeignKey(User, related_name="similar_users_from") + agrees = models.PositiveIntegerField(default=0) + disagrees = models.PositiveIntegerField(default=0) + exclude = models.BooleanField(default=False) + objects = SimilarUserManager() + class Meta: - unique_together = (('from_user', 'to_user'),) + unique_together = (('from_user', 'to_user'), ) def __unicode__(self): print u"%s %s similar to %s" % (self.from_user, self.exclude and 'is not' or 'is', self.to_user) + class IgnoredObject(models.Model): - user = models.ForeignKey(User) - content_type = models.ForeignKey(ContentType) - object_id = models.PositiveIntegerField() - - content_object = generic.GenericForeignKey() - + user = models.ForeignKey(User) + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = fields.GenericForeignKey() + class Meta: - unique_together = (('content_type', 'object_id'),) - + unique_together = (('content_type', 'object_id'), ) + def __unicode__(self): - return self.content_object \ No newline at end of file + return self.content_object diff --git a/djangoratings/templatetags/ratings.py b/djangoratings/templatetags/ratings.py index f2dee1b..92c9d56 100644 --- a/djangoratings/templatetags/ratings.py +++ b/djangoratings/templatetags/ratings.py @@ -25,7 +25,10 @@ def render(self, context): except (template.VariableDoesNotExist, AttributeError): return '' try: - vote = field.get_rating_for_user(request.user, request.META['REMOTE_ADDR'], request.COOKIES) + ip = request.META.get('HTTP_X_FORWARDED_FOR') or request.META['REMOTE_ADDR'] + if len(ip.split(",")) > 1: + ip = ip.split(",")[0] + vote = field.get_rating_for_user(request.user, ip, request.COOKIES) context[self.context_var] = vote except ObjectDoesNotExist: context[self.context_var] = 0 diff --git a/djangoratings/views.py b/djangoratings/views.py index a294fcb..15caada 100644 --- a/djangoratings/views.py +++ b/djangoratings/views.py @@ -29,13 +29,15 @@ def __call__(self, request, content_type_id, object_id, field_name, score): 'field': field, 'score': score, }) - - had_voted = bool(field.get_rating_for_user(request.user, request.META['REMOTE_ADDR'], request.COOKIES)) + ip = request.META.get('HTTP_X_FORWARDED_FOR') or request.META.get('REMOTE_ADDR') + if len(ip.split(",")) > 1: + ip = ip.split(",")[0] + had_voted = bool(field.get_rating_for_user(request.user, ip, request.COOKIES)) context['had_voted'] = had_voted try: - adds = field.add(score, request.user, request.META.get('REMOTE_ADDR'), request.COOKIES) + adds = field.add(score, request.user, ip, request.COOKIES) except IPLimitReached: return self.too_many_votes_from_ip_response(request, context) except AuthRequired: diff --git a/setup.py b/setup.py index 8361147..b296e54 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,6 @@ description='Generic Ratings in Django', url='http://github.com/dcramer/django-ratings', install_requires=[ - 'django', ], tests_require=tests_require, extras_require={'test': tests_require}, @@ -29,4 +28,4 @@ "Operating System :: OS Independent", "Topic :: Software Development" ], -) \ No newline at end of file +)