Skip to content
This repository was archived by the owner on Jul 17, 2018. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 75 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ django-ratings

A generic ratings module. The field itself appends two additional fields on the model, for optimization reasons. It adds ``<field>_score``, and ``<field>_votes`` fields, which are both integer fields.

This fork extends upon David Cramer's django-ratings in a way to make ratings generic to better support and adapt to different rating systems, such as votings, star ratings, like/dislike, flags and others.

============
Installation
============
Expand Down Expand Up @@ -46,11 +48,47 @@ to obtain a higher rating, you can use the ``weight`` kwarg::

``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.
* ``lower = 0`` - The lower value in the range for which values are accepted. For example, a lower value of -1, says the voting scores start at -1.
* ``upper = 2`` - The upper value in the range for which values are accepted. For example, an upper value of 1, says the highest possible voting score is 1.
* ``range = 2`` - Upper and range are synonymous. For example, a range of 2, says there are 2 possible vote scores (provided lower is not set, or is 0).
* ``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``.
* ``use_cookies = False`` - Use COOKIES to authenticate user votes. Works only if ``allow_anonymous = True``.
* ``values = [lower ... upper]`` - List of strings accepted as alternative to score integer values. For example, ``['clear', 'favorite']`` would make it so the voting system accepts eigher 'clear' or 'favorite' in addition to 0 and 1, respectively.
* ``titles = []`` - List of strings used to have verbose names the voting scores. For example, ``[_("Clear"), _("Favorite")]``.
* ``widget_template = 'djangoratings/_rating.html'`` - Accepts the template name used to display the widget.

Also available there are ``VotingField``, ``FavoriteField`` and ``FlagField``, with their anonymous alternatives::

from djangoratings.fields import VotingField

class MyModel(models.Model):
rating = VotingField() # accepting 'down', 'clear' and 'up'

``VotingField``'s default options are:

* ``lower = -1``
* ``upper = 1``
* ``values = ['down', 'clear', 'up']``
* ``titles = [_("Down"), _("Clear"), _("Up")]``
* ``widget_template = 'djangoratings/_voting.html'``

``FavoriteField``'s default options are:

* ``lower = 0``
* ``upper = 1``
* ``values = ['clear', 'favorite']``
* ``titles = [_("Clear"), _("Favorite")]``
* ``widget_template = 'djangoratings/_favorite.html'``

``FlagField``'s default options are:

* ``lower = 0``
* ``upper = 1``
* ``values = ['clear', 'flag']``
* ``titles = [_("Clear"), _("Flag")]``
* ``widget_template = 'djangoratings/_flag.html'``

===================
Using the model API
Expand Down Expand Up @@ -188,3 +226,38 @@ 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 %}

-------------
rating_widget
-------------

Uses ``widget_template`` passed to the field to render the rating field widget::

{% rating_widget on instance.field %}

If you want to use a different ``widget_template``, pass the template name as::

{% rating_widget on instance.field using "template_name.html" %}

The context is passed to the template and additionally, the template receives:

* ``content_type`` - The content type of the instance object.
* ``instance`` - The object instance.
* ``model`` - The model name for the object.
* ``app_label`` - The app label for the object.
* ``object_id`` - The object instance ID.
* ``field_name`` - The field name.
* ``had_voted`` - If the user has voted previously, the voted score.
* ``votes`` - Number of total votes.
* ``score`` - The overall voting score for the object.
* ``vote`` - The overall voting score for the object, as an integer.
* ``percent`` - The overall voting score for the object, as a percentage.
* ``real_percent`` - The overall voting score for the object, as a percentage (without taking into account the weights).
* ``positive`` - Number of positive votes (when applicable).
* ``negative`` - Number of negative votes (when applicable).
* ``ratings`` - a list of ``checked``, ``value`` and ``title``. For example::

[
{ 'checked': False, 'value': 'clear', 'title: 'Clear' },
{ 'checked': True, 'value': 'favorite', 'title: 'Favorite' },
]
121 changes: 91 additions & 30 deletions djangoratings/fields.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from django.db.models import IntegerField, PositiveIntegerField
from django.utils.translation import ugettext_lazy as _
from django.conf import settings

import forms
import itertools
from datetime import datetime

from models import Vote, Score
Expand All @@ -21,14 +21,17 @@
except ImportError:
from md5 import new as md5


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
Expand All @@ -42,17 +45,13 @@ 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)
return 100 * ((self.get_rating() - self.field.range_lower) / (self.field.range_upper - self.field.range_lower))

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)
return 100 * ((self.get_real_rating() - self.field.range_lower) / (self.field.range_upper - self.field.range_lower))

def get_ratings(self):
"""get_ratings()
Expand All @@ -64,7 +63,7 @@ def get_rating(self):
"""get_rating()

Returns the weighted average rating."""
if not (self.votes and self.score):
if not self.votes:
return 0
return float(self.score)/(self.votes+self.field.weight)

Expand All @@ -78,7 +77,7 @@ def get_real_rating(self):
"""get_rating()

Returns the unmodified average rating."""
if not (self.votes and self.score):
if not self.votes:
return 0
return float(self.score)/self.votes

Expand Down Expand Up @@ -112,7 +111,10 @@ def get_rating_for_user(self, user, ip_address=None, cookies={}):

try:
rating = Vote.objects.get(**kwargs)
return rating.score
try:
return self.field.values[rating.score - self.field.range_lower]
except IndexError:
pass
except Vote.MultipleObjectsReturned:
pass
except Vote.DoesNotExist:
Expand All @@ -123,6 +125,9 @@ def add(self, score, user, ip_address, cookies={}, commit=True):
"""add(score, user, ip_address)

Used to add a rating to an object."""
if score in self.field.types:
score = self.field.types[score]

try:
score = int(score)
except (ValueError, TypeError):
Expand All @@ -133,7 +138,7 @@ def add(self, score, user, ip_address, cookies={}, commit=True):
raise 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:
if score and (score < self.field.range_lower or score > self.field.range_upper):
raise InvalidRating("%s is not a valid choice for %s" % (score, self.field.name))

is_anonymous = (user is None or not user.is_authenticated())
Expand Down Expand Up @@ -205,32 +210,28 @@ def add(self, score, user, ip_address, cookies={}, commit=True):
else:
has_changed = True
self.votes += 1

if has_changed:
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))

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 = self.score,
votes = self.votes,
)

kwargs = dict(
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)
if (score.score != self.score or
score.votes != self.votes):
score.score = self.score
score.votes = self.votes
score.save()

# return value
Expand All @@ -257,6 +258,8 @@ def _get_score(self, default=None):
return getattr(self.instance, self.score_field_name, default)

def _set_score(self, value):
if value in self.field.types:
value = self.field.types[value]
return setattr(self.instance, self.score_field_name, value)

score = property(_get_score, _set_score)
Expand Down Expand Up @@ -294,6 +297,7 @@ def _update(self, commit=False):
if commit:
self.instance.save()


class RatingCreator(object):
def __init__(self, field):
self.field = field
Expand All @@ -313,6 +317,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.
Expand All @@ -321,11 +326,19 @@ def __init__(self, *args, **kwargs):
if 'choices' in kwargs:
raise TypeError("%s invalid attribute 'choices'" % (self.__class__.__name__,))
self.can_change_vote = kwargs.pop('can_change_vote', False)
self.weight = kwargs.pop('weight', 0)
self.range = kwargs.pop('range', 2)
self.allow_anonymous = kwargs.pop('allow_anonymous', False)
self.use_cookies = kwargs.pop('use_cookies', False)
self.allow_delete = kwargs.pop('allow_delete', False)
self.widget_template = kwargs.pop('widget_template', 'djangoratings/_rating.html')
self.weight = kwargs.pop('weight', 0)
self.range_lower = kwargs.pop('lower', 1)
self.range_upper = kwargs.pop('upper', None)
if self.range_upper is None:
self.range_upper = kwargs.pop('range', 2)
self.titles = kwargs.pop('titles', [])
self.values = kwargs.pop('values', range(self.range_lower, self.range_upper+1))
self.types = dict(zip(self.values, range(self.range_lower, self.range_upper+1)))
self.types[''] = 0
kwargs['editable'] = False
kwargs['default'] = 0
kwargs['blank'] = True
Expand Down Expand Up @@ -354,21 +367,21 @@ def contribute_to_class(self, cls, name):

setattr(cls, name, field)

def get_db_prep_save(self, value):
def get_db_prep_save(self, value, connection=None):
# XXX: what happens here?
pass

def get_db_prep_lookup(self, lookup_type, value):
def get_db_prep_lookup(self, lookup_type, value, connection=None, prepared=False):
# TODO: hack in support for __score and __votes
# 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 =
# return self.score_field.get_db_prep_lookup()
if lookup_type == 'exact':
return [self.get_db_prep_save(value)]
return [self.get_db_prep_save(value, connection)]
elif lookup_type == 'in':
return [self.get_db_prep_save(v) for v in value]
return [self.get_db_prep_save(v, connection) for v in value]
else:
return super(RatingField, self).get_db_prep_lookup(lookup_type, value)

Expand All @@ -384,3 +397,51 @@ class AnonymousRatingField(RatingField):
def __init__(self, *args, **kwargs):
kwargs['allow_anonymous'] = True
super(AnonymousRatingField, self).__init__(*args, **kwargs)


class VotingField(RatingField):
def __init__(self, *args, **kwargs):
kwargs['widget_template'] = kwargs.get('widget_template', 'djangoratings/_voting.html')
kwargs['lower'] = -1
kwargs['upper'] = 1
kwargs['titles'] = (_("Down"), _("Clear"), _("Up"))
kwargs['values'] = ('down', 'clear', 'up')
super(VotingField, self).__init__(*args, **kwargs)


class AnonymousVotingField(VotingField):
def __init__(self, *args, **kwargs):
kwargs['allow_anonymous'] = True
super(AnonymousVotingField, self).__init__(*args, **kwargs)


class FavoriteField(RatingField):
def __init__(self, *args, **kwargs):
kwargs['widget_template'] = kwargs.get('widget_template', 'djangoratings/_favorite.html')
kwargs['lower'] = 0
kwargs['upper'] = 1
kwargs['titles'] = (_("Clear"), _("Favorite"))
kwargs['values'] = ('clear', 'favorite')
super(FavoriteField, self).__init__(*args, **kwargs)


class AnonymousFavoriteField(FavoriteField):
def __init__(self, *args, **kwargs):
kwargs['allow_anonymous'] = True
super(AnonymousFavoriteField, self).__init__(*args, **kwargs)


class FlagField(RatingField):
def __init__(self, *args, **kwargs):
kwargs['widget_template'] = kwargs.get('widget_template', 'djangoratings/_flag.html')
kwargs['lower'] = 0
kwargs['upper'] = 1
kwargs['titles'] = (_("Clear"), _("Flag"))
kwargs['values'] = ('clear', 'flag')
super(FlagField, self).__init__(*args, **kwargs)


class AnonymousFlagField(FlagField):
def __init__(self, *args, **kwargs):
kwargs['allow_anonymous'] = True
super(AnonymousFlagField, self).__init__(*args, **kwargs)
4 changes: 4 additions & 0 deletions djangoratings/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from managers import VoteManager, SimilarUserManager


class Vote(models.Model):
content_type = models.ForeignKey(ContentType, related_name="votes")
object_id = models.PositiveIntegerField()
Expand Down Expand Up @@ -44,6 +45,7 @@ def partial_ip_address(self):
return '.'.join(ip)
partial_ip_address = property(partial_ip_address)


class Score(models.Model):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
Expand All @@ -59,6 +61,7 @@ class Meta:
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")
Expand All @@ -74,6 +77,7 @@ class Meta:
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)
Expand Down
Loading