├── .gitignore ├── CONTRIBUTORS ├── LICENSE ├── MANIFEST.in ├── README.rst ├── djangoratings ├── __init__.py ├── admin.py ├── default_settings.py ├── exceptions.py ├── fields.py ├── forms.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── update_recommendations.py ├── managers.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_add_mean_and_stddev.py │ ├── 0003_add_correlations.py │ ├── 0004_rethink_recommendations.py │ ├── 0005_add_exclusions.py │ ├── 0006_add_cookies.py │ └── __init__.py ├── models.py ├── runtests.py ├── templatetags │ ├── __init__.py │ └── ratings.py ├── tests.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *.pyc 4 | django_ratings.egg-info/ 5 | *.egg -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Contributors 2 | ============ 3 | 4 | James Pic 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, David Cramer 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include setup.py README.rst LICENSE MANIFEST.in 2 | global-exclude *~ -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ############## 2 | django-ratings 3 | ############## 4 | 5 | **This project is no longer maintained** 6 | 7 | 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. 8 | 9 | ============ 10 | Installation 11 | ============ 12 | 13 | You will need to add ``djangoratings`` to your ``INSTALLED_APPS``:: 14 | 15 | INSTALLED_APPS = ( 16 | 'django.contrib.admin', 17 | 'django.contrib.auth', 18 | 'django.contrib.contenttypes', 19 | 'django.contrib.sessions', 20 | 'djangoratings', 21 | ) 22 | 23 | Finally, run ``python manage.py syncdb`` in your application's directory to create the tables. 24 | 25 | ================= 26 | Setup your models 27 | ================= 28 | 29 | 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:: 30 | 31 | from djangoratings.fields import RatingField 32 | 33 | class MyModel(models.Model): 34 | rating = RatingField(range=5) # 5 possible rating values, 1-5 35 | 36 | Alternatively you could do something like:: 37 | 38 | from djangoratings.fields import AnonymousRatingField 39 | 40 | class MyModel(models.Model): 41 | rating = AnonymousRatingField(range=10) 42 | 43 | If you'd like to use the built-in weighting methods, to make it appear more difficult for an object 44 | to obtain a higher rating, you can use the ``weight`` kwarg:: 45 | 46 | class MyModel(models.Model): 47 | rating = RatingField(range=10, weight=10) 48 | 49 | ``RatingField`` allows the following options: 50 | 51 | * ``range = 2`` - The range in which values are accepted. For example, a range of 2, says there are 2 possible vote scores. 52 | * ``can_change_vote = False`` - Allow the modification of votes that have already been made. 53 | * ``allow_delete = False`` - Allow the deletion of existent votes. Works only if ``can_change_vote = True`` 54 | * ``allow_anonymous = False`` - Whether to allow anonymous votes. 55 | * ``use_cookies = False`` - Use COOKIES to authenticate user votes. Works only if ``allow_anonymous = True``. 56 | 57 | =================== 58 | Using the model API 59 | =================== 60 | 61 | And adding votes is also simple:: 62 | 63 | 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 64 | 65 | Retrieving votes is just as easy:: 66 | 67 | myinstance.rating.get_rating_for_user(request.user, request.META['REMOTE_ADDR'], request.COOKIES) # last param is optional - only if you use COOKIES-auth 68 | 69 | *New* You're also able to delete existent votes (if deletion enabled):: 70 | 71 | myinstance.rating.delete(request.user, request.META['REMOTE_ADDR'], request.COOKIES) # last param is optional - only if you use COOKIES-auth 72 | 73 | Accessing information about the rating of an object is also easy:: 74 | 75 | # these do not hit the database 76 | myinstance.rating.votes 77 | myinstance.rating.score 78 | 79 | How you can order by top-rated using an algorithm (example from Nibbits.com source):: 80 | 81 | # In this example, ``rating`` is the attribute name for your ``RatingField`` 82 | qs = qs.extra(select={ 83 | 'rating': '((100/%s*rating_score/(rating_votes+%s))+100)/2' % (MyModel.rating.range, MyModel.rating.weight) 84 | }) 85 | qs = qs.order_by('-rating') 86 | 87 | Get overall rating for your instance on a scale [0-range]:: 88 | 89 | myinstance.rating.get_rating() 90 | 91 | Get recent ratings for your instance:: 92 | 93 | # This returns ``Vote`` instances. 94 | myinstance.rating.get_ratings()[0:5] 95 | 96 | Get the percent of voters approval:: 97 | 98 | myinstance.rating.get_percent() 99 | 100 | Get that same percentage, but excluding your ``weight``:: 101 | 102 | myinstance.rating.get_real_percent() 103 | 104 | =============================== 105 | Generic Views: Processing Votes 106 | =============================== 107 | 108 | The best way to use the generic views is by extending it, or calling it within your own code:: 109 | 110 | from djangoratings.views import AddRatingFromModel 111 | 112 | urlpatterns = patterns('', 113 | url(r'rate-my-post/(?P\d+)/(?P\d+)/', AddRatingFromModel(), { 114 | 'app_label': 'blogs', 115 | 'model': 'post', 116 | 'field_name': 'rating', 117 | }), 118 | ) 119 | 120 | Another example, on Nibbits we use a basic API interface, and we simply call the ``AddRatingView`` within our own view:: 121 | 122 | from djangoratings.views import AddRatingView 123 | 124 | # For the sake of this actually looking like documentation: 125 | params = { 126 | 'content_type_id': 23, 127 | 'object_id': 34, 128 | 'field_name': 'ratings', # this should match the field name defined in your model 129 | 'score': 1, # the score value they're sending 130 | } 131 | response = AddRatingView()(request, **params) 132 | if response.status_code == 200: 133 | if response.content == 'Vote recorded.': 134 | request.user.add_xp(settings.XP_BONUSES['submit-rating']) 135 | return {'message': response.content, 'score': params['score']} 136 | return {'error': 9, 'message': response.content} 137 | 138 | ========================== 139 | COOKIE format 140 | ========================== 141 | 142 | *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. 143 | 144 | Example: vote-15.56.2c5504=20101213101523456000 145 | 146 | And this COOKIE lives in user's browser for 1 year (this period is also fixed for now) 147 | 148 | *This feature may change in the future* 149 | 150 | ========================== 151 | Limit Votes Per IP Address 152 | ========================== 153 | *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:: 154 | 155 | RATINGS_VOTES_PER_IP = 3 156 | 157 | ============= 158 | Template Tags 159 | ============= 160 | 161 | Right now django-ratings has limited support for template tags, and only for Django. 162 | Load a ratings template tag set. ```{% load ratings %}```. 163 | 164 | ----------------- 165 | rating_by_request 166 | ----------------- 167 | 168 | Retrieves the ``Vote`` cast by a user on a particular object and 169 | stores it in a context variable. If the user has not voted, the 170 | context variable will be 0:: 171 | 172 | {% rating_by_request request on instance.field as vote %} 173 | 174 | If you are using Coffin, a better approach might be:: 175 | 176 | {% with instance.field_name.get_rating_for_user(request.user, request.META['REMOTE_ADDR'], request.COOKIES) as vote %} 177 | Do some magic with {{ vote }} 178 | {% endwith %} 179 | 180 | To use the ``request`` context variable you will need to add ``django.core.context_processors.request`` to the ``TEMPLATE_CONTEXT_PROCESSORS`` setting. 181 | 182 | -------------- 183 | rating_by_user 184 | -------------- 185 | 186 | It is recommended that you use rating_by_request as you will gain full support 187 | for anonymous users if they are enabled 188 | 189 | Retrieves the ``Vote`` cast by a user on a particular object and 190 | stores it in a context variable. If the user has not voted, the 191 | context variable will be 0:: 192 | 193 | {% rating_by_user user on instance.field as vote %} 194 | -------------------------------------------------------------------------------- /djangoratings/__init__.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import warnings 3 | 4 | __version__ = (0, 3, 7) 5 | 6 | def _get_git_revision(path): 7 | revision_file = os.path.join(path, 'refs', 'heads', 'master') 8 | if not os.path.exists(revision_file): 9 | return None 10 | fh = open(revision_file, 'r') 11 | try: 12 | return fh.read() 13 | finally: 14 | fh.close() 15 | 16 | def get_revision(): 17 | """ 18 | :returns: Revision number of this branch/checkout, if available. None if 19 | no revision number can be determined. 20 | """ 21 | package_dir = os.path.dirname(__file__) 22 | checkout_dir = os.path.normpath(os.path.join(package_dir, '..')) 23 | path = os.path.join(checkout_dir, '.git') 24 | if os.path.exists(path): 25 | return _get_git_revision(path) 26 | return None 27 | 28 | __build__ = get_revision() 29 | 30 | def lazy_object(location): 31 | def inner(*args, **kwargs): 32 | parts = location.rsplit('.', 1) 33 | warnings.warn('`djangoratings.%s` is deprecated. Please use `%s` instead.' % (parts[1], location), DeprecationWarning) 34 | try: 35 | imp = __import__(parts[0], globals(), locals(), [parts[1]], -1) 36 | except: 37 | imp = __import__(parts[0], globals(), locals(), [parts[1]]) 38 | func = getattr(imp, parts[1]) 39 | if callable(func): 40 | return func(*args, **kwargs) 41 | return func 42 | return inner 43 | 44 | RatingField = lazy_object('djangoratings.fields.RatingField') 45 | AnonymousRatingField = lazy_object('djangoratings.fields.AnonymousRatingField') 46 | Rating = lazy_object('djangoratings.fields.Rating') -------------------------------------------------------------------------------- /djangoratings/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from models import Vote, Score 3 | 4 | class VoteAdmin(admin.ModelAdmin): 5 | list_display = ('content_object', 'user', 'ip_address', 'cookie', 'score', 'date_changed') 6 | list_filter = ('score', 'content_type', 'date_changed') 7 | search_fields = ('ip_address',) 8 | raw_id_fields = ('user',) 9 | 10 | class ScoreAdmin(admin.ModelAdmin): 11 | list_display = ('content_object', 'score', 'votes') 12 | list_filter = ('content_type',) 13 | 14 | admin.site.register(Vote, VoteAdmin) 15 | admin.site.register(Score, ScoreAdmin) 16 | -------------------------------------------------------------------------------- /djangoratings/default_settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | # Used to limit the number of unique IPs that can vote on a single object+field. 4 | # useful if you're getting rating spam by users registering multiple accounts 5 | RATINGS_VOTES_PER_IP = 3 -------------------------------------------------------------------------------- /djangoratings/exceptions.py: -------------------------------------------------------------------------------- 1 | class InvalidRating(ValueError): pass 2 | class AuthRequired(TypeError): pass 3 | class CannotChangeVote(Exception): pass 4 | class CannotDeleteVote(Exception): pass 5 | class IPLimitReached(Exception): pass -------------------------------------------------------------------------------- /djangoratings/fields.py: -------------------------------------------------------------------------------- 1 | from django.db.models import IntegerField, PositiveIntegerField 2 | from django.conf import settings 3 | 4 | import forms 5 | import itertools 6 | from datetime import datetime 7 | 8 | from models import Vote, Score 9 | from default_settings import RATINGS_VOTES_PER_IP 10 | from exceptions import * 11 | 12 | if 'django.contrib.contenttypes' not in settings.INSTALLED_APPS: 13 | raise ImportError("djangoratings requires django.contrib.contenttypes in your INSTALLED_APPS") 14 | 15 | from django.contrib.contenttypes.models import ContentType 16 | 17 | __all__ = ('Rating', 'RatingField', 'AnonymousRatingField') 18 | 19 | try: 20 | from hashlib import md5 21 | except ImportError: 22 | from md5 import new as md5 23 | 24 | try: 25 | from django.utils.timezone import now 26 | except ImportError: 27 | now = datetime.now 28 | 29 | def md5_hexdigest(value): 30 | return md5(value).hexdigest() 31 | 32 | class Rating(object): 33 | def __init__(self, score, votes): 34 | self.score = score 35 | self.votes = votes 36 | 37 | class RatingManager(object): 38 | def __init__(self, instance, field): 39 | self.content_type = None 40 | self.instance = instance 41 | self.field = field 42 | 43 | self.votes_field_name = "%s_votes" % (self.field.name,) 44 | self.score_field_name = "%s_score" % (self.field.name,) 45 | 46 | def get_percent(self): 47 | """get_percent() 48 | 49 | Returns the weighted percentage of the score from min-max values""" 50 | if not (self.votes and self.score): 51 | return 0 52 | return 100 * (self.get_rating() / self.field.range) 53 | 54 | def get_real_percent(self): 55 | """get_real_percent() 56 | 57 | Returns the unmodified percentage of the score based on a 0-point scale.""" 58 | if not (self.votes and self.score): 59 | return 0 60 | return 100 * (self.get_real_rating() / self.field.range) 61 | 62 | def get_ratings(self): 63 | """get_ratings() 64 | 65 | Returns a Vote QuerySet for this rating field.""" 66 | return Vote.objects.filter(content_type=self.get_content_type(), object_id=self.instance.pk, key=self.field.key) 67 | 68 | def get_rating(self): 69 | """get_rating() 70 | 71 | Returns the weighted average rating.""" 72 | if not (self.votes and self.score): 73 | return 0 74 | return float(self.score)/(self.votes+self.field.weight) 75 | 76 | def get_opinion_percent(self): 77 | """get_opinion_percent() 78 | 79 | Returns a neutral-based percentage.""" 80 | return (self.get_percent()+100)/2 81 | 82 | def get_real_rating(self): 83 | """get_rating() 84 | 85 | Returns the unmodified average rating.""" 86 | if not (self.votes and self.score): 87 | return 0 88 | return float(self.score)/self.votes 89 | 90 | def get_rating_for_user(self, user, ip_address=None, cookies={}): 91 | """get_rating_for_user(user, ip_address=None, cookie=None) 92 | 93 | Returns the rating for a user or anonymous IP.""" 94 | kwargs = dict( 95 | content_type = self.get_content_type(), 96 | object_id = self.instance.pk, 97 | key = self.field.key, 98 | ) 99 | 100 | if not (user and user.is_authenticated()): 101 | if not ip_address: 102 | raise ValueError('``user`` or ``ip_address`` must be present.') 103 | kwargs['user__isnull'] = True 104 | kwargs['ip_address'] = ip_address 105 | else: 106 | kwargs['user'] = user 107 | 108 | use_cookies = (self.field.allow_anonymous and self.field.use_cookies) 109 | if use_cookies: 110 | # TODO: move 'vote-%d.%d.%s' to settings or something 111 | cookie_name = 'vote-%d.%d.%s' % (kwargs['content_type'].pk, kwargs['object_id'], kwargs['key'][:6],) # -> md5_hexdigest? 112 | cookie = cookies.get(cookie_name) 113 | if cookie: 114 | kwargs['cookie'] = cookie 115 | else: 116 | kwargs['cookie__isnull'] = True 117 | 118 | try: 119 | rating = Vote.objects.get(**kwargs) 120 | return rating.score 121 | except Vote.MultipleObjectsReturned: 122 | pass 123 | except Vote.DoesNotExist: 124 | pass 125 | return 126 | 127 | def get_iterable_range(self): 128 | return range(1, self.field.range) #started from 1, because 0 is equal to delete 129 | 130 | def add(self, score, user, ip_address, cookies={}, commit=True): 131 | """add(score, user, ip_address) 132 | 133 | Used to add a rating to an object.""" 134 | try: 135 | score = int(score) 136 | except (ValueError, TypeError): 137 | raise InvalidRating("%s is not a valid choice for %s" % (score, self.field.name)) 138 | 139 | delete = (score == 0) 140 | if delete and not self.field.allow_delete: 141 | raise CannotDeleteVote("you are not allowed to delete votes for %s" % (self.field.name,)) 142 | # ... you're also can't delete your vote if you haven't permissions to change it. I leave this case for CannotChangeVote 143 | 144 | if score < 0 or score > self.field.range: 145 | raise InvalidRating("%s is not a valid choice for %s" % (score, self.field.name)) 146 | 147 | is_anonymous = (user is None or not user.is_authenticated()) 148 | if is_anonymous and not self.field.allow_anonymous: 149 | raise AuthRequired("user must be a user, not '%r'" % (user,)) 150 | 151 | if is_anonymous: 152 | user = None 153 | 154 | defaults = dict( 155 | score = score, 156 | ip_address = ip_address, 157 | ) 158 | 159 | kwargs = dict( 160 | content_type = self.get_content_type(), 161 | object_id = self.instance.pk, 162 | key = self.field.key, 163 | user = user, 164 | ) 165 | if not user: 166 | kwargs['ip_address'] = ip_address 167 | 168 | use_cookies = (self.field.allow_anonymous and self.field.use_cookies) 169 | if use_cookies: 170 | defaults['cookie'] = now().strftime('%Y%m%d%H%M%S%f') # -> md5_hexdigest? 171 | # TODO: move 'vote-%d.%d.%s' to settings or something 172 | cookie_name = 'vote-%d.%d.%s' % (kwargs['content_type'].pk, kwargs['object_id'], kwargs['key'][:6],) # -> md5_hexdigest? 173 | cookie = cookies.get(cookie_name) # try to get existent cookie value 174 | if not cookie: 175 | kwargs['cookie__isnull'] = True 176 | kwargs['cookie'] = cookie 177 | 178 | try: 179 | rating, created = Vote.objects.get(**kwargs), False 180 | except Vote.DoesNotExist: 181 | if delete: 182 | raise CannotDeleteVote("attempt to find and delete your vote for %s is failed" % (self.field.name,)) 183 | if getattr(settings, 'RATINGS_VOTES_PER_IP', RATINGS_VOTES_PER_IP): 184 | num_votes = Vote.objects.filter( 185 | content_type=kwargs['content_type'], 186 | object_id=kwargs['object_id'], 187 | key=kwargs['key'], 188 | ip_address=ip_address, 189 | ).count() 190 | if num_votes >= getattr(settings, 'RATINGS_VOTES_PER_IP', RATINGS_VOTES_PER_IP): 191 | raise IPLimitReached() 192 | kwargs.update(defaults) 193 | if use_cookies: 194 | # record with specified cookie was not found ... 195 | cookie = defaults['cookie'] # ... thus we need to replace old cookie (if presented) with new one 196 | kwargs.pop('cookie__isnull', '') # ... and remove 'cookie__isnull' (if presented) from .create()'s **kwargs 197 | rating, created = Vote.objects.create(**kwargs), True 198 | 199 | has_changed = False 200 | if not created: 201 | if self.field.can_change_vote: 202 | has_changed = True 203 | self.score -= rating.score 204 | # you can delete your vote only if you have permission to change your vote 205 | if not delete: 206 | rating.score = score 207 | rating.save() 208 | else: 209 | self.votes -= 1 210 | rating.delete() 211 | else: 212 | raise CannotChangeVote() 213 | else: 214 | has_changed = True 215 | self.votes += 1 216 | if has_changed: 217 | if not delete: 218 | self.score += rating.score 219 | if commit: 220 | self.instance.save() 221 | #setattr(self.instance, self.field.name, Rating(score=self.score, votes=self.votes)) 222 | 223 | defaults = dict( 224 | score = self.score, 225 | votes = self.votes, 226 | ) 227 | 228 | kwargs = dict( 229 | content_type = self.get_content_type(), 230 | object_id = self.instance.pk, 231 | key = self.field.key, 232 | ) 233 | 234 | try: 235 | score, created = Score.objects.get(**kwargs), False 236 | except Score.DoesNotExist: 237 | kwargs.update(defaults) 238 | score, created = Score.objects.create(**kwargs), True 239 | 240 | if not created: 241 | score.__dict__.update(defaults) 242 | score.save() 243 | 244 | # return value 245 | adds = {} 246 | if use_cookies: 247 | adds['cookie_name'] = cookie_name 248 | adds['cookie'] = cookie 249 | if delete: 250 | adds['deleted'] = True 251 | return adds 252 | 253 | def delete(self, user, ip_address, cookies={}, commit=True): 254 | return self.add(0, user, ip_address, cookies, commit) 255 | 256 | def _get_votes(self, default=None): 257 | return getattr(self.instance, self.votes_field_name, default) 258 | 259 | def _set_votes(self, value): 260 | return setattr(self.instance, self.votes_field_name, value) 261 | 262 | votes = property(_get_votes, _set_votes) 263 | 264 | def _get_score(self, default=None): 265 | return getattr(self.instance, self.score_field_name, default) 266 | 267 | def _set_score(self, value): 268 | return setattr(self.instance, self.score_field_name, value) 269 | 270 | score = property(_get_score, _set_score) 271 | 272 | def get_content_type(self): 273 | if self.content_type is None: 274 | self.content_type = ContentType.objects.get_for_model(self.instance) 275 | return self.content_type 276 | 277 | def _update(self, commit=False): 278 | """Forces an update of this rating (useful for when Vote objects are removed).""" 279 | votes = Vote.objects.filter( 280 | content_type = self.get_content_type(), 281 | object_id = self.instance.pk, 282 | key = self.field.key, 283 | ) 284 | obj_score = sum([v.score for v in votes]) 285 | obj_votes = len(votes) 286 | 287 | score, created = Score.objects.get_or_create( 288 | content_type = self.get_content_type(), 289 | object_id = self.instance.pk, 290 | key = self.field.key, 291 | defaults = dict( 292 | score = obj_score, 293 | votes = obj_votes, 294 | ) 295 | ) 296 | if not created: 297 | score.score = obj_score 298 | score.votes = obj_votes 299 | score.save() 300 | self.score = obj_score 301 | self.votes = obj_votes 302 | if commit: 303 | self.instance.save() 304 | 305 | class RatingCreator(object): 306 | def __init__(self, field): 307 | self.field = field 308 | self.votes_field_name = "%s_votes" % (self.field.name,) 309 | self.score_field_name = "%s_score" % (self.field.name,) 310 | 311 | def __get__(self, instance, type=None): 312 | if instance is None: 313 | return self.field 314 | #raise AttributeError('Can only be accessed via an instance.') 315 | return RatingManager(instance, self.field) 316 | 317 | def __set__(self, instance, value): 318 | if isinstance(value, Rating): 319 | setattr(instance, self.votes_field_name, value.votes) 320 | setattr(instance, self.score_field_name, value.score) 321 | else: 322 | raise TypeError("%s value must be a Rating instance, not '%r'" % (self.field.name, value)) 323 | 324 | class RatingField(IntegerField): 325 | """ 326 | A rating field contributes two columns to the model instead of the standard single column. 327 | """ 328 | def __init__(self, *args, **kwargs): 329 | if 'choices' in kwargs: 330 | raise TypeError("%s invalid attribute 'choices'" % (self.__class__.__name__,)) 331 | self.can_change_vote = kwargs.pop('can_change_vote', False) 332 | self.weight = kwargs.pop('weight', 0) 333 | self.range = kwargs.pop('range', 2) 334 | self.allow_anonymous = kwargs.pop('allow_anonymous', False) 335 | self.use_cookies = kwargs.pop('use_cookies', False) 336 | self.allow_delete = kwargs.pop('allow_delete', False) 337 | kwargs['editable'] = False 338 | kwargs['default'] = 0 339 | kwargs['blank'] = True 340 | super(RatingField, self).__init__(*args, **kwargs) 341 | 342 | def contribute_to_class(self, cls, name): 343 | self.name = name 344 | 345 | # Votes tally field 346 | self.votes_field = PositiveIntegerField( 347 | editable=False, default=0, blank=True) 348 | cls.add_to_class("%s_votes" % (self.name,), self.votes_field) 349 | 350 | # Score sum field 351 | self.score_field = IntegerField( 352 | editable=False, default=0, blank=True) 353 | cls.add_to_class("%s_score" % (self.name,), self.score_field) 354 | 355 | self.key = md5_hexdigest(self.name) 356 | 357 | field = RatingCreator(self) 358 | 359 | if not hasattr(cls, '_djangoratings'): 360 | cls._djangoratings = [] 361 | cls._djangoratings.append(self) 362 | 363 | setattr(cls, name, field) 364 | 365 | def get_db_prep_save(self, value): 366 | # XXX: what happens here? 367 | pass 368 | 369 | def get_db_prep_lookup(self, lookup_type, value): 370 | # TODO: hack in support for __score and __votes 371 | # TODO: order_by on this field should use the weighted algorithm 372 | raise NotImplementedError(self.get_db_prep_lookup) 373 | # if lookup_type in ('score', 'votes'): 374 | # lookup_type = 375 | # return self.score_field.get_db_prep_lookup() 376 | if lookup_type == 'exact': 377 | return [self.get_db_prep_save(value)] 378 | elif lookup_type == 'in': 379 | return [self.get_db_prep_save(v) for v in value] 380 | else: 381 | return super(RatingField, self).get_db_prep_lookup(lookup_type, value) 382 | 383 | def formfield(self, **kwargs): 384 | defaults = {'form_class': forms.RatingField} 385 | defaults.update(kwargs) 386 | return super(RatingField, self).formfield(**defaults) 387 | 388 | # TODO: flatten_data method 389 | 390 | 391 | class AnonymousRatingField(RatingField): 392 | def __init__(self, *args, **kwargs): 393 | kwargs['allow_anonymous'] = True 394 | super(AnonymousRatingField, self).__init__(*args, **kwargs) 395 | -------------------------------------------------------------------------------- /djangoratings/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | __all__ = ('RatingField',) 4 | 5 | class RatingField(forms.ChoiceField): 6 | pass -------------------------------------------------------------------------------- /djangoratings/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dcramer/django-ratings/4d00dedc920a4e32d650dc12d5f480c51fc6216c/djangoratings/management/__init__.py -------------------------------------------------------------------------------- /djangoratings/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dcramer/django-ratings/4d00dedc920a4e32d650dc12d5f480c51fc6216c/djangoratings/management/commands/__init__.py -------------------------------------------------------------------------------- /djangoratings/management/commands/update_recommendations.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import NoArgsCommand, CommandError 2 | 3 | from djangoratings.models import SimilarUser 4 | 5 | class Command(NoArgsCommand): 6 | def handle_noargs(self, **options): 7 | SimilarUser.objects.update_recommendations() -------------------------------------------------------------------------------- /djangoratings/managers.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Manager 2 | from django.db.models.query import QuerySet 3 | 4 | from django.contrib.contenttypes.models import ContentType 5 | import itertools 6 | 7 | class VoteQuerySet(QuerySet): 8 | def delete(self, *args, **kwargs): 9 | """Handles updating the related `votes` and `score` fields attached to the model.""" 10 | # XXX: circular import 11 | from fields import RatingField 12 | 13 | qs = self.distinct().values_list('content_type', 'object_id').order_by('content_type') 14 | 15 | to_update = [] 16 | for content_type, objects in itertools.groupby(qs, key=lambda x: x[0]): 17 | model_class = ContentType.objects.get(pk=content_type).model_class() 18 | if model_class: 19 | to_update.extend(list(model_class.objects.filter(pk__in=list(objects)[0]))) 20 | 21 | retval = super(VoteQuerySet, self).delete(*args, **kwargs) 22 | 23 | # TODO: this could be improved 24 | for obj in to_update: 25 | for field in getattr(obj, '_djangoratings', []): 26 | getattr(obj, field.name)._update(commit=False) 27 | obj.save() 28 | 29 | return retval 30 | 31 | class VoteManager(Manager): 32 | def get_query_set(self): 33 | return VoteQuerySet(self.model) 34 | 35 | def get_for_user_in_bulk(self, objects, user): 36 | objects = list(objects) 37 | if len(objects) > 0: 38 | ctype = ContentType.objects.get_for_model(objects[0]) 39 | votes = list(self.filter(content_type__pk=ctype.id, 40 | object_id__in=[obj._get_pk_val() \ 41 | for obj in objects], 42 | user__pk=user.id)) 43 | vote_dict = dict([(vote.object_id, vote) for vote in votes]) 44 | else: 45 | vote_dict = {} 46 | return vote_dict 47 | 48 | class SimilarUserManager(Manager): 49 | def get_recommendations(self, user, model_class, min_score=1): 50 | from djangoratings.models import Vote, IgnoredObject 51 | 52 | content_type = ContentType.objects.get_for_model(model_class) 53 | 54 | params = dict( 55 | v=Vote._meta.db_table, 56 | sm=self.model._meta.db_table, 57 | m=model_class._meta.db_table, 58 | io=IgnoredObject._meta.db_table, 59 | ) 60 | 61 | objects = model_class._default_manager.extra( 62 | tables=[params['v']], 63 | where=[ 64 | '%(v)s.object_id = %(m)s.id and %(v)s.content_type_id = %%s' % params, 65 | '%(v)s.user_id IN (select to_user_id from %(sm)s where from_user_id = %%s and exclude = 0)' % params, 66 | '%(v)s.score >= %%s' % params, 67 | # Exclude already rated maps 68 | '%(v)s.object_id NOT IN (select object_id from %(v)s where content_type_id = %(v)s.content_type_id and user_id = %%s)' % params, 69 | # IgnoredObject exclusions 70 | '%(v)s.object_id NOT IN (select object_id from %(io)s where content_type_id = %(v)s.content_type_id and user_id = %%s)' % params, 71 | ], 72 | params=[content_type.id, user.id, min_score, user.id, user.id] 73 | ).distinct() 74 | 75 | # objects = model_class._default_manager.filter(pk__in=content_type.votes.extra( 76 | # where=['user_id IN (select to_user_id from %s where from_user_id = %d and exclude = 0)' % (self.model._meta.db_table, user.pk)], 77 | # ).filter(score__gte=min_score).exclude( 78 | # object_id__in=IgnoredObject.objects.filter(content_type=content_type, user=user).values_list('object_id', flat=True), 79 | # ).exclude( 80 | # object_id__in=Vote.objects.filter(content_type=content_type, user=user).values_list('object_id', flat=True) 81 | # ).distinct().values_list('object_id', flat=True)) 82 | 83 | return objects 84 | 85 | def update_recommendations(self): 86 | # TODO: this is mysql only atm 87 | # TODO: this doesnt handle scores that have multiple values (e.g. 10 points, 5 stars) 88 | # due to it calling an agreement as score = score. We need to loop each rating instance 89 | # and express the condition based on the range. 90 | from djangoratings.models import Vote 91 | from django.db import connection 92 | cursor = connection.cursor() 93 | cursor.execute('begin') 94 | cursor.execute('truncate table %s' % (self.model._meta.db_table,)) 95 | cursor.execute("""insert into %(t1)s 96 | (to_user_id, from_user_id, agrees, disagrees, exclude) 97 | select v1.user_id, v2.user_id, 98 | sum(if(v2.score = v1.score, 1, 0)) as agrees, 99 | sum(if(v2.score != v1.score, 1, 0)) as disagrees, 0 100 | from %(t2)s as v1 101 | inner join %(t2)s as v2 102 | on v1.user_id != v2.user_id 103 | and v1.object_id = v2.object_id 104 | and v1.content_type_id = v2.content_type_id 105 | where v1.user_id is not null 106 | and v2.user_id is not null 107 | group by v1.user_id, v2.user_id 108 | having agrees / (disagrees + 0.0001) > 3 109 | on duplicate key update agrees = values(agrees), disagrees = values(disagrees);""" % dict( 110 | t1=self.model._meta.db_table, 111 | t2=Vote._meta.db_table, 112 | )) 113 | cursor.execute('commit') 114 | cursor.close() -------------------------------------------------------------------------------- /djangoratings/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | 2 | from south.db import db 3 | from django.db import models 4 | from djangoratings.models import * 5 | 6 | class Migration: 7 | 8 | def forwards(self, orm): 9 | 10 | # Adding model 'Score' 11 | db.create_table('djangoratings_score', ( 12 | ('id', orm['djangoratings.Score:id']), 13 | ('content_type', orm['djangoratings.Score:content_type']), 14 | ('object_id', orm['djangoratings.Score:object_id']), 15 | ('key', orm['djangoratings.Score:key']), 16 | ('score', orm['djangoratings.Score:score']), 17 | ('votes', orm['djangoratings.Score:votes']), 18 | )) 19 | db.send_create_signal('djangoratings', ['Score']) 20 | 21 | # Adding model 'Vote' 22 | db.create_table('djangoratings_vote', ( 23 | ('id', orm['djangoratings.Vote:id']), 24 | ('content_type', orm['djangoratings.Vote:content_type']), 25 | ('object_id', orm['djangoratings.Vote:object_id']), 26 | ('key', orm['djangoratings.Vote:key']), 27 | ('score', orm['djangoratings.Vote:score']), 28 | ('user', orm['djangoratings.Vote:user']), 29 | ('ip_address', orm['djangoratings.Vote:ip_address']), 30 | ('date_added', orm['djangoratings.Vote:date_added']), 31 | ('date_changed', orm['djangoratings.Vote:date_changed']), 32 | )) 33 | db.send_create_signal('djangoratings', ['Vote']) 34 | 35 | # Creating unique_together for [content_type, object_id, key, user, ip_address] on Vote. 36 | db.create_unique('djangoratings_vote', ['content_type_id', 'object_id', 'key', 'user_id', 'ip_address']) 37 | 38 | # Creating unique_together for [content_type, object_id, key] on Score. 39 | db.create_unique('djangoratings_score', ['content_type_id', 'object_id', 'key']) 40 | 41 | 42 | 43 | def backwards(self, orm): 44 | 45 | # Deleting unique_together for [content_type, object_id, key] on Score. 46 | db.delete_unique('djangoratings_score', ['content_type_id', 'object_id', 'key']) 47 | 48 | # Deleting unique_together for [content_type, object_id, key, user, ip_address] on Vote. 49 | db.delete_unique('djangoratings_vote', ['content_type_id', 'object_id', 'key', 'user_id', 'ip_address']) 50 | 51 | # Deleting model 'Score' 52 | db.delete_table('djangoratings_score') 53 | 54 | # Deleting model 'Vote' 55 | db.delete_table('djangoratings_vote') 56 | 57 | 58 | 59 | models = { 60 | 'auth.group': { 61 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 62 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 63 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}) 64 | }, 65 | 'auth.permission': { 66 | 'Meta': {'unique_together': "(('content_type', 'codename'),)"}, 67 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 68 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 69 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 70 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 71 | }, 72 | 'auth.user': { 73 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 74 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 75 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 76 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True'}), 77 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 78 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 79 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 80 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 81 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 82 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 83 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 84 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}), 85 | 'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True', 'null': 'True', 'blank': 'True'}) 86 | }, 87 | 'contenttypes.contenttype': { 88 | 'Meta': {'unique_together': "(('app_label', 'model'),)", 'db_table': "'django_content_type'"}, 89 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 90 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 91 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 92 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 93 | }, 94 | 'djangoratings.score': { 95 | 'Meta': {'unique_together': "(('content_type', 'object_id', 'key'),)"}, 96 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 97 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 98 | 'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}), 99 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 100 | 'score': ('django.db.models.fields.IntegerField', [], {}), 101 | 'votes': ('django.db.models.fields.PositiveIntegerField', [], {}) 102 | }, 103 | 'djangoratings.vote': { 104 | 'Meta': {'unique_together': "(('content_type', 'object_id', 'key', 'user', 'ip_address'),)"}, 105 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['contenttypes.ContentType']"}), 106 | 'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 107 | 'date_changed': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 108 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 109 | 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}), 110 | 'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}), 111 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 112 | 'score': ('django.db.models.fields.IntegerField', [], {}), 113 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'votes'", 'null': 'True', 'to': "orm['auth.User']"}) 114 | } 115 | } 116 | 117 | complete_apps = ['djangoratings'] 118 | -------------------------------------------------------------------------------- /djangoratings/migrations/0002_add_mean_and_stddev.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'Score.stddev' 12 | db.add_column('djangoratings_score', 'stddev', self.gf('django.db.models.fields.FloatField')(default=0.0), keep_default=False) 13 | 14 | # Adding field 'Score.mean' 15 | db.add_column('djangoratings_score', 'mean', self.gf('django.db.models.fields.FloatField')(default=0.0), keep_default=False) 16 | 17 | 18 | def backwards(self, orm): 19 | 20 | # Deleting field 'Score.stddev' 21 | db.delete_column('djangoratings_score', 'stddev') 22 | 23 | # Deleting field 'Score.mean' 24 | db.delete_column('djangoratings_score', 'mean') 25 | 26 | 27 | models = { 28 | 'auth.group': { 29 | 'Meta': {'object_name': 'Group'}, 30 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 31 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 32 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 33 | }, 34 | 'auth.permission': { 35 | 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 36 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 37 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 38 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 39 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 40 | }, 41 | 'auth.user': { 42 | 'Meta': {'object_name': 'User'}, 43 | '_battlenet_profiles': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'battlenet_profiles'", 'blank': 'True'}), 44 | 'avatar': ('django.db.models.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 45 | 'bio': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 46 | 'custom_title': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), 47 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 48 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 49 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 50 | 'gold': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), 51 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 52 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 53 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 54 | 'is_moderator': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 55 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 56 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 57 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 58 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 59 | 'level': ('django.db.models.fields.IntegerField', [], {'default': '1'}), 60 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 61 | 'signature': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), 62 | 'url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), 63 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 64 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), 65 | 'xp': ('django.db.models.fields.IntegerField', [], {'default': '0'}) 66 | }, 67 | 'contenttypes.contenttype': { 68 | 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 69 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 70 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 71 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 72 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 73 | }, 74 | 'djangoratings.score': { 75 | 'Meta': {'unique_together': "(('content_type', 'object_id', 'key'),)", 'object_name': 'Score'}, 76 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 77 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 78 | 'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}), 79 | 'mean': ('django.db.models.fields.FloatField', [], {'default': '0.0'}), 80 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 81 | 'score': ('django.db.models.fields.IntegerField', [], {}), 82 | 'stddev': ('django.db.models.fields.FloatField', [], {'default': '0.0'}), 83 | 'votes': ('django.db.models.fields.PositiveIntegerField', [], {}) 84 | }, 85 | 'djangoratings.vote': { 86 | 'Meta': {'unique_together': "(('content_type', 'object_id', 'key', 'user', 'ip_address'),)", 'object_name': 'Vote'}, 87 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['contenttypes.ContentType']"}), 88 | 'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 89 | 'date_changed': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 90 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 91 | 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}), 92 | 'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}), 93 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 94 | 'score': ('django.db.models.fields.IntegerField', [], {}), 95 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'votes'", 'null': 'True', 'to': "orm['auth.User']"}) 96 | } 97 | } 98 | 99 | complete_apps = ['djangoratings'] 100 | -------------------------------------------------------------------------------- /djangoratings/migrations/0003_add_correlations.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding model 'ScoreCorrelation' 12 | db.create_table('djangoratings_scorecorrelation', ( 13 | ('rank', self.gf('django.db.models.fields.FloatField')()), 14 | ('to_content_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='djr_sc_2', to=orm['contenttypes.ContentType'])), 15 | ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), 16 | ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='djr_sc_1', to=orm['contenttypes.ContentType'])), 17 | ('to_object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), 18 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 19 | )) 20 | db.send_create_signal('djangoratings', ['ScoreCorrelation']) 21 | 22 | 23 | def backwards(self, orm): 24 | 25 | # Deleting model 'ScoreCorrelation' 26 | db.delete_table('djangoratings_scorecorrelation') 27 | 28 | 29 | models = { 30 | 'auth.group': { 31 | 'Meta': {'object_name': 'Group'}, 32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 34 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 35 | }, 36 | 'auth.permission': { 37 | 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 38 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 39 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 40 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 41 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 42 | }, 43 | 'auth.user': { 44 | 'Meta': {'object_name': 'User'}, 45 | '_battlenet_profiles': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'battlenet_profiles'", 'blank': 'True'}), 46 | 'avatar': ('django.db.models.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 47 | 'bio': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 48 | 'custom_title': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), 49 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 50 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 51 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 52 | 'gold': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), 53 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 54 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 55 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 56 | 'is_moderator': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 57 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 58 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 59 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 60 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 61 | 'level': ('django.db.models.fields.IntegerField', [], {'default': '1'}), 62 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 63 | 'signature': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), 64 | 'url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), 65 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 66 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), 67 | 'xp': ('django.db.models.fields.IntegerField', [], {'default': '0'}) 68 | }, 69 | 'contenttypes.contenttype': { 70 | 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 71 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 72 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 73 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 74 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 75 | }, 76 | 'djangoratings.score': { 77 | 'Meta': {'unique_together': "(('content_type', 'object_id', 'key'),)", 'object_name': 'Score'}, 78 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 79 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 80 | 'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}), 81 | 'mean': ('django.db.models.fields.FloatField', [], {'default': '0.0'}), 82 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 83 | 'score': ('django.db.models.fields.IntegerField', [], {}), 84 | 'stddev': ('django.db.models.fields.FloatField', [], {'default': '0.0'}), 85 | 'votes': ('django.db.models.fields.PositiveIntegerField', [], {}) 86 | }, 87 | 'djangoratings.scorecorrelation': { 88 | 'Meta': {'unique_together': "(('content_type', 'object_id', 'to_content_type', 'to_object_id'),)", 'object_name': 'ScoreCorrelation'}, 89 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'djr_sc_1'", 'to': "orm['contenttypes.ContentType']"}), 90 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 91 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 92 | 'rank': ('django.db.models.fields.FloatField', [], {}), 93 | 'to_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'djr_sc_2'", 'to': "orm['contenttypes.ContentType']"}), 94 | 'to_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}) 95 | }, 96 | 'djangoratings.vote': { 97 | 'Meta': {'unique_together': "(('content_type', 'object_id', 'key', 'user', 'ip_address'),)", 'object_name': 'Vote'}, 98 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['contenttypes.ContentType']"}), 99 | 'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 100 | 'date_changed': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 101 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 102 | 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}), 103 | 'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}), 104 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 105 | 'score': ('django.db.models.fields.IntegerField', [], {}), 106 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'votes'", 'null': 'True', 'to': "orm['auth.User']"}) 107 | } 108 | } 109 | 110 | complete_apps = ['djangoratings'] 111 | -------------------------------------------------------------------------------- /djangoratings/migrations/0004_rethink_recommendations.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Deleting model 'ScoreCorrelation' 12 | db.delete_table('djangoratings_scorecorrelation') 13 | 14 | # Adding model 'SimilarUser' 15 | db.create_table('djangoratings_similaruser', ( 16 | ('to_user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='similar_users_from', to=orm['auth.User'])), 17 | ('agrees', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)), 18 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 19 | ('disagrees', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)), 20 | ('from_user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='similar_users', to=orm['auth.User'])), 21 | )) 22 | db.send_create_signal('djangoratings', ['SimilarUser']) 23 | 24 | # Deleting field 'Score.stddev' 25 | db.delete_column('djangoratings_score', 'stddev') 26 | 27 | # Deleting field 'Score.mean' 28 | db.delete_column('djangoratings_score', 'mean') 29 | 30 | 31 | def backwards(self, orm): 32 | 33 | # Adding model 'ScoreCorrelation' 34 | db.create_table('djangoratings_scorecorrelation', ( 35 | ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), 36 | ('to_content_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='djr_sc_2', to=orm['contenttypes.ContentType'])), 37 | ('rank', self.gf('django.db.models.fields.FloatField')()), 38 | ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='djr_sc_1', to=orm['contenttypes.ContentType'])), 39 | ('to_object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), 40 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 41 | )) 42 | db.send_create_signal('djangoratings', ['ScoreCorrelation']) 43 | 44 | # Deleting model 'SimilarUser' 45 | db.delete_table('djangoratings_similaruser') 46 | 47 | # Adding field 'Score.stddev' 48 | db.add_column('djangoratings_score', 'stddev', self.gf('django.db.models.fields.FloatField')(default=0.0), keep_default=False) 49 | 50 | # Adding field 'Score.mean' 51 | db.add_column('djangoratings_score', 'mean', self.gf('django.db.models.fields.FloatField')(default=0.0), keep_default=False) 52 | 53 | 54 | models = { 55 | 'auth.group': { 56 | 'Meta': {'object_name': 'Group'}, 57 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 58 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 59 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 60 | }, 61 | 'auth.permission': { 62 | 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 63 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 64 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 65 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 66 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 67 | }, 68 | 'auth.user': { 69 | 'Meta': {'object_name': 'User'}, 70 | '_battlenet_profiles': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'battlenet_profiles'", 'blank': 'True'}), 71 | 'avatar': ('django.db.models.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 72 | 'bio': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 73 | 'custom_title': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), 74 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 75 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 76 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 77 | 'gold': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), 78 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 79 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 80 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 81 | 'is_moderator': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 82 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 83 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 84 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 85 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 86 | 'level': ('django.db.models.fields.IntegerField', [], {'default': '1'}), 87 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 88 | 'signature': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), 89 | 'url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), 90 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 91 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), 92 | 'xp': ('django.db.models.fields.IntegerField', [], {'default': '0'}) 93 | }, 94 | 'contenttypes.contenttype': { 95 | 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 96 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 97 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 98 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 99 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 100 | }, 101 | 'djangoratings.score': { 102 | 'Meta': {'unique_together': "(('content_type', 'object_id', 'key'),)", 'object_name': 'Score'}, 103 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 104 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 105 | 'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}), 106 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 107 | 'score': ('django.db.models.fields.IntegerField', [], {}), 108 | 'votes': ('django.db.models.fields.PositiveIntegerField', [], {}) 109 | }, 110 | 'djangoratings.similaruser': { 111 | 'Meta': {'unique_together': "(('from_user', 'to_user'),)", 'object_name': 'SimilarUser'}, 112 | 'agrees': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), 113 | 'disagrees': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), 114 | 'from_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'similar_users'", 'to': "orm['auth.User']"}), 115 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 116 | 'to_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'similar_users_from'", 'to': "orm['auth.User']"}) 117 | }, 118 | 'djangoratings.vote': { 119 | 'Meta': {'unique_together': "(('content_type', 'object_id', 'key', 'user', 'ip_address'),)", 'object_name': 'Vote'}, 120 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['contenttypes.ContentType']"}), 121 | 'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 122 | 'date_changed': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 123 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 124 | 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}), 125 | 'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}), 126 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 127 | 'score': ('django.db.models.fields.IntegerField', [], {}), 128 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'votes'", 'null': 'True', 'to': "orm['auth.User']"}) 129 | } 130 | } 131 | 132 | complete_apps = ['djangoratings'] 133 | -------------------------------------------------------------------------------- /djangoratings/migrations/0005_add_exclusions.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding model 'IgnoredObject' 12 | db.create_table('djangoratings_ignoredobject', ( 13 | ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), 14 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 15 | ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])), 16 | ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), 17 | )) 18 | db.send_create_signal('djangoratings', ['IgnoredObject']) 19 | 20 | # Adding field 'SimilarUser.exclude' 21 | db.add_column('djangoratings_similaruser', 'exclude', self.gf('django.db.models.fields.BooleanField')(default=False, blank=True), keep_default=False) 22 | 23 | 24 | def backwards(self, orm): 25 | 26 | # Deleting model 'IgnoredObject' 27 | db.delete_table('djangoratings_ignoredobject') 28 | 29 | # Deleting field 'SimilarUser.exclude' 30 | db.delete_column('djangoratings_similaruser', 'exclude') 31 | 32 | 33 | models = { 34 | 'auth.group': { 35 | 'Meta': {'object_name': 'Group'}, 36 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 37 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 38 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 39 | }, 40 | 'auth.permission': { 41 | 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 42 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 43 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 44 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 45 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 46 | }, 47 | 'auth.user': { 48 | 'Meta': {'object_name': 'User'}, 49 | '_battlenet_profiles': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'battlenet_profiles'", 'blank': 'True'}), 50 | 'avatar': ('django.db.models.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 51 | 'bio': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 52 | 'custom_title': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), 53 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 54 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 55 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 56 | 'gold': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), 57 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 58 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 59 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 60 | 'is_moderator': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 61 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 62 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 63 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 64 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 65 | 'level': ('django.db.models.fields.IntegerField', [], {'default': '1'}), 66 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 67 | 'signature': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), 68 | 'url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), 69 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 70 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), 71 | 'xp': ('django.db.models.fields.IntegerField', [], {'default': '0'}) 72 | }, 73 | 'contenttypes.contenttype': { 74 | 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 75 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 76 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 77 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 78 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 79 | }, 80 | 'djangoratings.ignoredobject': { 81 | 'Meta': {'unique_together': "(('content_type', 'object_id'),)", 'object_name': 'IgnoredObject'}, 82 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 83 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 84 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 85 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 86 | }, 87 | 'djangoratings.score': { 88 | 'Meta': {'unique_together': "(('content_type', 'object_id', 'key'),)", 'object_name': 'Score'}, 89 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 90 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 91 | 'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}), 92 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 93 | 'score': ('django.db.models.fields.IntegerField', [], {}), 94 | 'votes': ('django.db.models.fields.PositiveIntegerField', [], {}) 95 | }, 96 | 'djangoratings.similaruser': { 97 | 'Meta': {'unique_together': "(('from_user', 'to_user'),)", 'object_name': 'SimilarUser'}, 98 | 'agrees': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), 99 | 'disagrees': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), 100 | 'exclude': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 101 | 'from_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'similar_users'", 'to': "orm['auth.User']"}), 102 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 103 | 'to_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'similar_users_from'", 'to': "orm['auth.User']"}) 104 | }, 105 | 'djangoratings.vote': { 106 | 'Meta': {'unique_together': "(('content_type', 'object_id', 'key', 'user', 'ip_address'),)", 'object_name': 'Vote'}, 107 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['contenttypes.ContentType']"}), 108 | 'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 109 | 'date_changed': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 110 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 111 | 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}), 112 | 'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}), 113 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 114 | 'score': ('django.db.models.fields.IntegerField', [], {}), 115 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'votes'", 'null': 'True', 'to': "orm['auth.User']"}) 116 | } 117 | } 118 | 119 | complete_apps = ['djangoratings'] 120 | -------------------------------------------------------------------------------- /djangoratings/migrations/0006_add_cookies.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Removing unique constraint on 'Vote', fields ['key', 'ip_address', 'object_id', 'content_type', 'user'] 12 | db.delete_unique('djangoratings_vote', ['key', 'ip_address', 'object_id', 'content_type_id', 'user_id']) 13 | 14 | # Adding field 'Vote.cookie' 15 | db.add_column('djangoratings_vote', 'cookie', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, blank=True), keep_default=False) 16 | 17 | # Adding unique constraint on 'Vote', fields ['content_type', 'object_id', 'cookie', 'user', 'key', 'ip_address'] 18 | db.create_unique('djangoratings_vote', ['content_type_id', 'object_id', 'cookie', 'user_id', 'key', 'ip_address']) 19 | 20 | 21 | def backwards(self, orm): 22 | 23 | # Removing unique constraint on 'Vote', fields ['content_type', 'object_id', 'cookie', 'user', 'key', 'ip_address'] 24 | db.delete_unique('djangoratings_vote', ['content_type_id', 'object_id', 'cookie', 'user_id', 'key', 'ip_address']) 25 | 26 | # Deleting field 'Vote.cookie' 27 | db.delete_column('djangoratings_vote', 'cookie') 28 | 29 | # Adding unique constraint on 'Vote', fields ['key', 'ip_address', 'object_id', 'content_type', 'user'] 30 | db.create_unique('djangoratings_vote', ['key', 'ip_address', 'object_id', 'content_type_id', 'user_id']) 31 | 32 | 33 | models = { 34 | 'auth.group': { 35 | 'Meta': {'object_name': 'Group'}, 36 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 37 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 38 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 39 | }, 40 | 'auth.permission': { 41 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 42 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 43 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 44 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 45 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 46 | }, 47 | 'auth.user': { 48 | 'Meta': {'object_name': 'User'}, 49 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 50 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 51 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 52 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 53 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 54 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 55 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 56 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 57 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 58 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 59 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 60 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 61 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 62 | }, 63 | 'contenttypes.contenttype': { 64 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 65 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 66 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 67 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 68 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 69 | }, 70 | 'djangoratings.ignoredobject': { 71 | 'Meta': {'unique_together': "(('content_type', 'object_id'),)", 'object_name': 'IgnoredObject'}, 72 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 73 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 74 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 75 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 76 | }, 77 | 'djangoratings.score': { 78 | 'Meta': {'unique_together': "(('content_type', 'object_id', 'key'),)", 'object_name': 'Score'}, 79 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 80 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 81 | 'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}), 82 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 83 | 'score': ('django.db.models.fields.IntegerField', [], {}), 84 | 'votes': ('django.db.models.fields.PositiveIntegerField', [], {}) 85 | }, 86 | 'djangoratings.similaruser': { 87 | 'Meta': {'unique_together': "(('from_user', 'to_user'),)", 'object_name': 'SimilarUser'}, 88 | 'agrees': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), 89 | 'disagrees': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), 90 | 'exclude': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 91 | 'from_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'similar_users'", 'to': "orm['auth.User']"}), 92 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 93 | 'to_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'similar_users_from'", 'to': "orm['auth.User']"}) 94 | }, 95 | 'djangoratings.vote': { 96 | 'Meta': {'unique_together': "(('content_type', 'object_id', 'key', 'user', 'ip_address', 'cookie'),)", 'object_name': 'Vote'}, 97 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['contenttypes.ContentType']"}), 98 | 'cookie': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), 99 | 'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 100 | 'date_changed': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 101 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 102 | 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}), 103 | 'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}), 104 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 105 | 'score': ('django.db.models.fields.IntegerField', [], {}), 106 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'votes'", 'null': 'True', 'to': "orm['auth.User']"}) 107 | } 108 | } 109 | 110 | complete_apps = ['djangoratings'] 111 | -------------------------------------------------------------------------------- /djangoratings/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dcramer/django-ratings/4d00dedc920a4e32d650dc12d5f480c51fc6216c/djangoratings/migrations/__init__.py -------------------------------------------------------------------------------- /djangoratings/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django.db import models 4 | from django.contrib.contenttypes.models import ContentType 5 | from django.contrib.contenttypes import generic 6 | from django.contrib.auth.models import User 7 | 8 | try: 9 | from django.utils.timezone import now 10 | except ImportError: 11 | now = datetime.now 12 | 13 | from managers import VoteManager, SimilarUserManager 14 | 15 | class Vote(models.Model): 16 | content_type = models.ForeignKey(ContentType, related_name="votes") 17 | object_id = models.PositiveIntegerField() 18 | key = models.CharField(max_length=32) 19 | score = models.IntegerField() 20 | user = models.ForeignKey(User, blank=True, null=True, related_name="votes") 21 | ip_address = models.IPAddressField() 22 | cookie = models.CharField(max_length=32, blank=True, null=True) 23 | date_added = models.DateTimeField(default=now, editable=False) 24 | date_changed = models.DateTimeField(default=now, editable=False) 25 | 26 | objects = VoteManager() 27 | 28 | content_object = generic.GenericForeignKey() 29 | 30 | class Meta: 31 | unique_together = (('content_type', 'object_id', 'key', 'user', 'ip_address', 'cookie')) 32 | 33 | def __unicode__(self): 34 | return u"%s voted %s on %s" % (self.user_display, self.score, self.content_object) 35 | 36 | def save(self, *args, **kwargs): 37 | self.date_changed = now() 38 | super(Vote, self).save(*args, **kwargs) 39 | 40 | def user_display(self): 41 | if self.user: 42 | return "%s (%s)" % (self.user.username, self.ip_address) 43 | return self.ip_address 44 | user_display = property(user_display) 45 | 46 | def partial_ip_address(self): 47 | ip = self.ip_address.split('.') 48 | ip[-1] = 'xxx' 49 | return '.'.join(ip) 50 | partial_ip_address = property(partial_ip_address) 51 | 52 | class Score(models.Model): 53 | content_type = models.ForeignKey(ContentType) 54 | object_id = models.PositiveIntegerField() 55 | key = models.CharField(max_length=32) 56 | score = models.IntegerField() 57 | votes = models.PositiveIntegerField() 58 | 59 | content_object = generic.GenericForeignKey() 60 | 61 | class Meta: 62 | unique_together = (('content_type', 'object_id', 'key'),) 63 | 64 | def __unicode__(self): 65 | return u"%s scored %s with %s votes" % (self.content_object, self.score, self.votes) 66 | 67 | class SimilarUser(models.Model): 68 | from_user = models.ForeignKey(User, related_name="similar_users") 69 | to_user = models.ForeignKey(User, related_name="similar_users_from") 70 | agrees = models.PositiveIntegerField(default=0) 71 | disagrees = models.PositiveIntegerField(default=0) 72 | exclude = models.BooleanField(default=False) 73 | 74 | objects = SimilarUserManager() 75 | 76 | class Meta: 77 | unique_together = (('from_user', 'to_user'),) 78 | 79 | def __unicode__(self): 80 | print u"%s %s similar to %s" % (self.from_user, self.exclude and 'is not' or 'is', self.to_user) 81 | 82 | class IgnoredObject(models.Model): 83 | user = models.ForeignKey(User) 84 | content_type = models.ForeignKey(ContentType) 85 | object_id = models.PositiveIntegerField() 86 | 87 | content_object = generic.GenericForeignKey() 88 | 89 | class Meta: 90 | unique_together = (('content_type', 'object_id'),) 91 | 92 | def __unicode__(self): 93 | return self.content_object -------------------------------------------------------------------------------- /djangoratings/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | 4 | from os.path import dirname, abspath 5 | 6 | from django.conf import settings 7 | 8 | if not settings.configured: 9 | settings.configure( 10 | DATABASE_ENGINE='sqlite3', 11 | INSTALLED_APPS=[ 12 | 'django.contrib.auth', 13 | 'django.contrib.contenttypes', 14 | 'djangoratings', 15 | ] 16 | ) 17 | 18 | from django.test.simple import run_tests 19 | 20 | 21 | def runtests(*test_args): 22 | if not test_args: 23 | test_args = ['djangoratings'] 24 | parent = dirname(abspath(__file__)) 25 | sys.path.insert(0, parent) 26 | failures = run_tests(test_args, verbosity=1, interactive=True) 27 | sys.exit(failures) 28 | 29 | 30 | if __name__ == '__main__': 31 | runtests(*sys.argv[1:]) -------------------------------------------------------------------------------- /djangoratings/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dcramer/django-ratings/4d00dedc920a4e32d650dc12d5f480c51fc6216c/djangoratings/templatetags/__init__.py -------------------------------------------------------------------------------- /djangoratings/templatetags/ratings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Template tags for Django 3 | """ 4 | # TODO: add in Jinja tags if Coffin is available 5 | 6 | from django import template 7 | from django.contrib.contenttypes.models import ContentType 8 | from django.db.models import ObjectDoesNotExist 9 | 10 | from djangoratings.models import Vote 11 | 12 | register = template.Library() 13 | 14 | class RatingByRequestNode(template.Node): 15 | def __init__(self, request, obj, context_var): 16 | self.request = request 17 | self.obj, self.field_name = obj.split('.') 18 | self.context_var = context_var 19 | 20 | def render(self, context): 21 | try: 22 | request = template.resolve_variable(self.request, context) 23 | obj = template.resolve_variable(self.obj, context) 24 | field = getattr(obj, self.field_name) 25 | except (template.VariableDoesNotExist, AttributeError): 26 | return '' 27 | try: 28 | vote = field.get_rating_for_user(request.user, request.META['REMOTE_ADDR'], request.COOKIES) 29 | context[self.context_var] = vote 30 | except ObjectDoesNotExist: 31 | context[self.context_var] = 0 32 | return '' 33 | 34 | def do_rating_by_request(parser, token): 35 | """ 36 | Retrieves the ``Vote`` cast by a user on a particular object and 37 | stores it in a context variable. If the user has not voted, the 38 | context variable will be 0. 39 | 40 | Example usage:: 41 | 42 | {% rating_by_request request on instance as vote %} 43 | """ 44 | 45 | bits = token.contents.split() 46 | if len(bits) != 6: 47 | raise template.TemplateSyntaxError("'%s' tag takes exactly five arguments" % bits[0]) 48 | if bits[2] != 'on': 49 | raise template.TemplateSyntaxError("second argument to '%s' tag must be 'on'" % bits[0]) 50 | if bits[4] != 'as': 51 | raise template.TemplateSyntaxError("fourth argument to '%s' tag must be 'as'" % bits[0]) 52 | return RatingByRequestNode(bits[1], bits[3], bits[5]) 53 | register.tag('rating_by_request', do_rating_by_request) 54 | 55 | class RatingByUserNode(RatingByRequestNode): 56 | def render(self, context): 57 | try: 58 | user = template.resolve_variable(self.request, context) 59 | obj = template.resolve_variable(self.obj, context) 60 | field = getattr(obj, self.field_name) 61 | except template.VariableDoesNotExist: 62 | return '' 63 | try: 64 | vote = field.get_rating_for_user(user) 65 | context[self.context_var] = vote 66 | except ObjectDoesNotExist: 67 | context[self.context_var] = 0 68 | return '' 69 | 70 | def do_rating_by_user(parser, token): 71 | """ 72 | Retrieves the ``Vote`` cast by a user on a particular object and 73 | stores it in a context variable. If the user has not voted, the 74 | context variable will be 0. 75 | 76 | Example usage:: 77 | 78 | {% rating_by_user user on instance as vote %} 79 | """ 80 | 81 | bits = token.contents.split() 82 | if len(bits) != 6: 83 | raise template.TemplateSyntaxError("'%s' tag takes exactly five arguments" % bits[0]) 84 | if bits[2] != 'on': 85 | raise template.TemplateSyntaxError("second argument to '%s' tag must be 'on'" % bits[0]) 86 | if bits[4] != 'as': 87 | raise template.TemplateSyntaxError("fourth argument to '%s' tag must be 'as'" % bits[0]) 88 | return RatingByUserNode(bits[1], bits[3], bits[5]) 89 | register.tag('rating_by_user', do_rating_by_user) 90 | -------------------------------------------------------------------------------- /djangoratings/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import random 3 | 4 | from django.db import models 5 | from django.contrib.auth.models import User 6 | from django.contrib.contenttypes.models import ContentType 7 | from django.conf import settings 8 | 9 | from exceptions import * 10 | from models import Vote, SimilarUser, IgnoredObject 11 | from fields import AnonymousRatingField, RatingField 12 | 13 | settings.RATINGS_VOTES_PER_IP = 1 14 | 15 | class RatingTestModel(models.Model): 16 | rating = AnonymousRatingField(range=2, can_change_vote=True) 17 | rating2 = RatingField(range=2, can_change_vote=False) 18 | 19 | def __unicode__(self): 20 | return unicode(self.pk) 21 | 22 | class RatingTestCase(unittest.TestCase): 23 | def testRatings(self): 24 | instance = RatingTestModel.objects.create() 25 | 26 | # Test adding votes 27 | instance.rating.add(score=1, user=None, ip_address='127.0.0.1') 28 | self.assertEquals(instance.rating.score, 1) 29 | self.assertEquals(instance.rating.votes, 1) 30 | 31 | # Test adding votes 32 | instance.rating.add(score=2, user=None, ip_address='127.0.0.2') 33 | self.assertEquals(instance.rating.score, 3) 34 | self.assertEquals(instance.rating.votes, 2) 35 | 36 | # Test changing of votes 37 | instance.rating.add(score=2, user=None, ip_address='127.0.0.1') 38 | self.assertEquals(instance.rating.score, 4) 39 | self.assertEquals(instance.rating.votes, 2) 40 | 41 | # Test users 42 | user = User.objects.create(username=str(random.randint(0, 100000000))) 43 | user2 = User.objects.create(username=str(random.randint(0, 100000000))) 44 | 45 | instance.rating.add(score=2, user=user, ip_address='127.0.0.3') 46 | self.assertEquals(instance.rating.score, 6) 47 | self.assertEquals(instance.rating.votes, 3) 48 | 49 | instance.rating2.add(score=2, user=user, ip_address='127.0.0.3') 50 | self.assertEquals(instance.rating2.score, 2) 51 | self.assertEquals(instance.rating2.votes, 1) 52 | 53 | self.assertRaises(IPLimitReached, instance.rating2.add, score=2, user=user2, ip_address='127.0.0.3') 54 | 55 | # Test deletion hooks 56 | Vote.objects.filter(ip_address='127.0.0.3').delete() 57 | 58 | instance = RatingTestModel.objects.get(pk=instance.pk) 59 | 60 | self.assertEquals(instance.rating.score, 4) 61 | self.assertEquals(instance.rating.votes, 2) 62 | self.assertEquals(instance.rating2.score, 0) 63 | self.assertEquals(instance.rating2.votes, 0) 64 | 65 | class RecommendationsTestCase(unittest.TestCase): 66 | def setUp(self): 67 | self.instance = RatingTestModel.objects.create() 68 | self.instance2 = RatingTestModel.objects.create() 69 | self.instance3 = RatingTestModel.objects.create() 70 | self.instance4 = RatingTestModel.objects.create() 71 | self.instance5 = RatingTestModel.objects.create() 72 | 73 | # Test users 74 | self.user = User.objects.create(username=str(random.randint(0, 100000000))) 75 | self.user2 = User.objects.create(username=str(random.randint(0, 100000000))) 76 | 77 | def testExclusions(self): 78 | Vote.objects.all().delete() 79 | 80 | self.instance.rating.add(score=1, user=self.user, ip_address='127.0.0.1') 81 | self.instance2.rating.add(score=1, user=self.user, ip_address='127.0.0.1') 82 | self.instance3.rating.add(score=1, user=self.user, ip_address='127.0.0.1') 83 | self.instance4.rating.add(score=1, user=self.user, ip_address='127.0.0.1') 84 | self.instance5.rating.add(score=1, user=self.user, ip_address='127.0.0.1') 85 | self.instance.rating.add(score=1, user=self.user2, ip_address='127.0.0.2') 86 | 87 | # we should only need to call this once 88 | SimilarUser.objects.update_recommendations() 89 | 90 | self.assertEquals(SimilarUser.objects.count(), 2) 91 | 92 | recs = list(SimilarUser.objects.get_recommendations(self.user2, RatingTestModel)) 93 | self.assertEquals(len(recs), 4) 94 | 95 | ct = ContentType.objects.get_for_model(RatingTestModel) 96 | 97 | IgnoredObject.objects.create(user=self.user2, content_type=ct, object_id=self.instance2.pk) 98 | 99 | recs = list(SimilarUser.objects.get_recommendations(self.user2, RatingTestModel)) 100 | self.assertEquals(len(recs), 3) 101 | 102 | IgnoredObject.objects.create(user=self.user2, content_type=ct, object_id=self.instance3.pk) 103 | IgnoredObject.objects.create(user=self.user2, content_type=ct, object_id=self.instance4.pk) 104 | 105 | recs = list(SimilarUser.objects.get_recommendations(self.user2, RatingTestModel)) 106 | self.assertEquals(len(recs), 1) 107 | self.assertEquals(recs, [self.instance5]) 108 | 109 | self.instance5.rating.add(score=1, user=self.user2, ip_address='127.0.0.2') 110 | recs = list(SimilarUser.objects.get_recommendations(self.user2, RatingTestModel)) 111 | self.assertEquals(len(recs), 0) 112 | 113 | def testSimilarUsers(self): 114 | Vote.objects.all().delete() 115 | 116 | self.instance.rating.add(score=1, user=self.user, ip_address='127.0.0.1') 117 | self.instance2.rating.add(score=1, user=self.user, ip_address='127.0.0.1') 118 | self.instance3.rating.add(score=1, user=self.user, ip_address='127.0.0.1') 119 | self.instance4.rating.add(score=1, user=self.user, ip_address='127.0.0.1') 120 | self.instance5.rating.add(score=1, user=self.user, ip_address='127.0.0.1') 121 | self.instance.rating.add(score=1, user=self.user2, ip_address='127.0.0.2') 122 | self.instance2.rating.add(score=1, user=self.user2, ip_address='127.0.0.2') 123 | self.instance3.rating.add(score=1, user=self.user2, ip_address='127.0.0.2') 124 | 125 | SimilarUser.objects.update_recommendations() 126 | 127 | self.assertEquals(SimilarUser.objects.count(), 2) 128 | 129 | recs = list(SimilarUser.objects.get_recommendations(self.user2, RatingTestModel)) 130 | self.assertEquals(len(recs), 2) 131 | 132 | self.instance4.rating.add(score=1, user=self.user2, ip_address='127.0.0.2') 133 | 134 | SimilarUser.objects.update_recommendations() 135 | 136 | self.assertEquals(SimilarUser.objects.count(), 2) 137 | 138 | recs = list(SimilarUser.objects.get_recommendations(self.user2, RatingTestModel)) 139 | self.assertEquals(len(recs), 1) 140 | self.assertEquals(recs, [self.instance5]) 141 | 142 | self.instance5.rating.add(score=1, user=self.user2, ip_address='127.0.0.2') 143 | 144 | SimilarUser.objects.update_recommendations() 145 | 146 | self.assertEquals(SimilarUser.objects.count(), 2) 147 | 148 | recs = list(SimilarUser.objects.get_recommendations(self.user2, RatingTestModel)) 149 | self.assertEquals(len(recs), 0) -------------------------------------------------------------------------------- /djangoratings/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes.models import ContentType 2 | from django.core.exceptions import ObjectDoesNotExist 3 | from django.http import HttpResponse, Http404 4 | 5 | from exceptions import * 6 | from django.conf import settings 7 | from default_settings import RATINGS_VOTES_PER_IP 8 | 9 | class AddRatingView(object): 10 | def __call__(self, request, content_type_id, object_id, field_name, score): 11 | """__call__(request, content_type_id, object_id, field_name, score) 12 | 13 | Adds a vote to the specified model field.""" 14 | 15 | try: 16 | instance = self.get_instance(content_type_id, object_id) 17 | except ObjectDoesNotExist: 18 | raise Http404('Object does not exist') 19 | 20 | context = self.get_context(request) 21 | context['instance'] = instance 22 | 23 | try: 24 | field = getattr(instance, field_name) 25 | except AttributeError: 26 | return self.invalid_field_response(request, context) 27 | 28 | context.update({ 29 | 'field': field, 30 | 'score': score, 31 | }) 32 | 33 | had_voted = bool(field.get_rating_for_user(request.user, request.META['REMOTE_ADDR'], request.COOKIES)) 34 | 35 | context['had_voted'] = had_voted 36 | 37 | try: 38 | adds = field.add(score, request.user, request.META.get('REMOTE_ADDR'), request.COOKIES) 39 | except IPLimitReached: 40 | return self.too_many_votes_from_ip_response(request, context) 41 | except AuthRequired: 42 | return self.authentication_required_response(request, context) 43 | except InvalidRating: 44 | return self.invalid_rating_response(request, context) 45 | except CannotChangeVote: 46 | return self.cannot_change_vote_response(request, context) 47 | except CannotDeleteVote: 48 | return self.cannot_delete_vote_response(request, context) 49 | if had_voted: 50 | return self.rating_changed_response(request, context, adds) 51 | return self.rating_added_response(request, context, adds) 52 | 53 | def get_context(self, request, context={}): 54 | return context 55 | 56 | def render_to_response(self, template, context, request): 57 | raise NotImplementedError 58 | 59 | def too_many_votes_from_ip_response(self, request, context): 60 | response = HttpResponse('Too many votes from this IP address for this object.') 61 | return response 62 | 63 | def rating_changed_response(self, request, context, adds={}): 64 | response = HttpResponse('Vote changed.') 65 | if 'cookie' in adds: 66 | cookie_name, cookie = adds['cookie_name'], adds['cookie'] 67 | if 'deleted' in adds: 68 | response.delete_cookie(cookie_name) 69 | else: 70 | response.set_cookie(cookie_name, cookie, 31536000, path='/') # TODO: move cookie max_age to settings 71 | return response 72 | 73 | def rating_added_response(self, request, context, adds={}): 74 | response = HttpResponse('Vote recorded.') 75 | if 'cookie' in adds: 76 | cookie_name, cookie = adds['cookie_name'], adds['cookie'] 77 | if 'deleted' in adds: 78 | response.delete_cookie(cookie_name) 79 | else: 80 | response.set_cookie(cookie_name, cookie, 31536000, path='/') # TODO: move cookie max_age to settings 81 | return response 82 | 83 | def authentication_required_response(self, request, context): 84 | response = HttpResponse('You must be logged in to vote.') 85 | response.status_code = 403 86 | return response 87 | 88 | def cannot_change_vote_response(self, request, context): 89 | response = HttpResponse('You have already voted.') 90 | response.status_code = 403 91 | return response 92 | 93 | def cannot_delete_vote_response(self, request, context): 94 | response = HttpResponse('You can\'t delete this vote.') 95 | response.status_code = 403 96 | return response 97 | 98 | def invalid_field_response(self, request, context): 99 | response = HttpResponse('Invalid field name.') 100 | response.status_code = 403 101 | return response 102 | 103 | def invalid_rating_response(self, request, context): 104 | response = HttpResponse('Invalid rating value.') 105 | response.status_code = 403 106 | return response 107 | 108 | def get_instance(self, content_type_id, object_id): 109 | return ContentType.objects.get(pk=content_type_id)\ 110 | .get_object_for_this_type(pk=object_id) 111 | 112 | 113 | class AddRatingFromModel(AddRatingView): 114 | def __call__(self, request, model, app_label, object_id, field_name, score): 115 | """__call__(request, model, app_label, object_id, field_name, score) 116 | 117 | Adds a vote to the specified model field.""" 118 | try: 119 | content_type = ContentType.objects.get(model=model, app_label=app_label) 120 | except ContentType.DoesNotExist: 121 | raise Http404('Invalid `model` or `app_label`.') 122 | 123 | return super(AddRatingFromModel, self).__call__(request, content_type.id, 124 | object_id, field_name, score) 125 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | tests_require = [ 6 | 'django', 7 | # also requires the disqus fork of haystack 8 | ] 9 | 10 | setup( 11 | name='django-ratings', 12 | version=".".join(map(str, __import__('djangoratings').__version__)), 13 | author='David Cramer', 14 | author_email='dcramer@gmail.com', 15 | description='Generic Ratings in Django', 16 | url='http://github.com/dcramer/django-ratings', 17 | install_requires=[ 18 | 'django', 19 | ], 20 | tests_require=tests_require, 21 | extras_require={'test': tests_require}, 22 | test_suite='djangoratings.runtests.runtests', 23 | packages=find_packages(), 24 | include_package_data=True, 25 | classifiers=[ 26 | "Framework :: Django", 27 | "Intended Audience :: Developers", 28 | "Intended Audience :: System Administrators", 29 | "Operating System :: OS Independent", 30 | "Topic :: Software Development" 31 | ], 32 | ) --------------------------------------------------------------------------------