From 10ecadc78fae0453e21134edb9c565e739f278a4 Mon Sep 17 00:00:00 2001 From: Tim Baxter Date: Wed, 7 Aug 2013 09:08:37 -0500 Subject: [PATCH 01/13] Updates for Django 1.5, nicer admin, extra template filters and truly generic voting without extra URLs --- voting/admin.py | 6 +- voting/models.py | 6 +- voting/templates/voting/confirm_vote.html | 6 ++ voting/templatetags/voting_tags.py | 39 ++++++- voting/views.py | 121 ++++++---------------- 5 files changed, 85 insertions(+), 93 deletions(-) create mode 100644 voting/templates/voting/confirm_vote.html diff --git a/voting/admin.py b/voting/admin.py index 65e836c..6179681 100644 --- a/voting/admin.py +++ b/voting/admin.py @@ -1,4 +1,8 @@ from django.contrib import admin from voting.models import Vote -admin.site.register(Vote) + +class VoteAdmin(admin.ModelAdmin): + list_display = ('user', 'content_type', 'vote') + +admin.site.register(Vote, VoteAdmin) diff --git a/voting/models.py b/voting/models.py index cebbd15..30010a4 100644 --- a/voting/models.py +++ b/voting/models.py @@ -1,10 +1,12 @@ +from django.conf import settings 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 voting.managers import VoteManager +UserModel = getattr(settings, "AUTH_USER_MODEL", "auth.User") + SCORES = ( (u'+1', +1), (u'-1', -1), @@ -14,7 +16,7 @@ class Vote(models.Model): """ A vote on an object by a User. """ - user = models.ForeignKey(User) + user = models.ForeignKey(UserModel) content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() object = generic.GenericForeignKey('content_type', 'object_id') diff --git a/voting/templates/voting/confirm_vote.html b/voting/templates/voting/confirm_vote.html new file mode 100644 index 0000000..826c025 --- /dev/null +++ b/voting/templates/voting/confirm_vote.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} + +{% block content %} +

Thank you!

+

Your vote has been confirmed.

+{% endblock %} \ No newline at end of file diff --git a/voting/templatetags/voting_tags.py b/voting/templatetags/voting_tags.py index 7fc4dc8..e0a657c 100644 --- a/voting/templatetags/voting_tags.py +++ b/voting/templatetags/voting_tags.py @@ -5,7 +5,6 @@ register = template.Library() -# Tags class ScoreForObjectNode(template.Node): def __init__(self, object, context_var): @@ -20,6 +19,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 +33,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 +49,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 +65,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 +81,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 +102,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 +119,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 +139,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 +159,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 @@ -228,4 +236,31 @@ def vote_display(vote, arg=None): return up return down -register.filter(vote_display) \ No newline at end of file + +@register.filter +def set_flag(score, arg=-3): + """ + Gets vote total and returns a "toxic" flag if the object has + been downvoted into oblivion. + Threshold defaults to -3, but can be set when the tag is called. + Example usage:: + {{ vote|set_flag }} + {{ vote|set_flag:"-5" }} + """ + if score and score <= arg: + return 'toxic' + return '' + + +@register.filter +def clean_score(score, arg=None): + """ + If the score is below 0, returns 0 so negative scores aren't public. + Example usage:: + {{ score.score|clean_score" }} + """ + if isinstance(score, dict): + score = score.get('score') + if not score or score < 0: + score = 0 + return score diff --git a/voting/views.py b/voting/views.py index 352f6ad..25d4dba 100644 --- a/voting/views.py +++ b/voting/views.py @@ -1,20 +1,25 @@ 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 +from django.http import HttpResponse, HttpResponseRedirect +from django.contrib.auth.decorators import login_required from django.template import loader, RequestContext from django.utils import simplejson -from voting.models import Vote +from models import Vote 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, + +@login_required +def generic_vote_on_object(request, model_name, object_id, direction, + post_vote_redirect=None, template_name=None, template_loader=loader, extra_context=None, context_processors=None, template_object_name='object', allow_xmlhttprequest=False): """ - Generic object vote function. + Really generic object vote function. + Gets object and model via content_type. + Expects URL format: + {% url 'generic_vote' 'content_type' object.id '' %} The given template will be used to confirm the vote if this view is fetched using GET; vote registration will only be performed if this @@ -34,38 +39,33 @@ def vote_on_object(request, model, direction, post_vote_redirect=None, direction The type of vote which will be registered for the object. """ - if allow_xmlhttprequest and request.is_ajax(): - return xmlhttprequest_vote_on_object(request, model, direction, - object_id=object_id, slug=slug, - slug_field=slug_field) - - 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 = {} - if object_id: - lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id - elif slug and slug_field: - lookup_kwargs['%s__exact' % slug_field] = slug - else: - raise AttributeError('Generic vote view must be called with either ' - 'object_id or slug and slug_field.') - try: - obj = model._default_manager.get(**lookup_kwargs) - except ObjectDoesNotExist: - raise Http404, 'No %s found for %s.' % (model._meta.app_label, lookup_kwargs) + # TO-DO: check by app_label, also + ctype = ContentType.objects.filter(model=model_name)[0] + obj = ctype.get_object_for_this_type(pk=object_id) + + if request.is_ajax() and request.method == 'GET': + return json_error_response('XMLHttpRequest votes can only be made using POST.') + Vote.objects.record_vote(obj, request.user, vote) + + if request.is_ajax(): + return HttpResponse(simplejson.dumps({'success': True, + 'score': Vote.objects.get_score(obj)})) + + if extra_context is None: + extra_context = {} + + # Look up the object to be voted on 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')): @@ -73,17 +73,12 @@ def vote_on_object(request, model, direction, post_vote_redirect=None, else: next = obj.get_absolute_url else: - raise AttributeError('Generic vote view must be called with either ' - 'post_vote_redirect, a "next" parameter in ' - 'the request, or the object being voted on ' - 'must define a get_absolute_url method or ' - 'property.') + raise AttributeError('Generic vote view must be called with either post_vote_redirect, a "next" parameter in the request, or the object being voted on must define a get_absolute_url method or property.') Vote.objects.record_vote(obj, request.user, vote) return HttpResponseRedirect(next) else: if not template_name: - template_name = '%s/%s_confirm_vote.html' % ( - model._meta.app_label, model._meta.object_name.lower()) + template_name = 'voting/confirm_vote.html' t = template_loader.get_template(template_name) c = RequestContext(request, { template_object_name: obj, @@ -97,57 +92,7 @@ def vote_on_object(request, model, direction, post_vote_redirect=None, response = HttpResponse(t.render(c)) return response -def json_error_response(error_message): + +def json_error_response(error_message, *args, **kwargs): 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): - """ - Generic object vote function for use via XMLHttpRequest. - - Properties of the resulting JSON object: - success - ``true`` if the vote was successfully processed, ``false`` - otherwise. - score - The object's updated score and number of votes if the vote - was successfully processed. - error_message - Contains an error message if the vote was not successfully - processed. - """ - if request.method == 'GET': - return json_error_response( - 'XMLHttpRequest votes can only be made using POST.') - if not request.user.is_authenticated(): - return json_error_response('Not authenticated.') - - try: - vote = dict(VOTE_DIRECTIONS)[direction] - except KeyError: - return json_error_response( - '\'%s\' is not a valid vote type.' % direction) - - # Look up the object to be voted on - lookup_kwargs = {} - if object_id: - lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id - elif slug and slug_field: - lookup_kwargs['%s__exact' % slug_field] = slug - else: - return json_error_response('Generic XMLHttpRequest vote view must be ' - 'called with either object_id or slug and ' - 'slug_field.') - try: - obj = model._default_manager.get(**lookup_kwargs) - except ObjectDoesNotExist: - return json_error_response( - 'No %s found for %s.' % (model._meta.verbose_name, lookup_kwargs)) - - # Vote and respond - Vote.objects.record_vote(obj, request.user, vote) - return HttpResponse(simplejson.dumps({ - 'success': True, - 'score': Vote.objects.get_score(obj), - })) From a641c4992f7e6fa06850ccff5dfe465e8a083212 Mon Sep 17 00:00:00 2001 From: Tim Baxter Date: Wed, 7 Aug 2013 09:21:40 -0500 Subject: [PATCH 02/13] Update overview.txt --- docs/overview.txt | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/docs/overview.txt b/docs/overview.txt index f7c97fd..a75c191 100644 --- a/docs/overview.txt +++ b/docs/overview.txt @@ -153,21 +153,17 @@ XMLHttpRequest ``POST``. The following sample URLconf demonstrates using a generic view for voting on a model, allowing for regular voting and XMLHttpRequest -voting at the same URL:: +voting at the same URL. Only one URL is required for all models, +by passing the model name when calling the URL:: from django.conf.urls.defaults import * - from voting.views import vote_on_object - from shop.apps.products.models import Widget - widget_dict = { - 'model': Widget, - 'template_object_name': 'widget', - 'allow_xmlhttprequest': True, - } + url( + name="generic_vote", + regex=r'^vote/(?P[-\w]+)/(?P\d+)/(?Pup|down)vote/$', + view='voting.views.vote_on_object' + ), - urlpatterns = patterns('', - (r'^widgets/(?P\d+)/(?Pup|down|clear)vote/?$', vote_on_object, widget_dict), - ) ``voting.views.vote_on_object`` -------------------------------- @@ -395,4 +391,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" }} From 19b6ef54f8d56410bb38dee02a6262e2afc74776 Mon Sep 17 00:00:00 2001 From: Tim Baxter Date: Wed, 7 Aug 2013 14:39:26 -0500 Subject: [PATCH 03/13] Update Manifest.in to include default template --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index df27391..27cecd0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,4 +4,5 @@ include LICENSE.txt include MANIFEST.in include README.txt recursive-include docs * +recursive-include voting/templates * recursive-include voting/tests * From 8588796deb41fca6478d1cb213bac19e9083e87e Mon Sep 17 00:00:00 2001 From: Tim Baxter Date: Wed, 7 Aug 2013 15:34:29 -0500 Subject: [PATCH 04/13] fix in manifest, minor pep-8 changes --- MANIFEST.in | 2 +- voting/templatetags/voting_tags.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 27cecd0..bc44c37 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,5 +4,5 @@ include LICENSE.txt include MANIFEST.in include README.txt recursive-include docs * -recursive-include voting/templates * +recursive-include voting/templates/voting * recursive-include voting/tests * diff --git a/voting/templatetags/voting_tags.py b/voting/templatetags/voting_tags.py index e0a657c..ef87952 100644 --- a/voting/templatetags/voting_tags.py +++ b/voting/templatetags/voting_tags.py @@ -187,8 +187,10 @@ 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): """ Creates an appropriate message asking the user to confirm the given vote @@ -206,8 +208,10 @@ def confirm_vote_message(object_description, vote_direction): register.simple_tag(confirm_vote_message) + # Filters + def vote_display(vote, arg=None): """ Given a string mapping values for up and down votes, returns one @@ -230,7 +234,7 @@ 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 From 53b1caaa981cefc2a02e6f9658643d434b0bca9e Mon Sep 17 00:00:00 2001 From: Tim Baxter Date: Mon, 21 Apr 2014 18:44:17 -0500 Subject: [PATCH 05/13] Migrated from simplejson to json and minor error check in manager --- .gitignore | 10 ++++++++++ voting/managers.py | 2 +- voting/views.py | 8 ++++---- 3 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a732794 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ + +django_voting.egg-info/dependency_links.txt + +django_voting.egg-info/PKG-INFO + +django_voting.egg-info/SOURCES.txt + +django_voting.egg-info/top_level.txt + +*.pyc diff --git a/voting/managers.py b/voting/managers.py index 3162c3b..71d6d57 100644 --- a/voting/managers.py +++ b/voting/managers.py @@ -138,7 +138,7 @@ def get_top(self, Model, limit=10, reversed=False): # 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': + if settings.DATABASES['default']['ENGINE'] == 'mysql': having_score = connection.ops.quote_name('score') else: having_score = 'SUM(vote)' diff --git a/voting/views.py b/voting/views.py index 25d4dba..016d86b 100644 --- a/voting/views.py +++ b/voting/views.py @@ -1,9 +1,9 @@ +import json + from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponse, HttpResponseRedirect from django.contrib.auth.decorators import login_required from django.template import loader, RequestContext -from django.utils import simplejson from models import Vote @@ -55,7 +55,7 @@ def generic_vote_on_object(request, model_name, object_id, direction, Vote.objects.record_vote(obj, request.user, vote) if request.is_ajax(): - return HttpResponse(simplejson.dumps({'success': True, + return HttpResponse(json.dumps({'success': True, 'score': Vote.objects.get_score(obj)})) if extra_context is None: @@ -94,5 +94,5 @@ def generic_vote_on_object(request, model_name, object_id, direction, def json_error_response(error_message, *args, **kwargs): - return HttpResponse(simplejson.dumps(dict(success=False, + return HttpResponse(json.dumps(dict(success=False, error_message=error_message))) From 9e50e1e5cb9790fa3a3269fddf28262feee804f0 Mon Sep 17 00:00:00 2001 From: Tim Baxter Date: Mon, 28 Jul 2014 13:38:50 -0500 Subject: [PATCH 06/13] convert string to int --- voting/templatetags/voting_tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voting/templatetags/voting_tags.py b/voting/templatetags/voting_tags.py index ef87952..4e19e38 100644 --- a/voting/templatetags/voting_tags.py +++ b/voting/templatetags/voting_tags.py @@ -251,7 +251,7 @@ def set_flag(score, arg=-3): {{ vote|set_flag }} {{ vote|set_flag:"-5" }} """ - if score and score <= arg: + if score and score <= int(arg): return 'toxic' return '' From a35bfd67e23c729952a6348532824ab0e90a9e7d Mon Sep 17 00:00:00 2001 From: Tim Baxter Date: Thu, 30 Oct 2014 12:24:00 -0500 Subject: [PATCH 07/13] Updated for packaging and distribution on pypi --- .gitignore | 10 ++++------ INSTALL.txt | 14 -------------- MANIFEST.in | 1 - setup.py | 33 +++++++++++---------------------- 4 files changed, 15 insertions(+), 43 deletions(-) delete mode 100644 INSTALL.txt diff --git a/.gitignore b/.gitignore index a732794..66adb4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,8 @@ -django_voting.egg-info/dependency_links.txt +django_voting.egg-info/* -django_voting.egg-info/PKG-INFO - -django_voting.egg-info/SOURCES.txt +*.pyc -django_voting.egg-info/top_level.txt +dist/tango-voting-0.2.tar.gz -*.pyc +tango_voting.egg-info/* diff --git a/INSTALL.txt b/INSTALL.txt deleted file mode 100644 index 56434af..0000000 --- a/INSTALL.txt +++ /dev/null @@ -1,14 +0,0 @@ -Thanks for downloading django-voting. - -To install it, run the following command inside this directory: - - python setup.py install - -Or if you'd prefer you can simply place the included ``voting`` -directory somewhere on your Python path, or symlink to it from -somewhere on your Python path; this is useful if you're working from a -Subversion checkout. - -Note that this application requires Python 2.3 or later, and Django -0.97-pre or later. You can obtain Python from http://www.python.org/ and -Django from http://www.djangoproject.com/. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index bc44c37..59e4ff9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,4 @@ include CHANGELOG.txt -include INSTALL.txt include LICENSE.txt include MANIFEST.in include README.txt diff --git a/setup.py b/setup.py index b61a615..1a90600 100644 --- a/setup.py +++ b/setup.py @@ -1,27 +1,16 @@ 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 -if version_tuple[2] is not None: - version = "%d.%d_%s" % version_tuple -else: - version = "%d.%d" % version_tuple[:2] +from setuptools import find_packages 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.tests'], + name = 'tango-voting', + version = '0.2', + author = 'Jonathan Buchanan/Tim Baxter', + author_email = 'mail.baxter@gmail.com', + url = 'https://github.com/tBaxter/django-voting', + license='LICENSE', + description = 'Generic voting application for Django, based on django-voting by Jonathan Buchanan', + packages = find_packages(), + include_package_data=True, classifiers = ['Development Status :: 4 - Beta', 'Environment :: Web Environment', 'Framework :: Django', @@ -30,4 +19,4 @@ 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Utilities'], -) \ No newline at end of file +) From 7ac3407cb06f052e3741de24f0ab3dba1d549d2f Mon Sep 17 00:00:00 2001 From: TB026891 Date: Mon, 11 Jan 2016 15:45:32 -0600 Subject: [PATCH 08/13] Updated for Django 1.8+ --- CHANGELOG.txt | 5 ++++- README.txt | 10 +++++----- setup.py | 32 +++++++++++++++++--------------- voting/models.py | 11 ++++++----- 4 files changed, 32 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 14cc54b..e77ea1a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ ======================= Django Voting Changelog -======================= \ No newline at end of file +======================= + +### 0.3.0 +Updated for Django 1.8+ diff --git a/README.txt b/README.txt index 9f55f61..b0a1149 100644 --- a/README.txt +++ b/README.txt @@ -1,10 +1,10 @@ ============= -Django Voting +Tango Voting ============= -This is a generic voting application for Django projects +This is a generic voting application for Django projects, based on Jonathan Buchanan's old [django-voting](http://code.google.com/p/django-voting/) app, with additional improvements and enhancements. -For installation instructions, see the file "INSTALL.txt" in this -directory; for instructions on how to use this application, and on + +For instructions on how to use this application, and on what it provides, see the file "overview.txt" in the "docs/" -directory. \ No newline at end of file +directory. diff --git a/setup.py b/setup.py index 1a90600..489685f 100644 --- a/setup.py +++ b/setup.py @@ -2,21 +2,23 @@ from setuptools import find_packages setup( - name = 'tango-voting', - version = '0.2', - author = 'Jonathan Buchanan/Tim Baxter', - author_email = 'mail.baxter@gmail.com', - url = 'https://github.com/tBaxter/django-voting', + name='tango-voting', + version='0.3', + author='Jonathan Buchanan/Tim Baxter', + author_email='mail.baxter@gmail.com', + url='https://github.com/tBaxter/django-voting', license='LICENSE', - description = 'Generic voting application for Django, based on django-voting by Jonathan Buchanan', - packages = find_packages(), + description="""Generic voting application for Django, + based on django-voting by Jonathan Buchanan""", + packages=find_packages(), include_package_data=True, - 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'], + 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/models.py b/voting/models.py index 30010a4..dde9922 100644 --- a/voting/models.py +++ b/voting/models.py @@ -1,5 +1,5 @@ from django.conf import settings -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models @@ -12,15 +12,16 @@ (u'-1', -1), ) + class Vote(models.Model): """ A vote on an object by a User. """ - user = models.ForeignKey(UserModel) + user = models.ForeignKey(UserModel) content_type = models.ForeignKey(ContentType) - object_id = models.PositiveIntegerField() - object = generic.GenericForeignKey('content_type', 'object_id') - vote = models.SmallIntegerField(choices=SCORES) + object_id = models.PositiveIntegerField() + object = GenericForeignKey('content_type', 'object_id') + vote = models.SmallIntegerField(choices=SCORES) objects = VoteManager() From f9fe3d4700ac7835eb27bfb5e053451603f2ba93 Mon Sep 17 00:00:00 2001 From: TB026891 Date: Thu, 28 Apr 2016 09:05:50 -0500 Subject: [PATCH 09/13] Call settings.AUTH_USER_MODEL correctly --- .gitignore | 2 ++ CHANGELOG.txt | 3 +++ setup.py | 2 +- voting/models.py | 5 ++--- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 66adb4b..725db95 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ django_voting.egg-info/* dist/tango-voting-0.2.tar.gz tango_voting.egg-info/* + +*.gz diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e77ea1a..eff2047 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -2,5 +2,8 @@ Django Voting Changelog ======================= +### 0.3.1 +Call settings.AUTH_USER_MODEL correctly + ### 0.3.0 Updated for Django 1.8+ diff --git a/setup.py b/setup.py index 489685f..690702d 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='tango-voting', - version='0.3', + version='0.3.1', author='Jonathan Buchanan/Tim Baxter', author_email='mail.baxter@gmail.com', url='https://github.com/tBaxter/django-voting', diff --git a/voting/models.py b/voting/models.py index dde9922..e756ff4 100644 --- a/voting/models.py +++ b/voting/models.py @@ -5,8 +5,6 @@ from voting.managers import VoteManager -UserModel = getattr(settings, "AUTH_USER_MODEL", "auth.User") - SCORES = ( (u'+1', +1), (u'-1', -1), @@ -17,7 +15,7 @@ class Vote(models.Model): """ A vote on an object by a User. """ - user = models.ForeignKey(UserModel) + user = models.ForeignKey(settings.AUTH_USER_MODEL) content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() object = GenericForeignKey('content_type', 'object_id') @@ -26,6 +24,7 @@ class Vote(models.Model): objects = VoteManager() class Meta: + app_label = 'voting' db_table = 'votes' # One vote per user per object unique_together = (('user', 'content_type', 'object_id'),) From 61cfd60403fded695c1977458c1c349217b89251 Mon Sep 17 00:00:00 2001 From: TBaxter Date: Wed, 27 Jun 2018 13:37:47 -0500 Subject: [PATCH 10/13] Updated foreignkey on_delete arg for 1.0 compatiblity --- CHANGELOG.txt | 3 +++ README.txt | 5 +++-- setup.py | 2 +- voting/models.py | 10 ++++++++-- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index eff2047..72b6699 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -2,6 +2,9 @@ Django Voting Changelog ======================= +### 0.3.2 +Update ForeignKeys to use on_delete for 2.0 compatibility. + ### 0.3.1 Call settings.AUTH_USER_MODEL correctly diff --git a/README.txt b/README.txt index b0a1149..29ace4b 100644 --- a/README.txt +++ b/README.txt @@ -2,8 +2,9 @@ Tango Voting ============= -This is a generic voting application for Django projects, based on Jonathan Buchanan's old [django-voting](http://code.google.com/p/django-voting/) app, with additional improvements and enhancements. - +This is a generic voting application for Django projects, +based on Jonathan Buchanan's old [django-voting](http://code.google.com/p/django-voting/) app, +with additional improvements and enhancements. For instructions on how to use this application, and on what it provides, see the file "overview.txt" in the "docs/" diff --git a/setup.py b/setup.py index 690702d..57bf4ac 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='tango-voting', - version='0.3.1', + version='0.3.2', author='Jonathan Buchanan/Tim Baxter', author_email='mail.baxter@gmail.com', url='https://github.com/tBaxter/django-voting', diff --git a/voting/models.py b/voting/models.py index e756ff4..7fbbad0 100644 --- a/voting/models.py +++ b/voting/models.py @@ -15,8 +15,14 @@ class Vote(models.Model): """ A vote on an object by a User. """ - user = models.ForeignKey(settings.AUTH_USER_MODEL) - content_type = models.ForeignKey(ContentType) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.PROTECT + ) + content_type = models.ForeignKey( + ContentType, + on_delete=models.PROTECT + ) object_id = models.PositiveIntegerField() object = GenericForeignKey('content_type', 'object_id') vote = models.SmallIntegerField(choices=SCORES) From cf2cbfc142d41693f9291d411491e04d637deb51 Mon Sep 17 00:00:00 2001 From: TBaxter Date: Fri, 29 Jun 2018 09:17:11 -0500 Subject: [PATCH 11/13] Updates for Django 2.0 --- CHANGELOG.txt | 3 + build/lib/voting/__init__.py | 1 + build/lib/voting/admin.py | 8 + build/lib/voting/managers.py | 203 +++++++++++++ build/lib/voting/models.py | 45 +++ .../voting/templates/voting/confirm_vote.html | 6 + build/lib/voting/templatetags/__init__.py | 0 build/lib/voting/templatetags/voting_tags.py | 270 ++++++++++++++++++ build/lib/voting/tests/__init__.py | 0 build/lib/voting/tests/models.py | 10 + build/lib/voting/tests/runtests.py | 8 + build/lib/voting/tests/settings.py | 27 ++ build/lib/voting/tests/tests.py | 86 ++++++ build/lib/voting/views.py | 98 +++++++ dist/tango_voting-0.3.2-py3-none-any.whl | Bin 0 -> 10956 bytes setup.py | 2 +- voting/views.py | 2 +- 17 files changed, 767 insertions(+), 2 deletions(-) create mode 100644 build/lib/voting/__init__.py create mode 100644 build/lib/voting/admin.py create mode 100644 build/lib/voting/managers.py create mode 100644 build/lib/voting/models.py create mode 100644 build/lib/voting/templates/voting/confirm_vote.html create mode 100644 build/lib/voting/templatetags/__init__.py create mode 100644 build/lib/voting/templatetags/voting_tags.py create mode 100644 build/lib/voting/tests/__init__.py create mode 100644 build/lib/voting/tests/models.py create mode 100644 build/lib/voting/tests/runtests.py create mode 100644 build/lib/voting/tests/settings.py create mode 100644 build/lib/voting/tests/tests.py create mode 100644 build/lib/voting/views.py create mode 100644 dist/tango_voting-0.3.2-py3-none-any.whl diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 72b6699..ec56b7d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -2,6 +2,9 @@ Django Voting Changelog ======================= +### 0.4.0 +Updated for Django 2.0 compatibility. + ### 0.3.2 Update ForeignKeys to use on_delete for 2.0 compatibility. diff --git a/build/lib/voting/__init__.py b/build/lib/voting/__init__.py new file mode 100644 index 0000000..449f64a --- /dev/null +++ b/build/lib/voting/__init__.py @@ -0,0 +1 @@ +VERSION = (0, 1, None) \ No newline at end of file diff --git a/build/lib/voting/admin.py b/build/lib/voting/admin.py new file mode 100644 index 0000000..6179681 --- /dev/null +++ b/build/lib/voting/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from voting.models import Vote + + +class VoteAdmin(admin.ModelAdmin): + list_display = ('user', 'content_type', 'vote') + +admin.site.register(Vote, VoteAdmin) diff --git a/build/lib/voting/managers.py b/build/lib/voting/managers.py new file mode 100644 index 0000000..71d6d57 --- /dev/null +++ b/build/lib/voting/managers.py @@ -0,0 +1,203 @@ +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.contrib.contenttypes.models import ContentType + +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): + """ + Get a dictionary containing the total score for ``obj`` and + 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]), + } + + def get_scores_in_bulk(self, objects): + """ + Get a dictionary mapping object ids to total score and number + of votes for each object. + """ + 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, + ).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') + + 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): + """ + Record a user's vote on a given object. Only allows a given user + to vote once, though that vote may be changed. + + A zero vote indicates that any existing vote should be removed. + """ + if vote not in (+1, 0, -1): + raise ValueError('Invalid vote (must be +1/0/-1)') + ctype = ContentType.objects.get_for_model(obj) + 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() + except models.ObjectDoesNotExist: + if vote != 0: + self.create(user=user, content_type=ctype, + object_id=obj._get_pk_val(), vote=vote) + + 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.DATABASES['default']['ENGINE'] == 'mysql': + having_score = connection.ops.quote_name('score') + else: + having_score = 'SUM(vote)' + if reversed: + having_sql = ' HAVING %(having_score)s < 0 ORDER BY %(having_score)s ASC LIMIT %%s' + 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() + + # Use in_bulk() to avoid O(limit) db hits. + objects = Model.objects.in_bulk([id for id, score in results]) + + # Yield each object, score pair. Because of the lazy nature of generic + # relations, missing objects are silently ignored. + for id, score in results: + if id in objects: + yield objects[id], int(score) + + def get_bottom(self, Model, limit=10): + """ + Get the bottom (i.e. most negative) N scored objects for a given + model. + + Yields (object, score) tuples. + """ + return self.get_top(Model, limit, True) + + def get_for_user(self, obj, user): + """ + Get the vote made on the given object by the given user, or + ``None`` if no matching vote exists. + """ + if not user.is_authenticated(): + return None + ctype = ContentType.objects.get_for_model(obj) + try: + vote = self.get(content_type=ctype, object_id=obj._get_pk_val(), + user=user) + except models.ObjectDoesNotExist: + vote = None + return vote + + def get_for_user_in_bulk(self, objects, user): + """ + Get a dictionary mapping object ids to votes made by the given + user on the corresponding objects. + """ + vote_dict = {} + if len(objects) > 0: + ctype = ContentType.objects.get_for_model(objects[0]) + votes = list(self.filter(content_type__pk=ctype.id, + object_id__in=[obj._get_pk_val() \ + for obj in objects], + user__pk=user.id)) + vote_dict = dict([(vote.object_id, vote) for vote in votes]) + return vote_dict diff --git a/build/lib/voting/models.py b/build/lib/voting/models.py new file mode 100644 index 0000000..7fbbad0 --- /dev/null +++ b/build/lib/voting/models.py @@ -0,0 +1,45 @@ +from django.conf import settings +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.db import models + +from voting.managers import VoteManager + +SCORES = ( + (u'+1', +1), + (u'-1', -1), +) + + +class Vote(models.Model): + """ + A vote on an object by a User. + """ + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.PROTECT + ) + content_type = models.ForeignKey( + ContentType, + on_delete=models.PROTECT + ) + object_id = models.PositiveIntegerField() + object = GenericForeignKey('content_type', 'object_id') + vote = models.SmallIntegerField(choices=SCORES) + + objects = VoteManager() + + class Meta: + app_label = 'voting' + db_table = 'votes' + # 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 is_upvote(self): + return self.vote == 1 + + def is_downvote(self): + return self.vote == -1 diff --git a/build/lib/voting/templates/voting/confirm_vote.html b/build/lib/voting/templates/voting/confirm_vote.html new file mode 100644 index 0000000..826c025 --- /dev/null +++ b/build/lib/voting/templates/voting/confirm_vote.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} + +{% block content %} +

Thank you!

+

Your vote has been confirmed.

+{% endblock %} \ No newline at end of file diff --git a/build/lib/voting/templatetags/__init__.py b/build/lib/voting/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/voting/templatetags/voting_tags.py b/build/lib/voting/templatetags/voting_tags.py new file mode 100644 index 0000000..4e19e38 --- /dev/null +++ b/build/lib/voting/templatetags/voting_tags.py @@ -0,0 +1,270 @@ +from django import template +from django.utils.html import escape + +from voting.models import Vote + +register = template.Library() + + +class ScoreForObjectNode(template.Node): + def __init__(self, object, context_var): + self.object = object + self.context_var = context_var + + def render(self, context): + try: + object = template.resolve_variable(self.object, context) + except template.VariableDoesNotExist: + return '' + context[self.context_var] = Vote.objects.get_score(object) + return '' + + +class ScoresForObjectsNode(template.Node): + def __init__(self, objects, context_var): + self.objects = objects + self.context_var = context_var + + def render(self, context): + try: + objects = template.resolve_variable(self.objects, context) + except template.VariableDoesNotExist: + return '' + 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 + self.object = object + self.context_var = context_var + + def render(self, context): + try: + user = template.resolve_variable(self.user, context) + object = template.resolve_variable(self.object, context) + except template.VariableDoesNotExist: + return '' + 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 + self.objects = objects + self.context_var = context_var + + def render(self, context): + try: + user = template.resolve_variable(self.user, context) + objects = template.resolve_variable(self.objects, context) + except template.VariableDoesNotExist: + return '' + 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 + self.dictionary = dictionary + self.context_var = context_var + + def render(self, context): + try: + dictionary = template.resolve_variable(self.dictionary, context) + item = template.resolve_variable(self.item, context) + except template.VariableDoesNotExist: + return '' + 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 + it's received and stores them in a context variable which has + ``score`` and ``num_votes`` properties. + + Example usage:: + + {% score_for_object widget as score %} + + {{ score.score }}point{{ score.score|pluralize }} + after {{ score.num_votes }} vote{{ score.num_votes|pluralize }} + """ + bits = token.contents.split() + if len(bits) != 4: + raise template.TemplateSyntaxError("'%s' tag takes exactly three arguments" % bits[0]) + if bits[2] != 'as': + 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 + votes they have received and stores them in a context variable. + + Example usage:: + + {% scores_for_objects widget_list as score_dict %} + """ + bits = token.contents.split() + if len(bits) != 4: + raise template.TemplateSyntaxError("'%s' tag takes exactly three arguments" % bits[0]) + if bits[2] != 'as': + 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 + stores it in a context variable. If the user has not voted, the + context variable will be ``None``. + + Example usage:: + + {% vote_by_user user on widget as vote %} + """ + bits = token.contents.split() + if len(bits) != 6: + raise template.TemplateSyntaxError("'%s' tag takes exactly five arguments" % bits[0]) + if bits[2] != 'on': + raise template.TemplateSyntaxError("second argument to '%s' tag must be 'on'" % bits[0]) + if bits[4] != 'as': + 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 + dictionary keyed with object ids and stores it in a context + variable. + + Example usage:: + + {% votes_by_user user on widget_list as vote_dict %} + """ + bits = token.contents.split() + if len(bits) != 6: + raise template.TemplateSyntaxError("'%s' tag takes exactly four arguments" % bits[0]) + if bits[2] != 'on': + raise template.TemplateSyntaxError("second argument to '%s' tag must be 'on'" % bits[0]) + if bits[4] != 'as': + 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 + returned by the ``votes_by_user`` and ``scores_for_objects`` + template tags - retrieves the value for the given object and + stores it in a context variable, storing ``None`` if no value + exists for the given object. + + Example usage:: + + {% dict_entry_for_item widget from vote_dict as vote %} + """ + bits = token.contents.split() + if len(bits) != 6: + raise template.TemplateSyntaxError("'%s' tag takes exactly five arguments" % bits[0]) + if bits[2] != 'from': + raise template.TemplateSyntaxError("second argument to '%s' tag must be 'from'" % bits[0]) + if bits[4] != 'as': + raise template.TemplateSyntaxError("fourth argument to '%s' tag must be 'as'" % bits[0]) + return DictEntryForItemNode(bits[1], bits[3], bits[5]) + +register.tag('score_for_object', do_score_for_object) +register.tag('scores_for_objects', do_scores_for_objects) +register.tag('vote_by_user', do_vote_by_user) +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): + """ + Creates an appropriate message asking the user to confirm the given vote + for the given object description. + + Example usage:: + + {% confirm_vote_message widget.title direction %} + """ + if vote_direction == 'clear': + message = 'Confirm clearing your vote for %s.' + else: + message = 'Confirm %s vote for %%s.' % vote_direction + return message % (escape(object_description),) + +register.simple_tag(confirm_vote_message) + + +# Filters + + +def vote_display(vote, arg=None): + """ + Given a string mapping values for up and down votes, returns one + of the strings according to the given ``Vote``: + + ========= ===================== ============= + Vote type Argument Outputs + ========= ===================== ============= + ``+1`` ``"Bodacious,Bogus"`` ``Bodacious`` + ``-1`` ``"Bodacious,Bogus"`` ``Bogus`` + ========= ===================== ============= + + If no string mapping is given, "Up" and "Down" will be used. + + Example usage:: + + {{ vote|vote_display:"Bodacious,Bogus" }} + """ + if arg is None: + arg = 'Up,Down' + bits = arg.split(',') + if len(bits) != 2: + return vote.vote # Invalid arg + up, down = bits + if vote.vote == 1: + return up + return down + + +@register.filter +def set_flag(score, arg=-3): + """ + Gets vote total and returns a "toxic" flag if the object has + been downvoted into oblivion. + Threshold defaults to -3, but can be set when the tag is called. + Example usage:: + {{ vote|set_flag }} + {{ vote|set_flag:"-5" }} + """ + if score and score <= int(arg): + return 'toxic' + return '' + + +@register.filter +def clean_score(score, arg=None): + """ + If the score is below 0, returns 0 so negative scores aren't public. + Example usage:: + {{ score.score|clean_score" }} + """ + if isinstance(score, dict): + score = score.get('score') + if not score or score < 0: + score = 0 + return score diff --git a/build/lib/voting/tests/__init__.py b/build/lib/voting/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/voting/tests/models.py b/build/lib/voting/tests/models.py new file mode 100644 index 0000000..eaf405b --- /dev/null +++ b/build/lib/voting/tests/models.py @@ -0,0 +1,10 @@ +from django.db import models + +class Item(models.Model): + name = models.CharField(max_length=50) + + def __str__(self): + return self.name + + class Meta: + ordering = ['name'] diff --git a/build/lib/voting/tests/runtests.py b/build/lib/voting/tests/runtests.py new file mode 100644 index 0000000..bc1ba37 --- /dev/null +++ b/build/lib/voting/tests/runtests.py @@ -0,0 +1,8 @@ +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/build/lib/voting/tests/settings.py b/build/lib/voting/tests/settings.py new file mode 100644 index 0000000..8d2506e --- /dev/null +++ b/build/lib/voting/tests/settings.py @@ -0,0 +1,27 @@ +import os + +DIRNAME = os.path.dirname(__file__) + +DATABASE_ENGINE = 'sqlite3' +DATABASE_NAME = os.path.join(DIRNAME, 'database.db') + +#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' + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'voting', + 'voting.tests', +) diff --git a/build/lib/voting/tests/tests.py b/build/lib/voting/tests/tests.py new file mode 100644 index 0000000..25bb5a8 --- /dev/null +++ b/build/lib/voting/tests/tests.py @@ -0,0 +1,86 @@ +r""" +>>> from django.contrib.auth.models import User +>>> from voting.models import Vote +>>> from voting.tests.models import Item + +########## +# Voting # +########## + +# 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) + +# 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([]) +{} +""" diff --git a/build/lib/voting/views.py b/build/lib/voting/views.py new file mode 100644 index 0000000..30d92a9 --- /dev/null +++ b/build/lib/voting/views.py @@ -0,0 +1,98 @@ +import json + +from django.contrib.contenttypes.models import ContentType +from django.http import HttpResponse, HttpResponseRedirect +from django.contrib.auth.decorators import login_required +from django.template import loader, RequestContext + +from .models import Vote + +VOTE_DIRECTIONS = (('up', 1), ('down', -1), ('clear', 0)) + + +@login_required +def generic_vote_on_object(request, model_name, object_id, direction, + post_vote_redirect=None, template_name=None, + template_loader=loader, extra_context=None, context_processors=None, + template_object_name='object', allow_xmlhttprequest=False): + """ + Really generic object vote function. + Gets object and model via content_type. + Expects URL format: + {% url 'generic_vote' 'content_type' object.id '' %} + + The given template will be used to confirm the vote if this view is + fetched using GET; vote registration will only be performed if this + view is POSTed. + + If ``allow_xmlhttprequest`` is ``True`` and an XMLHttpRequest is + detected by examining the ``HTTP_X_REQUESTED_WITH`` header, the + ``xmlhttp_vote_on_object`` view will be used to process the + request - this makes it trivial to implement voting via + XMLHttpRequest with a fallback for users who don't have JavaScript + enabled. + + Templates:``/_confirm_vote.html`` + Context: + object + The object being voted on. + direction + The type of vote which will be registered for the object. + """ + + try: + vote = dict(VOTE_DIRECTIONS)[direction] + except KeyError: + raise AttributeError('\'%s\' is not a valid vote type.' % direction) + + # TO-DO: check by app_label, also + ctype = ContentType.objects.filter(model=model_name)[0] + obj = ctype.get_object_for_this_type(pk=object_id) + + if request.is_ajax() and request.method == 'GET': + return json_error_response('XMLHttpRequest votes can only be made using POST.') + + Vote.objects.record_vote(obj, request.user, vote) + + if request.is_ajax(): + return HttpResponse(json.dumps({'success': True, + 'score': Vote.objects.get_score(obj)})) + + if extra_context is None: + extra_context = {} + + # Look up the object to be voted on + if request.method == 'POST': + if post_vote_redirect is not None: + next = post_vote_redirect + elif 'next' in request.REQUEST: + next = request.REQUEST['next'] + elif hasattr(obj, 'get_absolute_url'): + if callable(getattr(obj, 'get_absolute_url')): + next = obj.get_absolute_url() + else: + next = obj.get_absolute_url + else: + raise AttributeError('Generic vote view must be called with either post_vote_redirect, a "next" parameter in the request, or the object being voted on must define a get_absolute_url method or property.') + Vote.objects.record_vote(obj, request.user, vote) + return HttpResponseRedirect(next) + else: + if not template_name: + template_name = 'voting/confirm_vote.html' + t = template_loader.get_template(template_name) + c = RequestContext(request, { + template_object_name: obj, + 'direction': direction, + }, context_processors) + for key, value in extra_context.items(): + if callable(value): + c[key] = value() + else: + c[key] = value + response = HttpResponse(t.render(c)) + return response + + +def json_error_response(error_message, *args, **kwargs): + return HttpResponse(json.dumps(dict(success=False, + error_message=error_message))) diff --git a/dist/tango_voting-0.3.2-py3-none-any.whl b/dist/tango_voting-0.3.2-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..eaa95e414bd7c820104e396723afecfa540313e4 GIT binary patch literal 10956 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 F{{feXCIkQg literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py index 57bf4ac..1132d10 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='tango-voting', - version='0.3.2', + version='0.4.0', author='Jonathan Buchanan/Tim Baxter', author_email='mail.baxter@gmail.com', url='https://github.com/tBaxter/django-voting', diff --git a/voting/views.py b/voting/views.py index 016d86b..30d92a9 100644 --- a/voting/views.py +++ b/voting/views.py @@ -5,7 +5,7 @@ from django.contrib.auth.decorators import login_required from django.template import loader, RequestContext -from models import Vote +from .models import Vote VOTE_DIRECTIONS = (('up', 1), ('down', -1), ('clear', 0)) From 6d5a7dc3ccd43ca75d19867abc2e3e6d6a024225 Mon Sep 17 00:00:00 2001 From: TBaxter Date: Fri, 29 Jun 2018 14:15:02 -0500 Subject: [PATCH 12/13] Corrected is_anonymous check --- voting/managers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voting/managers.py b/voting/managers.py index 71d6d57..936c401 100644 --- a/voting/managers.py +++ b/voting/managers.py @@ -177,7 +177,7 @@ def get_for_user(self, obj, user): Get the vote made on the given object by the given user, or ``None`` if no matching vote exists. """ - if not user.is_authenticated(): + if not user.is_authenticated: return None ctype = ContentType.objects.get_for_model(obj) try: From fb234191fa0b2e565d02d4999e712d6fa4411b62 Mon Sep 17 00:00:00 2001 From: TBaxter Date: Fri, 29 Jun 2018 14:15:46 -0500 Subject: [PATCH 13/13] Corrected is_anonymous check --- CHANGELOG.txt | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ec56b7d..2a37a4f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -2,6 +2,9 @@ Django Voting Changelog ======================= +### 0.4.1 +Corrected is_authenticated check + ### 0.4.0 Updated for Django 2.0 compatibility. diff --git a/setup.py b/setup.py index 1132d10..c2edf35 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='tango-voting', - version='0.4.0', + version='0.4.1', author='Jonathan Buchanan/Tim Baxter', author_email='mail.baxter@gmail.com', url='https://github.com/tBaxter/django-voting',