From 99a8c99ad9d6685daa5d65b92ca80999273cb66b Mon Sep 17 00:00:00 2001 From: Philipp Wassibauer Date: Thu, 14 Oct 2010 15:48:06 +0200 Subject: [PATCH 01/34] not erasing zero votes since in my system it has to remember who voted what...even if it is 0, added a timestamp --- voting/managers.py | 12 ++++-------- voting/models.py | 2 ++ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/voting/managers.py b/voting/managers.py index 3162c3b..c9a705d 100644 --- a/voting/managers.py +++ b/voting/managers.py @@ -109,15 +109,11 @@ def record_vote(self, obj, user, vote): try: v = self.get(user=user, content_type=ctype, object_id=obj._get_pk_val()) - if vote == 0: - v.delete() - else: - v.vote = vote - v.save() + v.vote = vote + v.save() except models.ObjectDoesNotExist: - if vote != 0: - self.create(user=user, content_type=ctype, - object_id=obj._get_pk_val(), vote=vote) + self.create(user=user, content_type=ctype, + object_id=obj._get_pk_val(), vote=vote) def get_top(self, Model, limit=10, reversed=False): """ diff --git a/voting/models.py b/voting/models.py index cebbd15..c26663b 100644 --- a/voting/models.py +++ b/voting/models.py @@ -4,6 +4,7 @@ from django.db import models from voting.managers import VoteManager +from datetime import datetime SCORES = ( (u'+1', +1), @@ -19,6 +20,7 @@ class Vote(models.Model): object_id = models.PositiveIntegerField() object = generic.GenericForeignKey('content_type', 'object_id') vote = models.SmallIntegerField(choices=SCORES) + time_stamp = models.DateTimeField(editable = False , default=datetime.now ) objects = VoteManager() From a10a9f4ee6db82e7ad497dba80d500fd0aea6a71 Mon Sep 17 00:00:00 2001 From: Fabian Topfstedt Date: Mon, 19 Apr 2010 03:44:49 +0800 Subject: [PATCH 02/34] vote_on_object_with_lazy_model view --- voting/views.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/voting/views.py b/voting/views.py index 352f6ad..41750c9 100644 --- a/voting/views.py +++ b/voting/views.py @@ -1,4 +1,3 @@ -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.http import Http404, HttpResponse, HttpResponseRedirect from django.contrib.auth.views import redirect_to_login @@ -97,10 +96,27 @@ def vote_on_object(request, model, direction, post_vote_redirect=None, response = HttpResponse(t.render(c)) return response + +def vote_on_object_with_lazy_model(request, app_label, model_name, *args, + **kwargs): + """ + Generic object vote view that takes app_label and model_name instead + of a model class and calls ``vote_on_object`` view. + Returns HTTP 400 (Bad Request) if there is no model matching the app_label + and model_name. + """ + model = get_model(app_label, model_name) + if not model: + return HttpResponseBadRequest('Model %s.%s does not exist' % ( + app_label, model_name)) + return vote_on_object(request, model=model, *args, **kwargs) + + def json_error_response(error_message): return HttpResponse(simplejson.dumps(dict(success=False, error_message=error_message))) + def xmlhttprequest_vote_on_object(request, model, direction, object_id=None, slug=None, slug_field=None): """ From 84e233f8877d2e024b5b748f4c1d26554d6d0bfc Mon Sep 17 00:00:00 2001 From: Fabian Topfstedt Date: Mon, 19 Apr 2010 04:00:43 +0800 Subject: [PATCH 03/34] import get_model --- voting/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/voting/views.py b/voting/views.py index 41750c9..b90913b 100644 --- a/voting/views.py +++ b/voting/views.py @@ -1,4 +1,5 @@ from django.core.exceptions import ObjectDoesNotExist +from django.db.models import get_model from django.http import Http404, HttpResponse, HttpResponseRedirect from django.contrib.auth.views import redirect_to_login from django.template import loader, RequestContext From 45a0cc9f946b32a127b63c0e06a2c8182fbd3f42 Mon Sep 17 00:00:00 2001 From: Fabian Topfstedt Date: Mon, 19 Apr 2010 04:14:48 +0800 Subject: [PATCH 04/34] voting_vote url for generic votes --- voting/urls.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 voting/urls.py diff --git a/voting/urls.py b/voting/urls.py new file mode 100644 index 0000000..2bd83b7 --- /dev/null +++ b/voting/urls.py @@ -0,0 +1,13 @@ +from django.conf.urls.defaults import * + + +urlpatterns = patterns('', + url(r"^vote/(?P[\w-]+)/(?P[\w-]+)/"\ + "(?P\d+)/(?Pup|down|clear)/$", + "voting.views.vote_on_object_with_lazy_model", { + 'allow_xmlhttprequest': True, + }, + name="voting_vote" + ), +) + From d4e835201493766ce01a5e62398988b0b400d755 Mon Sep 17 00:00:00 2001 From: Fabian Topfstedt Date: Mon, 19 Apr 2010 04:23:52 +0800 Subject: [PATCH 05/34] regex supporting dots in app_label (namespaces) --- voting/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voting/urls.py b/voting/urls.py index 2bd83b7..33c063e 100644 --- a/voting/urls.py +++ b/voting/urls.py @@ -2,10 +2,10 @@ urlpatterns = patterns('', - url(r"^vote/(?P[\w-]+)/(?P[\w-]+)/"\ + url(r"^vote/(?P[\w-\.]+)/(?P\w+)/"\ "(?P\d+)/(?Pup|down|clear)/$", "voting.views.vote_on_object_with_lazy_model", { - 'allow_xmlhttprequest': True, + "allow_xmlhttprequest": True, }, name="voting_vote" ), From f377df505fd5264ebd0f31b1ff002de48ec05f2f Mon Sep 17 00:00:00 2001 From: Fabian Topfstedt Date: Mon, 19 Apr 2010 04:29:56 +0800 Subject: [PATCH 06/34] --- voting/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voting/urls.py b/voting/urls.py index 33c063e..8133e99 100644 --- a/voting/urls.py +++ b/voting/urls.py @@ -2,7 +2,7 @@ urlpatterns = patterns('', - url(r"^vote/(?P[\w-\.]+)/(?P\w+)/"\ + url(r"^vote/(?P[\w\.-]+)/(?P\w+)/"\ "(?P\d+)/(?Pup|down|clear)/$", "voting.views.vote_on_object_with_lazy_model", { "allow_xmlhttprequest": True, From f3a599f8347c398b89a625385fd394829e5b10bd Mon Sep 17 00:00:00 2001 From: Fabian Topfstedt Date: Mon, 19 Apr 2010 04:30:53 +0800 Subject: [PATCH 07/34] import HttpResponseBadRequest --- voting/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/voting/views.py b/voting/views.py index b90913b..7e83ad9 100644 --- a/voting/views.py +++ b/voting/views.py @@ -1,6 +1,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.db.models import get_model -from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.http import Http404, HttpResponse, HttpResponseBadRequest, \ + HttpResponseRedirect from django.contrib.auth.views import redirect_to_login from django.template import loader, RequestContext from django.utils import simplejson From b506bda4fd0ef46cbd82c6551646e667da9e1646 Mon Sep 17 00:00:00 2001 From: George Hickman Date: Thu, 10 Feb 2011 17:19:10 +0000 Subject: [PATCH 08/34] set docs overview file to restructured text extension --- docs/{overview.txt => overview.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{overview.txt => overview.rst} (100%) diff --git a/docs/overview.txt b/docs/overview.rst similarity index 100% rename from docs/overview.txt rename to docs/overview.rst From eaaacc3662f52ad94ab925c755725e480f38c1b5 Mon Sep 17 00:00:00 2001 From: h3 Date: Thu, 8 Sep 2011 00:34:11 -0300 Subject: [PATCH 09/34] SCORES tuples item order was wrong .. --- voting/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voting/models.py b/voting/models.py index cebbd15..da29fdc 100644 --- a/voting/models.py +++ b/voting/models.py @@ -6,8 +6,8 @@ from voting.managers import VoteManager SCORES = ( - (u'+1', +1), - (u'-1', -1), + (+1, u'+1'), + (-1, u'-1'), ) class Vote(models.Model): From a7b6ae27a3113d23e130fe275884740842fa9a90 Mon Sep 17 00:00:00 2001 From: Philipp Wassibauer Date: Mon, 30 Jan 2012 10:00:17 +0100 Subject: [PATCH 10/34] django 1.4 timestamp fix --- voting/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/voting/models.py b/voting/models.py index c26663b..41ddf6a 100644 --- a/voting/models.py +++ b/voting/models.py @@ -2,10 +2,12 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import User from django.db import models +from django.utils.timezone import now, utc from voting.managers import VoteManager from datetime import datetime + SCORES = ( (u'+1', +1), (u'-1', -1), @@ -20,7 +22,7 @@ class Vote(models.Model): object_id = models.PositiveIntegerField() object = generic.GenericForeignKey('content_type', 'object_id') vote = models.SmallIntegerField(choices=SCORES) - time_stamp = models.DateTimeField(editable = False , default=datetime.now ) + time_stamp = models.DateTimeField(editable = False , default=now ) objects = VoteManager() From 8205625ba99a331554ce431c7694c2dfc7d36417 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Thu, 26 Jul 2012 13:02:48 +0200 Subject: [PATCH 11/34] Cosmetic changes. --- docs/overview.txt | 2 +- voting/managers.py | 32 ++++++++++++++---------------- voting/models.py | 19 +++++++++++------- voting/templatetags/voting_tags.py | 16 +++++++++++++-- voting/urls.py | 7 +++---- voting/views.py | 13 +++++++----- 6 files changed, 53 insertions(+), 36 deletions(-) diff --git a/docs/overview.txt b/docs/overview.txt index f7c97fd..ba90d0d 100644 --- a/docs/overview.txt +++ b/docs/overview.txt @@ -395,4 +395,4 @@ If no string mapping is given, ``'Up'`` and ``'Down'`` will be used. Example usage:: - {{ vote|vote_display:"Bodacious,Bogus" }} \ No newline at end of file + {{ vote|vote_display:"Bodacious,Bogus" }} diff --git a/voting/managers.py b/voting/managers.py index c9a705d..2c04c9f 100644 --- a/voting/managers.py +++ b/voting/managers.py @@ -13,24 +13,22 @@ if supports_aggregates: class CoalesceWrapper(Aggregate): sql_template = 'COALESCE(%(function)s(%(field)s), %(default)s)' - - def __init__(self, lookup, **extra): + + def __init__(self, lookup, **extra): self.lookup = lookup self.extra = extra - + def _default_alias(self): return '%s__%s' % (self.lookup, self.__class__.__name__.lower()) default_alias = property(_default_alias) - + def add_to_query(self, query, alias, col, source, is_summary): super(CoalesceWrapper, self).__init__(col, source, is_summary, **self.extra) query.aggregate_select[alias] = self - class CoalesceSum(CoalesceWrapper): sql_function = 'SUM' - class CoalesceCount(CoalesceWrapper): sql_function = 'COUNT' @@ -62,38 +60,38 @@ def get_scores_in_bulk(self, objects): object_ids = [o._get_pk_val() for o in objects] if not object_ids: return {} - + ctype = ContentType.objects.get_for_model(objects[0]) - + if supports_aggregates: queryset = self.filter( - object_id__in = object_ids, - content_type = ctype, + object_id__in=object_ids, + content_type=ctype, ).values( 'object_id', ).annotate( - score = CoalesceSum('vote', default='0'), - num_votes = CoalesceCount('vote', default='0'), + score=CoalesceSum('vote', default='0'), + num_votes=CoalesceCount('vote', default='0'), ) else: queryset = self.filter( - object_id__in = object_ids, - content_type = ctype, + object_id__in=object_ids, + content_type=ctype, ).extra( - select = { + select={ 'score': 'COALESCE(SUM(vote), 0)', 'num_votes': 'COALESCE(COUNT(vote), 0)', } ).values('object_id', 'score', 'num_votes') queryset.query.group_by.append('object_id') - + vote_dict = {} for row in queryset: vote_dict[row['object_id']] = { 'score': int(row['score']), 'num_votes': int(row['num_votes']), } - + return vote_dict def record_vote(self, obj, user, vote): diff --git a/voting/models.py b/voting/models.py index fc92b2b..eda5a94 100644 --- a/voting/models.py +++ b/voting/models.py @@ -1,11 +1,15 @@ +from datetime import datetime from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import User from django.db import models -from django.utils.timezone import now, utc + +try: + from django.utils.timezone import now +except ImportError: + now = datetime.now from voting.managers import VoteManager -from datetime import datetime SCORES = ( @@ -13,16 +17,17 @@ (-1, u'-1'), ) + class Vote(models.Model): """ A vote on an object by a User. """ - user = models.ForeignKey(User) + user = models.ForeignKey(User) content_type = models.ForeignKey(ContentType) - object_id = models.PositiveIntegerField() - object = generic.GenericForeignKey('content_type', 'object_id') - vote = models.SmallIntegerField(choices=SCORES) - time_stamp = models.DateTimeField(editable = False , default=now ) + object_id = models.PositiveIntegerField() + object = generic.GenericForeignKey('content_type', 'object_id') + vote = models.SmallIntegerField(choices=SCORES) + time_stamp = models.DateTimeField(editable=False, default=now) objects = VoteManager() diff --git a/voting/templatetags/voting_tags.py b/voting/templatetags/voting_tags.py index 7fc4dc8..89113f4 100644 --- a/voting/templatetags/voting_tags.py +++ b/voting/templatetags/voting_tags.py @@ -7,6 +7,7 @@ # Tags + class ScoreForObjectNode(template.Node): def __init__(self, object, context_var): self.object = object @@ -20,6 +21,7 @@ def render(self, context): context[self.context_var] = Vote.objects.get_score(object) return '' + class ScoresForObjectsNode(template.Node): def __init__(self, objects, context_var): self.objects = objects @@ -33,6 +35,7 @@ def render(self, context): context[self.context_var] = Vote.objects.get_scores_in_bulk(objects) return '' + class VoteByUserNode(template.Node): def __init__(self, user, object, context_var): self.user = user @@ -48,6 +51,7 @@ def render(self, context): context[self.context_var] = Vote.objects.get_for_user(object, user) return '' + class VotesByUserNode(template.Node): def __init__(self, user, objects, context_var): self.user = user @@ -63,6 +67,7 @@ def render(self, context): context[self.context_var] = Vote.objects.get_for_user_in_bulk(objects, user) return '' + class DictEntryForItemNode(template.Node): def __init__(self, item, dictionary, context_var): self.item = item @@ -78,6 +83,7 @@ def render(self, context): context[self.context_var] = dictionary.get(item.id, None) return '' + def do_score_for_object(parser, token): """ Retrieves the total score for an object and the number of votes @@ -98,6 +104,7 @@ def do_score_for_object(parser, token): raise template.TemplateSyntaxError("second argument to '%s' tag must be 'as'" % bits[0]) return ScoreForObjectNode(bits[1], bits[3]) + def do_scores_for_objects(parser, token): """ Retrieves the total scores for a list of objects and the number of @@ -114,6 +121,7 @@ def do_scores_for_objects(parser, token): raise template.TemplateSyntaxError("second argument to '%s' tag must be 'as'" % bits[0]) return ScoresForObjectsNode(bits[1], bits[3]) + def do_vote_by_user(parser, token): """ Retrieves the ``Vote`` cast by a user on a particular object and @@ -133,6 +141,7 @@ def do_vote_by_user(parser, token): raise template.TemplateSyntaxError("fourth argument to '%s' tag must be 'as'" % bits[0]) return VoteByUserNode(bits[1], bits[3], bits[5]) + def do_votes_by_user(parser, token): """ Retrieves the votes cast by a user on a list of objects as a @@ -152,6 +161,7 @@ def do_votes_by_user(parser, token): raise template.TemplateSyntaxError("fourth argument to '%s' tag must be 'as'" % bits[0]) return VotesByUserNode(bits[1], bits[3], bits[5]) + def do_dict_entry_for_item(parser, token): """ Given an object and a dictionary keyed with object ids - as @@ -179,6 +189,7 @@ def do_dict_entry_for_item(parser, token): register.tag('votes_by_user', do_votes_by_user) register.tag('dict_entry_for_item', do_dict_entry_for_item) + # Simple Tags def confirm_vote_message(object_description, vote_direction): @@ -200,6 +211,7 @@ def confirm_vote_message(object_description, vote_direction): # Filters + def vote_display(vote, arg=None): """ Given a string mapping values for up and down votes, returns one @@ -222,10 +234,10 @@ def vote_display(vote, arg=None): arg = 'Up,Down' bits = arg.split(',') if len(bits) != 2: - return vote.vote # Invalid arg + return vote.vote # Invalid arg up, down = bits if vote.vote == 1: return up return down -register.filter(vote_display) \ No newline at end of file +register.filter(vote_display) diff --git a/voting/urls.py b/voting/urls.py index 8133e99..2550a6c 100644 --- a/voting/urls.py +++ b/voting/urls.py @@ -3,11 +3,10 @@ urlpatterns = patterns('', url(r"^vote/(?P[\w\.-]+)/(?P\w+)/"\ - "(?P\d+)/(?Pup|down|clear)/$", - "voting.views.vote_on_object_with_lazy_model", { - "allow_xmlhttprequest": True, + "(?P\d+)/(?Pup|down|clear)/$", + "voting.views.vote_on_object_with_lazy_model", { + "allow_xmlhttprequest": True, }, name="voting_vote" ), ) - diff --git a/voting/views.py b/voting/views.py index 7e83ad9..3f85c95 100644 --- a/voting/views.py +++ b/voting/views.py @@ -10,6 +10,7 @@ VOTE_DIRECTIONS = (('up', 1), ('down', -1), ('clear', 0)) + def vote_on_object(request, model, direction, post_vote_redirect=None, object_id=None, slug=None, slug_field=None, template_name=None, template_loader=loader, extra_context=None, context_processors=None, @@ -40,14 +41,15 @@ def vote_on_object(request, model, direction, post_vote_redirect=None, object_id=object_id, slug=slug, slug_field=slug_field) - if extra_context is None: extra_context = {} + if extra_context is None: + extra_context = {} if not request.user.is_authenticated(): return redirect_to_login(request.path) try: vote = dict(VOTE_DIRECTIONS)[direction] except KeyError: - raise AttributeError("'%s' is not a valid vote type." % vote_type) + raise AttributeError("'%s' is not a valid vote type." % direction) # Look up the object to be voted on lookup_kwargs = {} @@ -61,12 +63,13 @@ def vote_on_object(request, model, direction, post_vote_redirect=None, try: obj = model._default_manager.get(**lookup_kwargs) except ObjectDoesNotExist: - raise Http404, 'No %s found for %s.' % (model._meta.app_label, lookup_kwargs) + raise Http404('No %s found for %s.' % + (model._meta.app_label, lookup_kwargs)) if request.method == 'POST': if post_vote_redirect is not None: next = post_vote_redirect - elif request.REQUEST.has_key('next'): + elif 'next' in request.REQUEST: next = request.REQUEST['next'] elif hasattr(obj, 'get_absolute_url'): if callable(getattr(obj, 'get_absolute_url')): @@ -120,7 +123,7 @@ def json_error_response(error_message): def xmlhttprequest_vote_on_object(request, model, direction, - object_id=None, slug=None, slug_field=None): + object_id=None, slug=None, slug_field=None): """ Generic object vote function for use via XMLHttpRequest. From 05fc7d8a08462f4f05922bd958f12a28e5f98221 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Thu, 26 Jul 2012 14:38:55 +0200 Subject: [PATCH 12/34] Added VOTING_ZERO_VOTES_ALLOWED setting to decide whether votes with a zero value can be created. --- voting/managers.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/voting/managers.py b/voting/managers.py index 2c04c9f..f08913c 100644 --- a/voting/managers.py +++ b/voting/managers.py @@ -10,6 +10,8 @@ from django.contrib.contenttypes.models import ContentType +ZERO_VOTES_ALLOWED = getattr(settings, 'VOTING_ZERO_VOTES_ALLOWED', False) + if supports_aggregates: class CoalesceWrapper(Aggregate): sql_template = 'COALESCE(%(function)s(%(field)s), %(default)s)' @@ -107,9 +109,14 @@ def record_vote(self, obj, user, vote): try: v = self.get(user=user, content_type=ctype, object_id=obj._get_pk_val()) - v.vote = vote - v.save() + if vote == 0 and not ZERO_VOTES_ALLOWED: + v.delete() + else: + v.vote = vote + v.save() except models.ObjectDoesNotExist: + if not ZERO_VOTES_ALLOWED and vote == 0: + return self.create(user=user, content_type=ctype, object_id=obj._get_pk_val(), vote=vote) From c853ab5b777b04bf3f085298acbf611727135148 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Thu, 26 Jul 2012 14:39:31 +0200 Subject: [PATCH 13/34] Added South migrations. --- setup.py | 2 +- voting/migrations/0001_initial.py | 80 +++++++++++++++++++ .../0002_auto__add_field_vote_time_stamp.py | 70 ++++++++++++++++ voting/migrations/__init__.py | 0 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 voting/migrations/0001_initial.py create mode 100644 voting/migrations/0002_auto__add_field_vote_time_stamp.py create mode 100644 voting/migrations/__init__.py diff --git a/setup.py b/setup.py index b61a615..28ac03a 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ author = 'Jonathan Buchanan', author_email = 'jonathan.buchanan@gmail.com', url = 'http://code.google.com/p/django-voting/', - packages = ['voting', 'voting.templatetags', 'voting.tests'], + packages = ['voting', 'voting.templatetags', 'voting.migrations', 'voting.tests'], classifiers = ['Development Status :: 4 - Beta', 'Environment :: Web Environment', 'Framework :: Django', diff --git a/voting/migrations/0001_initial.py b/voting/migrations/0001_initial.py new file mode 100644 index 0000000..0529598 --- /dev/null +++ b/voting/migrations/0001_initial.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Vote' + db.create_table('votes', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])), + ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), + ('vote', self.gf('django.db.models.fields.SmallIntegerField')()), + )) + db.send_create_signal('voting', ['Vote']) + + # Adding unique constraint on 'Vote', fields ['user', 'content_type', 'object_id'] + db.create_unique('votes', ['user_id', 'content_type_id', 'object_id']) + + + def backwards(self, orm): + # Removing unique constraint on 'Vote', fields ['user', 'content_type', 'object_id'] + db.delete_unique('votes', ['user_id', 'content_type_id', 'object_id']) + + # Deleting model 'Vote' + db.delete_table('votes') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'voting.vote': { + 'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'Vote', 'db_table': "'votes'"}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'vote': ('django.db.models.fields.SmallIntegerField', [], {}) + } + } + + complete_apps = ['voting'] \ No newline at end of file diff --git a/voting/migrations/0002_auto__add_field_vote_time_stamp.py b/voting/migrations/0002_auto__add_field_vote_time_stamp.py new file mode 100644 index 0000000..9e52305 --- /dev/null +++ b/voting/migrations/0002_auto__add_field_vote_time_stamp.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Vote.time_stamp' + db.add_column('votes', 'time_stamp', + self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Vote.time_stamp' + db.delete_column('votes', 'time_stamp') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'voting.vote': { + 'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'Vote', 'db_table': "'votes'"}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'time_stamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'vote': ('django.db.models.fields.SmallIntegerField', [], {}) + } + } + + complete_apps = ['voting'] \ No newline at end of file diff --git a/voting/migrations/__init__.py b/voting/migrations/__init__.py new file mode 100644 index 0000000..e69de29 From 9e2d62a876456b15614b0e324c75b8d55bd482a3 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Thu, 26 Jul 2012 14:42:39 +0200 Subject: [PATCH 14/34] Updated license, changelog and version. --- .gitignore | 1 + CHANGELOG.txt | 11 +++++++++-- LICENSE.txt | 1 + setup.py | 48 ++++++++++++++++++++++++---------------------- voting/__init__.py | 2 +- 5 files changed, 37 insertions(+), 26 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a66c47a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.egg-info \ No newline at end of file diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 14cc54b..7e9f75d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,10 @@ -======================= Django Voting Changelog -======================= \ No newline at end of file +======================= + +0.2 +--- + +* Django 1.4 compatibility (timezone support) +* Added a ``time_stamp`` field to ``Vote`` model +* Added South migrations. +* \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt index 169aee3..d55a81a 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -2,6 +2,7 @@ django-voting ------------- Copyright (c) 2007, Jonathan Buchanan +Copyright (c) 2012, Jannis Leidel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/setup.py b/setup.py index 28ac03a..279e0c6 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,4 @@ from distutils.core import setup -from distutils.command.install import INSTALL_SCHEMES - -# Tell distutils to put the data_files in platform-specific installation -# locations. See here for an explanation: -# http://groups.google.com/group/comp.lang.python/browse_thread/thread/35ec7b2fed36eaec/2105ee4d9e8042cb -for scheme in INSTALL_SCHEMES.values(): - scheme['data'] = scheme['purelib'] # Dynamically calculate the version based on tagging.VERSION. version_tuple = __import__('voting').VERSION @@ -15,19 +8,28 @@ version = "%d.%d" % version_tuple[:2] setup( - name = 'django-voting', - version = version, - description = 'Generic voting application for Django', - author = 'Jonathan Buchanan', - author_email = 'jonathan.buchanan@gmail.com', - url = 'http://code.google.com/p/django-voting/', - packages = ['voting', 'voting.templatetags', 'voting.migrations', 'voting.tests'], - classifiers = ['Development Status :: 4 - Beta', - 'Environment :: Web Environment', - 'Framework :: Django', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Topic :: Utilities'], -) \ No newline at end of file + name='django-voting', + version=version, + description='Generic voting application for Django', + author='Jonathan Buchanan', + author_email='jonathan.buchanan@gmail.com', + maintainer='Jannis Leidel', + maintainer_email='jannis@leidel.info', + url='http://github.com/jezdez/django-voting/', + packages=[ + 'voting', + 'voting.migrations', + 'voting.templatetags', + 'voting.tests', + ], + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Web Environment', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Utilities', + ], +) diff --git a/voting/__init__.py b/voting/__init__.py index 449f64a..6b515fe 100644 --- a/voting/__init__.py +++ b/voting/__init__.py @@ -1 +1 @@ -VERSION = (0, 1, None) \ No newline at end of file +VERSION = (0, 2, None) From 43df2605bd3f014b2055c5f3ef60b0edb440269a Mon Sep 17 00:00:00 2001 From: kaleissin Date: Sat, 30 Nov 2013 23:07:09 +0100 Subject: [PATCH 15/34] Ignore *.pyc --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a66c47a..6188efd 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -*.egg-info \ No newline at end of file +*.egg-info +*.pyc +build From ee2f19c5349c207d25a721932b35001b9160439e Mon Sep 17 00:00:00 2001 From: kaleissin Date: Thu, 5 Dec 2013 22:38:26 +0100 Subject: [PATCH 16/34] Switch to unittests and test with tox --- .gitignore | 1 + tox.ini | 52 ++++++++++ voting/tests/runtests.py | 8 -- voting/tests/settings.py | 23 ++--- voting/tests/tests.py | 218 +++++++++++++++++++++++++-------------- 5 files changed, 200 insertions(+), 102 deletions(-) create mode 100644 tox.ini delete mode 100644 voting/tests/runtests.py diff --git a/.gitignore b/.gitignore index 6188efd..108a46c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.egg-info *.pyc build +.tox diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..00016e2 --- /dev/null +++ b/tox.ini @@ -0,0 +1,52 @@ +[tox] +envlist = + py26-django1.4, + py26-django1.5, + py26-django1.6, + py27-django1.4, + py27-django1.5, + py27-django1.6 + +[testenv] +commands = + django-admin.py test voting --settings=voting.tests.settings + +[testenv:py26-django1.4] +basepython = python2.6 +deps = + Django>=1.4.2,<1.5 + +[testenv:py26-django1.5] +basepython = python2.6 +deps = + Django>=1.5,<1.6 + +[testenv:py26-django1.6] +basepython = python2.6 +deps = + Django>=1.6 + +[testenv:py27-django1.4] +basepython = python2.7 +deps = + Django>=1.4.2,<1.5 + +[testenv:py27-django1.5] +basepython = python2.7 +deps = + Django>=1.5,<1.6 + +[testenv:py27-django1.6] +basepython = python2.7 +deps = + Django>=1.6 + +[testenv:py33-django1.5] +basepython = python3.3 +deps = + Django>=1.5,<1.6 + +[testenv:py33-django1.6] +basepython = python3.3 +deps = + Django>=1.6 diff --git a/voting/tests/runtests.py b/voting/tests/runtests.py deleted file mode 100644 index bc1ba37..0000000 --- a/voting/tests/runtests.py +++ /dev/null @@ -1,8 +0,0 @@ -import os, sys -os.environ['DJANGO_SETTINGS_MODULE'] = 'voting.tests.settings' - -from django.test.simple import run_tests - -failures = run_tests(None, verbosity=9) -if failures: - sys.exit(failures) diff --git a/voting/tests/settings.py b/voting/tests/settings.py index 8d2506e..2a2fb2d 100644 --- a/voting/tests/settings.py +++ b/voting/tests/settings.py @@ -2,24 +2,17 @@ DIRNAME = os.path.dirname(__file__) -DATABASE_ENGINE = 'sqlite3' -DATABASE_NAME = os.path.join(DIRNAME, 'database.db') +DATABASES = { + 'default': { + 'ENGINE':'django.db.backends.sqlite3', + 'NAME': ':memory:' + } +} -#DATABASE_ENGINE = 'mysql' -#DATABASE_NAME = 'tagging_test' -#DATABASE_USER = 'root' -#DATABASE_PASSWORD = '' -#DATABASE_HOST = 'localhost' -#DATABASE_PORT = '3306' - -#DATABASE_ENGINE = 'postgresql_psycopg2' -#DATABASE_NAME = 'tagging_test' -#DATABASE_USER = 'postgres' -#DATABASE_PASSWORD = '' -#DATABASE_HOST = 'localhost' -#DATABASE_PORT = '5432' +SECRET_KEY = 'foo' INSTALLED_APPS = ( + 'django.contrib.sessions', 'django.contrib.auth', 'django.contrib.contenttypes', 'voting', diff --git a/voting/tests/tests.py b/voting/tests/tests.py index 25bb5a8..a34475b 100644 --- a/voting/tests/tests.py +++ b/voting/tests/tests.py @@ -1,86 +1,146 @@ -r""" ->>> from django.contrib.auth.models import User ->>> from voting.models import Vote ->>> from voting.tests.models import Item +from django.contrib.auth.models import User +from django.test import TestCase -########## -# Voting # -########## +from voting.models import Vote +from voting.tests.models import Item # Basic voting ############################################################### ->>> i1 = Item.objects.create(name='test1') ->>> users = [] ->>> for username in ['u1', 'u2', 'u3', 'u4']: -... users.append(User.objects.create_user(username, '%s@test.com' % username, 'test')) ->>> Vote.objects.get_score(i1) -{'score': 0, 'num_votes': 0} ->>> Vote.objects.record_vote(i1, users[0], +1) ->>> Vote.objects.get_score(i1) -{'score': 1, 'num_votes': 1} ->>> Vote.objects.record_vote(i1, users[0], -1) ->>> Vote.objects.get_score(i1) -{'score': -1, 'num_votes': 1} ->>> Vote.objects.record_vote(i1, users[0], 0) ->>> Vote.objects.get_score(i1) -{'score': 0, 'num_votes': 0} ->>> for user in users: -... Vote.objects.record_vote(i1, user, +1) ->>> Vote.objects.get_score(i1) -{'score': 4, 'num_votes': 4} ->>> for user in users[:2]: -... Vote.objects.record_vote(i1, user, 0) ->>> Vote.objects.get_score(i1) -{'score': 2, 'num_votes': 2} ->>> for user in users[:2]: -... Vote.objects.record_vote(i1, user, -1) ->>> Vote.objects.get_score(i1) -{'score': 0, 'num_votes': 4} - ->>> Vote.objects.record_vote(i1, user, -2) -Traceback (most recent call last): - ... -ValueError: Invalid vote (must be +1/0/-1) +class BasicVotingTests(TestCase): + + def setUp(self): + self.item = Item.objects.create(name='test1') + self.users = [] + for username in ['u1', 'u2', 'u3', 'u4']: + self.users.append(User.objects.create_user(username, '%s@test.com' % username, 'test')) + + def test_novotes(self): + result = Vote.objects.get_score(self.item) + self.assertEqual(result, {'score': 0, 'num_votes': 0}) + + def test_onevoteplus(self): + Vote.objects.record_vote(self.item, self.users[0], +1) + result = Vote.objects.get_score(self.item) + self.assertEqual(result, {'score': 1, 'num_votes': 1}) + + def test_onevoteminus(self): + Vote.objects.record_vote(self.item, self.users[0], -1) + result = Vote.objects.get_score(self.item) + self.assertEqual(result, {'score': -1, 'num_votes': 1}) + + def test_onevotezero(self): + Vote.objects.record_vote(self.item, self.users[0], 0) + result = Vote.objects.get_score(self.item) + self.assertEqual(result, {'score': 0, 'num_votes': 0}) + + def test_allvoteplus(self): + for user in self.users: + Vote.objects.record_vote(self.item, user, +1) + result = Vote.objects.get_score(self.item) + self.assertEqual(result, {'score': 4, 'num_votes': 4}) + for user in self.users[:2]: + Vote.objects.record_vote(self.item, user, 0) + result = Vote.objects.get_score(self.item) + self.assertEqual(result, {'score': 2, 'num_votes': 2}) + for user in self.users[:2]: + Vote.objects.record_vote(self.item, user, -1) + result = Vote.objects.get_score(self.item) + self.assertEqual(result, {'score': 0, 'num_votes': 4}) + + def test_wrongvote(self): + try: + Vote.objects.record_vote(self.item, self.users[0], -2) + except ValueError as e: + self.assertEqual(e.args[0], "Invalid vote (must be +1/0/-1)") + else: + self.fail("Did nor raise 'ValueError: Invalid vote (must be +1/0/-1)'") # Retrieval of votes ######################################################### ->>> i2 = Item.objects.create(name='test2') ->>> i3 = Item.objects.create(name='test3') ->>> i4 = Item.objects.create(name='test4') ->>> Vote.objects.record_vote(i2, users[0], +1) ->>> Vote.objects.record_vote(i3, users[0], -1) ->>> Vote.objects.record_vote(i4, users[0], 0) ->>> vote = Vote.objects.get_for_user(i2, users[0]) ->>> (vote.vote, vote.is_upvote(), vote.is_downvote()) -(1, True, False) ->>> vote = Vote.objects.get_for_user(i3, users[0]) ->>> (vote.vote, vote.is_upvote(), vote.is_downvote()) -(-1, False, True) ->>> Vote.objects.get_for_user(i4, users[0]) is None -True - -# In bulk ->>> votes = Vote.objects.get_for_user_in_bulk([i1, i2, i3, i4], users[0]) ->>> [(id, vote.vote) for id, vote in votes.items()] -[(1, -1), (2, 1), (3, -1)] ->>> Vote.objects.get_for_user_in_bulk([], users[0]) -{} - ->>> for user in users[1:]: -... Vote.objects.record_vote(i2, user, +1) -... Vote.objects.record_vote(i3, user, +1) -... Vote.objects.record_vote(i4, user, +1) ->>> list(Vote.objects.get_top(Item)) -[(, 4), (, 3), (, 2)] ->>> for user in users[1:]: -... Vote.objects.record_vote(i2, user, -1) -... Vote.objects.record_vote(i3, user, -1) -... Vote.objects.record_vote(i4, user, -1) ->>> list(Vote.objects.get_bottom(Item)) -[(, -4), (, -3), (, -2)] - ->>> Vote.objects.get_scores_in_bulk([i1, i2, i3, i4]) -{1: {'score': 0, 'num_votes': 4}, 2: {'score': -2, 'num_votes': 4}, 3: {'score': -4, 'num_votes': 4}, 4: {'score': -3, 'num_votes': 3}} ->>> Vote.objects.get_scores_in_bulk([]) -{} -""" +class VoteRetrievalTests(TestCase): + def setUp(self): + self.items = [] + for name in ['test1', 'test2', 'test3', 'test4']: + self.items.append(Item.objects.create(name=name)) + self.users = [] + for username in ['u1', 'u2', 'u3', 'u4']: + self.users.append(User.objects.create_user(username, '%s@test.com' % username, 'test')) + for user in self.users: + Vote.objects.record_vote(self.items[0], user, +1) + for user in self.users[:2]: + Vote.objects.record_vote(self.items[0], user, 0) + for user in self.users[:2]: + Vote.objects.record_vote(self.items[0], user, -1) + Vote.objects.record_vote(self.items[1], self.users[0], +1) + Vote.objects.record_vote(self.items[2], self.users[0], -1) + Vote.objects.record_vote(self.items[3], self.users[0], 0) + + def test_get_pos_vote(self): + vote = Vote.objects.get_for_user(self.items[1], self.users[0]) + result = (vote.vote, vote.is_upvote(), vote.is_downvote()) + expected = (1, True, False) + self.assertEqual(result, expected) + + def test_get_neg_vote(self): + vote = Vote.objects.get_for_user(self.items[2], self.users[0]) + result = (vote.vote, vote.is_upvote(), vote.is_downvote()) + expected = (-1, False, True) + self.assertEqual(result, expected) + + def test_get_zero_vote(self): + self.assertTrue(Vote.objects.get_for_user(self.items[3], self.users[0]) is None) + + def test_in_bulk1(self): + votes = Vote.objects.get_for_user_in_bulk(self.items, + self.users[0]) + self.assertEqual( + [(id, vote.vote) for id, vote in votes.items()], + [(1, -1), (2, 1), (3, -1)]) + + def test_empty_items(self): + result = Vote.objects.get_for_user_in_bulk([], self.users[0]) + self.assertEqual(result, {}) + + def test_get_top(self): + for user in self.users[1:]: + Vote.objects.record_vote(self.items[1], user, +1) + Vote.objects.record_vote(self.items[2], user, +1) + Vote.objects.record_vote(self.items[3], user, +1) + result = list(Vote.objects.get_top(Item)) + expected = [(self.items[1], 4), (self.items[3], 3), (self.items[2], 2)] + self.assertEqual(result, expected) + + def test_get_bottom(self): + for user in self.users[1:]: + Vote.objects.record_vote(self.items[1], user, +1) + Vote.objects.record_vote(self.items[2], user, +1) + Vote.objects.record_vote(self.items[3], user, +1) + for user in self.users[1:]: + Vote.objects.record_vote(self.items[1], user, -1) + Vote.objects.record_vote(self.items[2], user, -1) + Vote.objects.record_vote(self.items[3], user, -1) + result = list(Vote.objects.get_bottom(Item)) + expected = [(self.items[2], -4), (self.items[3], -3), (self.items[1], -2)] + self.assertEqual(result, expected) + + def test_get_scores_in_bulk(self): + for user in self.users[1:]: + Vote.objects.record_vote(self.items[1], user, +1) + Vote.objects.record_vote(self.items[2], user, +1) + Vote.objects.record_vote(self.items[3], user, +1) + for user in self.users[1:]: + Vote.objects.record_vote(self.items[1], user, -1) + Vote.objects.record_vote(self.items[2], user, -1) + Vote.objects.record_vote(self.items[3], user, -1) + result = Vote.objects.get_scores_in_bulk(self.items) + expected = { + 1: {'score': 0, 'num_votes': 4}, + 2: {'score': -2, 'num_votes': 4}, + 3: {'score': -4, 'num_votes': 4}, + 4: {'score': -3, 'num_votes': 3}, + } + self.assertEqual(result, expected) + + def test_get_scores_in_bulk_no_items(self): + result = Vote.objects.get_scores_in_bulk([]) + self.assertEqual(result, {}) From f6aace7d236309d2d80ef95c126696f6314d6048 Mon Sep 17 00:00:00 2001 From: kaleissin Date: Thu, 5 Dec 2013 22:46:32 +0100 Subject: [PATCH 17/34] Use aggregate() and annotate() instead of raw sql --- voting/managers.py | 121 +++++++++++---------------------------------- 1 file changed, 28 insertions(+), 93 deletions(-) diff --git a/voting/managers.py b/voting/managers.py index f08913c..ed2efce 100644 --- a/voting/managers.py +++ b/voting/managers.py @@ -1,40 +1,10 @@ from django.conf import settings -from django.db import connection, models - -try: - from django.db.models.sql.aggregates import Aggregate -except ImportError: - supports_aggregates = False -else: - supports_aggregates = True - +from django.db import models +from django.db.models import Sum, Count from django.contrib.contenttypes.models import ContentType ZERO_VOTES_ALLOWED = getattr(settings, 'VOTING_ZERO_VOTES_ALLOWED', False) -if supports_aggregates: - class CoalesceWrapper(Aggregate): - sql_template = 'COALESCE(%(function)s(%(field)s), %(default)s)' - - def __init__(self, lookup, **extra): - self.lookup = lookup - self.extra = extra - - def _default_alias(self): - return '%s__%s' % (self.lookup, self.__class__.__name__.lower()) - default_alias = property(_default_alias) - - def add_to_query(self, query, alias, col, source, is_summary): - super(CoalesceWrapper, self).__init__(col, source, is_summary, **self.extra) - query.aggregate_select[alias] = self - - class CoalesceSum(CoalesceWrapper): - sql_function = 'SUM' - - class CoalesceCount(CoalesceWrapper): - sql_function = 'COUNT' - - class VoteManager(models.Manager): def get_score(self, obj): """ @@ -42,17 +12,13 @@ def get_score(self, obj): the number of votes it's received. """ ctype = ContentType.objects.get_for_model(obj) - result = self.filter(object_id=obj._get_pk_val(), - content_type=ctype).extra( - select={ - 'score': 'COALESCE(SUM(vote), 0)', - 'num_votes': 'COALESCE(COUNT(vote), 0)', - }).values_list('score', 'num_votes')[0] - - return { - 'score': int(result[0]), - 'num_votes': int(result[1]), - } + result = (self.filter(object_id=obj._get_pk_val(), content_type=ctype) + .aggregate(score=Sum('vote'), num_votes=Count('vote')) + ) + + if result['score'] is None: + result['score'] = 0 + return result def get_scores_in_bulk(self, objects): """ @@ -65,27 +31,15 @@ def get_scores_in_bulk(self, objects): ctype = ContentType.objects.get_for_model(objects[0]) - if supports_aggregates: - queryset = self.filter( - object_id__in=object_ids, - content_type=ctype, - ).values( - 'object_id', - ).annotate( - score=CoalesceSum('vote', default='0'), - num_votes=CoalesceCount('vote', default='0'), - ) - else: - queryset = self.filter( - object_id__in=object_ids, - content_type=ctype, - ).extra( - select={ - 'score': 'COALESCE(SUM(vote), 0)', - 'num_votes': 'COALESCE(COUNT(vote), 0)', - } - ).values('object_id', 'score', 'num_votes') - queryset.query.group_by.append('object_id') + queryset = self.filter( + object_id__in=object_ids, + content_type=ctype, + ).values( + 'object_id', + ).annotate( + score=Sum('vote'), + num_votes=Count('vote') + ) vote_dict = {} for row in queryset: @@ -120,47 +74,28 @@ def record_vote(self, obj, user, vote): self.create(user=user, content_type=ctype, object_id=obj._get_pk_val(), vote=vote) - def get_top(self, Model, limit=10, reversed=False): + def get_top(self, model, limit=10, reversed=False): """ Get the top N scored objects for a given model. Yields (object, score) tuples. """ - ctype = ContentType.objects.get_for_model(Model) - query = """ - SELECT object_id, SUM(vote) as %s - FROM %s - WHERE content_type_id = %%s - GROUP BY object_id""" % ( - connection.ops.quote_name('score'), - connection.ops.quote_name(self.model._meta.db_table), - ) - - # MySQL has issues with re-using the aggregate function in the - # HAVING clause, so we alias the score and use this alias for - # its benefit. - if settings.DATABASE_ENGINE == 'mysql': - having_score = connection.ops.quote_name('score') - else: - having_score = 'SUM(vote)' + ctype = ContentType.objects.get_for_model(model) + results = self.filter(content_type=ctype).values('object_id').annotate(score=Sum('vote')) if reversed: - having_sql = ' HAVING %(having_score)s < 0 ORDER BY %(having_score)s ASC LIMIT %%s' + results = results.order_by('score') else: - having_sql = ' HAVING %(having_score)s > 0 ORDER BY %(having_score)s DESC LIMIT %%s' - query += having_sql % { - 'having_score': having_score, - } - - cursor = connection.cursor() - cursor.execute(query, [ctype.id, limit]) - results = cursor.fetchall() + results = results.order_by('-score') # Use in_bulk() to avoid O(limit) db hits. - objects = Model.objects.in_bulk([id for id, score in results]) + objects = model.objects.in_bulk([item['object_id'] for item in results[:limit]]) # Yield each object, score pair. Because of the lazy nature of generic # relations, missing objects are silently ignored. - for id, score in results: + for item in results[:limit]: + id, score = item['object_id'], item['score'] + if not score: + continue if id in objects: yield objects[id], int(score) From ebbc1409a214fbf4c3cfeaed0f825c288b34896d Mon Sep 17 00:00:00 2001 From: kaleissin Date: Fri, 6 Dec 2013 20:05:38 +0100 Subject: [PATCH 18/34] Prettified get_score() --- voting/managers.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/voting/managers.py b/voting/managers.py index ed2efce..3f4ec2e 100644 --- a/voting/managers.py +++ b/voting/managers.py @@ -12,8 +12,12 @@ def get_score(self, obj): the number of votes it's received. """ ctype = ContentType.objects.get_for_model(obj) - result = (self.filter(object_id=obj._get_pk_val(), content_type=ctype) - .aggregate(score=Sum('vote'), num_votes=Count('vote')) + result = self.filter( + object_id=obj._get_pk_val(), + content_type=ctype + ).aggregate( + score=Sum('vote'), + num_votes=Count('vote') ) if result['score'] is None: From 5809fc832340e3ee5d798fa347e4933e874bdb8b Mon Sep 17 00:00:00 2001 From: kaleissin Date: Thu, 17 Apr 2014 23:31:45 +0200 Subject: [PATCH 19/34] Allow older django to find the tests --- voting/tests/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/voting/tests/__init__.py b/voting/tests/__init__.py index e69de29..25d4e24 100644 --- a/voting/tests/__init__.py +++ b/voting/tests/__init__.py @@ -0,0 +1,3 @@ +import django +if django.VERSION[0] == 1 and django.VERSION[1] < 6: + from .tests import * From 66fc2ad88e3fbfaa09f95cfff0aab6e9ca3009b7 Mon Sep 17 00:00:00 2001 From: kaleissin Date: Thu, 17 Apr 2014 23:35:35 +0200 Subject: [PATCH 20/34] Make tox work with Django>=1.6 --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 00016e2..f061ad1 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,8 @@ envlist = py27-django1.6 [testenv] +setenv = + PYTHONPATH = {toxinidir} commands = django-admin.py test voting --settings=voting.tests.settings From 1a1c6b725ffd5e8359d6368060bd2949f5cdd2c1 Mon Sep 17 00:00:00 2001 From: kaleissin Date: Thu, 17 Apr 2014 23:38:13 +0200 Subject: [PATCH 21/34] Django 1.7 will soon be out --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index f061ad1..3b4e274 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ deps = [testenv:py26-django1.6] basepython = python2.6 deps = - Django>=1.6 + Django>=1.6,<1.7 [testenv:py27-django1.4] basepython = python2.7 @@ -41,7 +41,7 @@ deps = [testenv:py27-django1.6] basepython = python2.7 deps = - Django>=1.6 + Django>=1.6,<1.7 [testenv:py33-django1.5] basepython = python3.3 @@ -51,4 +51,4 @@ deps = [testenv:py33-django1.6] basepython = python3.3 deps = - Django>=1.6 + Django>=1.6,<1.7 From 6aa56cd7ccdcd63c4a71671d16f7b0ffd58f44a5 Mon Sep 17 00:00:00 2001 From: kaleissin Date: Thu, 17 Apr 2014 23:39:23 +0200 Subject: [PATCH 22/34] Turn on testing for python 3.3 3.4 will have to wait due to a bug in virtualenv that should be fixed in a release higher than 1.11.4. --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3b4e274..75469b6 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,9 @@ envlist = py26-django1.6, py27-django1.4, py27-django1.5, - py27-django1.6 + py27-django1.6, + py33-django1.5, + py33-django1.6 [testenv] setenv = From 05d5a6a98e2ef4f8f820ff28089ee76934b039d0 Mon Sep 17 00:00:00 2001 From: kaleissin Date: Fri, 18 Apr 2014 00:08:27 +0200 Subject: [PATCH 23/34] Test-coverage --- .gitignore | 3 +++ tox.ini | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 108a46c..4e63254 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ *.egg-info *.pyc build +htmlcov +MANIFEST .tox +.coverage diff --git a/tox.ini b/tox.ini index 75469b6..9095e09 100644 --- a/tox.ini +++ b/tox.ini @@ -7,13 +7,23 @@ envlist = py27-django1.5, py27-django1.6, py33-django1.5, - py33-django1.6 + py33-django1.6, + coverage [testenv] setenv = + DJANGO_SETTINGS_MODULE = voting.tests.settings PYTHONPATH = {toxinidir} commands = - django-admin.py test voting --settings=voting.tests.settings + django-admin.py test voting + +[testenv:coverage] +basepython = python2.7 +commands = + coverage run --branch --omit=.tox/*,*/tests/*.py,*/migrations/*.py {envbindir}/django-admin.py test voting +deps = + coverage>=3.7, + Django>=1.6,<1.7 [testenv:py26-django1.4] basepython = python2.6 From 55fa073f4e592c5ef803df6382e9c42d40d9b477 Mon Sep 17 00:00:00 2001 From: kaleissin Date: Fri, 18 Apr 2014 00:19:15 +0200 Subject: [PATCH 24/34] Check that unicode(Vote) works for python < 3 This will have to change for python 3.x. Also pushes coverage above 90% for this file :) --- voting/tests/tests.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/voting/tests/tests.py b/voting/tests/tests.py index a34475b..72c2a30 100644 --- a/voting/tests/tests.py +++ b/voting/tests/tests.py @@ -14,6 +14,12 @@ def setUp(self): for username in ['u1', 'u2', 'u3', 'u4']: self.users.append(User.objects.create_user(username, '%s@test.com' % username, 'test')) + def test_print_model(self): + Vote.objects.record_vote(self.item, self.users[0], +1) + expected = 'u1: 1 on test1' + result = Vote.objects.all()[0] + self.assertEqual(unicode(result), expected) + def test_novotes(self): result = Vote.objects.get_score(self.item) self.assertEqual(result, {'score': 0, 'num_votes': 0}) From 358f2c8e6d928036473382647cc691aac35aebaf Mon Sep 17 00:00:00 2001 From: kaleissin Date: Fri, 18 Apr 2014 00:25:26 +0200 Subject: [PATCH 25/34] print(vote) now works on both python2.x and 3.x --- voting/models.py | 12 +++++++++--- voting/tests/tests.py | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/voting/models.py b/voting/models.py index eda5a94..abb6d56 100644 --- a/voting/models.py +++ b/voting/models.py @@ -1,4 +1,10 @@ +# coding: utf-8 + +from __future__ import unicode_literals + from datetime import datetime + +from django.utils.encoding import python_2_unicode_compatible from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import User @@ -17,7 +23,7 @@ (-1, u'-1'), ) - +@python_2_unicode_compatible class Vote(models.Model): """ A vote on an object by a User. @@ -36,8 +42,8 @@ class Meta: # One vote per user per object unique_together = (('user', 'content_type', 'object_id'),) - def __unicode__(self): - return u'%s: %s on %s' % (self.user, self.vote, self.object) + def __str__(self): + return '%s: %s on %s' % (self.user, self.vote, self.object) def is_upvote(self): return self.vote == 1 diff --git a/voting/tests/tests.py b/voting/tests/tests.py index 72c2a30..aea9472 100644 --- a/voting/tests/tests.py +++ b/voting/tests/tests.py @@ -18,7 +18,7 @@ def test_print_model(self): Vote.objects.record_vote(self.item, self.users[0], +1) expected = 'u1: 1 on test1' result = Vote.objects.all()[0] - self.assertEqual(unicode(result), expected) + self.assertEqual(str(result), expected) def test_novotes(self): result = Vote.objects.get_score(self.item) From 53fdb4e23bfc743250744518c1fe2a86b29f6bb2 Mon Sep 17 00:00:00 2001 From: kaleissin Date: Fri, 18 Apr 2014 00:44:31 +0200 Subject: [PATCH 26/34] Passes 2to3 All strings now unicode (luckily no bytestrings used) --- voting/managers.py | 4 ++++ voting/models.py | 4 ++-- voting/templatetags/voting_tags.py | 4 ++++ voting/tests/settings.py | 4 ++++ voting/tests/tests.py | 6 +++++- voting/urls.py | 4 ++++ voting/views.py | 4 ++++ 7 files changed, 27 insertions(+), 3 deletions(-) diff --git a/voting/managers.py b/voting/managers.py index 3f4ec2e..23e6d22 100644 --- a/voting/managers.py +++ b/voting/managers.py @@ -1,3 +1,7 @@ +# coding: utf-8 + +from __future__ import unicode_literals + from django.conf import settings from django.db import models from django.db.models import Sum, Count diff --git a/voting/models.py b/voting/models.py index abb6d56..6b76cf2 100644 --- a/voting/models.py +++ b/voting/models.py @@ -19,8 +19,8 @@ SCORES = ( - (+1, u'+1'), - (-1, u'-1'), + (+1, '+1'), + (-1, '-1'), ) @python_2_unicode_compatible diff --git a/voting/templatetags/voting_tags.py b/voting/templatetags/voting_tags.py index 89113f4..b1c1aad 100644 --- a/voting/templatetags/voting_tags.py +++ b/voting/templatetags/voting_tags.py @@ -1,3 +1,7 @@ +# coding: utf-8 + +from __future__ import unicode_literals + from django import template from django.utils.html import escape diff --git a/voting/tests/settings.py b/voting/tests/settings.py index 2a2fb2d..bc5c899 100644 --- a/voting/tests/settings.py +++ b/voting/tests/settings.py @@ -1,3 +1,7 @@ +# coding: utf-8 + +from __future__ import unicode_literals + import os DIRNAME = os.path.dirname(__file__) diff --git a/voting/tests/tests.py b/voting/tests/tests.py index aea9472..01d049a 100644 --- a/voting/tests/tests.py +++ b/voting/tests/tests.py @@ -1,4 +1,8 @@ -from django.contrib.auth.models import User +# coding: utf-8 + +from __future__ import unicode_literals + +from django.contrib.auth.models import User from django.test import TestCase from voting.models import Vote diff --git a/voting/urls.py b/voting/urls.py index 2550a6c..0d1584f 100644 --- a/voting/urls.py +++ b/voting/urls.py @@ -1,3 +1,7 @@ +# coding: utf-8 + +from __future__ import unicode_literals + from django.conf.urls.defaults import * diff --git a/voting/views.py b/voting/views.py index 3f85c95..33ae4b0 100644 --- a/voting/views.py +++ b/voting/views.py @@ -1,3 +1,7 @@ +# coding: utf-8 + +from __future__ import unicode_literals + from django.core.exceptions import ObjectDoesNotExist from django.db.models import get_model from django.http import Http404, HttpResponse, HttpResponseBadRequest, \ From 0bdf795d64225d4b68ce14502b46bfb9b1f3ece2 Mon Sep 17 00:00:00 2001 From: kaleissin Date: Fri, 18 Apr 2014 00:52:13 +0200 Subject: [PATCH 27/34] Set up Travis.CI to use tox --- .travis.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .travis.yaml diff --git a/.travis.yaml b/.travis.yaml new file mode 100644 index 0000000..16fe2c5 --- /dev/null +++ b/.travis.yaml @@ -0,0 +1,15 @@ +language: python +install: + - pip install tox +script: + - tox +env: + - TOXENV=py26-django1.4, + - TOXENV=py26-django1.5, + - TOXENV=py26-django1.6, + - TOXENV=py27-django1.4, + - TOXENV=py27-django1.5, + - TOXENV=py27-django1.6, + - TOXENV=py33-django1.5, + - TOXENV=py33-django1.6, + - TOXENV=coverage From 81573cbbd7524ae6f28e493b22f8dca2ae431bfb Mon Sep 17 00:00:00 2001 From: kaleissin Date: Fri, 18 Apr 2014 00:57:01 +0200 Subject: [PATCH 28/34] What, me typo? --- .travis.yaml => .travis.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .travis.yaml => .travis.yml (100%) diff --git a/.travis.yaml b/.travis.yml similarity index 100% rename from .travis.yaml rename to .travis.yml From 1b81d5704d4058b82d2180833c6b49516be04e7b Mon Sep 17 00:00:00 2001 From: kaleissin Date: Fri, 18 Apr 2014 01:01:07 +0200 Subject: [PATCH 29/34] Travis is fiddly. Will this work? --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 16fe2c5..f9c8846 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: python install: - pip install tox script: - - tox + - tox -e $TOX_ENV env: - TOXENV=py26-django1.4, - TOXENV=py26-django1.5, From 47bf5c6a03dd7efbc3784f82f800089df0d8dfe2 Mon Sep 17 00:00:00 2001 From: kaleissin Date: Fri, 18 Apr 2014 01:04:41 +0200 Subject: [PATCH 30/34] Don't create .travis.yml by sedding tox.ini Spurious commas? --- .travis.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index f9c8846..a541080 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,12 +4,12 @@ install: script: - tox -e $TOX_ENV env: - - TOXENV=py26-django1.4, - - TOXENV=py26-django1.5, - - TOXENV=py26-django1.6, - - TOXENV=py27-django1.4, - - TOXENV=py27-django1.5, - - TOXENV=py27-django1.6, - - TOXENV=py33-django1.5, - - TOXENV=py33-django1.6, + - TOXENV=py26-django1.4 + - TOXENV=py26-django1.5 + - TOXENV=py26-django1.6 + - TOXENV=py27-django1.4 + - TOXENV=py27-django1.5 + - TOXENV=py27-django1.6 + - TOXENV=py33-django1.5 + - TOXENV=py33-django1.6 - TOXENV=coverage From 7929d0897a465f9dbd0c90a0218c4e788641744c Mon Sep 17 00:00:00 2001 From: kaleissin Date: Fri, 18 Apr 2014 01:09:57 +0200 Subject: [PATCH 31/34] Cargo-cult programming This works for other people... *crosses fingers* --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a541080..d2c8847 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,7 @@ language: python +python: 2.7 install: - pip install tox -script: - - tox -e $TOX_ENV env: - TOXENV=py26-django1.4 - TOXENV=py26-django1.5 @@ -13,3 +12,5 @@ env: - TOXENV=py33-django1.5 - TOXENV=py33-django1.6 - TOXENV=coverage +script: + - tox -e $TOX_ENV From 17604278d062788e7e61f7c075f1beaf71a931d1 Mon Sep 17 00:00:00 2001 From: kaleissin Date: Fri, 18 Apr 2014 01:13:55 +0200 Subject: [PATCH 32/34] ... These things do happen this late Time for more tea --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d2c8847..42bd900 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,4 +13,4 @@ env: - TOXENV=py33-django1.6 - TOXENV=coverage script: - - tox -e $TOX_ENV + - tox -e $TOXENV From 933bbe6983d993eb47ee3930e5bd76d319988a72 Mon Sep 17 00:00:00 2001 From: Karthik Ravindra Date: Fri, 19 Dec 2014 15:56:06 -0500 Subject: [PATCH 33/34] Update views.py simplejson is removed in django 1.7 --- voting/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/voting/views.py b/voting/views.py index 33ae4b0..7d5993b 100644 --- a/voting/views.py +++ b/voting/views.py @@ -8,7 +8,7 @@ HttpResponseRedirect from django.contrib.auth.views import redirect_to_login from django.template import loader, RequestContext -from django.utils import simplejson +import json from voting.models import Vote @@ -122,7 +122,7 @@ def vote_on_object_with_lazy_model(request, app_label, model_name, *args, def json_error_response(error_message): - return HttpResponse(simplejson.dumps(dict(success=False, + return HttpResponse(json.dumps(dict(success=False, error_message=error_message))) @@ -172,7 +172,7 @@ def xmlhttprequest_vote_on_object(request, model, direction, # Vote and respond Vote.objects.record_vote(obj, request.user, vote) - return HttpResponse(simplejson.dumps({ + return HttpResponse(json.dumps({ 'success': True, 'score': Vote.objects.get_score(obj), })) From 6882a6ca6631383d7a40529f6fc9ce72232cf5b8 Mon Sep 17 00:00:00 2001 From: Kubilay Kocak Date: Fri, 12 Jun 2015 00:19:50 +1000 Subject: [PATCH 34/34] Update url to new repository location --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 279e0c6..ef7894b 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ author_email='jonathan.buchanan@gmail.com', maintainer='Jannis Leidel', maintainer_email='jannis@leidel.info', - url='http://github.com/jezdez/django-voting/', + url='https://github.com/pjdelport/django-voting', packages=[ 'voting', 'voting.migrations',