├── voting ├── __init__.py ├── tests │ ├── __init__.py │ ├── TEST │ ├── urls.py │ ├── models.py │ ├── manage.py │ ├── settings.py │ └── tests.py ├── templatetags │ ├── __init__.py │ └── voting_tags.py ├── admin.py ├── vote_types.py ├── middleware.py ├── models.py ├── views.py └── managers.py ├── INSTALL.txt ├── .gitignore ├── setup.py ├── README.rst └── LICENCE.txt /voting/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /voting/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /voting/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /voting/tests/TEST: -------------------------------------------------------------------------------- 1 | python manage.py test tests --settings=settings 2 | -------------------------------------------------------------------------------- /voting/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | 4 | urlpatterns = patterns('',) 5 | -------------------------------------------------------------------------------- /INSTALL.txt: -------------------------------------------------------------------------------- 1 | 2 | there is no setup.py yet.. 3 | 4 | but if you put the voting directory somewhere on you python path 5 | i should just work. 6 | -------------------------------------------------------------------------------- /voting/tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class Item(models.Model): 4 | name = models.CharField(max_length=50) 5 | 6 | def __str__(self): 7 | return self.name 8 | 9 | class Meta: 10 | ordering = ['name'] 11 | 12 | -------------------------------------------------------------------------------- /voting/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from models import Vote 4 | 5 | class VoteAdmin(admin.ModelAdmin): 6 | date_hierarchy = "time_stamp" 7 | list_display = ('direction', 'user', 'time_stamp') 8 | list_filter = ('user', 'time_stamp', ) 9 | 10 | admin.site.register(Vote, VoteAdmin) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | database.sqlite3 2 | dev.db 3 | *.pid 4 | *.socks 5 | *.db 6 | RUN 7 | democracy_log.txt 8 | emocracy_log.txt 9 | thirdparty_log.txt 10 | settings_local.py 11 | .ropeproject/ 12 | *.pyc 13 | *.pyo 14 | *.DS_Store 15 | *.mo 16 | *.swp 17 | *.swo 18 | *~ 19 | *.sqlite3 20 | *.log 21 | democracy/media/admin 22 | thirdparty/media/admin 23 | -------------------------------------------------------------------------------- /voting/tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /voting/tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | ROOT_URLCONF = 'urls' 4 | 5 | DIRNAME = os.path.dirname(__file__) 6 | 7 | DATABASE_ENGINE = 'sqlite3' 8 | DATABASE_NAME = os.path.join(DIRNAME, 'database.db') 9 | 10 | #DATABASE_ENGINE = 'mysql' 11 | #DATABASE_NAME = 'tagging_test' 12 | #DATABASE_USER = 'root' 13 | #DATABASE_PASSWORD = '' 14 | #DATABASE_HOST = 'localhost' 15 | #DATABASE_PORT = '3306' 16 | 17 | #DATABASE_ENGINE = 'postgresql_psycopg2' 18 | #DATABASE_NAME = 'tagging_test' 19 | #DATABASE_USER = 'postgres' 20 | #DATABASE_PASSWORD = '' 21 | #DATABASE_HOST = 'localhost' 22 | #DATABASE_PORT = '5432' 23 | 24 | INSTALLED_APPS = ( 25 | 'django.contrib.auth', 26 | 'django.contrib.contenttypes', 27 | 'voting', 28 | 'voting.tests', 29 | ) 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | import datetime 3 | 4 | setup( 5 | name = 'django-voting', 6 | version = "0.2", 7 | description = 'Generic voting application for Django', 8 | author = 'Jonathan Buchanan, Stephan Preeker', 9 | author_email = 'jonathan.buchanan@gmail.com, spreeker@gmail.com', 10 | url = 'http://github.com/spreeker/django-voting2', 11 | packages = ['voting', 'voting.templatetags', 'voting.tests'], 12 | classifiers = ['Development Status :: 4 - Beta', 13 | 'Environment :: Web Environment', 14 | 'Framework :: Django', 15 | 'Intended Audience :: Developers', 16 | 'License :: OSI Approved :: BSD License', 17 | 'Operating System :: OS Independent', 18 | 'Programming Language :: Python', 19 | 'Topic :: Utilities'], 20 | ) 21 | -------------------------------------------------------------------------------- /voting/vote_types.py: -------------------------------------------------------------------------------- 1 | """ 2 | Specify all kinds of different kinds of possible votes. 3 | 4 | this needs a more portable solution. 5 | 6 | note: it is i18n enabled. 7 | """ 8 | 9 | from django.utils.translation import ugettext_lazy as _ 10 | 11 | votes = { 12 | -1 : _(u"Against"), 13 | 0 : _(u"blank"), 14 | 1 : _(u"For"), 15 | } 16 | 17 | blank_votes = { 18 | # content related problems with issues: 19 | 10 : _(u'Unconvincing'), 20 | 11 : _(u'Not political'), 21 | 12 : _(u'Can\'t completely agree'), 22 | # form related problems with issues": 23 | 13 : _(u"Needs more work"), 24 | 14 : _(u"Badly worded"), 25 | 15 : _(u"Duplicate"), 26 | 16 : _(u'Unrelated source'), 27 | # personal considerations: 28 | 17 : _(u'I need to know more'), 29 | 18 : _(u'Ask me later'), 30 | 19 : _(u'Too personal'), 31 | } 32 | normal_votes = votes.copy() 33 | normal_votes.update(blank_votes) 34 | 35 | multiply_votes = { 36 | 20 : _("Multiply"), 37 | } 38 | 39 | possible_votes = normal_votes.copy() 40 | possible_votes.update(multiply_votes) 41 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | note 2 | ==== 3 | 4 | there are other, better maintained voting apps out there now. 5 | 6 | 7 | django-voting2 8 | ============== 9 | 10 | This app is heavily derived from the django-voting app. 11 | which can be found here:: 12 | 13 | http://code.google.com/p/django-voting/ 14 | 15 | I need to create an overview file like this one:: 16 | 17 | http://django-voting.googlecode.com/svn/trunk/docs/overview.txt 18 | 19 | This app is close to the original django-voting but improved in a few ways: 20 | 21 | Support for more vote types. 22 | I added support for many more votes types, easy changeable by editing voting/vote_types.py 23 | Now not only up and down votes but any kind of vote is supported. 24 | 25 | No more raw sql 26 | The original code contained raw sql. The newer django ORM has support for aggregation 27 | 28 | More manager functions 29 | I added more aggregation functions like 30 | get_controversial, which looks op voted objects which are have about the same amount of up and down votes. 31 | 32 | many small code clean ups, 33 | more consistent functions arguments, small but necessarily changes to the template tags to support more vote types. 34 | 35 | 36 | TODO 37 | ==== 38 | 39 | if you find this code useful don't hesitate to let me know. 40 | -------------------------------------------------------------------------------- /voting/middleware.py: -------------------------------------------------------------------------------- 1 | from profiles.models import UserProfile 2 | from issue.models import Issue 3 | from gamelogic import actions 4 | """ 5 | I wanted to proved voting for anynymous users. But once they registered i wanted their 6 | session votes saved as real votes. I did it with the use of some middleware. 7 | 8 | If you make your views save votes in the vote_history session variable with 9 | { object_id , direction } key values in it. on registration their votes will be saved 10 | as real votes if you add this code to your middleware. 11 | 12 | """ 13 | 14 | class VoteHistory: 15 | """ 16 | if a user did a bunch of votes when he or she was not logged in 17 | they should be loaded. 18 | """ 19 | def process_request(self, request): 20 | if request.user.is_authenticated(): 21 | if request.session.has_key("vote_history"): 22 | vote_history = request.session["vote_history"] 23 | del request.session["vote_history"] 24 | self.save_votes(request, request.user, vote_history) 25 | return None 26 | 27 | def save_votes(self, request, user, votes): 28 | """bulk load users votes to REAL saved votes. 29 | """ 30 | for issue_id, direction in votes.items(): 31 | try: 32 | issue = Issue.objects.get(id=issue_id) 33 | except Issue.DoesNotExist: 34 | continue 35 | try: 36 | actions.vote(user, issue, int(direction), keep_private=False) 37 | except ValueError: 38 | #wrong direction code. 39 | pass 40 | -------------------------------------------------------------------------------- /voting/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is the core of the democracy voting system. 3 | The Vote model in this module can be generically 4 | linked to anything that users whould be able to vote 5 | on. 6 | 7 | This module implements: 8 | -voting database interactions 9 | 10 | Note that game rules should not be added to this module. 11 | """ 12 | 13 | from datetime import datetime 14 | from django.db import models, IntegrityError 15 | from django.contrib.auth.models import User 16 | from django.contrib.contenttypes.models import ContentType 17 | from django.contrib.contenttypes import generic 18 | from django.utils.translation import ugettext_lazy as _ 19 | 20 | from voting.managers import VoteManager 21 | from voting.vote_types import possible_votes 22 | 23 | class Vote(models.Model): 24 | user = models.ForeignKey(User) 25 | 26 | content_type = models.ForeignKey(ContentType) 27 | object_id = models.PositiveIntegerField() 28 | payload = generic.GenericForeignKey('content_type', 'object_id') 29 | # vote AKA direction. 30 | direction = models.IntegerField(choices = possible_votes.items(), default=1 ) 31 | time_stamp = models.DateTimeField(editable = False , default=datetime.now ) 32 | # optional **kwargs 33 | is_archived = models.BooleanField(default = False) 34 | keep_private = models.BooleanField(default = False) 35 | api_interface = models.IntegerField(null=True , blank=True) #key naar oauth consumer 36 | 37 | objects = VoteManager() 38 | 39 | def __unicode__(self): 40 | return u"%s on %s by %s" % (self.direction, self.payload, self.user.username) 41 | 42 | class Meta: 43 | db_table = 'votes' 44 | -------------------------------------------------------------------------------- /voting/tests/tests.py: -------------------------------------------------------------------------------- 1 | r""" 2 | # to exectute these tests: 3 | # python manage.py test tests --settings=voting.tests.settings 4 | # 5 | >>> from django.contrib.auth.models import User 6 | >>> from voting.models import Vote 7 | >>> from voting.tests.models import Item 8 | 9 | ########## 10 | # Voting # 11 | ########## 12 | 13 | # Basic voting ############################################################### 14 | 15 | >>> i1 = Item.objects.create(name='i1') 16 | >>> users = [] 17 | >>> for username in ['u1', 'u2', 'u3', 'u4']: 18 | ... users.append(User.objects.create_user(username, '%s@test.com' % username, 'test')) 19 | >>> Vote.objects.get_object_votes(i1) 20 | {} 21 | >>> Vote.objects.record_vote(users[0], i1, 1) 22 | (False, False, ) 23 | >>> Vote.objects.get_object_votes(i1) 24 | {1: 1} 25 | >>> _, _, _ = Vote.objects.record_vote(users[0], i1, -1) 26 | >>> Vote.objects.get_object_votes(i1) 27 | {-1: 1} 28 | >>> for user in users: 29 | ... _, _, _ = Vote.objects.record_vote(user, i1, +1) 30 | >>> Vote.objects.get_object_votes(i1) 31 | {1: 4} 32 | >>> for user in users[:2]: 33 | ... _,_,_ = Vote.objects.record_vote(user, i1, 10) 34 | >>> Vote.objects.get_object_votes(i1) 35 | {0: 2, 1: 2, 10: 2} 36 | >>> for user in users[:2]: 37 | ... _,_,_ = Vote.objects.record_vote(user, i1, -1) 38 | >>> i2 = Item.objects.create(name='i2') 39 | >>> _,_,_ = Vote.objects.record_vote(users[0], i2, 10) 40 | >>> Vote.objects.get_object_votes(i1) 41 | {1: 2, -1: 2} 42 | >>> try: 43 | ... Vote.objects.record_vote(i1, user, -2) 44 | ... except ValueError: 45 | ... pass 46 | 47 | # Retrieval of votes ######################################################### 48 | 49 | >>> i3 = Item.objects.create(name='i3') 50 | >>> i4 = Item.objects.create(name='i4') 51 | >>> _,_,_ = Vote.objects.record_vote( users[0], i2, +1) 52 | >>> _ = Vote.objects.record_vote(users[0], i3, -1) 53 | >>> _ = Vote.objects.record_vote(users[0], i4, 11) 54 | >>> vote = Vote.objects.get_for_user(users[0], i2) 55 | >>> (vote.direction) 56 | 1 57 | >>> vote = Vote.objects.get_for_user(users[0], i3 ) 58 | >>> (vote.direction) 59 | -1 60 | >>> vote = Vote.objects.get_for_user(users[0], i4) 61 | >>> vote.direction 62 | 11 63 | 64 | ## In bulk 65 | 66 | >>> votes = Vote.objects.get_for_user_in_bulk(users[0], [i1, i2, i3, i4]) 67 | >>> [(id, vote.direction) for id, vote in votes.items()] 68 | [(1, -1), (2, 1), (3, -1), (4, 11)] 69 | >>> Vote.objects.get_for_user_in_bulk(users[0], []) 70 | {} 71 | 72 | >>> for user in users[1:]: 73 | ... _ = Vote.objects.record_vote(user, i2, +1) 74 | ... _ = Vote.objects.record_vote(user, i3, +1) 75 | ... _ = Vote.objects.record_vote(user, i4, +1) 76 | 77 | >>> votes = Vote.objects.get_for_user_in_bulk(users[1], [i1, i2, i3, i4]) 78 | >>> [(id, vote.direction) for id, vote in votes.items()] 79 | [(1, -1), (2, 1), (3, 1), (4, 1)] 80 | 81 | >>> Vote.objects.get_for_objects_in_bulk([i1, i2, i3, i4]) 82 | {1: {1: 2, -1: 2}, 2: {1: 4}, 3: {1: 3, -1: 1}, 4: {0: 1, 1: 3, 11: 1}} 83 | >>> Vote.objects.get_for_objects_in_bulk([]) 84 | {} 85 | 86 | # Popular ################################################# 87 | 88 | [(1, 4), (2, 4), (3, 4), (4, 4), (6, 2), (5, 1)] 89 | >>> qs = Vote.objects.get_controversial(Item, min_tv=2) 90 | >>> qs.values_list('object_id' , 'avg') 91 | [(1, 0.0)] 92 | >>> qs = Vote.objects.get_top(Item) 93 | >>> qs.values_list('object_id' , 'score') 94 | [(2, 1.0), (4, 1.0), (3, 0.5), (1, 0.0)] 95 | >>> qs = Vote.objects.get_count(Item) 96 | >>> qs.values_list('object_id' , 'score') 97 | [(2, 4), (3, 3), (4, 3), (1, 2)] 98 | """ 99 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | django-voting2 2 | -------------- 3 | 4 | Copyright (c) 2009, Stephan Preeker 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | 24 | django-voting 25 | ------------- 26 | 27 | Copyright (c) 2007, Jonathan Buchanan 28 | 29 | Permission is hereby granted, free of charge, to any person obtaining a copy of 30 | this software and associated documentation files (the "Software"), to deal in 31 | the Software without restriction, including without limitation the rights to 32 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 33 | the Software, and to permit persons to whom the Software is furnished to do so, 34 | subject to the following conditions: 35 | 36 | The above copyright notice and this permission notice shall be included in all 37 | copies or substantial portions of the Software. 38 | 39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 40 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 41 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 42 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 43 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 44 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 45 | 46 | CheeseRater 47 | ----------- 48 | 49 | Copyright (c) 2007, Jacob Kaplan-Moss 50 | All rights reserved. 51 | 52 | Redistribution and use in source and binary forms, with or without modification, 53 | are permitted provided that the following conditions are met: 54 | 55 | 1. Redistributions of source code must retain the above copyright notice, 56 | this list of conditions and the following disclaimer. 57 | 58 | 2. Redistributions in binary form must reproduce the above copyright 59 | notice, this list of conditions and the following disclaimer in the 60 | documentation and/or other materials provided with the distribution. 61 | 62 | 3. Neither the name of Django nor the names of its contributors may be used 63 | to endorse or promote products derived from this software without 64 | specific prior written permission. 65 | 66 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 67 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 68 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 69 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 70 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 71 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 72 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 73 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 74 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 75 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 76 | -------------------------------------------------------------------------------- /voting/views.py: -------------------------------------------------------------------------------- 1 | # Vote! 2 | 3 | from django.contrib.contenttypes.models import ContentType 4 | from django.core.exceptions import ObjectDoesNotExist 5 | from django.http import Http404, HttpResponse, HttpResponseRedirect 6 | from django.contrib.auth.views import redirect_to_login 7 | from django.template import loader, RequestContext 8 | from django.utils import simplejson 9 | 10 | from voting.models import Vote 11 | from voting.models import possible_votes 12 | 13 | def vote_on_object(request, model, direction, post_vote_redirect=None, 14 | object_id=None, slug=None, slug_field=None, template_name=None, 15 | template_loader=loader, extra_context=None, context_processors=None, 16 | template_object_name='object', allow_xmlhttprequest=False): 17 | """ 18 | Generic object vote function. 19 | 20 | The given template will be used to confirm the vote if this view is 21 | fetched using GET; vote registration will only be performed if this 22 | view is POSTed. 23 | 24 | If ``allow_xmlhttprequest`` is ``True`` and an XMLHttpRequest is 25 | detected by examining the ``HTTP_X_REQUESTED_WITH`` header, the 26 | ``xmlhttp_vote_on_object`` view will be used to process the 27 | request - this makes it trivial to implement voting via 28 | XMLHttpRequest with a fallback for users who don't have JavaScript 29 | enabled. 30 | 31 | Templates:``/_confirm_vote.html`` 32 | Context: 33 | object 34 | The object being voted on. 35 | direction 36 | The type of vote which will be registered for the object. 37 | """ 38 | if allow_xmlhttprequest and request.is_ajax(): 39 | return xmlhttprequest_vote_on_object(request, model, direction, 40 | object_id=object_id, slug=slug, 41 | slug_field=slug_field) 42 | if extra_context is None: extra_context = {} 43 | if not request.user.is_authenticated(): 44 | return redirect_to_login(request.path) 45 | 46 | try: 47 | direction = int(direction) 48 | vote = possible_votes[direction] 49 | except KeyError: 50 | raise AttributeError("'%s' is not a valid vote type." % direction ) 51 | 52 | # Look up the object to be voted on 53 | lookup_kwargs = {} 54 | if object_id: 55 | lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id 56 | elif slug and slug_field: 57 | lookup_kwargs['%s__exact' % slug_field] = slug 58 | else: 59 | raise AttributeError('Generic vote view must be called with either ' 60 | 'object_id or slug and slug_field.') 61 | try: 62 | obj = model._default_manager.get(**lookup_kwargs) 63 | except ObjectDoesNotExist: 64 | raise Http404, 'No %s found for %s.' % (model._meta.app_label, lookup_kwargs) 65 | 66 | if request.method == 'POST': 67 | if post_vote_redirect is not None: 68 | next = post_vote_redirect 69 | elif request.REQUEST.has_key('next'): 70 | next = request.REQUEST['next'] 71 | elif hasattr(obj, 'get_absolute_url'): 72 | if callable(getattr(obj, 'get_absolute_url')): 73 | next = obj.get_absolute_url() 74 | else: 75 | next = obj.get_absolute_url 76 | else: 77 | raise AttributeError('Generic vote view must be called with either ' 78 | 'post_vote_redirect, a "next" parameter in ' 79 | 'the request, or the object being voted on ' 80 | 'must define a get_absolute_url method or ' 81 | 'property.') 82 | Vote.objects.record_vote(request.user, obj, direction) 83 | return HttpResponseRedirect(next) 84 | else: 85 | if not template_name: 86 | template_name = '%s/%s_confirm_vote.html' % ( 87 | model._meta.app_label, model._meta.object_name.lower()) 88 | t = template_loader.get_template(template_name) 89 | c = RequestContext(request, { 90 | template_object_name: obj, 91 | 'direction': direction, 92 | }, context_processors) 93 | for key, value in extra_context.items(): 94 | if callable(value): 95 | c[key] = value() 96 | else: 97 | c[key] = value 98 | response = HttpResponse(t.render(c)) 99 | return response 100 | 101 | def json_error_response(error_message): 102 | return HttpResponse(simplejson.dumps(dict(success=False, 103 | error_message=error_message))) 104 | 105 | def xmlhttprequest_vote_on_object(request, model, direction, 106 | object_id=None, slug=None, slug_field=None): 107 | """ 108 | Generic object vote function for use via XMLHttpRequest. 109 | 110 | Properties of the resulting JSON object: 111 | success 112 | ``true`` if the vote was successfully processed, ``false`` 113 | otherwise. 114 | score 115 | The object's updated score and number of votes if the vote 116 | was successfully processed. 117 | error_message 118 | Contains an error message if the vote was not successfully 119 | processed. 120 | """ 121 | if request.method == 'GET': 122 | return json_error_response( 123 | 'XMLHttpRequest votes can only be made using POST.') 124 | if not request.user.is_authenticated(): 125 | return json_error_response('Not authenticated.') 126 | 127 | try: 128 | vote = possible_votes[direction] 129 | except KeyError: 130 | return json_error_response( 131 | '\'%s\' is not a valid vote type.' % direction) 132 | 133 | # Look up the object to be voted on 134 | lookup_kwargs = {} 135 | if object_id: 136 | lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id 137 | elif slug and slug_field: 138 | lookup_kwargs['%s__exact' % slug_field] = slug 139 | else: 140 | return json_error_response('Generic XMLHttpRequest vote view must be ' 141 | 'called with either object_id or slug and ' 142 | 'slug_field.') 143 | try: 144 | obj = model._default_manager.get(**lookup_kwargs) 145 | except ObjectDoesNotExist: 146 | return json_error_response( 147 | 'No %s found for %s.' % (model._meta.verbose_name, lookup_kwargs)) 148 | 149 | # Vote and respond 150 | Vote.objects.record_vote(request.user, obj, vote) 151 | return HttpResponse(simplejson.dumps({ 152 | 'success': True, 153 | 'score': Vote.objects.get_object_votes(obj), 154 | })) 155 | -------------------------------------------------------------------------------- /voting/templatetags/voting_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.utils.html import escape 3 | 4 | from voting.models import Vote 5 | from voting.managers import possible_votes 6 | 7 | 8 | register = template.Library() 9 | 10 | class VoteByUserNode(template.Node): 11 | def __init__(self, user, object, context_var): 12 | self.user = user 13 | self.object = object 14 | self.context_var = context_var 15 | 16 | def render(self, context): 17 | try: 18 | user = template.resolve_variable(self.user, context) 19 | object = template.resolve_variable(self.object, context) 20 | except template.VariableDoesNotExist: 21 | return '' 22 | context[self.context_var] = Vote.objects.get_for_user(user, object) 23 | return '' 24 | 25 | class VotesByUserNode(template.Node): 26 | def __init__(self, user, objects, context_var): 27 | self.user = user 28 | self.objects = objects 29 | self.context_var = context_var 30 | #TODO if anonymous get votes from session 31 | def render(self, context): 32 | try: 33 | user = template.resolve_variable(self.user, context) 34 | objects = template.resolve_variable(self.objects, context) 35 | except template.VariableDoesNotExist: 36 | return '' 37 | context[self.context_var] = Vote.objects.get_for_user_in_bulk(user, objects) 38 | return '' 39 | 40 | class VotesForObjectNode(template.Node): 41 | def __init__(self, object, context_var): 42 | self.object = object 43 | self.context_var = context_var 44 | 45 | def render(self, context): 46 | try: 47 | object = template.resolve_variable(self.object, context) 48 | except template.VariableDoesNotExist: 49 | return '' 50 | 51 | vote_counts = Vote.objects.get_object_votes(object) 52 | votes = dict.fromkeys( possible_votes.keys(), 0 ) 53 | votes.update(vote_counts) 54 | votes[2] = votes[-1] # we cannot index -1 in templates. 55 | context[self.context_var] = votes 56 | return '' 57 | 58 | class VotesForObjectsNode(template.Node): 59 | def __init__(self, objects, context_var): 60 | self.objects = objects 61 | self.context_var = context_var 62 | 63 | def render(self, context): 64 | try: 65 | objects = template.resolve_variable(self.objects, context) 66 | except template.VariableDoesNotExist: 67 | return '' 68 | 69 | counts_on_objects = Vote.objects.get_for_objects_in_bulk(objects) 70 | 71 | for object_id in counts_on_objects.keys(): 72 | # all vote types default to 0. 73 | vote_counts = dict.fromkeys(possible_votes.keys(), 0) 74 | vote_counts.update(counts_on_objects[object_id]) 75 | vote_counts[2] = vote_counts[-1] # we cannot index -1 in templates. 76 | vote_counts.pop(-1) 77 | counts_on_objects[object_id] = vote_counts 78 | 79 | context[self.context_var] = counts_on_objects 80 | return '' 81 | 82 | class DictEntryForItemNode(template.Node): 83 | def __init__(self, item, dictionary, context_var): 84 | self.item = item 85 | self.dictionary = dictionary 86 | self.context_var = context_var 87 | 88 | def render(self, context): 89 | try: 90 | dictionary = template.resolve_variable(self.dictionary, context) 91 | item = template.resolve_variable(self.item, context) 92 | except template.VariableDoesNotExist: 93 | return '' 94 | context[self.context_var] = dictionary.get(item.id, None) 95 | return '' 96 | 97 | 98 | def get_vote_by_user(parser, token): 99 | """ 100 | Retrieves the ``Vote`` cast by a user on a particular object and 101 | stores it in a context variable. If the user has not voted, the 102 | context variable will be ``None``. 103 | 104 | Example usage:: 105 | 106 | {% vote_by_user user on widget as vote %} 107 | """ 108 | bits = token.contents.split() 109 | if len(bits) != 6: 110 | raise template.TemplateSyntaxError("'%s' tag takes exactly five arguments" % bits[0]) 111 | if bits[2] != 'on': 112 | raise template.TemplateSyntaxError("second argument to '%s' tag must be 'on'" % bits[0]) 113 | if bits[4] != 'as': 114 | raise template.TemplateSyntaxError("fourth argument to '%s' tag must be 'as'" % bits[0]) 115 | return VoteByUserNode(bits[1], bits[3], bits[5]) 116 | 117 | def get_votes_by_user(parser, token): 118 | """ 119 | Retrieves the votes cast by a user on a list of objects as a 120 | dictionary keyed with object ids and stores it in a context 121 | variable. 122 | 123 | Example usage:: 124 | 125 | {% votes_by_user user on widget_list as vote_dict %} 126 | """ 127 | bits = token.contents.split() 128 | if len(bits) != 6: 129 | raise template.TemplateSyntaxError("'%s' tag takes exactly four arguments" % bits[0]) 130 | if bits[2] != 'on': 131 | raise template.TemplateSyntaxError("second argument to '%s' tag must be 'on'" % bits[0]) 132 | if bits[4] != 'as': 133 | raise template.TemplateSyntaxError("fourth argument to '%s' tag must be 'as'" % bits[0]) 134 | return VotesByUserNode(bits[1], bits[3], bits[5]) 135 | 136 | def get_vote_counts_for_object(parser, token): 137 | """ 138 | Retrieves number of votes for an object 139 | it's received and stores them in a context variable which has 140 | ``vote`` and ``num_votes`` properties. 141 | 142 | Example usage:: 143 | 144 | {% vote_counts_for_object widget as votes %} 145 | 146 | {{ score.score }}point{{ score.score|pluralize }} 147 | after {{ score.num_votes }} vote{{ score.num_votes|pluralize }} 148 | """ 149 | bits = token.contents.split() 150 | if len(bits) != 4: 151 | raise template.TemplateSyntaxError("'%s' tag takes exactly three arguments" % bits[0]) 152 | if bits[2] != 'as': 153 | raise template.TemplateSyntaxError("second argument to '%s' tag must be 'as'" % bits[0]) 154 | return VotesForObjectNode(bits[1], bits[3]) 155 | 156 | def get_vote_counts_for_objects(parser, token): 157 | """ 158 | Retrieves the total vote count for a list of objects and the number of 159 | votes they have received and stores them in a context variable. 160 | 161 | Example usage:: 162 | 163 | {% vote_counts_for_objects widget_list as vote_dict %} 164 | """ 165 | bits = token.contents.split() 166 | if len(bits) != 4: 167 | raise template.TemplateSyntaxError("'%s' tag takes exactly three arguments" % bits[0]) 168 | if bits[2] != 'as': 169 | raise template.TemplateSyntaxError("second argument to '%s' tag must be 'as'" % bits[0]) 170 | return VotesForObjectsNode(bits[1], bits[3]) 171 | 172 | def get_dict_entry_for_item(parser, token): 173 | """ 174 | Given an object and a dictionary keyed with object ids - as 175 | returned by the ``votes_by_user`` and ``vote_counts_for_objects`` 176 | template tags - retrieves the value for the given object and 177 | stores it in a context variable, storing ``None`` if no value 178 | exists for the given object. 179 | 180 | Example usage:: 181 | 182 | {% dict_entry_for_item widget from vote_dict as vote %} 183 | """ 184 | bits = token.contents.split() 185 | if len(bits) != 6: 186 | raise template.TemplateSyntaxError("'%s' tag takes exactly five arguments" % bits[0]) 187 | if bits[2] != 'from': 188 | raise template.TemplateSyntaxError("second argument to '%s' tag must be 'from'" % bits[0]) 189 | if bits[4] != 'as': 190 | raise template.TemplateSyntaxError("fourth argument to '%s' tag must be 'as'" % bits[0]) 191 | return DictEntryForItemNode(bits[1], bits[3], bits[5]) 192 | 193 | 194 | register.tag('vote_by_user', get_vote_by_user) 195 | register.tag('votes_by_user', get_votes_by_user) 196 | register.tag('dict_entry_for_item', get_dict_entry_for_item) 197 | register.tag('vote_counts_for_object', get_vote_counts_for_object) 198 | register.tag('vote_counts_for_objects', get_vote_counts_for_objects) 199 | 200 | # Filters 201 | 202 | def vote_display(vote): 203 | """ 204 | Given a string mapping values for up and down votes, returns one 205 | of the strings according to the given ``Vote``: 206 | 207 | Example usage:: 208 | 209 | {{ vote|vote_display }} 210 | """ 211 | vote = vote if vote != 2 else -1 # dealing with the -1 case. 212 | return possible_votes[vote] 213 | 214 | register.filter(vote_display) 215 | 216 | 217 | -------------------------------------------------------------------------------- /voting/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models, IntegrityError 2 | from django.db import connection 3 | from django.db.models import Avg, Count, Sum 4 | 5 | from django.contrib.auth.models import User 6 | from django.contrib.contenttypes.models import ContentType 7 | from django.contrib.contenttypes import generic 8 | from django.utils.translation import ugettext_lazy as _ 9 | from django.core.exceptions import ObjectDoesNotExist 10 | 11 | #XXX likely to change 12 | from vote_types import votes, blank_votes, normal_votes 13 | from vote_types import possible_votes, multiply_votes 14 | 15 | class VoteManager(models.Manager): 16 | 17 | def get_for_object(self, obj): 18 | """ 19 | Get queryset for votes on object 20 | """ 21 | ctype = ContentType.objects.get_for_model(obj) 22 | return self.filter(content_type = ctype.pk, 23 | object_id = obj.pk) 24 | 25 | def get_for_model(self, Model): 26 | ctype = ContentType.objects.get_for_model(Model) 27 | return self.filter(content_type = ctype.pk) 28 | 29 | def get_for_user(self, user , obj): 30 | """ 31 | Get the vote made on an give object by user return None if it not exists 32 | """ 33 | object_id = obj._get_pk_val() 34 | ctype = ContentType.objects.get_for_model(obj) 35 | try: 36 | return self.get(content_type=ctype, object_id=object_id, is_archived=False, user=user ) 37 | except ObjectDoesNotExist: 38 | return None 39 | 40 | def get_for_user_in_bulk(self, user, objects): 41 | """ 42 | Get dictinary mapping object to vote for user on given objects. 43 | """ 44 | object_ids = [o._get_pk_val() for o in objects] 45 | if not object_ids: 46 | return {} 47 | if not user.is_authenticated(): 48 | return {} 49 | 50 | queryset = self.filter( user=user ) 51 | queryset = queryset.filter( is_archived=False ) 52 | ctype = ContentType.objects.get_for_model(objects[0]) 53 | queryset = queryset.filter(content_type=ctype, object_id__in=object_ids) 54 | votes = list(queryset) 55 | vote_dict = dict([( vote.object_id, vote ) for vote in votes ]) 56 | return vote_dict 57 | 58 | def get_user_votes(self, user, Model=None, obj=None): 59 | """ 60 | Get queryset for active votes by user 61 | """ 62 | queryset = self.filter(user=user, is_archived=False) 63 | if Model: 64 | ctype = ContentType.objects.get_for_model(Model) 65 | queryset = queryset.filter(content_type=ctype,) 66 | if obj: 67 | object_id = obj._get_pk_val() 68 | queryset = queryset.filter(object_id=object_id) 69 | 70 | return queryset.order_by("-time_stamp") 71 | 72 | def get_object_votes(self, obj, all=False): 73 | """ 74 | Get a dictionary mapping vote to votecount 75 | """ 76 | object_id = obj._get_pk_val() 77 | ctype = ContentType.objects.get_for_model(obj) 78 | queryset = self.filter(content_type=ctype, object_id=object_id) 79 | 80 | if not all: 81 | queryset = queryset.filter(is_archived=False) # only pick active votes 82 | 83 | queryset = queryset.values('direction') 84 | queryset = queryset.annotate(vcount=Count("direction")).order_by() 85 | 86 | vote_dict = {} 87 | 88 | for count in queryset: 89 | if count['direction'] >= 10 : # sum up all blank votes 90 | vote_dict[0] = vote_dict.get(0,0) + count['vcount'] 91 | vote_dict[count['direction']] = count['vcount'] 92 | 93 | return vote_dict 94 | 95 | def get_for_objects_in_bulk(self, objects, all=False): 96 | """ 97 | Get a dictinary mapping objects ids to dictinary 98 | which maps direction to votecount 99 | """ 100 | object_ids = [o._get_pk_val() for o in objects] 101 | if not object_ids: 102 | return {} 103 | ctype = ContentType.objects.get_for_model(objects[0]) 104 | queryset = self.filter(content_type=ctype, object_id__in=object_ids) 105 | 106 | if not all: # only pick active votes 107 | queryset = queryset.filter(is_archived=False) 108 | 109 | queryset = queryset.values('object_id', 'direction',) 110 | queryset = queryset.annotate(vcount=Count("direction")).order_by() 111 | 112 | vote_dict = {} 113 | for votecount in queryset: 114 | object_id = votecount['object_id'] 115 | votes = vote_dict.setdefault(object_id , {}) 116 | if votecount['direction'] >= 10: # sum up all blank votes 117 | votes[0] = votes.get(0,0) + votecount['vcount'] 118 | votes[votecount['direction']] = votecount['vcount'] 119 | 120 | return vote_dict 121 | 122 | def get_popular(self, Model, object_ids=None, reverse=False, min_tv=2): 123 | """ return queryset ordered by popularity 124 | 125 | min_tv = minimum total votes, to put in a threshold. 126 | """ 127 | ctype = ContentType.objects.get_for_model(Model) 128 | queryset = self.filter(content_type=ctype,) 129 | queryset = queryset.filter(is_archived=False) 130 | 131 | if object_ids: # to get the most popular from a list 132 | queryset = queryset.filter(object_id__in=object_ids) 133 | 134 | queryset = queryset.values('object_id',) 135 | queryset = queryset.annotate(score=Count("direction")).order_by() 136 | queryset = queryset.filter(score__gt=min_tv) 137 | 138 | if reverse: 139 | return queryset.order_by('score') 140 | return queryset.order_by('-score') 141 | 142 | 143 | def get_count(self, Model, object_ids=None, direction=1): 144 | """ 145 | Find list ordered by count of votes for a direction. 146 | """ 147 | ctype = ContentType.objects.get_for_model(Model) 148 | queryset = self.filter(content_type=ctype,) 149 | queryset = queryset.filter(is_archived=False) 150 | 151 | if object_ids: # to get the most popular from a list 152 | queryset = queryset.filter(object_id__in=object_ids) 153 | 154 | queryset = queryset.values('object_id',) 155 | queryset = queryset.filter(direction=direction) 156 | queryset = queryset.annotate(score=Count("direction")).order_by() 157 | queryset = queryset.order_by('-score') 158 | 159 | return queryset 160 | 161 | def get_top(self, Model, object_ids=None, reverse=False, min_tv=1): 162 | """ 163 | Find the votes which are possitive recieved. 164 | 165 | min_tv = minimum total votes, to put in a threshold. 166 | """ 167 | ctype = ContentType.objects.get_for_model(Model) 168 | queryset = self.filter(content_type=ctype,) 169 | queryset = queryset.filter(is_archived=False) 170 | 171 | if object_ids: # to get the most popular from a list 172 | queryset = queryset.filter(object_id__in=object_ids) 173 | 174 | queryset = queryset.values('object_id',) 175 | queryset = queryset.filter(direction__in=[-1,1]) 176 | queryset = queryset.annotate(totalvotes=Count("direction")) 177 | queryset = queryset.filter(totalvotes__gt=min_tv) 178 | queryset = queryset.annotate(score=Avg("direction")).order_by() 179 | if reverse: 180 | queryset = queryset.order_by('score') 181 | else: 182 | queryset = queryset.order_by('-score') 183 | 184 | return queryset 185 | 186 | def get_bottom(self, Model, object_ids=None, min_tv=2): 187 | """ 188 | Find the votes which are worst recieved 189 | """ 190 | queryset = self.get_top(Model, object_ids, reverse=True, min_tv=2) 191 | 192 | return queryset 193 | 194 | def get_controversial(self, Model, object_ids=None, min_tv=1): 195 | """ 196 | return queryset ordered by controversy , 197 | meaning it divides the ppl in 50/50. 198 | since for is 1 and against is -1, a score close to 0 199 | indicates controversy. 200 | 201 | this is working by aproximation and should be good enough for most cases. 202 | """ 203 | ctype = ContentType.objects.get_for_model(Model) 204 | queryset = self.filter(content_type=ctype,) 205 | queryset = queryset.filter(is_archived=False) 206 | queryset = queryset.filter(direction__in=[-1,1]) 207 | 208 | if object_ids: # to get the most popular from a list 209 | queryset = queryset.filter(object_id__in=object_ids) 210 | elif min_tv > 1: 211 | queryset = queryset.annotate(totalvotes=Count("direction")) 212 | queryset = queryset.filter(totalvotes__gt=min_tv) 213 | 214 | queryset = queryset.values('object_id',) 215 | queryset = queryset.annotate(avg=Avg("direction")) 216 | queryset = queryset.order_by('avg') 217 | queryset = queryset.filter(avg__gt= -0.3 ) 218 | queryset = queryset.filter(avg__lt= 0.3 ) 219 | #queryset = queryset.values_list('object_id' , 'avg') 220 | 221 | return queryset 222 | 223 | def get_for_direction(self, Model, directions=[1,-1]): 224 | """ 225 | return object_ids with a specific direction. 226 | """ 227 | ctype = ContentType.objects.get_for_model(Model) 228 | queryset = self.filter(content_type=ctype,) 229 | queryset = queryset.filter(is_archived=False) 230 | queryset = queryset.filter(direction__in=directions) 231 | queryset = queryset.values('object_id',) 232 | 233 | return queryset 234 | 235 | def record_vote(self, user, obj, direction, 236 | keep_private=False, api_interface=None, 237 | directions=normal_votes.keys()): 238 | """ 239 | Archive old votes by switching the is_archived flag to True 240 | for all the previous votes on by . 241 | And we check for and dismiss a repeated vote. 242 | We save old votes for research, probable interesting 243 | opinion changes. 244 | """ 245 | if not direction in directions: 246 | raise ValueError('Invalid vote %s must be in %s' % (direction, directions)) 247 | 248 | ctype = ContentType.objects.get_for_model(obj) 249 | votes = self.filter(user=user, content_type=ctype, object_id=obj._get_pk_val(), is_archived=False) 250 | votes = votes.filter(direction__in=directions) 251 | 252 | voted_already = False 253 | repeated_vote = False 254 | if votes: 255 | voted_already = True 256 | for v in votes: 257 | if direction == v.direction: #check if you do the same vote again. 258 | repeated_vote = True 259 | else: 260 | v.is_archived = True 261 | v.save() 262 | vote = None 263 | if not repeated_vote: 264 | vote = self.create( user=user, content_type=ctype, 265 | object_id=obj._get_pk_val(), direction=direction, 266 | api_interface=api_interface, is_archived=False, 267 | keep_private=keep_private 268 | ) 269 | vote.save() 270 | return repeated_vote, voted_already, vote 271 | 272 | # Generic annotation code to annotate queryset from other Models with 273 | # data from the Vote table 274 | # 275 | # NOTE: my experience is that below raw sql works but is slow. 276 | # 277 | #issues = vote_annotate(issues, Vote.payload, 'vote', Sum, desc=False) 278 | # 279 | # more info: 280 | # 281 | # http://djangosnippets.org/snippets/2034/ 282 | # http://github.com/coleifer/django-simple-ratings/ 283 | # 284 | from django.contrib.contenttypes.models import ContentType 285 | from django.db import connection, models 286 | 287 | def vote_annotate(queryset, gfk_field, aggregate_field, aggregator=models.Sum, desc=True): 288 | ordering = desc and '-vscore' or 'vscore' 289 | content_type = ContentType.objects.get_for_model(queryset.model) 290 | 291 | qn = connection.ops.quote_name 292 | 293 | # collect the params we'll be using 294 | params = ( 295 | aggregator.name, # the function that's doing the aggregation 296 | qn(aggregate_field), # the field containing the value to aggregate 297 | qn(gfk_field.model._meta.db_table), # table holding gfk'd item info 298 | qn(gfk_field.ct_field + '_id'), # the content_type field on the GFK 299 | content_type.pk, # the content_type id we need to match 300 | qn(gfk_field.fk_field), # the object_id field on the GFK 301 | qn(queryset.model._meta.db_table), # the table and pk from the main 302 | qn(queryset.model._meta.pk.name) # part of the query 303 | ) 304 | 305 | extra = """ 306 | SELECT %s(%s) AS aggregate_score 307 | FROM %s 308 | WHERE ( 309 | %s=%s AND 310 | %s=%s.%s AND 311 | is_archived=FALSE AND 312 | "votes"."direction" IN (-1,1)) 313 | """ % params 314 | 315 | queryset = queryset.extra(select={ 316 | 'vscore': extra 317 | }, 318 | order_by=[ordering] 319 | ) 320 | 321 | return queryset 322 | --------------------------------------------------------------------------------