diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa462f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.pyc +*~ +.DS_Store +.AppleDouble +*.swp +*.egg-info +*.egg +*.EGG +*.EGG-INFO +bin +build +develop-eggs +.hg +.svn diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..e96a7e6 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +The primary author of django-voting is Scott Robinson. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d790862 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2014 Scott Robinson + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..748e721 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +Django-Voting +============= + +Django-Voting is a basic app for tracking the number of votes placed for +any object in the database. It provides the ability to upvote and downvote. + +This project is based on the [django-hitcount][1] by Damon Timm. + +TODO +---- + +Most of the basic functionality you'd need for a simple voting system has +been implemented. Possible additions are: + +- Better JavaScript support for ajax calls +- Better UI/utilities for the admin site +- Rate-limiting to avoid vote-spamming +- Options for allowing users to vote multiple times on an object + +Installation: +------------- + +Simplest way to formally install is to run: + + ./setup.py install + +Or, you could do a PIP installation: + + pip install -e git://github.com/scottwrobinson/django-voting.git#egg=django-voting + +Or, you can link the source to your `site-packages` directory. This is useful +if you plan on pulling future changes and don't want to keep running +`./setup.py install`. + + cd ~/src + git clone https://github.com/scottwrobinson/django-voting.git + sudo ln -s `pwd`/django-voting/voting `python -c "from distutils.sysconfig import get_python_lib; print get_python_lib()"`/voting + + +Then modify your settings.py, adding the package voting in INSTALLED_APPS + + INSTALLED_APPS = ( + '...', + 'voting', + ) + + +You need to add one line to your urls.py file. +---------------------------------------------- + urlpatterns = patterns('', + url(r'^site/ajax/vote$', # you can change this url if you would like + update_vote_count_ajax, + name='votecount_update_ajax'), # keep this name the same + + +Ajax Call +--------- +The ajax call to the `update_vote_count_ajax` view requires two variables: + +- direction + The vote direction. +1 for upvote and -1 for downvote. +- votecount_pk + The pk of the object to be voted on. Can be retrieved using the {% get_vote_object_pk for [object] %} tag (see below). +- csrfmiddlewaretoken (optional) + The CSRF token. Only required if CSRF validation is enabled. + +The ajax view returns two variables back: + +- status + "success" for successful votes and "failed" for votes that did not get recorded. +- net_change + The net change of the object's vote total. + +Custom Template Tags +-------------------- +Don't forget to load the custom tags with: `{% load voting_tags %}` + +- Return total votes for an object: + {% get_vote_count for [object] %} + +- Get total votes for an object as a specified variable: + {% get_vote_count for [object] as [var] %} + +- Get total votes for an object over a certain time period: + {% get_vote_count for [object] within ["days=1,minutes=30"] %} + +- Get total votes for an object over a certain time period as a variable: + {% get_vote_count for [object] within ["days=1,minutes=30"] as [var] %} + +- Get or create the pk for the given object: + {% get_vote_object_pk for [object] %} + +- Get or create the pk for the given object as a specified variable: + {% get_vote_object_pk for [object] as [var] %} + +[1]:https://github.com/thornomad/django-hitcount \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..bc68762 --- /dev/null +++ b/setup.py @@ -0,0 +1,28 @@ +import os +from setuptools import setup, find_packages + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + +setup( + name = "django-voting", + version = "0.1", + url = 'http://github.com/scottwrobinson/django-voting', + license = 'BSD', + description = "Django voting application that tracks the number of votes for any DB objects.", + long_description = read('README.md'), + + author = 'Scott Robinson', + + packages = find_packages(), + + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Plugins', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Programming Language :: Python', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], +) diff --git a/voting/__init__.py b/voting/__init__.py new file mode 100644 index 0000000..3b53efa --- /dev/null +++ b/voting/__init__.py @@ -0,0 +1,12 @@ +VERSION = (0, 1, 0, 'beta', 1) + +def get_version(): + version = '%s.%s' % (VERSION[0], VERSION[1]) + if VERSION[2]: + version = '%s.%s' % (version, VERSION[2]) + else: + if VERSION[3] != 'final': + version = '%s %s' % (version, VERSION[3]) + return version + +__version__ = get_version() diff --git a/voting/actions.py b/voting/actions.py new file mode 100644 index 0000000..dce3257 --- /dev/null +++ b/voting/actions.py @@ -0,0 +1,22 @@ +from django.core.exceptions import PermissionDenied + +def delete_queryset(modeladmin, request, queryset): + # TODO + # + # Right now, when you delete a vote there is no warning or "turing back". + # Consider adding a "are you sure you want to do this?" as is + # implemented in django's contrib.admin.actions file. + + if not modeladmin.has_delete_permission(request): + raise PermissionDenied + else: + if queryset.count() == 1: + msg = "1 vote was" + else: + msg = "%s votes were" % queryset.count() + + for obj in queryset.iterator(): + obj.delete() # calling it this way to get custom delete() method + + modeladmin.message_user(request, "%s successfully deleted." % msg) +delete_queryset.short_description = "DELETE selected votes" diff --git a/voting/admin.py b/voting/admin.py new file mode 100644 index 0000000..35119fb --- /dev/null +++ b/voting/admin.py @@ -0,0 +1,57 @@ +from django.contrib import admin + +from voting.models import Vote, VoteCount +from voting import actions + +def created_format(obj): + ''' + Format the created time for the admin. PS: I am not happy with this. + ''' + return "%s" % obj.date_created.strftime("%m/%d/%y
%H:%M:%S") +created_format.short_description = "Date (UTC)" +created_format.allow_tags = True +created_format.admin_order_field = 'date_created' + + +class VoteAdmin(admin.ModelAdmin): + list_display = (created_format,'user','ip','votecount') + search_fields = ('ip',) + date_hierarchy = 'date_created' + actions = [ actions.delete_queryset, + ] + + def __init__(self, *args, **kwargs): + super(VoteAdmin, self).__init__(*args, **kwargs) + self.list_display_links = (None,) + + def get_actions(self, request): + # Override the default `get_actions` to ensure that our model's + # `delete()` method is called. + actions = super(VoteAdmin, self).get_actions(request) + if 'delete_selected' in actions: + del actions['delete_selected'] + return actions + +# TODO: Add inlines to the VoteCount object so we can see a list of the recent +# votes for the object. For this inline to work, we need to: +# a) be able to see the vote data but *not* edit it +# b) have the `delete` command actually alter the VoteCount +# c) remove the ability to 'add new vote' +# +#class VoteInline(admin.TabularInline): +# model = Vote +# fk_name = 'votecount' +# extra = 0 + +class VoteCountAdmin(admin.ModelAdmin): + list_display = ('content_object','vote_sum','modified') + fields = ('upvotes', 'downvotes') + + # TODO: Another option + #The fields option, unlike list_display, may only contain names of fields on the model or the form specified by form. It may contain callables only if they are listed in readonly_fields. + + # TODO - when above is ready + #inlines = [ VoteInline, ] + +admin.site.register(Vote, VoteAdmin) +admin.site.register(VoteCount, VoteCountAdmin) diff --git a/voting/models.py b/voting/models.py new file mode 100644 index 0000000..f1c3dcb --- /dev/null +++ b/voting/models.py @@ -0,0 +1,187 @@ +import datetime + +from django.db import models +from django.conf import settings +from django.db.models import F +from django.utils import timezone + +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic +from django.utils.encoding import force_text + +from django.dispatch import Signal + +AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') + +VOTE_DIRECTIONS = (('up', 1), ('down', -1)) +UPVOTE = 1 +DOWNVOTE = -1 + +# EXCEPTIONS # + +class DuplicateContentObject(Exception): + 'If content_object already exists for this model' + pass + +class VoteCountManager(models.Manager): + def for_object(self, obj): + """ + QuerySet for all votes for a particular model (either an instance or + a class). + """ + ct = ContentType.objects.get_for_model(obj) + qs = self.get_queryset().filter(content_type=ct) + if isinstance(obj, models.Model): + qs = qs.filter(object_pk=force_text(obj._get_pk_val())) + return qs + + def get_for_object(self, obj): + ct = ContentType.objects.get_for_model(obj) + pk = force_text(obj._get_pk_val()) + return self.get_queryset().get(content_type=ct, object_pk=pk) + +# MODELS # + +class VoteCount(models.Model): + ''' + Model that stores the vote totals for any content object. + + ''' + + objects = VoteCountManager() + + upvotes = models.PositiveIntegerField(default=0) + downvotes = models.PositiveIntegerField(default=0) + modified = models.DateTimeField(default=timezone.now) + content_type = models.ForeignKey(ContentType, + verbose_name="content type", + related_name="content_type_set_for_%(class)s",) + object_pk = models.TextField('object ID') + content_object = generic.GenericForeignKey('content_type', 'object_pk') + + @property + def vote_sum(self): + return self.upvotes - self.downvotes + + class Meta: + #ordering = ( '-vote_sum', ) # TODO: Ordering is not free, so should we really include this? + #unique_together = (("content_type", "object_pk"),) + get_latest_by = 'modified' + #db_table = 'votecount_vote_count' + verbose_name = 'Vote Count' + verbose_name_plural = 'Vote Counts' + + def __unicode__(self): + return u'%s' % self.content_object + + def save(self, *args, **kwargs): + self.modified = timezone.now() + + if not self.pk and self.object_pk and self.content_type: + # Because we are using a models.TextField() for `object_pk` to + # allow *any* primary key type (integer or text), we + # can't use `unique_together` or `unique=True` to gaurantee + # that only one VoteCount object exists for a given object. + # + # This is just a simple hack - if there is no `self.pk` + # set, it checks the database once to see if the `content_type` + # and `object_pk` exist together (uniqueness). Obviously, this + # is not fool proof - if someone sets their own `id` or `pk` + # when initializing the VoteCount object, we could get a duplicate. + if VoteCount.objects.filter( + object_pk=self.object_pk).filter(content_type=self.content_type): + raise DuplicateContentObject, "A VoteCount object already " + \ + "exists for this content_object." + + super(VoteCount, self).save(*args, **kwargs) + + # TODO: Add kwarg for specifying if we want count for upvotes, downvotes, or all votes + def votes_in_last(self, **kwargs): + ''' + Returns number of votes for an object during a given time period. THIS + IS NOT THE VOTE SUM, just the number of votes cast (upvote OR downvote). + + This will only work for as long as votes are saved in the Vote database. + If you are purging your database after 45 days, for example, that means + that asking for votes in the last 60 days will return an incorrect + number as that the longest period it can search will be 45 days. + + For example: votes_in_last(days=7). + + Accepts days, seconds, microseconds, milliseconds, minutes, + hours, and weeks. It's creating a datetime.timedelta object. + ''' + assert kwargs, "Must provide at least one timedelta arg (eg, days=1)" + period = timezone.now() - datetime.timedelta(**kwargs) + return self.vote_set.filter(date_created__gte=period).count() + + def get_content_object_url(self): + ''' + Django has this in its contrib.comments.model file -- seems worth + implementing though it may take a couple steps. + ''' + pass + +class VoteManager(models.Manager): + def for_votecount(self, votecount): + """ + QuerySet for all votes for a particular votecount. + """ + qs = self.get_queryset() + if isinstance(votecount, VoteCount): + qs = qs.filter(votecount=votecount) + return qs + +class Vote(models.Model): + ''' + Model captures a single Vote by a user. + + None of the fields are editable because they are all dynamically created. + ''' + + objects = VoteManager() + + user = models.ForeignKey(AUTH_USER_MODEL, editable=False) + votecount = models.ForeignKey(VoteCount, editable=False) + direction = models.IntegerField(choices=VOTE_DIRECTIONS) + ip = models.GenericIPAddressField(editable=False) + date_created = models.DateTimeField(editable=False) + + class Meta: + ordering = ( '-date_created', ) + get_latest_by = 'date_created' + + def __unicode__(self): + return u'Vote: %s' % self.pk + + def save(self, *args, **kwargs): + ''' + The first time the object is created and saved, we set + the date_created field. + ''' + if not self.date_created: + if self.direction == UPVOTE: + self.votecount.upvotes += 1 + else: + self.votecount.downvotes += 1 + self.votecount.save() + self.date_created = timezone.now() + + super(Vote, self).save(*args, **kwargs) + + + def delete(self, save_votecount=False): + ''' + If a Vote is deleted and save_votecount=True, it will preserve the + VoteCount object's total. However, under normal circumstances, a + delete() will trigger a subtraction from the VoteCount object's total. + + NOTE: This doesn't work at all during a queryset.delete(). + ''' + if not save_votecount: + if self.direction == UPVOTE: + self.votecount.upvotes -= 1 + else: + self.votecount.downvotes -= 1 + self.votecount.save() + super(Vote, self).delete() diff --git a/voting/templatetags/__init__.py b/voting/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/voting/templatetags/voting_tags.py b/voting/templatetags/voting_tags.py new file mode 100644 index 0000000..e7b77cf --- /dev/null +++ b/voting/templatetags/voting_tags.py @@ -0,0 +1,183 @@ +from django import template +from django.template import TemplateSyntaxError +from django.contrib.contenttypes.models import ContentType +from django.core.urlresolvers import reverse +from django.core.exceptions import MultipleObjectsReturned + +from voting.models import VoteCount + +register = template.Library() + + +def get_target_ctype_pk(context, object_expr): + # I don't really understand how this is working, but I took it from the + # comment app in django.contrib and the removed it from the Node. + try: + obj = object_expr.resolve(context) + except template.VariableDoesNotExist: + return None, None + + return ContentType.objects.get_for_model(obj), obj.pk + + +def return_period_from_string(arg): + ''' + Takes a string such as "days=1,seconds=30" and strips the quotes + and returns a dictionary with the key/value pairs + ''' + period = {} + + if arg[0] == '"' and arg[-1] == '"': + opt = arg[1:-1] #remove quotes + else: + opt = arg + + for o in opt.split(","): + key, value = o.split("=") + period[str(key)] = int(value) + + return period + + +class GetVoteCount(template.Node): + + def handle_token(cls, parser, token): + args = token.contents.split() + + # {% get_vote_count for [obj] %} + if len(args) == 3 and args[1] == 'for': + return cls(object_expr = parser.compile_filter(args[2])) + + # {% get_vote_count for [obj] as [var] %} + elif len(args) == 5 and args[1] == 'for' and args[3] == 'as': + return cls(object_expr = parser.compile_filter(args[2]), + as_varname = args[4],) + + # {% get_vote_count for [obj] within ["days=1,minutes=30"] %} + elif len(args) == 5 and args[1] == 'for' and args[3] == 'within': + return cls(object_expr = parser.compile_filter(args[2]), + period = return_period_from_string(args[4])) + + # {% get_vote_count for [obj] within ["days=1,minutes=30"] as [var] %} + elif len(args) == 7 and args [1] == 'for' and \ + args[3] == 'within' and args[5] == 'as': + return cls(object_expr = parser.compile_filter(args[2]), + as_varname = args[6], + period = return_period_from_string(args[4])) + + else: # TODO - should there be more troubleshooting prior to bailing? + raise TemplateSyntaxError, \ + "'get_vote_count' requires " + \ + "'for [object] in [timeframe] as [variable]' " + \ + "(got %r)" % args + + handle_token = classmethod(handle_token) + + + def __init__(self, object_expr, as_varname=None, period=None): + self.object_expr = object_expr + self.as_varname = as_varname + self.period = period + + + def render(self, context): + ctype, object_pk = get_target_ctype_pk(context, self.object_expr) + + try: + obj, created = VoteCount.objects.get_or_create(content_type=ctype, + object_pk=object_pk) + except MultipleObjectsReturned: + # from voting.models + # Because we are using a models.TextField() for `object_pk` to + # allow *any* primary key type (integer or text), we + # can't use `unique_together` or `unique=True` to gaurantee + # that only one VoteCount object exists for a given object. + + # remove duplicate + items = VoteCount.objects.all().filter(content_type=ctype, object_pk=object_pk) + obj = items[0] + for extra_items in items[1:]: + extra_items.delete() + + if self.period: # if user sets a time period, use it + try: + votes = obj.votes_in_last(**self.period) + except: + votes = '[votecount error w/ time period]' + else: + votes = obj.vote_sum + + if self.as_varname: # if user gives us a variable to return + context[self.as_varname] = str(votes) + return '' + else: + return str(votes) + + +def get_vote_count(parser, token): + ''' + Returns vote counts for an object. + + - Return total votes for an object: + {% get_vote_count for [object] %} + + - Get total votes for an object as a specified variable: + {% get_vote_count for [object] as [var] %} + + - Get total votes for an object over a certain time period: + {% get_vote_count for [object] within ["days=1,minutes=30"] %} + + - Get total votes for an object over a certain time period as a variable: + {% get_vote_count for [object] within ["days=1,minutes=30"] as [var] %} + + The time arguments need to follow datetime.timedelta's limitations: + Accepts days, seconds, microseconds, milliseconds, minutes, + hours, and weeks. + ''' + return GetVoteCount.handle_token(parser, token) + +register.tag('get_vote_count', get_vote_count) + +class GetVoteObjectPk(template.Node): + + def handle_token(cls, parser, token): + args = token.contents.split() + + # {% get_vote_object_pk for [obj] %} + if len(args) == 3 and args[1] == 'for': + return cls(object_expr = parser.compile_filter(args[2])) + + # {% get_vote_object_pk for [obj] as [var] %} + elif len(args) == 5 and args[1] == 'for' and args[3] == 'as': + return cls(object_expr = parser.compile_filter(args[2]), + as_varname = args[4],) + else: + raise TemplateSyntaxError, \ + "'get_vote_object_pk' requires " + \ + "'for [object] as [var]'" + \ + "(got %r)" % args + + handle_token = classmethod(handle_token) + + def __init__(self, object_expr, as_varname=None): + self.object_expr = object_expr + self.as_varname = as_varname + + def render(self, context): + ctype, object_pk = get_target_ctype_pk(context, self.object_expr) + obj, created = VoteCount.objects.get_or_create(content_type=ctype, + object_pk=object_pk) + + if self.as_varname: # if user gives us a variable to return + context[self.as_varname] = str(obj.pk) + return '' + else: + return str(obj.pk) + +def get_vote_object_pk(parser, token): + ''' + Gets or creates the pk for the given object. + ''' + return GetVoteObjectPk.handle_token(parser, token) + +register.tag('get_vote_object_pk', get_vote_object_pk) \ No newline at end of file diff --git a/voting/utils.py b/voting/utils.py new file mode 100644 index 0000000..5764668 --- /dev/null +++ b/voting/utils.py @@ -0,0 +1,37 @@ +import re + +from django.conf import settings + +# this is not intended to be an all-knowing IP address regex +IP_RE = re.compile('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}') + +def get_ip(request): + """ + Retrieves the remote IP address from the request data. If the user is + behind a proxy, they may have a comma-separated list of IP addresses, so + we need to account for that. In such a case, only the first IP in the + list will be retrieved. Also, some hosts that use a proxy will put the + REMOTE_ADDR into HTTP_X_FORWARDED_FOR. This will handle pulling back the + IP from the proper place. + + **NOTE** This function was taken from django-tracking (MIT LICENSE) + http://code.google.com/p/django-tracking/ + """ + + # if neither header contain a value, just use local loopback + ip_address = request.META.get('HTTP_X_FORWARDED_FOR', + request.META.get('REMOTE_ADDR', '127.0.0.1')) + if ip_address: + # make sure we have one and only one IP + try: + ip_address = IP_RE.match(ip_address) + if ip_address: + ip_address = ip_address.group(0) + else: + # no IP, probably from some dirty proxy or other device + # throw in some bogus IP + ip_address = '10.0.0.1' + except IndexError: + pass + + return ip_address diff --git a/voting/views.py b/voting/views.py new file mode 100644 index 0000000..3da6c4d --- /dev/null +++ b/voting/views.py @@ -0,0 +1,109 @@ +import json + +from django.http import Http404, HttpResponse, HttpResponseBadRequest +from django.conf import settings +from django.contrib.contenttypes.models import ContentType + +from voting.utils import get_ip +from voting.models import Vote, VoteCount, UPVOTE, DOWNVOTE + +def _update_vote_count(request, votecount, direction): + ''' + Evaluates a request's Vote and corresponding VoteCount object and, + after a bit of clever logic, either ignores the request or registers + a new Vote. + + This is NOT a view! But should be used within a view ... + + Returns True if the request was considered a Vote; returns False if not. + ''' + user = request.user + ip = get_ip(request) + + net_change = 0 + + # TODO: Rate-limit users based on IP to avoid spamming + #votes_per_ip_limit = getattr(settings, 'VOTECOUNT_VOTES_PER_IP_LIMIT', 0) + # check limit on votes from a unique ip address (VOTECOUNT_VOTES_PER_IP_LIMIT) + #if votes_per_ip_limit: + # if qs.filter(ip__exact=ip).count() > votes_per_ip_limit: + # return net_change, False + + # Check if the user already voted + try: + prev_vote = Vote.objects.get(user=user, votecount=votecount) + + # Since the user already voted, remove it. Then check if the new vote + # is in a different direction, and if so, create that one + prev_direction = prev_vote.direction + prev_vote.delete() + net_change -= prev_direction + + if prev_direction != direction: + # TODO: Is there a better way to refresh this? Like in save()/delete() methods? + # Reload VoteCount from DB since the previous delete() just changed its up/downvote totals + votecount = VoteCount.objects.get(id=votecount.id) + + vote = Vote(votecount=votecount, direction=direction, ip=get_ip(request)) + vote.user = user + vote.save() + net_change += direction + except Vote.DoesNotExist: + vote = Vote(votecount=votecount, direction=direction, ip=get_ip(request)) + vote.user = user + vote.save() + net_change += direction + + return net_change, True + +def json_error_response(error_message): + return HttpResponse(json.dumps(dict(success=False, error_message=error_message))) + +# TODO better status responses - consider model after django-voting, +# right now the django handling isn't great. should return the current +# vote count so we could update it via javascript (since each view will +# be one behind). +def update_vote_count_ajax(request): + ''' + Ajax call that can be used to update a vote count. + + See template tags for how to implement. + ''' + + # make sure this is an ajax request + if not request.is_ajax(): + raise Http404() + + if request.method == "GET": + return json_error_response("Votes counted via POST only.") + + if not request.user.is_authenticated(): + return json_error_response('You must be authenticated to vote.') + + # TODO: Should probably use a form for validating this + # Parse inputs + try: + votecount_pk = request.POST.get('votecount_pk') + direction = int(request.POST.get('direction')) + except ValueError: + return HttpResponseBadRequest("Invalid vote direction") + + # Verify direction is valid + if direction != UPVOTE and direction != DOWNVOTE: + return HttpResponseBadRequest("Invalid vote direction") + + # Verify VoteCount pk is valid + try: + votecount = VoteCount.objects.get(pk=votecount_pk) + except: + return HttpResponseBadRequest("VoteCount object_pk not working") + + net_change, result = _update_vote_count(request, votecount, direction) + + if result: + status = "success" + else: + status = "failed" + + json_response = json.dumps({'status': status, 'net_change':net_change}) + return HttpResponse(json_response, content_type="application/json")