├── CHANGELOG.txt ├── INSTALL.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.txt ├── docs └── overview.txt ├── setup.py └── voting ├── __init__.py ├── admin.py ├── managers.py ├── models.py ├── templatetags ├── __init__.py └── voting_tags.py ├── tests ├── __init__.py ├── models.py ├── runtests.py ├── settings.py └── tests.py └── views.py /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | ======================= 2 | Django Voting Changelog 3 | ======================= -------------------------------------------------------------------------------- /INSTALL.txt: -------------------------------------------------------------------------------- 1 | Thanks for downloading django-voting. 2 | 3 | To install it, run the following command inside this directory: 4 | 5 | python setup.py install 6 | 7 | Or if you'd prefer you can simply place the included ``voting`` 8 | directory somewhere on your Python path, or symlink to it from 9 | somewhere on your Python path; this is useful if you're working from a 10 | Subversion checkout. 11 | 12 | Note that this application requires Python 2.3 or later, and Django 13 | 0.97-pre or later. You can obtain Python from http://www.python.org/ and 14 | Django from http://www.djangoproject.com/. -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | django-voting 2 | ------------- 3 | 4 | Copyright (c) 2007, Jonathan Buchanan 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 | CheeseRater 24 | ----------- 25 | 26 | Copyright (c) 2007, Jacob Kaplan-Moss 27 | All rights reserved. 28 | 29 | Redistribution and use in source and binary forms, with or without modification, 30 | are permitted provided that the following conditions are met: 31 | 32 | 1. Redistributions of source code must retain the above copyright notice, 33 | this list of conditions and the following disclaimer. 34 | 35 | 2. Redistributions in binary form must reproduce the above copyright 36 | notice, this list of conditions and the following disclaimer in the 37 | documentation and/or other materials provided with the distribution. 38 | 39 | 3. Neither the name of Django nor the names of its contributors may be used 40 | to endorse or promote products derived from this software without 41 | specific prior written permission. 42 | 43 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 44 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 45 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 46 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 47 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 48 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 49 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 50 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 51 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 52 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.txt 2 | include INSTALL.txt 3 | include LICENSE.txt 4 | include MANIFEST.in 5 | include README.txt 6 | recursive-include docs * 7 | recursive-include voting/tests * 8 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | ============= 2 | Django Voting 3 | ============= 4 | 5 | This is a generic voting application for Django projects 6 | 7 | For installation instructions, see the file "INSTALL.txt" in this 8 | directory; for instructions on how to use this application, and on 9 | what it provides, see the file "overview.txt" in the "docs/" 10 | directory. -------------------------------------------------------------------------------- /docs/overview.txt: -------------------------------------------------------------------------------- 1 | ============== 2 | Django Voting 3 | ============== 4 | 5 | A generic voting application for Django projects, which allows 6 | registering of votes for any ``Model`` instance. 7 | 8 | 9 | Installation 10 | ============ 11 | 12 | Google Code recommends doing the Subversion checkout like so:: 13 | 14 | svn checkout http://django-voting.googlecode.com/svn/trunk/ django-voting 15 | 16 | But the hyphen in the application name can cause issues installing 17 | into a DB, so it's really better to do this:: 18 | 19 | svn checkout http://django-voting.googlecode.com/svn/trunk/ voting 20 | 21 | If you've already downloaded, rename the directory before installing. 22 | 23 | To install django-voting, do the following: 24 | 25 | 1. Put the ``voting`` folder somewhere on your Python path. 26 | 2. Put ``'voting'`` in your ``INSTALLED_APPS`` setting. 27 | 3. Run the command ``manage.py syncdb``. 28 | 29 | The ``syncdb`` command creates the necessary database tables and 30 | creates permission objects for all installed apps that need them. 31 | 32 | That's it! 33 | 34 | 35 | Votes 36 | ===== 37 | 38 | Votes are represented by the ``Vote`` model, which lives in the 39 | ``voting.models`` module. 40 | 41 | API reference 42 | ------------- 43 | 44 | Fields 45 | ~~~~~~ 46 | 47 | ``Vote`` objects have the following fields: 48 | 49 | * ``user`` -- The user who made the vote. Users are represented by 50 | the ``django.contrib.auth.models.User`` model. 51 | * ``content_type`` -- The ContentType of the object voted on. 52 | * ``object_id`` -- The id of the object voted on. 53 | * ``object`` -- The object voted on. 54 | * ``vote`` -- The vote which was made: ``+1`` or ``-1``. 55 | 56 | Methods 57 | ~~~~~~~ 58 | 59 | ``Vote`` objects have the following custom methods: 60 | 61 | * ``is_upvote`` -- Returns ``True`` if ``vote`` is ``+1``. 62 | 63 | * ``is_downvote`` -- Returns ``True`` if ``vote`` is ``-1``. 64 | 65 | Manager functions 66 | ~~~~~~~~~~~~~~~~~ 67 | 68 | The ``Vote`` model has a custom manager that has the following helper 69 | functions: 70 | 71 | * ``record_vote(obj, user, vote)`` -- Record a user's vote on a 72 | given object. Only allows a given user to vote once on any given 73 | object, but the vote may be changed. 74 | 75 | ``vote`` must be one of ``1`` (up vote), ``-1`` (down vote) or 76 | ``0`` (remove vote). 77 | 78 | * ``get_score(obj)`` -- Gets the total score for ``obj`` and the 79 | total number of votes it's received. 80 | 81 | Returns a dictionary with ``score`` and ``num_votes`` keys. 82 | 83 | * ``get_scores_in_bulk(objects)`` -- Gets score and vote count 84 | details for all the given objects. Score details consist of a 85 | dictionary which has ``score`` and ``num_vote`` keys. 86 | 87 | Returns a dictionary mapping object ids to score details. 88 | 89 | * ``get_top(Model, limit=10, reversed=False)`` -- Gets the top 90 | ``limit`` scored objects for a given model. 91 | 92 | If ``reversed`` is ``True``, the bottom ``limit`` scored objects 93 | are retrieved instead. 94 | 95 | Yields ``(object, score)`` tuples. 96 | 97 | * ``get_bottom(Model, limit=10)`` -- A convenience method which 98 | calls ``get_top`` with ``reversed=True``. 99 | 100 | Gets the bottom (i.e. most negative) ``limit`` scored objects 101 | for a given model. 102 | 103 | Yields ``(object, score)`` tuples. 104 | 105 | * ``get_for_user(obj, user)`` -- Gets the vote made on the given 106 | object by the given user, or ``None`` if no matching vote 107 | exists. 108 | 109 | * ``get_for_user_in_bulk(objects, user)`` -- Gets the votes 110 | made on all the given objects by the given user. 111 | 112 | Returns a dictionary mapping object ids to votes. 113 | 114 | Basic usage 115 | ----------- 116 | 117 | Recording votes and retrieving scores 118 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 119 | 120 | Votes are recorded using the ``record_vote`` helper function:: 121 | 122 | >>> from django.contrib.auth.models import User 123 | >>> from shop.apps.products.models import Widget 124 | >>> from voting.models import Vote 125 | >>> user = User.objects.get(pk=1) 126 | >>> widget = Widget.objects.get(pk=1) 127 | >>> Vote.objects.record_vote(widget, user, +1) 128 | 129 | The score for an object can be retrieved using the ``get_score`` 130 | helper function:: 131 | 132 | >>> Vote.objects.get_score(widget) 133 | {'score': 1, 'num_votes': 1} 134 | 135 | If the same user makes another vote on the same object, their vote 136 | is either modified or deleted, as appropriate:: 137 | 138 | >>> Vote.objects.record_vote(widget, user, -1) 139 | >>> Vote.objects.get_score(widget) 140 | {'score': -1, 'num_votes': 1} 141 | >>> Vote.objects.record_vote(widget, user, 0) 142 | >>> Vote.objects.get_score(widget) 143 | {'score': 0, 'num_votes': 0} 144 | 145 | 146 | Generic Views 147 | ============= 148 | 149 | The ``voting.views`` module contains views to handle a couple of 150 | common cases: displaying a page to confirm a vote when it is requested 151 | via ``GET`` and making the vote itself via ``POST``, or voting via 152 | XMLHttpRequest ``POST``. 153 | 154 | The following sample URLconf demonstrates using a generic view for 155 | voting on a model, allowing for regular voting and XMLHttpRequest 156 | voting at the same URL:: 157 | 158 | from django.conf.urls.defaults import * 159 | from voting.views import vote_on_object 160 | from shop.apps.products.models import Widget 161 | 162 | widget_dict = { 163 | 'model': Widget, 164 | 'template_object_name': 'widget', 165 | 'allow_xmlhttprequest': True, 166 | } 167 | 168 | urlpatterns = patterns('', 169 | (r'^widgets/(?P\d+)/(?Pup|down|clear)vote/?$', vote_on_object, widget_dict), 170 | ) 171 | 172 | ``voting.views.vote_on_object`` 173 | -------------------------------- 174 | 175 | **Description:** 176 | 177 | A view that displays a confirmation page and votes on an object. The 178 | given object will only be voted on if the request method is ``POST``. 179 | If this view is fetched via ``GET``, it will display a confirmation 180 | page that should contain a form that ``POST``\s to the same URL. 181 | 182 | **Required arguments:** 183 | 184 | * ``model``: The Django model class of the object that will be 185 | voted on. 186 | 187 | * Either ``object_id`` or (``slug`` *and* ``slug_field``) is 188 | required. 189 | 190 | If you provide ``object_id``, it should be the value of the 191 | primary-key field for the object being voted on. 192 | 193 | Otherwise, ``slug`` should be the slug of the given object, and 194 | ``slug_field`` should be the name of the slug field in the 195 | ``QuerySet``'s model. 196 | 197 | * ``direction``: The kind of vote to be made, must be one of 198 | ``'up'``, ``'down'`` or ``'clear'``. 199 | 200 | * Either a ``post_vote_redirect`` argument defining a URL must 201 | be supplied, or a ``next`` parameter must supply a URL in the 202 | request when the vote is ``POST``\ed, or the object being voted 203 | on must define a ``get_absolute_url`` method or property. 204 | 205 | The view checks for these in the order given above. 206 | 207 | **Optional arguments:** 208 | 209 | * ``allow_xmlhttprequest``: A boolean that designates whether this 210 | view should also allow votes to be made via XMLHttpRequest. 211 | 212 | If this is ``True``, the request headers will be check for an 213 | ``HTTP_X_REQUESTED_WITH`` header which has a value of 214 | ``XMLHttpRequest``. If this header is found, processing of the 215 | current request is delegated to 216 | ``voting.views.xmlhttprequest_vote_on_object``. 217 | 218 | * ``template_name``: The full name of a template to use in 219 | rendering the page. This lets you override the default template 220 | name (see below). 221 | 222 | * ``template_loader``: The template loader to use when loading the 223 | template. By default, it's ``django.template.loader``. 224 | 225 | * ``extra_context``: A dictionary of values to add to the template 226 | context. By default, this is an empty dictionary. If a value in 227 | the dictionary is callable, the generic view will call it just 228 | before rendering the template. 229 | 230 | * ``context_processors``: A list of template-context processors to 231 | apply to the view's template. 232 | 233 | * ``template_object_name``: Designates the name of the template 234 | variable to use in the template context. By default, this is 235 | ``'object'``. 236 | 237 | **Template name:** 238 | 239 | If ``template_name`` isn't specified, this view will use the template 240 | ``/_confirm_vote.html`` by default. 241 | 242 | **Template context:** 243 | 244 | In addition to ``extra_context``, the template's context will be: 245 | 246 | * ``object``: The original object that's about to be voted on. 247 | This variable's name depends on the ``template_object_name`` 248 | parameter, which is ``'object'`` by default. If 249 | ``template_object_name`` is ``'foo'``, this variable's name will 250 | be ``foo``. 251 | 252 | * ``direction``: The argument which was given for the vote's 253 | ``direction`` (see above). 254 | 255 | ``voting.views.xmlhttprequest_vote_on_object`` 256 | ----------------------------------------------- 257 | 258 | **Description:** 259 | 260 | A view for use in voting on objects via XMLHttpRequest. The given 261 | object will only be voted on if the request method is ``POST``. This 262 | view will respond with JSON text instead of rendering a template or 263 | redirecting. 264 | 265 | **Required arguments:** 266 | 267 | * ``model``: The Django model class of the object that will be 268 | voted on. 269 | 270 | * Either ``object_id`` or (``slug`` *and* ``slug_field``) is 271 | required. 272 | 273 | If you provide ``object_id``, it should be the value of the 274 | primary-key field for the object being voted on. 275 | 276 | Otherwise, ``slug`` should be the slug of the given object, and 277 | ``slug_field`` should be the name of the slug field in the 278 | ``QuerySet``'s model. 279 | 280 | * ``direction``: The kind of vote to be made, must be one of 281 | ``'up'``, ``'down'`` or ``'clear'``. 282 | 283 | **JSON text context:** 284 | 285 | The context provided by the JSON text returned will be: 286 | 287 | * ``success``: ``true`` if the vote was successfully processed, 288 | ``false`` otherwise. 289 | 290 | * ``score``: an object containing a ``score`` property, which 291 | holds the object's updated score, and a ``num_votes`` property, 292 | which holds the total number of votes cast for the object. 293 | 294 | * ``error_message``: if the vote was not successfully processed, 295 | this property will contain an error message. 296 | 297 | 298 | Template tags 299 | ============= 300 | 301 | The ``voting.templatetags.voting_tags`` module defines a number of 302 | template tags which may be used to retrieve and display voting 303 | details. 304 | 305 | Tag reference 306 | ------------- 307 | 308 | score_for_object 309 | ~~~~~~~~~~~~~~~~ 310 | 311 | Retrieves the total score for an object and the number of votes 312 | it's received, storing them in a context variable which has ``score`` 313 | and ``num_votes`` properties. 314 | 315 | Example usage:: 316 | 317 | {% score_for_object widget as score %} 318 | 319 | {{ score.score }} point{{ score.score|pluralize }} 320 | after {{ score.num_votes }} vote{{ score.num_votes|pluralize }} 321 | 322 | scores_for_objects 323 | ~~~~~~~~~~~~~~~~~~ 324 | 325 | Retrieves the total scores and number of votes cast for a list of 326 | objects as a dictionary keyed with the objects' ids and stores it in a 327 | context variable. 328 | 329 | Example usage:: 330 | 331 | {% scores_for_objects widget_list as scores %} 332 | 333 | vote_by_user 334 | ~~~~~~~~~~~~ 335 | 336 | Retrieves the ``Vote`` cast by a user on a particular object and 337 | stores it in a context variable. If the user has not voted, the 338 | context variable will be ``None``. 339 | 340 | Example usage:: 341 | 342 | {% vote_by_user user on widget as vote %} 343 | 344 | votes_by_user 345 | ~~~~~~~~~~~~~ 346 | 347 | Retrieves the votes cast by a user on a list of objects as a 348 | dictionary keyed with object ids and stores it in a context 349 | variable. 350 | 351 | Example usage:: 352 | 353 | {% votes_by_user user on widget_list as vote_dict %} 354 | 355 | dict_entry_for_item 356 | ~~~~~~~~~~~~~~~~~~~ 357 | 358 | Given an object and a dictionary keyed with object ids - as returned 359 | by the ``votes_by_user`` and ``scores_for_objects`` template tags - 360 | retrieves the value for the given object and stores it in a context 361 | variable, storing ``None`` if no value exists for the given object. 362 | 363 | Example usage:: 364 | 365 | {% dict_entry_for_item widget from vote_dict as vote %} 366 | 367 | confirm_vote_message 368 | ~~~~~~~~~~~~~~~~~~~~ 369 | 370 | Intended for use in vote confirmatio templates, creates an appropriate 371 | message asking the user to confirm the given vote for the given object 372 | description. 373 | 374 | Example usage:: 375 | 376 | {% confirm_vote_message widget.title direction %} 377 | 378 | Filter reference 379 | ---------------- 380 | 381 | vote_display 382 | ~~~~~~~~~~~~ 383 | 384 | Given a string mapping values for up and down votes, returns one 385 | of the strings according to the given ``Vote``: 386 | 387 | ========= ===================== ============= 388 | Vote type Argument Outputs 389 | ========= ===================== ============= 390 | ``+1`` ``'Bodacious,Bogus'`` ``Bodacious`` 391 | ``-1`` ``'Bodacious,Bogus'`` ``Bogus`` 392 | ========= ===================== ============= 393 | 394 | If no string mapping is given, ``'Up'`` and ``'Down'`` will be used. 395 | 396 | Example usage:: 397 | 398 | {{ vote|vote_display:"Bodacious,Bogus" }} -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | from distutils.command.install import INSTALL_SCHEMES 3 | 4 | # Tell distutils to put the data_files in platform-specific installation 5 | # locations. See here for an explanation: 6 | # http://groups.google.com/group/comp.lang.python/browse_thread/thread/35ec7b2fed36eaec/2105ee4d9e8042cb 7 | for scheme in INSTALL_SCHEMES.values(): 8 | scheme['data'] = scheme['purelib'] 9 | 10 | # Dynamically calculate the version based on tagging.VERSION. 11 | version_tuple = __import__('voting').VERSION 12 | if version_tuple[2] is not None: 13 | version = "%d.%d_%s" % version_tuple 14 | else: 15 | version = "%d.%d" % version_tuple[:2] 16 | 17 | setup( 18 | name = 'django-voting', 19 | version = version, 20 | description = 'Generic voting application for Django', 21 | author = 'Jonathan Buchanan', 22 | author_email = 'jonathan.buchanan@gmail.com', 23 | url = 'http://code.google.com/p/django-voting/', 24 | packages = ['voting', 'voting.templatetags', 'voting.tests'], 25 | classifiers = ['Development Status :: 4 - Beta', 26 | 'Environment :: Web Environment', 27 | 'Framework :: Django', 28 | 'Intended Audience :: Developers', 29 | 'License :: OSI Approved :: BSD License', 30 | 'Operating System :: OS Independent', 31 | 'Programming Language :: Python', 32 | 'Topic :: Utilities'], 33 | ) -------------------------------------------------------------------------------- /voting/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (0, 1, None) -------------------------------------------------------------------------------- /voting/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from voting.models import Vote 3 | 4 | admin.site.register(Vote) 5 | -------------------------------------------------------------------------------- /voting/managers.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import connection, models 3 | 4 | try: 5 | from django.db.models.sql.aggregates import Aggregate 6 | except ImportError: 7 | supports_aggregates = False 8 | else: 9 | supports_aggregates = True 10 | 11 | from django.contrib.contenttypes.models import ContentType 12 | 13 | if supports_aggregates: 14 | class CoalesceWrapper(Aggregate): 15 | sql_template = 'COALESCE(%(function)s(%(field)s), %(default)s)' 16 | 17 | def __init__(self, lookup, **extra): 18 | self.lookup = lookup 19 | self.extra = extra 20 | 21 | def _default_alias(self): 22 | return '%s__%s' % (self.lookup, self.__class__.__name__.lower()) 23 | default_alias = property(_default_alias) 24 | 25 | def add_to_query(self, query, alias, col, source, is_summary): 26 | super(CoalesceWrapper, self).__init__(col, source, is_summary, **self.extra) 27 | query.aggregate_select[alias] = self 28 | 29 | 30 | class CoalesceSum(CoalesceWrapper): 31 | sql_function = 'SUM' 32 | 33 | 34 | class CoalesceCount(CoalesceWrapper): 35 | sql_function = 'COUNT' 36 | 37 | 38 | class VoteManager(models.Manager): 39 | def get_score(self, obj): 40 | """ 41 | Get a dictionary containing the total score for ``obj`` and 42 | the number of votes it's received. 43 | """ 44 | ctype = ContentType.objects.get_for_model(obj) 45 | result = self.filter(object_id=obj._get_pk_val(), 46 | content_type=ctype).extra( 47 | select={ 48 | 'score': 'COALESCE(SUM(vote), 0)', 49 | 'num_votes': 'COALESCE(COUNT(vote), 0)', 50 | }).values_list('score', 'num_votes')[0] 51 | 52 | return { 53 | 'score': int(result[0]), 54 | 'num_votes': int(result[1]), 55 | } 56 | 57 | def get_scores_in_bulk(self, objects): 58 | """ 59 | Get a dictionary mapping object ids to total score and number 60 | of votes for each object. 61 | """ 62 | object_ids = [o._get_pk_val() for o in objects] 63 | if not object_ids: 64 | return {} 65 | 66 | ctype = ContentType.objects.get_for_model(objects[0]) 67 | 68 | if supports_aggregates: 69 | queryset = self.filter( 70 | object_id__in = object_ids, 71 | content_type = ctype, 72 | ).values( 73 | 'object_id', 74 | ).annotate( 75 | score = CoalesceSum('vote', default='0'), 76 | num_votes = CoalesceCount('vote', default='0'), 77 | ) 78 | else: 79 | queryset = self.filter( 80 | object_id__in = object_ids, 81 | content_type = ctype, 82 | ).extra( 83 | select = { 84 | 'score': 'COALESCE(SUM(vote), 0)', 85 | 'num_votes': 'COALESCE(COUNT(vote), 0)', 86 | } 87 | ).values('object_id', 'score', 'num_votes') 88 | queryset.query.group_by.append('object_id') 89 | 90 | vote_dict = {} 91 | for row in queryset: 92 | vote_dict[row['object_id']] = { 93 | 'score': int(row['score']), 94 | 'num_votes': int(row['num_votes']), 95 | } 96 | 97 | return vote_dict 98 | 99 | def record_vote(self, obj, user, vote): 100 | """ 101 | Record a user's vote on a given object. Only allows a given user 102 | to vote once, though that vote may be changed. 103 | 104 | A zero vote indicates that any existing vote should be removed. 105 | """ 106 | if vote not in (+1, 0, -1): 107 | raise ValueError('Invalid vote (must be +1/0/-1)') 108 | ctype = ContentType.objects.get_for_model(obj) 109 | try: 110 | v = self.get(user=user, content_type=ctype, 111 | object_id=obj._get_pk_val()) 112 | if vote == 0: 113 | v.delete() 114 | else: 115 | v.vote = vote 116 | v.save() 117 | except models.ObjectDoesNotExist: 118 | if vote != 0: 119 | self.create(user=user, content_type=ctype, 120 | object_id=obj._get_pk_val(), vote=vote) 121 | 122 | def get_top(self, Model, limit=10, reversed=False): 123 | """ 124 | Get the top N scored objects for a given model. 125 | 126 | Yields (object, score) tuples. 127 | """ 128 | ctype = ContentType.objects.get_for_model(Model) 129 | query = """ 130 | SELECT object_id, SUM(vote) as %s 131 | FROM %s 132 | WHERE content_type_id = %%s 133 | GROUP BY object_id""" % ( 134 | connection.ops.quote_name('score'), 135 | connection.ops.quote_name(self.model._meta.db_table), 136 | ) 137 | 138 | # MySQL has issues with re-using the aggregate function in the 139 | # HAVING clause, so we alias the score and use this alias for 140 | # its benefit. 141 | if settings.DATABASE_ENGINE == 'mysql': 142 | having_score = connection.ops.quote_name('score') 143 | else: 144 | having_score = 'SUM(vote)' 145 | if reversed: 146 | having_sql = ' HAVING %(having_score)s < 0 ORDER BY %(having_score)s ASC LIMIT %%s' 147 | else: 148 | having_sql = ' HAVING %(having_score)s > 0 ORDER BY %(having_score)s DESC LIMIT %%s' 149 | query += having_sql % { 150 | 'having_score': having_score, 151 | } 152 | 153 | cursor = connection.cursor() 154 | cursor.execute(query, [ctype.id, limit]) 155 | results = cursor.fetchall() 156 | 157 | # Use in_bulk() to avoid O(limit) db hits. 158 | objects = Model.objects.in_bulk([id for id, score in results]) 159 | 160 | # Yield each object, score pair. Because of the lazy nature of generic 161 | # relations, missing objects are silently ignored. 162 | for id, score in results: 163 | if id in objects: 164 | yield objects[id], int(score) 165 | 166 | def get_bottom(self, Model, limit=10): 167 | """ 168 | Get the bottom (i.e. most negative) N scored objects for a given 169 | model. 170 | 171 | Yields (object, score) tuples. 172 | """ 173 | return self.get_top(Model, limit, True) 174 | 175 | def get_for_user(self, obj, user): 176 | """ 177 | Get the vote made on the given object by the given user, or 178 | ``None`` if no matching vote exists. 179 | """ 180 | if not user.is_authenticated(): 181 | return None 182 | ctype = ContentType.objects.get_for_model(obj) 183 | try: 184 | vote = self.get(content_type=ctype, object_id=obj._get_pk_val(), 185 | user=user) 186 | except models.ObjectDoesNotExist: 187 | vote = None 188 | return vote 189 | 190 | def get_for_user_in_bulk(self, objects, user): 191 | """ 192 | Get a dictionary mapping object ids to votes made by the given 193 | user on the corresponding objects. 194 | """ 195 | vote_dict = {} 196 | if len(objects) > 0: 197 | ctype = ContentType.objects.get_for_model(objects[0]) 198 | votes = list(self.filter(content_type__pk=ctype.id, 199 | object_id__in=[obj._get_pk_val() \ 200 | for obj in objects], 201 | user__pk=user.id)) 202 | vote_dict = dict([(vote.object_id, vote) for vote in votes]) 203 | return vote_dict 204 | -------------------------------------------------------------------------------- /voting/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes import generic 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.contrib.auth.models import User 4 | from django.db import models 5 | 6 | from voting.managers import VoteManager 7 | 8 | SCORES = ( 9 | (u'+1', +1), 10 | (u'-1', -1), 11 | ) 12 | 13 | class Vote(models.Model): 14 | """ 15 | A vote on an object by a User. 16 | """ 17 | user = models.ForeignKey(User) 18 | content_type = models.ForeignKey(ContentType) 19 | object_id = models.PositiveIntegerField() 20 | object = generic.GenericForeignKey('content_type', 'object_id') 21 | vote = models.SmallIntegerField(choices=SCORES) 22 | 23 | objects = VoteManager() 24 | 25 | class Meta: 26 | db_table = 'votes' 27 | # One vote per user per object 28 | unique_together = (('user', 'content_type', 'object_id'),) 29 | 30 | def __unicode__(self): 31 | return u'%s: %s on %s' % (self.user, self.vote, self.object) 32 | 33 | def is_upvote(self): 34 | return self.vote == 1 35 | 36 | def is_downvote(self): 37 | return self.vote == -1 38 | -------------------------------------------------------------------------------- /voting/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brosner/django-voting/3b53109324a51dfc6245bcd31f10726882eee5c9/voting/templatetags/__init__.py -------------------------------------------------------------------------------- /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 | 6 | register = template.Library() 7 | 8 | # Tags 9 | 10 | class ScoreForObjectNode(template.Node): 11 | def __init__(self, object, context_var): 12 | self.object = object 13 | self.context_var = context_var 14 | 15 | def render(self, context): 16 | try: 17 | object = template.resolve_variable(self.object, context) 18 | except template.VariableDoesNotExist: 19 | return '' 20 | context[self.context_var] = Vote.objects.get_score(object) 21 | return '' 22 | 23 | class ScoresForObjectsNode(template.Node): 24 | def __init__(self, objects, context_var): 25 | self.objects = objects 26 | self.context_var = context_var 27 | 28 | def render(self, context): 29 | try: 30 | objects = template.resolve_variable(self.objects, context) 31 | except template.VariableDoesNotExist: 32 | return '' 33 | context[self.context_var] = Vote.objects.get_scores_in_bulk(objects) 34 | return '' 35 | 36 | class VoteByUserNode(template.Node): 37 | def __init__(self, user, object, context_var): 38 | self.user = user 39 | self.object = object 40 | self.context_var = context_var 41 | 42 | def render(self, context): 43 | try: 44 | user = template.resolve_variable(self.user, context) 45 | object = template.resolve_variable(self.object, context) 46 | except template.VariableDoesNotExist: 47 | return '' 48 | context[self.context_var] = Vote.objects.get_for_user(object, user) 49 | return '' 50 | 51 | class VotesByUserNode(template.Node): 52 | def __init__(self, user, objects, context_var): 53 | self.user = user 54 | self.objects = objects 55 | self.context_var = context_var 56 | 57 | def render(self, context): 58 | try: 59 | user = template.resolve_variable(self.user, context) 60 | objects = template.resolve_variable(self.objects, context) 61 | except template.VariableDoesNotExist: 62 | return '' 63 | context[self.context_var] = Vote.objects.get_for_user_in_bulk(objects, user) 64 | return '' 65 | 66 | class DictEntryForItemNode(template.Node): 67 | def __init__(self, item, dictionary, context_var): 68 | self.item = item 69 | self.dictionary = dictionary 70 | self.context_var = context_var 71 | 72 | def render(self, context): 73 | try: 74 | dictionary = template.resolve_variable(self.dictionary, context) 75 | item = template.resolve_variable(self.item, context) 76 | except template.VariableDoesNotExist: 77 | return '' 78 | context[self.context_var] = dictionary.get(item.id, None) 79 | return '' 80 | 81 | def do_score_for_object(parser, token): 82 | """ 83 | Retrieves the total score for an object and the number of votes 84 | it's received and stores them in a context variable which has 85 | ``score`` and ``num_votes`` properties. 86 | 87 | Example usage:: 88 | 89 | {% score_for_object widget as score %} 90 | 91 | {{ score.score }}point{{ score.score|pluralize }} 92 | after {{ score.num_votes }} vote{{ score.num_votes|pluralize }} 93 | """ 94 | bits = token.contents.split() 95 | if len(bits) != 4: 96 | raise template.TemplateSyntaxError("'%s' tag takes exactly three arguments" % bits[0]) 97 | if bits[2] != 'as': 98 | raise template.TemplateSyntaxError("second argument to '%s' tag must be 'as'" % bits[0]) 99 | return ScoreForObjectNode(bits[1], bits[3]) 100 | 101 | def do_scores_for_objects(parser, token): 102 | """ 103 | Retrieves the total scores for a list of objects and the number of 104 | votes they have received and stores them in a context variable. 105 | 106 | Example usage:: 107 | 108 | {% scores_for_objects widget_list as score_dict %} 109 | """ 110 | bits = token.contents.split() 111 | if len(bits) != 4: 112 | raise template.TemplateSyntaxError("'%s' tag takes exactly three arguments" % bits[0]) 113 | if bits[2] != 'as': 114 | raise template.TemplateSyntaxError("second argument to '%s' tag must be 'as'" % bits[0]) 115 | return ScoresForObjectsNode(bits[1], bits[3]) 116 | 117 | def do_vote_by_user(parser, token): 118 | """ 119 | Retrieves the ``Vote`` cast by a user on a particular object and 120 | stores it in a context variable. If the user has not voted, the 121 | context variable will be ``None``. 122 | 123 | Example usage:: 124 | 125 | {% vote_by_user user on widget as vote %} 126 | """ 127 | bits = token.contents.split() 128 | if len(bits) != 6: 129 | raise template.TemplateSyntaxError("'%s' tag takes exactly five 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 VoteByUserNode(bits[1], bits[3], bits[5]) 135 | 136 | def do_votes_by_user(parser, token): 137 | """ 138 | Retrieves the votes cast by a user on a list of objects as a 139 | dictionary keyed with object ids and stores it in a context 140 | variable. 141 | 142 | Example usage:: 143 | 144 | {% votes_by_user user on widget_list as vote_dict %} 145 | """ 146 | bits = token.contents.split() 147 | if len(bits) != 6: 148 | raise template.TemplateSyntaxError("'%s' tag takes exactly four arguments" % bits[0]) 149 | if bits[2] != 'on': 150 | raise template.TemplateSyntaxError("second argument to '%s' tag must be 'on'" % bits[0]) 151 | if bits[4] != 'as': 152 | raise template.TemplateSyntaxError("fourth argument to '%s' tag must be 'as'" % bits[0]) 153 | return VotesByUserNode(bits[1], bits[3], bits[5]) 154 | 155 | def do_dict_entry_for_item(parser, token): 156 | """ 157 | Given an object and a dictionary keyed with object ids - as 158 | returned by the ``votes_by_user`` and ``scores_for_objects`` 159 | template tags - retrieves the value for the given object and 160 | stores it in a context variable, storing ``None`` if no value 161 | exists for the given object. 162 | 163 | Example usage:: 164 | 165 | {% dict_entry_for_item widget from vote_dict as vote %} 166 | """ 167 | bits = token.contents.split() 168 | if len(bits) != 6: 169 | raise template.TemplateSyntaxError("'%s' tag takes exactly five arguments" % bits[0]) 170 | if bits[2] != 'from': 171 | raise template.TemplateSyntaxError("second argument to '%s' tag must be 'from'" % bits[0]) 172 | if bits[4] != 'as': 173 | raise template.TemplateSyntaxError("fourth argument to '%s' tag must be 'as'" % bits[0]) 174 | return DictEntryForItemNode(bits[1], bits[3], bits[5]) 175 | 176 | register.tag('score_for_object', do_score_for_object) 177 | register.tag('scores_for_objects', do_scores_for_objects) 178 | register.tag('vote_by_user', do_vote_by_user) 179 | register.tag('votes_by_user', do_votes_by_user) 180 | register.tag('dict_entry_for_item', do_dict_entry_for_item) 181 | 182 | # Simple Tags 183 | 184 | def confirm_vote_message(object_description, vote_direction): 185 | """ 186 | Creates an appropriate message asking the user to confirm the given vote 187 | for the given object description. 188 | 189 | Example usage:: 190 | 191 | {% confirm_vote_message widget.title direction %} 192 | """ 193 | if vote_direction == 'clear': 194 | message = 'Confirm clearing your vote for %s.' 195 | else: 196 | message = 'Confirm %s vote for %%s.' % vote_direction 197 | return message % (escape(object_description),) 198 | 199 | register.simple_tag(confirm_vote_message) 200 | 201 | # Filters 202 | 203 | def vote_display(vote, arg=None): 204 | """ 205 | Given a string mapping values for up and down votes, returns one 206 | of the strings according to the given ``Vote``: 207 | 208 | ========= ===================== ============= 209 | Vote type Argument Outputs 210 | ========= ===================== ============= 211 | ``+1`` ``"Bodacious,Bogus"`` ``Bodacious`` 212 | ``-1`` ``"Bodacious,Bogus"`` ``Bogus`` 213 | ========= ===================== ============= 214 | 215 | If no string mapping is given, "Up" and "Down" will be used. 216 | 217 | Example usage:: 218 | 219 | {{ vote|vote_display:"Bodacious,Bogus" }} 220 | """ 221 | if arg is None: 222 | arg = 'Up,Down' 223 | bits = arg.split(',') 224 | if len(bits) != 2: 225 | return vote.vote # Invalid arg 226 | up, down = bits 227 | if vote.vote == 1: 228 | return up 229 | return down 230 | 231 | register.filter(vote_display) -------------------------------------------------------------------------------- /voting/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brosner/django-voting/3b53109324a51dfc6245bcd31f10726882eee5c9/voting/tests/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /voting/tests/runtests.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | os.environ['DJANGO_SETTINGS_MODULE'] = 'voting.tests.settings' 3 | 4 | from django.test.simple import run_tests 5 | 6 | failures = run_tests(None, verbosity=9) 7 | if failures: 8 | sys.exit(failures) 9 | -------------------------------------------------------------------------------- /voting/tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | DIRNAME = os.path.dirname(__file__) 4 | 5 | DATABASE_ENGINE = 'sqlite3' 6 | DATABASE_NAME = os.path.join(DIRNAME, 'database.db') 7 | 8 | #DATABASE_ENGINE = 'mysql' 9 | #DATABASE_NAME = 'tagging_test' 10 | #DATABASE_USER = 'root' 11 | #DATABASE_PASSWORD = '' 12 | #DATABASE_HOST = 'localhost' 13 | #DATABASE_PORT = '3306' 14 | 15 | #DATABASE_ENGINE = 'postgresql_psycopg2' 16 | #DATABASE_NAME = 'tagging_test' 17 | #DATABASE_USER = 'postgres' 18 | #DATABASE_PASSWORD = '' 19 | #DATABASE_HOST = 'localhost' 20 | #DATABASE_PORT = '5432' 21 | 22 | INSTALLED_APPS = ( 23 | 'django.contrib.auth', 24 | 'django.contrib.contenttypes', 25 | 'voting', 26 | 'voting.tests', 27 | ) 28 | -------------------------------------------------------------------------------- /voting/tests/tests.py: -------------------------------------------------------------------------------- 1 | r""" 2 | >>> from django.contrib.auth.models import User 3 | >>> from voting.models import Vote 4 | >>> from voting.tests.models import Item 5 | 6 | ########## 7 | # Voting # 8 | ########## 9 | 10 | # Basic voting ############################################################### 11 | 12 | >>> i1 = Item.objects.create(name='test1') 13 | >>> users = [] 14 | >>> for username in ['u1', 'u2', 'u3', 'u4']: 15 | ... users.append(User.objects.create_user(username, '%s@test.com' % username, 'test')) 16 | >>> Vote.objects.get_score(i1) 17 | {'score': 0, 'num_votes': 0} 18 | >>> Vote.objects.record_vote(i1, users[0], +1) 19 | >>> Vote.objects.get_score(i1) 20 | {'score': 1, 'num_votes': 1} 21 | >>> Vote.objects.record_vote(i1, users[0], -1) 22 | >>> Vote.objects.get_score(i1) 23 | {'score': -1, 'num_votes': 1} 24 | >>> Vote.objects.record_vote(i1, users[0], 0) 25 | >>> Vote.objects.get_score(i1) 26 | {'score': 0, 'num_votes': 0} 27 | >>> for user in users: 28 | ... Vote.objects.record_vote(i1, user, +1) 29 | >>> Vote.objects.get_score(i1) 30 | {'score': 4, 'num_votes': 4} 31 | >>> for user in users[:2]: 32 | ... Vote.objects.record_vote(i1, user, 0) 33 | >>> Vote.objects.get_score(i1) 34 | {'score': 2, 'num_votes': 2} 35 | >>> for user in users[:2]: 36 | ... Vote.objects.record_vote(i1, user, -1) 37 | >>> Vote.objects.get_score(i1) 38 | {'score': 0, 'num_votes': 4} 39 | 40 | >>> Vote.objects.record_vote(i1, user, -2) 41 | Traceback (most recent call last): 42 | ... 43 | ValueError: Invalid vote (must be +1/0/-1) 44 | 45 | # Retrieval of votes ######################################################### 46 | 47 | >>> i2 = Item.objects.create(name='test2') 48 | >>> i3 = Item.objects.create(name='test3') 49 | >>> i4 = Item.objects.create(name='test4') 50 | >>> Vote.objects.record_vote(i2, users[0], +1) 51 | >>> Vote.objects.record_vote(i3, users[0], -1) 52 | >>> Vote.objects.record_vote(i4, users[0], 0) 53 | >>> vote = Vote.objects.get_for_user(i2, users[0]) 54 | >>> (vote.vote, vote.is_upvote(), vote.is_downvote()) 55 | (1, True, False) 56 | >>> vote = Vote.objects.get_for_user(i3, users[0]) 57 | >>> (vote.vote, vote.is_upvote(), vote.is_downvote()) 58 | (-1, False, True) 59 | >>> Vote.objects.get_for_user(i4, users[0]) is None 60 | True 61 | 62 | # In bulk 63 | >>> votes = Vote.objects.get_for_user_in_bulk([i1, i2, i3, i4], users[0]) 64 | >>> [(id, vote.vote) for id, vote in votes.items()] 65 | [(1, -1), (2, 1), (3, -1)] 66 | >>> Vote.objects.get_for_user_in_bulk([], users[0]) 67 | {} 68 | 69 | >>> for user in users[1:]: 70 | ... Vote.objects.record_vote(i2, user, +1) 71 | ... Vote.objects.record_vote(i3, user, +1) 72 | ... Vote.objects.record_vote(i4, user, +1) 73 | >>> list(Vote.objects.get_top(Item)) 74 | [(, 4), (, 3), (, 2)] 75 | >>> for user in users[1:]: 76 | ... Vote.objects.record_vote(i2, user, -1) 77 | ... Vote.objects.record_vote(i3, user, -1) 78 | ... Vote.objects.record_vote(i4, user, -1) 79 | >>> list(Vote.objects.get_bottom(Item)) 80 | [(, -4), (, -3), (, -2)] 81 | 82 | >>> Vote.objects.get_scores_in_bulk([i1, i2, i3, i4]) 83 | {1: {'score': 0, 'num_votes': 4}, 2: {'score': -2, 'num_votes': 4}, 3: {'score': -4, 'num_votes': 4}, 4: {'score': -3, 'num_votes': 3}} 84 | >>> Vote.objects.get_scores_in_bulk([]) 85 | {} 86 | """ 87 | -------------------------------------------------------------------------------- /voting/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes.models import ContentType 2 | from django.core.exceptions import ObjectDoesNotExist 3 | from django.http import Http404, HttpResponse, HttpResponseRedirect 4 | from django.contrib.auth.views import redirect_to_login 5 | from django.template import loader, RequestContext 6 | from django.utils import simplejson 7 | 8 | from voting.models import Vote 9 | 10 | VOTE_DIRECTIONS = (('up', 1), ('down', -1), ('clear', 0)) 11 | 12 | def vote_on_object(request, model, direction, post_vote_redirect=None, 13 | object_id=None, slug=None, slug_field=None, template_name=None, 14 | template_loader=loader, extra_context=None, context_processors=None, 15 | template_object_name='object', allow_xmlhttprequest=False): 16 | """ 17 | Generic object vote function. 18 | 19 | The given template will be used to confirm the vote if this view is 20 | fetched using GET; vote registration will only be performed if this 21 | view is POSTed. 22 | 23 | If ``allow_xmlhttprequest`` is ``True`` and an XMLHttpRequest is 24 | detected by examining the ``HTTP_X_REQUESTED_WITH`` header, the 25 | ``xmlhttp_vote_on_object`` view will be used to process the 26 | request - this makes it trivial to implement voting via 27 | XMLHttpRequest with a fallback for users who don't have JavaScript 28 | enabled. 29 | 30 | Templates:``/_confirm_vote.html`` 31 | Context: 32 | object 33 | The object being voted on. 34 | direction 35 | The type of vote which will be registered for the object. 36 | """ 37 | if allow_xmlhttprequest and request.is_ajax(): 38 | return xmlhttprequest_vote_on_object(request, model, direction, 39 | object_id=object_id, slug=slug, 40 | slug_field=slug_field) 41 | 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 | vote = dict(VOTE_DIRECTIONS)[direction] 48 | except KeyError: 49 | raise AttributeError("'%s' is not a valid vote type." % vote_type) 50 | 51 | # Look up the object to be voted on 52 | lookup_kwargs = {} 53 | if object_id: 54 | lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id 55 | elif slug and slug_field: 56 | lookup_kwargs['%s__exact' % slug_field] = slug 57 | else: 58 | raise AttributeError('Generic vote view must be called with either ' 59 | 'object_id or slug and slug_field.') 60 | try: 61 | obj = model._default_manager.get(**lookup_kwargs) 62 | except ObjectDoesNotExist: 63 | raise Http404, 'No %s found for %s.' % (model._meta.app_label, lookup_kwargs) 64 | 65 | if request.method == 'POST': 66 | if post_vote_redirect is not None: 67 | next = post_vote_redirect 68 | elif request.REQUEST.has_key('next'): 69 | next = request.REQUEST['next'] 70 | elif hasattr(obj, 'get_absolute_url'): 71 | if callable(getattr(obj, 'get_absolute_url')): 72 | next = obj.get_absolute_url() 73 | else: 74 | next = obj.get_absolute_url 75 | else: 76 | raise AttributeError('Generic vote view must be called with either ' 77 | 'post_vote_redirect, a "next" parameter in ' 78 | 'the request, or the object being voted on ' 79 | 'must define a get_absolute_url method or ' 80 | 'property.') 81 | Vote.objects.record_vote(obj, request.user, vote) 82 | return HttpResponseRedirect(next) 83 | else: 84 | if not template_name: 85 | template_name = '%s/%s_confirm_vote.html' % ( 86 | model._meta.app_label, model._meta.object_name.lower()) 87 | t = template_loader.get_template(template_name) 88 | c = RequestContext(request, { 89 | template_object_name: obj, 90 | 'direction': direction, 91 | }, context_processors) 92 | for key, value in extra_context.items(): 93 | if callable(value): 94 | c[key] = value() 95 | else: 96 | c[key] = value 97 | response = HttpResponse(t.render(c)) 98 | return response 99 | 100 | def json_error_response(error_message): 101 | return HttpResponse(simplejson.dumps(dict(success=False, 102 | error_message=error_message))) 103 | 104 | def xmlhttprequest_vote_on_object(request, model, direction, 105 | object_id=None, slug=None, slug_field=None): 106 | """ 107 | Generic object vote function for use via XMLHttpRequest. 108 | 109 | Properties of the resulting JSON object: 110 | success 111 | ``true`` if the vote was successfully processed, ``false`` 112 | otherwise. 113 | score 114 | The object's updated score and number of votes if the vote 115 | was successfully processed. 116 | error_message 117 | Contains an error message if the vote was not successfully 118 | processed. 119 | """ 120 | if request.method == 'GET': 121 | return json_error_response( 122 | 'XMLHttpRequest votes can only be made using POST.') 123 | if not request.user.is_authenticated(): 124 | return json_error_response('Not authenticated.') 125 | 126 | try: 127 | vote = dict(VOTE_DIRECTIONS)[direction] 128 | except KeyError: 129 | return json_error_response( 130 | '\'%s\' is not a valid vote type.' % direction) 131 | 132 | # Look up the object to be voted on 133 | lookup_kwargs = {} 134 | if object_id: 135 | lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id 136 | elif slug and slug_field: 137 | lookup_kwargs['%s__exact' % slug_field] = slug 138 | else: 139 | return json_error_response('Generic XMLHttpRequest vote view must be ' 140 | 'called with either object_id or slug and ' 141 | 'slug_field.') 142 | try: 143 | obj = model._default_manager.get(**lookup_kwargs) 144 | except ObjectDoesNotExist: 145 | return json_error_response( 146 | 'No %s found for %s.' % (model._meta.verbose_name, lookup_kwargs)) 147 | 148 | # Vote and respond 149 | Vote.objects.record_vote(obj, request.user, vote) 150 | return HttpResponse(simplejson.dumps({ 151 | 'success': True, 152 | 'score': Vote.objects.get_score(obj), 153 | })) 154 | --------------------------------------------------------------------------------