From 664ec4ef86c206e0f83f3ca8a6191d546361e4ce Mon Sep 17 00:00:00 2001 From: Urtzi Odriozola Date: Mon, 11 Jan 2016 16:12:02 +0100 Subject: [PATCH 01/18] Django custom user compatible --- voting/models.py | 3 ++- voting/utils/__init__.py | 0 voting/utils/user_model.py | 7 +++++++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 voting/utils/__init__.py create mode 100644 voting/utils/user_model.py diff --git a/voting/models.py b/voting/models.py index 6b76cf2..fdfa4b8 100644 --- a/voting/models.py +++ b/voting/models.py @@ -7,7 +7,6 @@ from django.utils.encoding import python_2_unicode_compatible from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType -from django.contrib.auth.models import User from django.db import models try: @@ -16,7 +15,9 @@ now = datetime.now from voting.managers import VoteManager +from voting.utils.user_models import get_user_model_name +User = get_user_model_name() SCORES = ( (+1, '+1'), diff --git a/voting/utils/__init__.py b/voting/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/voting/utils/user_model.py b/voting/utils/user_model.py new file mode 100644 index 0000000..165c894 --- /dev/null +++ b/voting/utils/user_model.py @@ -0,0 +1,7 @@ +from django.conf import settings + +def get_user_model_name(): + """ + Returns the app_label.object_name string for the user model. + """ + return getattr(settings, "AUTH_USER_MODEL", "auth.User") \ No newline at end of file From 2bd1cacea4eb8736f7a741fecc5b2f8cebfa9009 Mon Sep 17 00:00:00 2001 From: Urtzi Odriozola Date: Mon, 11 Jan 2016 16:14:12 +0100 Subject: [PATCH 02/18] Import error fixed --- voting/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voting/models.py b/voting/models.py index fdfa4b8..65425be 100644 --- a/voting/models.py +++ b/voting/models.py @@ -15,7 +15,7 @@ now = datetime.now from voting.managers import VoteManager -from voting.utils.user_models import get_user_model_name +from voting.utils.user_model import get_user_model_name User = get_user_model_name() From e4ccedbe43c99105a07557f41c4065ae3a208af6 Mon Sep 17 00:00:00 2001 From: jatsu Date: Wed, 10 Feb 2016 15:54:54 +0100 Subject: [PATCH 03/18] Adapted to Django 1.9 --- setup.py | 2 +- voting/models.py | 4 ++-- voting/views.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index ef7894b..5a8ad7a 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ url='https://github.com/pjdelport/django-voting', packages=[ 'voting', - 'voting.migrations', + #'voting.migrations', 'voting.templatetags', 'voting.tests', ], diff --git a/voting/models.py b/voting/models.py index 65425be..32b3b93 100644 --- a/voting/models.py +++ b/voting/models.py @@ -5,7 +5,7 @@ from datetime import datetime from django.utils.encoding import python_2_unicode_compatible -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models @@ -32,7 +32,7 @@ class Vote(models.Model): user = models.ForeignKey(User) content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() - object = generic.GenericForeignKey('content_type', 'object_id') + object = GenericForeignKey('content_type', 'object_id') vote = models.SmallIntegerField(choices=SCORES) time_stamp = models.DateTimeField(editable=False, default=now) diff --git a/voting/views.py b/voting/views.py index 7d5993b..309d5c3 100644 --- a/voting/views.py +++ b/voting/views.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.core.exceptions import ObjectDoesNotExist -from django.db.models import get_model +from django.apps import apps from django.http import Http404, HttpResponse, HttpResponseBadRequest, \ HttpResponseRedirect from django.contrib.auth.views import redirect_to_login @@ -114,7 +114,7 @@ def vote_on_object_with_lazy_model(request, app_label, model_name, *args, Returns HTTP 400 (Bad Request) if there is no model matching the app_label and model_name. """ - model = get_model(app_label, model_name) + model = app.get_model(app_label, model_name) if not model: return HttpResponseBadRequest('Model %s.%s does not exist' % ( app_label, model_name)) From 24c284ae75079b06cba7e7b6eaf601cfbc15e650 Mon Sep 17 00:00:00 2001 From: Urtzi Odriozola Date: Thu, 11 Feb 2016 10:36:29 +0100 Subject: [PATCH 04/18] Get user methods --- {voting => src/voting}/utils/__init__.py | 0 {voting => src/voting}/utils/user_model.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {voting => src/voting}/utils/__init__.py (100%) rename {voting => src/voting}/utils/user_model.py (100%) diff --git a/voting/utils/__init__.py b/src/voting/utils/__init__.py similarity index 100% rename from voting/utils/__init__.py rename to src/voting/utils/__init__.py diff --git a/voting/utils/user_model.py b/src/voting/utils/user_model.py similarity index 100% rename from voting/utils/user_model.py rename to src/voting/utils/user_model.py From 0680e4b7c51b0a23e0293593f49b0dcc77cde50b Mon Sep 17 00:00:00 2001 From: Urtzi Odriozola Date: Thu, 18 Feb 2016 09:51:50 +0100 Subject: [PATCH 05/18] get method added --- src/voting/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/voting/views.py b/src/voting/views.py index 8afe752..4751eb9 100644 --- a/src/voting/views.py +++ b/src/voting/views.py @@ -74,7 +74,7 @@ def vote_on_object(request, model, direction, post_vote_redirect=None, if post_vote_redirect is not None: next = post_vote_redirect elif 'next' in request.POST: - next = request.POST['next'] + next = request.POST.get('next','') elif hasattr(obj, 'get_absolute_url'): if callable(getattr(obj, 'get_absolute_url')): next = obj.get_absolute_url() From 103c8f46bc653a229d9923e42cf9eee874658604 Mon Sep 17 00:00:00 2001 From: Urtzi Odriozola Date: Thu, 16 Jan 2020 14:31:58 +0100 Subject: [PATCH 06/18] django-voting 2.0 --- src/voting/managers.py | 2 +- src/voting/migrations/0001_initial.py | 4 ++-- src/voting/models.py | 4 ++-- src/voting/templatetags/voting_tags.py | 16 ++++++++-------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/voting/managers.py b/src/voting/managers.py index 23e6d22..af362c6 100644 --- a/src/voting/managers.py +++ b/src/voting/managers.py @@ -121,7 +121,7 @@ def get_for_user(self, obj, user): Get the vote made on the given object by the given user, or ``None`` if no matching vote exists. """ - if not user.is_authenticated(): + if not user.is_authenticated: return None ctype = ContentType.objects.get_for_model(obj) try: diff --git a/src/voting/migrations/0001_initial.py b/src/voting/migrations/0001_initial.py index 240da06..7331fdf 100644 --- a/src/voting/migrations/0001_initial.py +++ b/src/voting/migrations/0001_initial.py @@ -21,8 +21,8 @@ class Migration(migrations.Migration): ('object_id', models.PositiveIntegerField()), ('vote', models.SmallIntegerField(choices=[(1, '+1'), (-1, '-1')])), ('time_stamp', models.DateTimeField(editable=False, default=django.utils.timezone.now)), - ('content_type', models.ForeignKey(to='contenttypes.ContentType')), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ('content_type', models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE)), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), ], options={ 'db_table': 'votes', diff --git a/src/voting/models.py b/src/voting/models.py index 32b3b93..1714b84 100644 --- a/src/voting/models.py +++ b/src/voting/models.py @@ -29,8 +29,8 @@ class Vote(models.Model): """ A vote on an object by a User. """ - user = models.ForeignKey(User) - content_type = models.ForeignKey(ContentType) + user = models.ForeignKey(User, on_delete=models.CASCADE) + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() object = GenericForeignKey('content_type', 'object_id') vote = models.SmallIntegerField(choices=SCORES) diff --git a/src/voting/templatetags/voting_tags.py b/src/voting/templatetags/voting_tags.py index b1c1aad..0dfaced 100644 --- a/src/voting/templatetags/voting_tags.py +++ b/src/voting/templatetags/voting_tags.py @@ -19,7 +19,7 @@ def __init__(self, object, context_var): def render(self, context): try: - object = template.resolve_variable(self.object, context) + object = template.Variable(self.object).resolve(context) except template.VariableDoesNotExist: return '' context[self.context_var] = Vote.objects.get_score(object) @@ -33,7 +33,7 @@ def __init__(self, objects, context_var): def render(self, context): try: - objects = template.resolve_variable(self.objects, context) + objects = template.Variable(self.object).resolve(context) except template.VariableDoesNotExist: return '' context[self.context_var] = Vote.objects.get_scores_in_bulk(objects) @@ -48,8 +48,8 @@ def __init__(self, user, object, context_var): def render(self, context): try: - user = template.resolve_variable(self.user, context) - object = template.resolve_variable(self.object, context) + user = template.Variable(self.user).resolve(context) + object = template.Variable(self.object).resolve(context) except template.VariableDoesNotExist: return '' context[self.context_var] = Vote.objects.get_for_user(object, user) @@ -64,8 +64,8 @@ def __init__(self, user, objects, context_var): def render(self, context): try: - user = template.resolve_variable(self.user, context) - objects = template.resolve_variable(self.objects, context) + user = template.Variable(self.user).resolve(context) + objects = template.Variable(self.object).resolve(context) except template.VariableDoesNotExist: return '' context[self.context_var] = Vote.objects.get_for_user_in_bulk(objects, user) @@ -80,8 +80,8 @@ def __init__(self, item, dictionary, context_var): def render(self, context): try: - dictionary = template.resolve_variable(self.dictionary, context) - item = template.resolve_variable(self.item, context) + dictionary = template.Variable(self.dictionary).resolve(context) + item = template.Variable(self.item).resolve(context) except template.VariableDoesNotExist: return '' context[self.context_var] = dictionary.get(item.id, None) From 3f8104a6b9d64874bf8d606c15bf9c440d808436 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 10 Mar 2021 21:05:41 +0100 Subject: [PATCH 07/18] Run black. --- setup.py | 40 ++-- src/voting/managers.py | 76 +++--- src/voting/migrations/0001_initial.py | 45 +++- src/voting/models.py | 14 +- src/voting/south_migrations/0001_initial.py | 220 +++++++++++++----- .../0002_auto__add_field_vote_time_stamp.py | 201 ++++++++++++---- src/voting/templatetags/voting_tags.py | 115 +++++---- src/voting/urls.py | 8 +- src/voting/utils/user_model.py | 3 +- src/voting/views.py | 150 +++++++----- tests/test_app/models.py | 3 +- tests/test_models.py | 55 ++--- tests/test_settings.py | 25 +- 13 files changed, 632 insertions(+), 323 deletions(-) diff --git a/setup.py b/setup.py index 3c030ab..5db3906 100644 --- a/setup.py +++ b/setup.py @@ -2,32 +2,30 @@ setup( - name='django-voting', + name="django-voting", use_scm_version=True, - description='Generic voting application for Django', - author='Jonathan Buchanan', - author_email='jonathan.buchanan@gmail.com', - maintainer='Jannis Leidel', - maintainer_email='jannis@leidel.info', - url='https://github.com/pjdelport/django-voting', - - package_dir = {'':'src'}, - packages=find_packages('src'), - + description="Generic voting application for Django", + author="Jonathan Buchanan", + author_email="jonathan.buchanan@gmail.com", + maintainer="Jannis Leidel", + maintainer_email="jannis@leidel.info", + url="https://github.com/pjdelport/django-voting", + package_dir={"": "src"}, + packages=find_packages("src"), setup_requires=[ - 'setuptools_scm', + "setuptools_scm", ], install_requires=[ - 'Django >=1.7', + "Django >=1.7", ], classifiers=[ - 'Development Status :: 4 - Beta', - 'Environment :: Web Environment', - 'Framework :: Django', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Topic :: Utilities', + "Development Status :: 4 - Beta", + "Environment :: Web Environment", + "Framework :: Django", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Topic :: Utilities", ], ) diff --git a/src/voting/managers.py b/src/voting/managers.py index af362c6..8475691 100644 --- a/src/voting/managers.py +++ b/src/voting/managers.py @@ -7,7 +7,8 @@ from django.db.models import Sum, Count from django.contrib.contenttypes.models import ContentType -ZERO_VOTES_ALLOWED = getattr(settings, 'VOTING_ZERO_VOTES_ALLOWED', False) +ZERO_VOTES_ALLOWED = getattr(settings, "VOTING_ZERO_VOTES_ALLOWED", False) + class VoteManager(models.Manager): def get_score(self, obj): @@ -16,16 +17,12 @@ def get_score(self, obj): the number of votes it's received. """ ctype = ContentType.objects.get_for_model(obj) - result = self.filter( - object_id=obj._get_pk_val(), - content_type=ctype - ).aggregate( - score=Sum('vote'), - num_votes=Count('vote') + result = self.filter(object_id=obj._get_pk_val(), content_type=ctype).aggregate( + score=Sum("vote"), num_votes=Count("vote") ) - if result['score'] is None: - result['score'] = 0 + if result["score"] is None: + result["score"] = 0 return result def get_scores_in_bulk(self, objects): @@ -39,21 +36,22 @@ def get_scores_in_bulk(self, objects): ctype = ContentType.objects.get_for_model(objects[0]) - queryset = self.filter( - object_id__in=object_ids, - content_type=ctype, - ).values( - 'object_id', - ).annotate( - score=Sum('vote'), - num_votes=Count('vote') + queryset = ( + self.filter( + object_id__in=object_ids, + content_type=ctype, + ) + .values( + "object_id", + ) + .annotate(score=Sum("vote"), num_votes=Count("vote")) ) vote_dict = {} for row in queryset: - vote_dict[row['object_id']] = { - 'score': int(row['score']), - 'num_votes': int(row['num_votes']), + vote_dict[row["object_id"]] = { + "score": int(row["score"]), + "num_votes": int(row["num_votes"]), } return vote_dict @@ -66,11 +64,10 @@ def record_vote(self, obj, user, vote): A zero vote indicates that any existing vote should be removed. """ if vote not in (+1, 0, -1): - raise ValueError('Invalid vote (must be +1/0/-1)') + raise ValueError("Invalid vote (must be +1/0/-1)") ctype = ContentType.objects.get_for_model(obj) try: - v = self.get(user=user, content_type=ctype, - object_id=obj._get_pk_val()) + v = self.get(user=user, content_type=ctype, object_id=obj._get_pk_val()) if vote == 0 and not ZERO_VOTES_ALLOWED: v.delete() else: @@ -79,8 +76,9 @@ def record_vote(self, obj, user, vote): except models.ObjectDoesNotExist: if not ZERO_VOTES_ALLOWED and vote == 0: return - self.create(user=user, content_type=ctype, - object_id=obj._get_pk_val(), vote=vote) + self.create( + user=user, content_type=ctype, object_id=obj._get_pk_val(), vote=vote + ) def get_top(self, model, limit=10, reversed=False): """ @@ -89,19 +87,23 @@ def get_top(self, model, limit=10, reversed=False): Yields (object, score) tuples. """ ctype = ContentType.objects.get_for_model(model) - results = self.filter(content_type=ctype).values('object_id').annotate(score=Sum('vote')) + results = ( + self.filter(content_type=ctype) + .values("object_id") + .annotate(score=Sum("vote")) + ) if reversed: - results = results.order_by('score') + results = results.order_by("score") else: - results = results.order_by('-score') + results = results.order_by("-score") # Use in_bulk() to avoid O(limit) db hits. - objects = model.objects.in_bulk([item['object_id'] for item in results[:limit]]) + objects = model.objects.in_bulk([item["object_id"] for item in results[:limit]]) # Yield each object, score pair. Because of the lazy nature of generic # relations, missing objects are silently ignored. for item in results[:limit]: - id, score = item['object_id'], item['score'] + id, score = item["object_id"], item["score"] if not score: continue if id in objects: @@ -125,8 +127,7 @@ def get_for_user(self, obj, user): return None ctype = ContentType.objects.get_for_model(obj) try: - vote = self.get(content_type=ctype, object_id=obj._get_pk_val(), - user=user) + vote = self.get(content_type=ctype, object_id=obj._get_pk_val(), user=user) except models.ObjectDoesNotExist: vote = None return vote @@ -139,9 +140,12 @@ def get_for_user_in_bulk(self, objects, user): vote_dict = {} if len(objects) > 0: ctype = ContentType.objects.get_for_model(objects[0]) - votes = list(self.filter(content_type__pk=ctype.id, - object_id__in=[obj._get_pk_val() \ - for obj in objects], - user__pk=user.id)) + votes = list( + self.filter( + content_type__pk=ctype.id, + object_id__in=[obj._get_pk_val() for obj in objects], + user__pk=user.id, + ) + ) vote_dict = dict([(vote.object_id, vote) for vote in votes]) return vote_dict diff --git a/src/voting/migrations/0001_initial.py b/src/voting/migrations/0001_initial.py index 7331fdf..67fcbdb 100644 --- a/src/voting/migrations/0001_initial.py +++ b/src/voting/migrations/0001_initial.py @@ -9,28 +9,51 @@ class Migration(migrations.Migration): dependencies = [ - ('contenttypes', '0001_initial'), + ("contenttypes", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='Vote', + name="Vote", fields=[ - ('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')), - ('object_id', models.PositiveIntegerField()), - ('vote', models.SmallIntegerField(choices=[(1, '+1'), (-1, '-1')])), - ('time_stamp', models.DateTimeField(editable=False, default=django.utils.timezone.now)), - ('content_type', models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE)), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + primary_key=True, + auto_created=True, + serialize=False, + verbose_name="ID", + ), + ), + ("object_id", models.PositiveIntegerField()), + ("vote", models.SmallIntegerField(choices=[(1, "+1"), (-1, "-1")])), + ( + "time_stamp", + models.DateTimeField( + editable=False, default=django.utils.timezone.now + ), + ), + ( + "content_type", + models.ForeignKey( + to="contenttypes.ContentType", on_delete=models.CASCADE + ), + ), + ( + "user", + models.ForeignKey( + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), + ), ], options={ - 'db_table': 'votes', + "db_table": "votes", }, bases=(models.Model,), ), migrations.AlterUniqueTogether( - name='vote', - unique_together=set([('user', 'content_type', 'object_id')]), + name="vote", + unique_together=set([("user", "content_type", "object_id")]), ), ] diff --git a/src/voting/models.py b/src/voting/models.py index 1714b84..c3384ed 100644 --- a/src/voting/models.py +++ b/src/voting/models.py @@ -20,31 +20,33 @@ User = get_user_model_name() SCORES = ( - (+1, '+1'), - (-1, '-1'), + (+1, "+1"), + (-1, "-1"), ) + @python_2_unicode_compatible class Vote(models.Model): """ A vote on an object by a User. """ + user = models.ForeignKey(User, on_delete=models.CASCADE) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() - object = GenericForeignKey('content_type', 'object_id') + object = GenericForeignKey("content_type", "object_id") vote = models.SmallIntegerField(choices=SCORES) time_stamp = models.DateTimeField(editable=False, default=now) objects = VoteManager() class Meta: - db_table = 'votes' + db_table = "votes" # One vote per user per object - unique_together = (('user', 'content_type', 'object_id'),) + unique_together = (("user", "content_type", "object_id"),) def __str__(self): - return '%s: %s on %s' % (self.user, self.vote, self.object) + return "%s: %s on %s" % (self.user, self.vote, self.object) def is_upvote(self): return self.vote == 1 diff --git a/src/voting/south_migrations/0001_initial.py b/src/voting/south_migrations/0001_initial.py index 0529598..b835a1d 100644 --- a/src/voting/south_migrations/0001_initial.py +++ b/src/voting/south_migrations/0001_initial.py @@ -6,75 +6,185 @@ class Migration(SchemaMigration): - def forwards(self, orm): # Adding model 'Vote' - db.create_table('votes', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), - ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])), - ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), - ('vote', self.gf('django.db.models.fields.SmallIntegerField')()), - )) - db.send_create_signal('voting', ['Vote']) + db.create_table( + "votes", + ( + ("id", self.gf("django.db.models.fields.AutoField")(primary_key=True)), + ( + "user", + self.gf("django.db.models.fields.related.ForeignKey")( + to=orm["auth.User"] + ), + ), + ( + "content_type", + self.gf("django.db.models.fields.related.ForeignKey")( + to=orm["contenttypes.ContentType"] + ), + ), + ( + "object_id", + self.gf("django.db.models.fields.PositiveIntegerField")(), + ), + ("vote", self.gf("django.db.models.fields.SmallIntegerField")()), + ), + ) + db.send_create_signal("voting", ["Vote"]) # Adding unique constraint on 'Vote', fields ['user', 'content_type', 'object_id'] - db.create_unique('votes', ['user_id', 'content_type_id', 'object_id']) - + db.create_unique("votes", ["user_id", "content_type_id", "object_id"]) def backwards(self, orm): # Removing unique constraint on 'Vote', fields ['user', 'content_type', 'object_id'] - db.delete_unique('votes', ['user_id', 'content_type_id', 'object_id']) + db.delete_unique("votes", ["user_id", "content_type_id", "object_id"]) # Deleting model 'Vote' - db.delete_table('votes') - + db.delete_table("votes") models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + "auth.group": { + "Meta": {"object_name": "Group"}, + "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), + "name": ( + "django.db.models.fields.CharField", + [], + {"unique": "True", "max_length": "80"}, + ), + "permissions": ( + "django.db.models.fields.related.ManyToManyField", + [], + { + "to": "orm['auth.Permission']", + "symmetrical": "False", + "blank": "True", + }, + ), + }, + "auth.permission": { + "Meta": { + "ordering": "('content_type__app_label', 'content_type__model', 'codename')", + "unique_together": "(('content_type', 'codename'),)", + "object_name": "Permission", + }, + "codename": ( + "django.db.models.fields.CharField", + [], + {"max_length": "100"}, + ), + "content_type": ( + "django.db.models.fields.related.ForeignKey", + [], + {"to": "orm['contenttypes.ContentType']"}, + ), + "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), + "name": ("django.db.models.fields.CharField", [], {"max_length": "50"}), }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + "auth.user": { + "Meta": {"object_name": "User"}, + "date_joined": ( + "django.db.models.fields.DateTimeField", + [], + {"default": "datetime.datetime.now"}, + ), + "email": ( + "django.db.models.fields.EmailField", + [], + {"max_length": "75", "blank": "True"}, + ), + "first_name": ( + "django.db.models.fields.CharField", + [], + {"max_length": "30", "blank": "True"}, + ), + "groups": ( + "django.db.models.fields.related.ManyToManyField", + [], + {"to": "orm['auth.Group']", "symmetrical": "False", "blank": "True"}, + ), + "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), + "is_active": ( + "django.db.models.fields.BooleanField", + [], + {"default": "True"}, + ), + "is_staff": ( + "django.db.models.fields.BooleanField", + [], + {"default": "False"}, + ), + "is_superuser": ( + "django.db.models.fields.BooleanField", + [], + {"default": "False"}, + ), + "last_login": ( + "django.db.models.fields.DateTimeField", + [], + {"default": "datetime.datetime.now"}, + ), + "last_name": ( + "django.db.models.fields.CharField", + [], + {"max_length": "30", "blank": "True"}, + ), + "password": ( + "django.db.models.fields.CharField", + [], + {"max_length": "128"}, + ), + "user_permissions": ( + "django.db.models.fields.related.ManyToManyField", + [], + { + "to": "orm['auth.Permission']", + "symmetrical": "False", + "blank": "True", + }, + ), + "username": ( + "django.db.models.fields.CharField", + [], + {"unique": "True", "max_length": "30"}, + ), }, - 'auth.user': { - 'Meta': {'object_name': 'User'}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + "contenttypes.contenttype": { + "Meta": { + "ordering": "('name',)", + "unique_together": "(('app_label', 'model'),)", + "object_name": "ContentType", + "db_table": "'django_content_type'", + }, + "app_label": ( + "django.db.models.fields.CharField", + [], + {"max_length": "100"}, + ), + "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), + "model": ("django.db.models.fields.CharField", [], {"max_length": "100"}), + "name": ("django.db.models.fields.CharField", [], {"max_length": "100"}), }, - 'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + "voting.vote": { + "Meta": { + "unique_together": "(('user', 'content_type', 'object_id'),)", + "object_name": "Vote", + "db_table": "'votes'", + }, + "content_type": ( + "django.db.models.fields.related.ForeignKey", + [], + {"to": "orm['contenttypes.ContentType']"}, + ), + "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), + "object_id": ("django.db.models.fields.PositiveIntegerField", [], {}), + "user": ( + "django.db.models.fields.related.ForeignKey", + [], + {"to": "orm['auth.User']"}, + ), + "vote": ("django.db.models.fields.SmallIntegerField", [], {}), }, - 'voting.vote': { - 'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'Vote', 'db_table': "'votes'"}, - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), - 'vote': ('django.db.models.fields.SmallIntegerField', [], {}) - } } - complete_apps = ['voting'] \ No newline at end of file + complete_apps = ["voting"] diff --git a/src/voting/south_migrations/0002_auto__add_field_vote_time_stamp.py b/src/voting/south_migrations/0002_auto__add_field_vote_time_stamp.py index 9e52305..1f39ce1 100644 --- a/src/voting/south_migrations/0002_auto__add_field_vote_time_stamp.py +++ b/src/voting/south_migrations/0002_auto__add_field_vote_time_stamp.py @@ -6,65 +6,168 @@ class Migration(SchemaMigration): - def forwards(self, orm): # Adding field 'Vote.time_stamp' - db.add_column('votes', 'time_stamp', - self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now), - keep_default=False) - + db.add_column( + "votes", + "time_stamp", + self.gf("django.db.models.fields.DateTimeField")( + default=datetime.datetime.now + ), + keep_default=False, + ) def backwards(self, orm): # Deleting field 'Vote.time_stamp' - db.delete_column('votes', 'time_stamp') - + db.delete_column("votes", "time_stamp") models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + "auth.group": { + "Meta": {"object_name": "Group"}, + "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), + "name": ( + "django.db.models.fields.CharField", + [], + {"unique": "True", "max_length": "80"}, + ), + "permissions": ( + "django.db.models.fields.related.ManyToManyField", + [], + { + "to": "orm['auth.Permission']", + "symmetrical": "False", + "blank": "True", + }, + ), + }, + "auth.permission": { + "Meta": { + "ordering": "('content_type__app_label', 'content_type__model', 'codename')", + "unique_together": "(('content_type', 'codename'),)", + "object_name": "Permission", + }, + "codename": ( + "django.db.models.fields.CharField", + [], + {"max_length": "100"}, + ), + "content_type": ( + "django.db.models.fields.related.ForeignKey", + [], + {"to": "orm['contenttypes.ContentType']"}, + ), + "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), + "name": ("django.db.models.fields.CharField", [], {"max_length": "50"}), }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + "auth.user": { + "Meta": {"object_name": "User"}, + "date_joined": ( + "django.db.models.fields.DateTimeField", + [], + {"default": "datetime.datetime.now"}, + ), + "email": ( + "django.db.models.fields.EmailField", + [], + {"max_length": "75", "blank": "True"}, + ), + "first_name": ( + "django.db.models.fields.CharField", + [], + {"max_length": "30", "blank": "True"}, + ), + "groups": ( + "django.db.models.fields.related.ManyToManyField", + [], + {"to": "orm['auth.Group']", "symmetrical": "False", "blank": "True"}, + ), + "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), + "is_active": ( + "django.db.models.fields.BooleanField", + [], + {"default": "True"}, + ), + "is_staff": ( + "django.db.models.fields.BooleanField", + [], + {"default": "False"}, + ), + "is_superuser": ( + "django.db.models.fields.BooleanField", + [], + {"default": "False"}, + ), + "last_login": ( + "django.db.models.fields.DateTimeField", + [], + {"default": "datetime.datetime.now"}, + ), + "last_name": ( + "django.db.models.fields.CharField", + [], + {"max_length": "30", "blank": "True"}, + ), + "password": ( + "django.db.models.fields.CharField", + [], + {"max_length": "128"}, + ), + "user_permissions": ( + "django.db.models.fields.related.ManyToManyField", + [], + { + "to": "orm['auth.Permission']", + "symmetrical": "False", + "blank": "True", + }, + ), + "username": ( + "django.db.models.fields.CharField", + [], + {"unique": "True", "max_length": "30"}, + ), }, - 'auth.user': { - 'Meta': {'object_name': 'User'}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + "contenttypes.contenttype": { + "Meta": { + "ordering": "('name',)", + "unique_together": "(('app_label', 'model'),)", + "object_name": "ContentType", + "db_table": "'django_content_type'", + }, + "app_label": ( + "django.db.models.fields.CharField", + [], + {"max_length": "100"}, + ), + "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), + "model": ("django.db.models.fields.CharField", [], {"max_length": "100"}), + "name": ("django.db.models.fields.CharField", [], {"max_length": "100"}), }, - 'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + "voting.vote": { + "Meta": { + "unique_together": "(('user', 'content_type', 'object_id'),)", + "object_name": "Vote", + "db_table": "'votes'", + }, + "content_type": ( + "django.db.models.fields.related.ForeignKey", + [], + {"to": "orm['contenttypes.ContentType']"}, + ), + "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), + "object_id": ("django.db.models.fields.PositiveIntegerField", [], {}), + "time_stamp": ( + "django.db.models.fields.DateTimeField", + [], + {"default": "datetime.datetime.now"}, + ), + "user": ( + "django.db.models.fields.related.ForeignKey", + [], + {"to": "orm['auth.User']"}, + ), + "vote": ("django.db.models.fields.SmallIntegerField", [], {}), }, - 'voting.vote': { - 'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'Vote', 'db_table': "'votes'"}, - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), - 'time_stamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), - 'vote': ('django.db.models.fields.SmallIntegerField', [], {}) - } } - complete_apps = ['voting'] \ No newline at end of file + complete_apps = ["voting"] diff --git a/src/voting/templatetags/voting_tags.py b/src/voting/templatetags/voting_tags.py index 0dfaced..f47b1d0 100644 --- a/src/voting/templatetags/voting_tags.py +++ b/src/voting/templatetags/voting_tags.py @@ -21,9 +21,9 @@ def render(self, context): try: object = template.Variable(self.object).resolve(context) except template.VariableDoesNotExist: - return '' + return "" context[self.context_var] = Vote.objects.get_score(object) - return '' + return "" class ScoresForObjectsNode(template.Node): @@ -35,9 +35,9 @@ def render(self, context): try: objects = template.Variable(self.object).resolve(context) except template.VariableDoesNotExist: - return '' + return "" context[self.context_var] = Vote.objects.get_scores_in_bulk(objects) - return '' + return "" class VoteByUserNode(template.Node): @@ -51,9 +51,9 @@ def render(self, context): user = template.Variable(self.user).resolve(context) object = template.Variable(self.object).resolve(context) except template.VariableDoesNotExist: - return '' + return "" context[self.context_var] = Vote.objects.get_for_user(object, user) - return '' + return "" class VotesByUserNode(template.Node): @@ -67,9 +67,9 @@ def render(self, context): user = template.Variable(self.user).resolve(context) objects = template.Variable(self.object).resolve(context) except template.VariableDoesNotExist: - return '' + return "" context[self.context_var] = Vote.objects.get_for_user_in_bulk(objects, user) - return '' + return "" class DictEntryForItemNode(template.Node): @@ -83,9 +83,9 @@ def render(self, context): dictionary = template.Variable(self.dictionary).resolve(context) item = template.Variable(self.item).resolve(context) except template.VariableDoesNotExist: - return '' + return "" context[self.context_var] = dictionary.get(item.id, None) - return '' + return "" def do_score_for_object(parser, token): @@ -103,9 +103,13 @@ def do_score_for_object(parser, token): """ bits = token.contents.split() if len(bits) != 4: - raise template.TemplateSyntaxError("'%s' tag takes exactly three arguments" % bits[0]) - if bits[2] != 'as': - raise template.TemplateSyntaxError("second argument to '%s' tag must be 'as'" % bits[0]) + raise template.TemplateSyntaxError( + "'%s' tag takes exactly three arguments" % bits[0] + ) + if bits[2] != "as": + raise template.TemplateSyntaxError( + "second argument to '%s' tag must be 'as'" % bits[0] + ) return ScoreForObjectNode(bits[1], bits[3]) @@ -120,9 +124,13 @@ def do_scores_for_objects(parser, token): """ bits = token.contents.split() if len(bits) != 4: - raise template.TemplateSyntaxError("'%s' tag takes exactly three arguments" % bits[0]) - if bits[2] != 'as': - raise template.TemplateSyntaxError("second argument to '%s' tag must be 'as'" % bits[0]) + raise template.TemplateSyntaxError( + "'%s' tag takes exactly three arguments" % bits[0] + ) + if bits[2] != "as": + raise template.TemplateSyntaxError( + "second argument to '%s' tag must be 'as'" % bits[0] + ) return ScoresForObjectsNode(bits[1], bits[3]) @@ -138,11 +146,17 @@ def do_vote_by_user(parser, token): """ bits = token.contents.split() if len(bits) != 6: - raise template.TemplateSyntaxError("'%s' tag takes exactly five arguments" % bits[0]) - if bits[2] != 'on': - raise template.TemplateSyntaxError("second argument to '%s' tag must be 'on'" % bits[0]) - if bits[4] != 'as': - raise template.TemplateSyntaxError("fourth argument to '%s' tag must be 'as'" % bits[0]) + raise template.TemplateSyntaxError( + "'%s' tag takes exactly five arguments" % bits[0] + ) + if bits[2] != "on": + raise template.TemplateSyntaxError( + "second argument to '%s' tag must be 'on'" % bits[0] + ) + if bits[4] != "as": + raise template.TemplateSyntaxError( + "fourth argument to '%s' tag must be 'as'" % bits[0] + ) return VoteByUserNode(bits[1], bits[3], bits[5]) @@ -158,11 +172,17 @@ def do_votes_by_user(parser, token): """ bits = token.contents.split() if len(bits) != 6: - raise template.TemplateSyntaxError("'%s' tag takes exactly four arguments" % bits[0]) - if bits[2] != 'on': - raise template.TemplateSyntaxError("second argument to '%s' tag must be 'on'" % bits[0]) - if bits[4] != 'as': - raise template.TemplateSyntaxError("fourth argument to '%s' tag must be 'as'" % bits[0]) + raise template.TemplateSyntaxError( + "'%s' tag takes exactly four arguments" % bits[0] + ) + if bits[2] != "on": + raise template.TemplateSyntaxError( + "second argument to '%s' tag must be 'on'" % bits[0] + ) + if bits[4] != "as": + raise template.TemplateSyntaxError( + "fourth argument to '%s' tag must be 'as'" % bits[0] + ) return VotesByUserNode(bits[1], bits[3], bits[5]) @@ -180,22 +200,30 @@ def do_dict_entry_for_item(parser, token): """ bits = token.contents.split() if len(bits) != 6: - raise template.TemplateSyntaxError("'%s' tag takes exactly five arguments" % bits[0]) - if bits[2] != 'from': - raise template.TemplateSyntaxError("second argument to '%s' tag must be 'from'" % bits[0]) - if bits[4] != 'as': - raise template.TemplateSyntaxError("fourth argument to '%s' tag must be 'as'" % bits[0]) + raise template.TemplateSyntaxError( + "'%s' tag takes exactly five arguments" % bits[0] + ) + if bits[2] != "from": + raise template.TemplateSyntaxError( + "second argument to '%s' tag must be 'from'" % bits[0] + ) + if bits[4] != "as": + raise template.TemplateSyntaxError( + "fourth argument to '%s' tag must be 'as'" % bits[0] + ) return DictEntryForItemNode(bits[1], bits[3], bits[5]) -register.tag('score_for_object', do_score_for_object) -register.tag('scores_for_objects', do_scores_for_objects) -register.tag('vote_by_user', do_vote_by_user) -register.tag('votes_by_user', do_votes_by_user) -register.tag('dict_entry_for_item', do_dict_entry_for_item) + +register.tag("score_for_object", do_score_for_object) +register.tag("scores_for_objects", do_scores_for_objects) +register.tag("vote_by_user", do_vote_by_user) +register.tag("votes_by_user", do_votes_by_user) +register.tag("dict_entry_for_item", do_dict_entry_for_item) # Simple Tags + def confirm_vote_message(object_description, vote_direction): """ Creates an appropriate message asking the user to confirm the given vote @@ -205,12 +233,16 @@ def confirm_vote_message(object_description, vote_direction): {% confirm_vote_message widget.title direction %} """ - if vote_direction == 'clear': - message = 'Confirm clearing your vote for %s.' + if vote_direction == "clear": + message = "Confirm clearing your vote for %s." else: - message = 'Confirm %s vote for %%s.' % vote_direction + message = ( + "Confirm %s vote for %%s." + % vote_direction + ) return message % (escape(object_description),) + register.simple_tag(confirm_vote_message) # Filters @@ -235,8 +267,8 @@ def vote_display(vote, arg=None): {{ vote|vote_display:"Bodacious,Bogus" }} """ if arg is None: - arg = 'Up,Down' - bits = arg.split(',') + arg = "Up,Down" + bits = arg.split(",") if len(bits) != 2: return vote.vote # Invalid arg up, down = bits @@ -244,4 +276,5 @@ def vote_display(vote, arg=None): return up return down + register.filter(vote_display) diff --git a/src/voting/urls.py b/src/voting/urls.py index b65313c..a5c28a2 100644 --- a/src/voting/urls.py +++ b/src/voting/urls.py @@ -7,11 +7,13 @@ urlpatterns = [ - url(r"^vote/(?P[\w\.-]+)/(?P\w+)/"\ + url( + r"^vote/(?P[\w\.-]+)/(?P\w+)/" "(?P\d+)/(?Pup|down|clear)/$", - vote_on_object_with_lazy_model, { + vote_on_object_with_lazy_model, + { "allow_xmlhttprequest": True, }, - name="voting_vote" + name="voting_vote", ), ] diff --git a/src/voting/utils/user_model.py b/src/voting/utils/user_model.py index 165c894..5ef2e5c 100644 --- a/src/voting/utils/user_model.py +++ b/src/voting/utils/user_model.py @@ -1,7 +1,8 @@ from django.conf import settings + def get_user_model_name(): """ Returns the app_label.object_name string for the user model. """ - return getattr(settings, "AUTH_USER_MODEL", "auth.User") \ No newline at end of file + return getattr(settings, "AUTH_USER_MODEL", "auth.User") diff --git a/src/voting/views.py b/src/voting/views.py index 4751eb9..71aecad 100644 --- a/src/voting/views.py +++ b/src/voting/views.py @@ -4,21 +4,36 @@ from django.core.exceptions import ObjectDoesNotExist from django.apps import apps -from django.http import Http404, HttpResponse, HttpResponseBadRequest, \ - HttpResponseRedirect +from django.http import ( + Http404, + HttpResponse, + HttpResponseBadRequest, + HttpResponseRedirect, +) from django.contrib.auth.views import redirect_to_login from django.template import loader, RequestContext import json from voting.models import Vote -VOTE_DIRECTIONS = (('up', 1), ('down', -1), ('clear', 0)) - - -def vote_on_object(request, model, direction, post_vote_redirect=None, - object_id=None, slug=None, slug_field=None, template_name=None, - template_loader=loader, extra_context=None, context_processors=None, - template_object_name='object', allow_xmlhttprequest=False): +VOTE_DIRECTIONS = (("up", 1), ("down", -1), ("clear", 0)) + + +def vote_on_object( + request, + model, + direction, + post_vote_redirect=None, + object_id=None, + slug=None, + slug_field=None, + template_name=None, + template_loader=loader, + extra_context=None, + context_processors=None, + template_object_name="object", + allow_xmlhttprequest=False, +): """ Generic object vote function. @@ -41,9 +56,14 @@ def vote_on_object(request, model, direction, post_vote_redirect=None, The type of vote which will be registered for the object. """ if allow_xmlhttprequest and request.is_ajax(): - return xmlhttprequest_vote_on_object(request, model, direction, - object_id=object_id, slug=slug, - slug_field=slug_field) + return xmlhttprequest_vote_on_object( + request, + model, + direction, + object_id=object_id, + slug=slug, + slug_field=slug_field, + ) if extra_context is None: extra_context = {} @@ -58,45 +78,54 @@ def vote_on_object(request, model, direction, post_vote_redirect=None, # Look up the object to be voted on lookup_kwargs = {} if object_id: - lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id + lookup_kwargs["%s__exact" % model._meta.pk.name] = object_id elif slug and slug_field: - lookup_kwargs['%s__exact' % slug_field] = slug + lookup_kwargs["%s__exact" % slug_field] = slug else: - raise AttributeError('Generic vote view must be called with either ' - 'object_id or slug and slug_field.') + raise AttributeError( + "Generic vote view must be called with either " + "object_id or slug and slug_field." + ) try: obj = model._default_manager.get(**lookup_kwargs) except ObjectDoesNotExist: - raise Http404('No %s found for %s.' % - (model._meta.app_label, lookup_kwargs)) + raise Http404("No %s found for %s." % (model._meta.app_label, lookup_kwargs)) - if request.method == 'POST': + if request.method == "POST": if post_vote_redirect is not None: next = post_vote_redirect - elif 'next' in request.POST: - next = request.POST.get('next','') - elif hasattr(obj, 'get_absolute_url'): - if callable(getattr(obj, 'get_absolute_url')): + elif "next" in request.POST: + next = request.POST.get("next", "") + elif hasattr(obj, "get_absolute_url"): + if callable(getattr(obj, "get_absolute_url")): next = obj.get_absolute_url() else: next = obj.get_absolute_url else: - raise AttributeError('Generic vote view must be called with either ' - 'post_vote_redirect, a "next" parameter in ' - 'the request, or the object being voted on ' - 'must define a get_absolute_url method or ' - 'property.') + raise AttributeError( + "Generic vote view must be called with either " + 'post_vote_redirect, a "next" parameter in ' + "the request, or the object being voted on " + "must define a get_absolute_url method or " + "property." + ) Vote.objects.record_vote(obj, request.user, vote) return HttpResponseRedirect(next) else: if not template_name: - template_name = '%s/%s_confirm_vote.html' % ( - model._meta.app_label, model._meta.object_name.lower()) + template_name = "%s/%s_confirm_vote.html" % ( + model._meta.app_label, + model._meta.object_name.lower(), + ) t = template_loader.get_template(template_name) - c = RequestContext(request, { - template_object_name: obj, - 'direction': direction, - }, context_processors) + c = RequestContext( + request, + { + template_object_name: obj, + "direction": direction, + }, + context_processors, + ) for key, value in extra_context.items(): if callable(value): c[key] = value() @@ -106,8 +135,7 @@ def vote_on_object(request, model, direction, post_vote_redirect=None, return response -def vote_on_object_with_lazy_model(request, app_label, model_name, *args, - **kwargs): +def vote_on_object_with_lazy_model(request, app_label, model_name, *args, **kwargs): """ Generic object vote view that takes app_label and model_name instead of a model class and calls ``vote_on_object`` view. @@ -116,18 +144,19 @@ def vote_on_object_with_lazy_model(request, app_label, model_name, *args, """ model = apps.get_model(app_label, model_name) if not model: - return HttpResponseBadRequest('Model %s.%s does not exist' % ( - app_label, model_name)) + return HttpResponseBadRequest( + "Model %s.%s does not exist" % (app_label, model_name) + ) return vote_on_object(request, model=model, *args, **kwargs) def json_error_response(error_message): - return HttpResponse(json.dumps(dict(success=False, - error_message=error_message))) + return HttpResponse(json.dumps(dict(success=False, error_message=error_message))) -def xmlhttprequest_vote_on_object(request, model, direction, - object_id=None, slug=None, slug_field=None): +def xmlhttprequest_vote_on_object( + request, model, direction, object_id=None, slug=None, slug_field=None +): """ Generic object vote function for use via XMLHttpRequest. @@ -142,37 +171,42 @@ def xmlhttprequest_vote_on_object(request, model, direction, Contains an error message if the vote was not successfully processed. """ - if request.method == 'GET': - return json_error_response( - 'XMLHttpRequest votes can only be made using POST.') + if request.method == "GET": + return json_error_response("XMLHttpRequest votes can only be made using POST.") if not request.user.is_authenticated(): - return json_error_response('Not authenticated.') + return json_error_response("Not authenticated.") try: vote = dict(VOTE_DIRECTIONS)[direction] except KeyError: - return json_error_response( - '\'%s\' is not a valid vote type.' % direction) + return json_error_response("'%s' is not a valid vote type." % direction) # Look up the object to be voted on lookup_kwargs = {} if object_id: - lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id + lookup_kwargs["%s__exact" % model._meta.pk.name] = object_id elif slug and slug_field: - lookup_kwargs['%s__exact' % slug_field] = slug + lookup_kwargs["%s__exact" % slug_field] = slug else: - return json_error_response('Generic XMLHttpRequest vote view must be ' - 'called with either object_id or slug and ' - 'slug_field.') + return json_error_response( + "Generic XMLHttpRequest vote view must be " + "called with either object_id or slug and " + "slug_field." + ) try: obj = model._default_manager.get(**lookup_kwargs) except ObjectDoesNotExist: return json_error_response( - 'No %s found for %s.' % (model._meta.verbose_name, lookup_kwargs)) + "No %s found for %s." % (model._meta.verbose_name, lookup_kwargs) + ) # Vote and respond Vote.objects.record_vote(obj, request.user, vote) - return HttpResponse(json.dumps({ - 'success': True, - 'score': Vote.objects.get_score(obj), - })) + return HttpResponse( + json.dumps( + { + "success": True, + "score": Vote.objects.get_score(obj), + } + ) + ) diff --git a/tests/test_app/models.py b/tests/test_app/models.py index eaf405b..b87df11 100644 --- a/tests/test_app/models.py +++ b/tests/test_app/models.py @@ -1,5 +1,6 @@ from django.db import models + class Item(models.Model): name = models.CharField(max_length=50) @@ -7,4 +8,4 @@ def __str__(self): return self.name class Meta: - ordering = ['name'] + ordering = ["name"] diff --git a/tests/test_models.py b/tests/test_models.py index 1dc2f03..bcd1578 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -5,57 +5,59 @@ from django.contrib.auth.models import User from django.test import TestCase -from voting.models import Vote +from voting.models import Vote from test_app.models import Item # Basic voting ############################################################### -class BasicVotingTests(TestCase): +class BasicVotingTests(TestCase): def setUp(self): - self.item = Item.objects.create(name='test1') + self.item = Item.objects.create(name="test1") self.users = [] - for username in ['u1', 'u2', 'u3', 'u4']: - self.users.append(User.objects.create_user(username, '%s@test.com' % username, 'test')) + for username in ["u1", "u2", "u3", "u4"]: + self.users.append( + User.objects.create_user(username, "%s@test.com" % username, "test") + ) def test_print_model(self): Vote.objects.record_vote(self.item, self.users[0], +1) - expected = 'u1: 1 on test1' + expected = "u1: 1 on test1" result = Vote.objects.all()[0] self.assertEqual(str(result), expected) def test_novotes(self): result = Vote.objects.get_score(self.item) - self.assertEqual(result, {'score': 0, 'num_votes': 0}) + self.assertEqual(result, {"score": 0, "num_votes": 0}) def test_onevoteplus(self): Vote.objects.record_vote(self.item, self.users[0], +1) result = Vote.objects.get_score(self.item) - self.assertEqual(result, {'score': 1, 'num_votes': 1}) + self.assertEqual(result, {"score": 1, "num_votes": 1}) def test_onevoteminus(self): Vote.objects.record_vote(self.item, self.users[0], -1) result = Vote.objects.get_score(self.item) - self.assertEqual(result, {'score': -1, 'num_votes': 1}) + self.assertEqual(result, {"score": -1, "num_votes": 1}) def test_onevotezero(self): Vote.objects.record_vote(self.item, self.users[0], 0) result = Vote.objects.get_score(self.item) - self.assertEqual(result, {'score': 0, 'num_votes': 0}) + self.assertEqual(result, {"score": 0, "num_votes": 0}) def test_allvoteplus(self): for user in self.users: Vote.objects.record_vote(self.item, user, +1) result = Vote.objects.get_score(self.item) - self.assertEqual(result, {'score': 4, 'num_votes': 4}) + self.assertEqual(result, {"score": 4, "num_votes": 4}) for user in self.users[:2]: Vote.objects.record_vote(self.item, user, 0) result = Vote.objects.get_score(self.item) - self.assertEqual(result, {'score': 2, 'num_votes': 2}) + self.assertEqual(result, {"score": 2, "num_votes": 2}) for user in self.users[:2]: Vote.objects.record_vote(self.item, user, -1) result = Vote.objects.get_score(self.item) - self.assertEqual(result, {'score': 0, 'num_votes': 4}) + self.assertEqual(result, {"score": 0, "num_votes": 4}) def test_wrongvote(self): try: @@ -65,16 +67,18 @@ def test_wrongvote(self): else: self.fail("Did nor raise 'ValueError: Invalid vote (must be +1/0/-1)'") -# Retrieval of votes ######################################################### +# Retrieval of votes class VoteRetrievalTests(TestCase): def setUp(self): self.items = [] - for name in ['test1', 'test2', 'test3', 'test4']: + for name in ["test1", "test2", "test3", "test4"]: self.items.append(Item.objects.create(name=name)) self.users = [] - for username in ['u1', 'u2', 'u3', 'u4']: - self.users.append(User.objects.create_user(username, '%s@test.com' % username, 'test')) + for username in ["u1", "u2", "u3", "u4"]: + self.users.append( + User.objects.create_user(username, "%s@test.com" % username, "test") + ) for user in self.users: Vote.objects.record_vote(self.items[0], user, +1) for user in self.users[:2]: @@ -101,12 +105,11 @@ def test_get_zero_vote(self): self.assertTrue(Vote.objects.get_for_user(self.items[3], self.users[0]) is None) def test_in_bulk1(self): - votes = Vote.objects.get_for_user_in_bulk(self.items, - self.users[0]) + votes = Vote.objects.get_for_user_in_bulk(self.items, self.users[0]) self.assertEqual( - [(id, vote.vote) for id, vote in votes.items()], - [(1, -1), (2, 1), (3, -1)]) - + [(id, vote.vote) for id, vote in votes.items()], [(1, -1), (2, 1), (3, -1)] + ) + def test_empty_items(self): result = Vote.objects.get_for_user_in_bulk([], self.users[0]) self.assertEqual(result, {}) @@ -144,10 +147,10 @@ def test_get_scores_in_bulk(self): Vote.objects.record_vote(self.items[3], user, -1) result = Vote.objects.get_scores_in_bulk(self.items) expected = { - 1: {'score': 0, 'num_votes': 4}, - 2: {'score': -2, 'num_votes': 4}, - 3: {'score': -4, 'num_votes': 4}, - 4: {'score': -3, 'num_votes': 3}, + 1: {"score": 0, "num_votes": 4}, + 2: {"score": -2, "num_votes": 4}, + 3: {"score": -4, "num_votes": 4}, + 4: {"score": -3, "num_votes": 3}, } self.assertEqual(result, expected) diff --git a/tests/test_settings.py b/tests/test_settings.py index 888b797..7e28806 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -6,26 +6,21 @@ DIRNAME = os.path.dirname(__file__) -DATABASES = { - 'default': { - 'ENGINE':'django.db.backends.sqlite3', - 'NAME': ':memory:' - } -} +DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} -SECRET_KEY = 'foo' +SECRET_KEY = "foo" INSTALLED_APPS = ( - 'django.contrib.sessions', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'voting', - 'test_app', + "django.contrib.sessions", + "django.contrib.auth", + "django.contrib.contenttypes", + "voting", + "test_app", ) MIDDLEWARE_CLASSES = [ - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", ] -ROOT_URLCONF = 'voting.urls' +ROOT_URLCONF = "voting.urls" From 4fc8123e85fb9d904f5fa62e65ad4318921ac5bb Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 10 Mar 2021 21:08:27 +0100 Subject: [PATCH 08/18] More fixes, like removing South. --- src/voting/managers.py | 4 - src/voting/migrations/0001_initial.py | 3 - src/voting/models.py | 4 - src/voting/south_migrations/0001_initial.py | 190 ------------------ .../0002_auto__add_field_vote_time_stamp.py | 173 ---------------- src/voting/south_migrations/__init__.py | 0 src/voting/templatetags/voting_tags.py | 9 +- src/voting/urls.py | 4 - src/voting/views.py | 4 - tests/test_models.py | 12 +- tests/test_settings.py | 4 - 11 files changed, 7 insertions(+), 400 deletions(-) delete mode 100644 src/voting/south_migrations/0001_initial.py delete mode 100644 src/voting/south_migrations/0002_auto__add_field_vote_time_stamp.py delete mode 100644 src/voting/south_migrations/__init__.py diff --git a/src/voting/managers.py b/src/voting/managers.py index 8475691..0a30482 100644 --- a/src/voting/managers.py +++ b/src/voting/managers.py @@ -1,7 +1,3 @@ -# coding: utf-8 - -from __future__ import unicode_literals - from django.conf import settings from django.db import models from django.db.models import Sum, Count diff --git a/src/voting/migrations/0001_initial.py b/src/voting/migrations/0001_initial.py index 67fcbdb..b4843d0 100644 --- a/src/voting/migrations/0001_initial.py +++ b/src/voting/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations import django.utils.timezone from django.conf import settings diff --git a/src/voting/models.py b/src/voting/models.py index c3384ed..5413a17 100644 --- a/src/voting/models.py +++ b/src/voting/models.py @@ -1,7 +1,3 @@ -# coding: utf-8 - -from __future__ import unicode_literals - from datetime import datetime from django.utils.encoding import python_2_unicode_compatible diff --git a/src/voting/south_migrations/0001_initial.py b/src/voting/south_migrations/0001_initial.py deleted file mode 100644 index b835a1d..0000000 --- a/src/voting/south_migrations/0001_initial.py +++ /dev/null @@ -1,190 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - def forwards(self, orm): - # Adding model 'Vote' - db.create_table( - "votes", - ( - ("id", self.gf("django.db.models.fields.AutoField")(primary_key=True)), - ( - "user", - self.gf("django.db.models.fields.related.ForeignKey")( - to=orm["auth.User"] - ), - ), - ( - "content_type", - self.gf("django.db.models.fields.related.ForeignKey")( - to=orm["contenttypes.ContentType"] - ), - ), - ( - "object_id", - self.gf("django.db.models.fields.PositiveIntegerField")(), - ), - ("vote", self.gf("django.db.models.fields.SmallIntegerField")()), - ), - ) - db.send_create_signal("voting", ["Vote"]) - - # Adding unique constraint on 'Vote', fields ['user', 'content_type', 'object_id'] - db.create_unique("votes", ["user_id", "content_type_id", "object_id"]) - - def backwards(self, orm): - # Removing unique constraint on 'Vote', fields ['user', 'content_type', 'object_id'] - db.delete_unique("votes", ["user_id", "content_type_id", "object_id"]) - - # Deleting model 'Vote' - db.delete_table("votes") - - models = { - "auth.group": { - "Meta": {"object_name": "Group"}, - "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), - "name": ( - "django.db.models.fields.CharField", - [], - {"unique": "True", "max_length": "80"}, - ), - "permissions": ( - "django.db.models.fields.related.ManyToManyField", - [], - { - "to": "orm['auth.Permission']", - "symmetrical": "False", - "blank": "True", - }, - ), - }, - "auth.permission": { - "Meta": { - "ordering": "('content_type__app_label', 'content_type__model', 'codename')", - "unique_together": "(('content_type', 'codename'),)", - "object_name": "Permission", - }, - "codename": ( - "django.db.models.fields.CharField", - [], - {"max_length": "100"}, - ), - "content_type": ( - "django.db.models.fields.related.ForeignKey", - [], - {"to": "orm['contenttypes.ContentType']"}, - ), - "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), - "name": ("django.db.models.fields.CharField", [], {"max_length": "50"}), - }, - "auth.user": { - "Meta": {"object_name": "User"}, - "date_joined": ( - "django.db.models.fields.DateTimeField", - [], - {"default": "datetime.datetime.now"}, - ), - "email": ( - "django.db.models.fields.EmailField", - [], - {"max_length": "75", "blank": "True"}, - ), - "first_name": ( - "django.db.models.fields.CharField", - [], - {"max_length": "30", "blank": "True"}, - ), - "groups": ( - "django.db.models.fields.related.ManyToManyField", - [], - {"to": "orm['auth.Group']", "symmetrical": "False", "blank": "True"}, - ), - "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), - "is_active": ( - "django.db.models.fields.BooleanField", - [], - {"default": "True"}, - ), - "is_staff": ( - "django.db.models.fields.BooleanField", - [], - {"default": "False"}, - ), - "is_superuser": ( - "django.db.models.fields.BooleanField", - [], - {"default": "False"}, - ), - "last_login": ( - "django.db.models.fields.DateTimeField", - [], - {"default": "datetime.datetime.now"}, - ), - "last_name": ( - "django.db.models.fields.CharField", - [], - {"max_length": "30", "blank": "True"}, - ), - "password": ( - "django.db.models.fields.CharField", - [], - {"max_length": "128"}, - ), - "user_permissions": ( - "django.db.models.fields.related.ManyToManyField", - [], - { - "to": "orm['auth.Permission']", - "symmetrical": "False", - "blank": "True", - }, - ), - "username": ( - "django.db.models.fields.CharField", - [], - {"unique": "True", "max_length": "30"}, - ), - }, - "contenttypes.contenttype": { - "Meta": { - "ordering": "('name',)", - "unique_together": "(('app_label', 'model'),)", - "object_name": "ContentType", - "db_table": "'django_content_type'", - }, - "app_label": ( - "django.db.models.fields.CharField", - [], - {"max_length": "100"}, - ), - "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), - "model": ("django.db.models.fields.CharField", [], {"max_length": "100"}), - "name": ("django.db.models.fields.CharField", [], {"max_length": "100"}), - }, - "voting.vote": { - "Meta": { - "unique_together": "(('user', 'content_type', 'object_id'),)", - "object_name": "Vote", - "db_table": "'votes'", - }, - "content_type": ( - "django.db.models.fields.related.ForeignKey", - [], - {"to": "orm['contenttypes.ContentType']"}, - ), - "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), - "object_id": ("django.db.models.fields.PositiveIntegerField", [], {}), - "user": ( - "django.db.models.fields.related.ForeignKey", - [], - {"to": "orm['auth.User']"}, - ), - "vote": ("django.db.models.fields.SmallIntegerField", [], {}), - }, - } - - complete_apps = ["voting"] diff --git a/src/voting/south_migrations/0002_auto__add_field_vote_time_stamp.py b/src/voting/south_migrations/0002_auto__add_field_vote_time_stamp.py deleted file mode 100644 index 1f39ce1..0000000 --- a/src/voting/south_migrations/0002_auto__add_field_vote_time_stamp.py +++ /dev/null @@ -1,173 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - def forwards(self, orm): - # Adding field 'Vote.time_stamp' - db.add_column( - "votes", - "time_stamp", - self.gf("django.db.models.fields.DateTimeField")( - default=datetime.datetime.now - ), - keep_default=False, - ) - - def backwards(self, orm): - # Deleting field 'Vote.time_stamp' - db.delete_column("votes", "time_stamp") - - models = { - "auth.group": { - "Meta": {"object_name": "Group"}, - "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), - "name": ( - "django.db.models.fields.CharField", - [], - {"unique": "True", "max_length": "80"}, - ), - "permissions": ( - "django.db.models.fields.related.ManyToManyField", - [], - { - "to": "orm['auth.Permission']", - "symmetrical": "False", - "blank": "True", - }, - ), - }, - "auth.permission": { - "Meta": { - "ordering": "('content_type__app_label', 'content_type__model', 'codename')", - "unique_together": "(('content_type', 'codename'),)", - "object_name": "Permission", - }, - "codename": ( - "django.db.models.fields.CharField", - [], - {"max_length": "100"}, - ), - "content_type": ( - "django.db.models.fields.related.ForeignKey", - [], - {"to": "orm['contenttypes.ContentType']"}, - ), - "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), - "name": ("django.db.models.fields.CharField", [], {"max_length": "50"}), - }, - "auth.user": { - "Meta": {"object_name": "User"}, - "date_joined": ( - "django.db.models.fields.DateTimeField", - [], - {"default": "datetime.datetime.now"}, - ), - "email": ( - "django.db.models.fields.EmailField", - [], - {"max_length": "75", "blank": "True"}, - ), - "first_name": ( - "django.db.models.fields.CharField", - [], - {"max_length": "30", "blank": "True"}, - ), - "groups": ( - "django.db.models.fields.related.ManyToManyField", - [], - {"to": "orm['auth.Group']", "symmetrical": "False", "blank": "True"}, - ), - "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), - "is_active": ( - "django.db.models.fields.BooleanField", - [], - {"default": "True"}, - ), - "is_staff": ( - "django.db.models.fields.BooleanField", - [], - {"default": "False"}, - ), - "is_superuser": ( - "django.db.models.fields.BooleanField", - [], - {"default": "False"}, - ), - "last_login": ( - "django.db.models.fields.DateTimeField", - [], - {"default": "datetime.datetime.now"}, - ), - "last_name": ( - "django.db.models.fields.CharField", - [], - {"max_length": "30", "blank": "True"}, - ), - "password": ( - "django.db.models.fields.CharField", - [], - {"max_length": "128"}, - ), - "user_permissions": ( - "django.db.models.fields.related.ManyToManyField", - [], - { - "to": "orm['auth.Permission']", - "symmetrical": "False", - "blank": "True", - }, - ), - "username": ( - "django.db.models.fields.CharField", - [], - {"unique": "True", "max_length": "30"}, - ), - }, - "contenttypes.contenttype": { - "Meta": { - "ordering": "('name',)", - "unique_together": "(('app_label', 'model'),)", - "object_name": "ContentType", - "db_table": "'django_content_type'", - }, - "app_label": ( - "django.db.models.fields.CharField", - [], - {"max_length": "100"}, - ), - "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), - "model": ("django.db.models.fields.CharField", [], {"max_length": "100"}), - "name": ("django.db.models.fields.CharField", [], {"max_length": "100"}), - }, - "voting.vote": { - "Meta": { - "unique_together": "(('user', 'content_type', 'object_id'),)", - "object_name": "Vote", - "db_table": "'votes'", - }, - "content_type": ( - "django.db.models.fields.related.ForeignKey", - [], - {"to": "orm['contenttypes.ContentType']"}, - ), - "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), - "object_id": ("django.db.models.fields.PositiveIntegerField", [], {}), - "time_stamp": ( - "django.db.models.fields.DateTimeField", - [], - {"default": "datetime.datetime.now"}, - ), - "user": ( - "django.db.models.fields.related.ForeignKey", - [], - {"to": "orm['auth.User']"}, - ), - "vote": ("django.db.models.fields.SmallIntegerField", [], {}), - }, - } - - complete_apps = ["voting"] diff --git a/src/voting/south_migrations/__init__.py b/src/voting/south_migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/voting/templatetags/voting_tags.py b/src/voting/templatetags/voting_tags.py index f47b1d0..89e2375 100644 --- a/src/voting/templatetags/voting_tags.py +++ b/src/voting/templatetags/voting_tags.py @@ -1,7 +1,3 @@ -# coding: utf-8 - -from __future__ import unicode_literals - from django import template from django.utils.html import escape @@ -9,9 +5,6 @@ register = template.Library() -# Tags - - class ScoreForObjectNode(template.Node): def __init__(self, object, context_var): self.object = object @@ -65,7 +58,7 @@ def __init__(self, user, objects, context_var): def render(self, context): try: user = template.Variable(self.user).resolve(context) - objects = template.Variable(self.object).resolve(context) + objects = template.Variable(self.objects).resolve(context) except template.VariableDoesNotExist: return "" context[self.context_var] = Vote.objects.get_for_user_in_bulk(objects, user) diff --git a/src/voting/urls.py b/src/voting/urls.py index a5c28a2..c9f61d5 100644 --- a/src/voting/urls.py +++ b/src/voting/urls.py @@ -1,7 +1,3 @@ -# coding: utf-8 - -from __future__ import unicode_literals - from django.conf.urls import url from .views import vote_on_object_with_lazy_model diff --git a/src/voting/views.py b/src/voting/views.py index 71aecad..c1ebbed 100644 --- a/src/voting/views.py +++ b/src/voting/views.py @@ -1,7 +1,3 @@ -# coding: utf-8 - -from __future__ import unicode_literals - from django.core.exceptions import ObjectDoesNotExist from django.apps import apps from django.http import ( diff --git a/tests/test_models.py b/tests/test_models.py index bcd1578..d9fcbfd 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,17 +1,15 @@ # coding: utf-8 - -from __future__ import unicode_literals - from django.contrib.auth.models import User from django.test import TestCase from voting.models import Vote from test_app.models import Item -# Basic voting ############################################################### - class BasicVotingTests(TestCase): + """ + Basic voting + """ def setUp(self): self.item = Item.objects.create(name="test1") self.users = [] @@ -68,8 +66,10 @@ def test_wrongvote(self): self.fail("Did nor raise 'ValueError: Invalid vote (must be +1/0/-1)'") -# Retrieval of votes class VoteRetrievalTests(TestCase): + """ + Retrieval of votes + """ def setUp(self): self.items = [] for name in ["test1", "test2", "test3", "test4"]: diff --git a/tests/test_settings.py b/tests/test_settings.py index 7e28806..df3b94a 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,7 +1,3 @@ -# coding: utf-8 - -from __future__ import unicode_literals - import os DIRNAME = os.path.dirname(__file__) From 47accfe05e233b94d16ade2c87c999a105e05d32 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 10 Mar 2021 21:23:06 +0100 Subject: [PATCH 09/18] Add __version__. --- src/voting/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/voting/__init__.py b/src/voting/__init__.py index e69de29..81712ca 100644 --- a/src/voting/__init__.py +++ b/src/voting/__init__.py @@ -0,0 +1,7 @@ +from pkg_resources import get_distribution, DistributionNotFound + +try: + __version__ = get_distribution("django-voting").version +except DistributionNotFound: + # package is not installed + __version__ = None From d6942a62de081882d018288aab99503f7475770c Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 10 Mar 2021 21:23:11 +0100 Subject: [PATCH 10/18] Remove old code. --- src/voting/models.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/voting/models.py b/src/voting/models.py index 5413a17..90ac5b4 100644 --- a/src/voting/models.py +++ b/src/voting/models.py @@ -1,6 +1,5 @@ from datetime import datetime -from django.utils.encoding import python_2_unicode_compatible from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models @@ -21,7 +20,6 @@ ) -@python_2_unicode_compatible class Vote(models.Model): """ A vote on an object by a User. From 2669222b62a2c38b1c4ffc77c64901314aa0748f Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 10 Mar 2021 21:23:19 +0100 Subject: [PATCH 11/18] More compat code. --- src/voting/urls.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/voting/urls.py b/src/voting/urls.py index c9f61d5..28104eb 100644 --- a/src/voting/urls.py +++ b/src/voting/urls.py @@ -1,9 +1,10 @@ -from django.conf.urls import url +from django.urls import re_path + from .views import vote_on_object_with_lazy_model urlpatterns = [ - url( + re_path( r"^vote/(?P[\w\.-]+)/(?P\w+)/" "(?P\d+)/(?Pup|down|clear)/$", vote_on_object_with_lazy_model, From 01deeed3641159baa5519d4a28872c218b742fc1 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 10 Mar 2021 21:37:50 +0100 Subject: [PATCH 12/18] Add GHA test workflow. --- .coveragerc | 5 +-- .github/workflows/test.yml | 51 ++++++++++++++++++++++++++ .gitignore | 1 + .travis.yml | 19 ---------- CHANGELOG.rst | 5 ++- MANIFEST.in | 2 + setup.py | 16 ++++++-- src/voting/templatetags/voting_tags.py | 1 + tests/test_models.py | 1 - tests/test_settings.py | 2 + tox.ini | 34 ++++++++++++++--- 11 files changed, 103 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/test.yml delete mode 100644 .travis.yml create mode 100644 MANIFEST.in diff --git a/.coveragerc b/.coveragerc index 75322c5..fdf965f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,8 +1,5 @@ -# http://nedbatchelder.com/code/coverage/config.html - [run] branch = True -source = src/ +source = voting omit = */migrations/*.py - */south_migrations/*.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..510f920 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,51 @@ +name: Test + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + max-parallel: 5 + matrix: + python-version: ['3.6', '3.7', '3.8', '3.9'] + django-version: ['2.2', '3.0', '3.1', 'main'] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache + uses: actions/cache@v2 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: + ${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}-${{ hashFiles('**/tox.ini') }} + restore-keys: | + ${{ matrix.python-version }}-v1- + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade tox tox-gh-actions + + - name: Tox tests + run: | + tox -v + env: + DJANGO: ${{ matrix.django-version }} + + - name: Upload coverage + uses: codecov/codecov-action@v1 + with: + name: Python ${{ matrix.python-version }} diff --git a/.gitignore b/.gitignore index d3e5c31..469d35e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ dist/ # Coverage .coverage +coverage.xml htmlcov diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0f63f95..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: python -python: 2.7 -install: - - pip install tox -env: - - TOXENV=py27-django1.8 - - TOXENV=py27-django1.9 - - TOXENV=py34-django1.8 - - TOXENV=py34-django1.9 - - TOXENV=py35-django1.8 - - TOXENV=py35-django1.9 - - TOXENV=pypy-django1.8 - - TOXENV=pypy-django1.9 - - TOXENV=py27-django1.10 - - TOXENV=py35-django1.10 - - TOXENV=py34-django1.10 - - TOXENV=pypy-django1.10 -script: - - tox -e $TOXENV diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9ad6827..899e200 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,14 +1,17 @@ Django Voting Changelog ======================= -0.3 (unreleased) +1.0 (unreleased) ---------------- * Drop the ``voting.VERSION`` field, for now. Client code can use APIs such as ``pkg_resources.get_distribution()`` to query or depend on specific versions of django-voting. + * Added Django migrations. +* Drop South migrations. + 0.2 (2012-07-26) ---------------- diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2479736 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include LICENSE.rst README.rst tox.ini CHANGELOG.rst +recursive-include docs * diff --git a/setup.py b/setup.py index 5db3906..0362526 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,11 @@ from setuptools import setup, find_packages - setup( name="django-voting", - use_scm_version=True, + use_scm_version={"version_scheme": "post-release"}, description="Generic voting application for Django", + long_description=open("README.rst").read(), + long_description_content_type="text/x-rst", author="Jonathan Buchanan", author_email="jonathan.buchanan@gmail.com", maintainer="Jannis Leidel", @@ -16,16 +17,25 @@ "setuptools_scm", ], install_requires=[ - "Django >=1.7", + "Django >=2.2", ], classifiers=[ "Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: Django", + "Framework :: Django :: 2.2", + "Framework :: Django :: 3.0", + "Framework :: Django :: 3.1", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3 :: Only', + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Topic :: Utilities", ], ) diff --git a/src/voting/templatetags/voting_tags.py b/src/voting/templatetags/voting_tags.py index 89e2375..7ca5fa1 100644 --- a/src/voting/templatetags/voting_tags.py +++ b/src/voting/templatetags/voting_tags.py @@ -5,6 +5,7 @@ register = template.Library() + class ScoreForObjectNode(template.Node): def __init__(self, object, context_var): self.object = object diff --git a/tests/test_models.py b/tests/test_models.py index d9fcbfd..760215a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,4 +1,3 @@ -# coding: utf-8 from django.contrib.auth.models import User from django.test import TestCase diff --git a/tests/test_settings.py b/tests/test_settings.py index df3b94a..7396afd 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -20,3 +20,5 @@ ] ROOT_URLCONF = "voting.urls" + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/tox.ini b/tox.ini index d7e6499..242c3bd 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,37 @@ [tox] envlist = - py{27,34,35,py}-django1.{8,9,10} + py{36,37,38,39}-dj{22,30,31} + py{38,39}-djmain [testenv] +usedevelop = True deps = - django1.8: Django ~=1.8.0 - django1.9: Django ~=1.9.0 - django1.10: Django ~=1.10.0 + coverage + dj22: django>=2.2,<3.0 + dj30: django>=3.0,<3.1 + dj31: Django>=3.1,<3.2 + djmain: https://github.com/django/django/archive/main.tar.gz setenv = DJANGO_SETTINGS_MODULE = test_settings PYTHONPATH = {toxinidir}/tests commands = - django-admin check - django-admin test tests + {envbindir}/django-admin check + coverage run {envbindir}/django-admin test tests + coverage report + coverage xml +ignore_outcome = + djmain: True + +[gh-actions] +python = + 3.6: py36 + 3.7: py37 + 3.8: py38 + 3.9: py39 + +[gh-actions:env] +DJANGO = + 2.2: dj22 + 3.0: dj30 + 3.1: dj31 + main: djmain From 3bdd812245bf4640bcca18163258b895414f520e Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 10 Mar 2021 21:41:21 +0100 Subject: [PATCH 13/18] Minor update. --- .github/workflows/test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 510f920..02df125 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,10 +4,11 @@ on: [push, pull_request] jobs: build: + name: build (Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }}) runs-on: ubuntu-latest + strategy: fail-fast: false - max-parallel: 5 matrix: python-version: ['3.6', '3.7', '3.8', '3.9'] django-version: ['2.2', '3.0', '3.1', 'main'] @@ -34,7 +35,7 @@ jobs: restore-keys: | ${{ matrix.python-version }}-v1- - - name: Install Python dependencies + - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install --upgrade tox tox-gh-actions From 3c7ad8d9eb0aa59deb3e8da7dd12c251d8b63add Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 10 Mar 2021 21:48:03 +0100 Subject: [PATCH 14/18] Use pytest. --- README.rst | 32 -------------------------------- src/voting/models.py | 8 +------- tox.ini | 9 +++++---- 3 files changed, 6 insertions(+), 43 deletions(-) diff --git a/README.rst b/README.rst index d110827..47960e3 100644 --- a/README.rst +++ b/README.rst @@ -25,35 +25,3 @@ Usage ===== See "overview.txt" in the "docs/" directory. - - -Test suite -========== - -The tests can be run with Django's test runner:: - - django-admin test tests - - -or with pytest:: - - pip install pytest-django - py.test - - -Coverage --------- - -A code coverage report can be collected directly with ``coverage``:: - - pip install coverage - coverage run $(which django-admin) test tests - coverage report - coverage html - - -or with pytest:: - - pip install pytest-cov - py.test --cov - py.test --cov --cov-report=html diff --git a/src/voting/models.py b/src/voting/models.py index 90ac5b4..00a38df 100644 --- a/src/voting/models.py +++ b/src/voting/models.py @@ -1,13 +1,7 @@ -from datetime import datetime - from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models - -try: - from django.utils.timezone import now -except ImportError: - now = datetime.now +from django.utils.timezone import now from voting.managers import VoteManager from voting.utils.user_model import get_user_model_name diff --git a/tox.ini b/tox.ini index 242c3bd..42ccd9a 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,10 @@ envlist = [testenv] usedevelop = True deps = - coverage + pytest + pytest-cov + pytest-django + pytest-flake8 dj22: django>=2.2,<3.0 dj30: django>=3.0,<3.1 dj31: Django>=3.1,<3.2 @@ -16,9 +19,7 @@ setenv = PYTHONPATH = {toxinidir}/tests commands = {envbindir}/django-admin check - coverage run {envbindir}/django-admin test tests - coverage report - coverage xml + pytest --cov voting --cov-append --cov-branch --cov-report term-missing --cov-report=xml ignore_outcome = djmain: True From e05e120f062a52568422180ff40a72e5015f7acd Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 10 Mar 2021 21:52:09 +0100 Subject: [PATCH 15/18] Add badges. --- README.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 47960e3..45bf0e9 100644 --- a/README.rst +++ b/README.rst @@ -2,8 +2,19 @@ Django Voting ============= -This is a generic voting application for Django projects +.. image:: https://jazzband.co/static/img/badge.svg + :target: https://jazzband.co/ + :alt: Jazzband + +.. image:: https://github.com/jazzband/django-voting/workflows/Test/badge.svg + :target: https://github.com/jazzband/django-voting/actions + :alt: GitHub Actions +.. image:: https://codecov.io/gh/jazzband/django-voting/branch/main/graph/badge.svg + :target: https://codecov.io/gh/jazzband/django-voting + :alt: Code coverage + +This is a generic voting application for Django projects Installation ============ From 5060786a9a569baef4d6230f6fc0e8615b4cf99e Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 10 Mar 2021 21:52:15 +0100 Subject: [PATCH 16/18] Add release workflow. --- .github/workflows/release.yml | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..05f6c8c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,40 @@ +name: Release + +on: + push: + tags: + - '*' + +jobs: + build: + if: github.repository == 'jazzband/django-voting' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install dependencies + run: | + python -m pip install -U pip + python -m pip install -U setuptools twine wheel + + - name: Build package + run: | + python setup.py --version + python setup.py sdist --format=gztar bdist_wheel + twine check dist/* + + - name: Upload packages to Jazzband + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@master + with: + user: jazzband + password: ${{ secrets.JAZZBAND_RELEASE_KEY }} + repository_url: https://jazzband.co/projects/django-voting/upload From 1512f8e2cbfba3af970aa0e274a8a45588771d03 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 10 Mar 2021 22:03:39 +0100 Subject: [PATCH 17/18] Update docs. --- CHANGELOG.rst | 21 --- README.rst | 388 +++++++++++++++++++++++++++++++++++++++++++++- docs/overview.rst | 372 -------------------------------------------- 3 files changed, 385 insertions(+), 396 deletions(-) delete mode 100644 CHANGELOG.rst delete mode 100644 docs/overview.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst deleted file mode 100644 index 899e200..0000000 --- a/CHANGELOG.rst +++ /dev/null @@ -1,21 +0,0 @@ -Django Voting Changelog -======================= - -1.0 (unreleased) ----------------- - -* Drop the ``voting.VERSION`` field, for now. - Client code can use APIs such as ``pkg_resources.get_distribution()`` - to query or depend on specific versions of django-voting. - -* Added Django migrations. - -* Drop South migrations. - - -0.2 (2012-07-26) ----------------- - -* Django 1.4 compatibility (timezone support) -* Added a ``time_stamp`` field to ``Vote`` model -* Added South migrations. diff --git a/README.rst b/README.rst index 45bf0e9..dbd588c 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,8 @@ Django Voting :target: https://codecov.io/gh/jazzband/django-voting :alt: Code coverage -This is a generic voting application for Django projects +A generic voting application for Django projects, which allows +registering of votes for any ``Model`` instance. Installation ============ @@ -31,8 +32,389 @@ Installation 3. Run ``django-admin migrate`` - Usage ===== -See "overview.txt" in the "docs/" directory. +Votes +----- + +Votes are represented by the ``Vote`` model, which lives in the +``voting.models`` module. + +Votes are recorded using the ``record_vote`` helper function:: + + >>> from django.contrib.auth.models import User + >>> from shop.apps.products.models import Widget + >>> from voting.models import Vote + >>> user = User.objects.get(pk=1) + >>> widget = Widget.objects.get(pk=1) + >>> Vote.objects.record_vote(widget, user, +1) + +The score for an object can be retrieved using the ``get_score`` +helper function:: + + >>> Vote.objects.get_score(widget) + {'score': 1, 'num_votes': 1} + +If the same user makes another vote on the same object, their vote +is either modified or deleted, as appropriate:: + + >>> Vote.objects.record_vote(widget, user, -1) + >>> Vote.objects.get_score(widget) + {'score': -1, 'num_votes': 1} + >>> Vote.objects.record_vote(widget, user, 0) + >>> Vote.objects.get_score(widget) + {'score': 0, 'num_votes': 0} + +Fields +~~~~~~ + +``Vote`` objects have the following fields: + + * ``user`` -- The user who made the vote. Users are represented by + the ``django.contrib.auth.models.User`` model. + * ``content_type`` -- The ContentType of the object voted on. + * ``object_id`` -- The id of the object voted on. + * ``object`` -- The object voted on. + * ``vote`` -- The vote which was made: ``+1`` or ``-1``. + +Methods +~~~~~~~ + +``Vote`` objects have the following custom methods: + + * ``is_upvote`` -- Returns ``True`` if ``vote`` is ``+1``. + + * ``is_downvote`` -- Returns ``True`` if ``vote`` is ``-1``. + +Manager functions +~~~~~~~~~~~~~~~~~ + +The ``Vote`` model has a custom manager that has the following helper +functions: + + * ``record_vote(obj, user, vote)`` -- Record a user's vote on a + given object. Only allows a given user to vote once on any given + object, but the vote may be changed. + + ``vote`` must be one of ``1`` (up vote), ``-1`` (down vote) or + ``0`` (remove vote). + + * ``get_score(obj)`` -- Gets the total score for ``obj`` and the + total number of votes it's received. + + Returns a dictionary with ``score`` and ``num_votes`` keys. + + * ``get_scores_in_bulk(objects)`` -- Gets score and vote count + details for all the given objects. Score details consist of a + dictionary which has ``score`` and ``num_vote`` keys. + + Returns a dictionary mapping object ids to score details. + + * ``get_top(Model, limit=10, reversed=False)`` -- Gets the top + ``limit`` scored objects for a given model. + + If ``reversed`` is ``True``, the bottom ``limit`` scored objects + are retrieved instead. + + Yields ``(object, score)`` tuples. + + * ``get_bottom(Model, limit=10)`` -- A convenience method which + calls ``get_top`` with ``reversed=True``. + + Gets the bottom (i.e. most negative) ``limit`` scored objects + for a given model. + + Yields ``(object, score)`` tuples. + + * ``get_for_user(obj, user)`` -- Gets the vote made on the given + object by the given user, or ``None`` if no matching vote + exists. + + * ``get_for_user_in_bulk(objects, user)`` -- Gets the votes + made on all the given objects by the given user. + + Returns a dictionary mapping object ids to votes. + +Generic Views +------------- + +The ``voting.views`` module contains views to handle a couple of +common cases: displaying a page to confirm a vote when it is requested +via ``GET`` and making the vote itself via ``POST``, or voting via +XMLHttpRequest ``POST``. + +The following sample URLconf demonstrates using a generic view for +voting on a model, allowing for regular voting and XMLHttpRequest +voting at the same URL:: + + from django.urls import re_path + from voting.views import vote_on_object + from shop.apps.products.models import Widget + + widget_kwargs = { + 'model': Widget, + 'template_object_name': 'widget', + 'allow_xmlhttprequest': True, + } + + urlpatterns = [ + re_path( + r"^widgets/(?P\d+)/(?Pup|down|clear)vote/?$", + vote_on_object, + kwargs=widget_kwargs, + ), + ] + +``voting.views.vote_on_object`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Description:** + +A view that displays a confirmation page and votes on an object. The +given object will only be voted on if the request method is ``POST``. +If this view is fetched via ``GET``, it will display a confirmation +page that should contain a form that ``POST``\s to the same URL. + +**Required arguments:** + + * ``model``: The Django model class of the object that will be + voted on. + + * Either ``object_id`` or (``slug`` *and* ``slug_field``) is + required. + + If you provide ``object_id``, it should be the value of the + primary-key field for the object being voted on. + + Otherwise, ``slug`` should be the slug of the given object, and + ``slug_field`` should be the name of the slug field in the + ``QuerySet``'s model. + + * ``direction``: The kind of vote to be made, must be one of + ``'up'``, ``'down'`` or ``'clear'``. + + * Either a ``post_vote_redirect`` argument defining a URL must + be supplied, or a ``next`` parameter must supply a URL in the + request when the vote is ``POST``\ed, or the object being voted + on must define a ``get_absolute_url`` method or property. + + The view checks for these in the order given above. + +**Optional arguments:** + + * ``allow_xmlhttprequest``: A boolean that designates whether this + view should also allow votes to be made via XMLHttpRequest. + + If this is ``True``, the request headers will be check for an + ``HTTP_X_REQUESTED_WITH`` header which has a value of + ``XMLHttpRequest``. If this header is found, processing of the + current request is delegated to + ``voting.views.xmlhttprequest_vote_on_object``. + + * ``template_name``: The full name of a template to use in + rendering the page. This lets you override the default template + name (see below). + + * ``template_loader``: The template loader to use when loading the + template. By default, it's ``django.template.loader``. + + * ``extra_context``: A dictionary of values to add to the template + context. By default, this is an empty dictionary. If a value in + the dictionary is callable, the generic view will call it just + before rendering the template. + + * ``context_processors``: A list of template-context processors to + apply to the view's template. + + * ``template_object_name``: Designates the name of the template + variable to use in the template context. By default, this is + ``'object'``. + +**Template name:** + +If ``template_name`` isn't specified, this view will use the template +``/_confirm_vote.html`` by default. + +**Template context:** + +In addition to ``extra_context``, the template's context will be: + + * ``object``: The original object that's about to be voted on. + This variable's name depends on the ``template_object_name`` + parameter, which is ``'object'`` by default. If + ``template_object_name`` is ``'foo'``, this variable's name will + be ``foo``. + + * ``direction``: The argument which was given for the vote's + ``direction`` (see above). + +``voting.views.xmlhttprequest_vote_on_object`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Description:** + +A view for use in voting on objects via XMLHttpRequest. The given +object will only be voted on if the request method is ``POST``. This +view will respond with JSON text instead of rendering a template or +redirecting. + +**Required arguments:** + + * ``model``: The Django model class of the object that will be + voted on. + + * Either ``object_id`` or (``slug`` *and* ``slug_field``) is + required. + + If you provide ``object_id``, it should be the value of the + primary-key field for the object being voted on. + + Otherwise, ``slug`` should be the slug of the given object, and + ``slug_field`` should be the name of the slug field in the + ``QuerySet``'s model. + + * ``direction``: The kind of vote to be made, must be one of + ``'up'``, ``'down'`` or ``'clear'``. + +**JSON text context:** + +The context provided by the JSON text returned will be: + + * ``success``: ``true`` if the vote was successfully processed, + ``false`` otherwise. + + * ``score``: an object containing a ``score`` property, which + holds the object's updated score, and a ``num_votes`` property, + which holds the total number of votes cast for the object. + + * ``error_message``: if the vote was not successfully processed, + this property will contain an error message. + + +Template tags +------------- + +The ``voting.templatetags.voting_tags`` module defines a number of +template tags which may be used to retrieve and display voting +details. + +Tag reference +~~~~~~~~~~~~~ + +score_for_object +```````````````` + +Retrieves the total score for an object and the number of votes +it's received, storing them in a context variable which has ``score`` +and ``num_votes`` properties. + +Example usage:: + + {% score_for_object widget as score %} + + {{ score.score }} point{{ score.score|pluralize }} + after {{ score.num_votes }} vote{{ score.num_votes|pluralize }} + +scores_for_objects +`````````````````` + +Retrieves the total scores and number of votes cast for a list of +objects as a dictionary keyed with the objects' ids and stores it in a +context variable. + +Example usage:: + + {% scores_for_objects widget_list as scores %} + +vote_by_user +```````````` + +Retrieves the ``Vote`` cast by a user on a particular object and +stores it in a context variable. If the user has not voted, the +context variable will be ``None``. + +Example usage:: + + {% vote_by_user user on widget as vote %} + +votes_by_user +````````````` + +Retrieves the votes cast by a user on a list of objects as a +dictionary keyed with object ids and stores it in a context +variable. + +Example usage:: + + {% votes_by_user user on widget_list as vote_dict %} + +dict_entry_for_item +``````````````````` + +Given an object and a dictionary keyed with object ids - as returned +by the ``votes_by_user`` and ``scores_for_objects`` template tags - +retrieves the value for the given object and stores it in a context +variable, storing ``None`` if no value exists for the given object. + +Example usage:: + + {% dict_entry_for_item widget from vote_dict as vote %} + +confirm_vote_message +```````````````````` + +Intended for use in vote confirmatio templates, creates an appropriate +message asking the user to confirm the given vote for the given object +description. + +Example usage:: + + {% confirm_vote_message widget.title direction %} + +Filter reference +~~~~~~~~~~~~~~~~ + +vote_display +```````````` + +Given a string mapping values for up and down votes, returns one +of the strings according to the given ``Vote``: + +========= ===================== ============= +Vote type Argument Outputs +========= ===================== ============= +``+1`` ``'Bodacious,Bogus'`` ``Bodacious`` +``-1`` ``'Bodacious,Bogus'`` ``Bogus`` +========= ===================== ============= + +If no string mapping is given, ``'Up'`` and ``'Down'`` will be used. + +Example usage:: + + {{ vote|vote_display:"Bodacious,Bogus" }} + +Changelog +========= + +1.0 (unreleased) +---------------- + +* Drop the ``voting.VERSION`` field, for now. + Client code can use APIs such as ``pkg_resources.get_distribution()`` + to query or depend on specific versions of django-voting. + +* Added Django migrations. + +* Drop South migrations. + +* Add Django 2.2, 3.0, 3.1 support, drop support for all versions before that. + +* Move CI to GitHub Actions: https://github.com/jazzband/django-voting/actions + +0.2 (2012-07-26) +---------------- + +* Django 1.4 compatibility (timezone support) +* Added a ``time_stamp`` field to ``Vote`` model +* Added South migrations. diff --git a/docs/overview.rst b/docs/overview.rst deleted file mode 100644 index 3ad3d92..0000000 --- a/docs/overview.rst +++ /dev/null @@ -1,372 +0,0 @@ -============== -Django Voting -============== - -A generic voting application for Django projects, which allows -registering of votes for any ``Model`` instance. - - -Votes -===== - -Votes are represented by the ``Vote`` model, which lives in the -``voting.models`` module. - -API reference -------------- - -Fields -~~~~~~ - -``Vote`` objects have the following fields: - - * ``user`` -- The user who made the vote. Users are represented by - the ``django.contrib.auth.models.User`` model. - * ``content_type`` -- The ContentType of the object voted on. - * ``object_id`` -- The id of the object voted on. - * ``object`` -- The object voted on. - * ``vote`` -- The vote which was made: ``+1`` or ``-1``. - -Methods -~~~~~~~ - -``Vote`` objects have the following custom methods: - - * ``is_upvote`` -- Returns ``True`` if ``vote`` is ``+1``. - - * ``is_downvote`` -- Returns ``True`` if ``vote`` is ``-1``. - -Manager functions -~~~~~~~~~~~~~~~~~ - -The ``Vote`` model has a custom manager that has the following helper -functions: - - * ``record_vote(obj, user, vote)`` -- Record a user's vote on a - given object. Only allows a given user to vote once on any given - object, but the vote may be changed. - - ``vote`` must be one of ``1`` (up vote), ``-1`` (down vote) or - ``0`` (remove vote). - - * ``get_score(obj)`` -- Gets the total score for ``obj`` and the - total number of votes it's received. - - Returns a dictionary with ``score`` and ``num_votes`` keys. - - * ``get_scores_in_bulk(objects)`` -- Gets score and vote count - details for all the given objects. Score details consist of a - dictionary which has ``score`` and ``num_vote`` keys. - - Returns a dictionary mapping object ids to score details. - - * ``get_top(Model, limit=10, reversed=False)`` -- Gets the top - ``limit`` scored objects for a given model. - - If ``reversed`` is ``True``, the bottom ``limit`` scored objects - are retrieved instead. - - Yields ``(object, score)`` tuples. - - * ``get_bottom(Model, limit=10)`` -- A convenience method which - calls ``get_top`` with ``reversed=True``. - - Gets the bottom (i.e. most negative) ``limit`` scored objects - for a given model. - - Yields ``(object, score)`` tuples. - - * ``get_for_user(obj, user)`` -- Gets the vote made on the given - object by the given user, or ``None`` if no matching vote - exists. - - * ``get_for_user_in_bulk(objects, user)`` -- Gets the votes - made on all the given objects by the given user. - - Returns a dictionary mapping object ids to votes. - -Basic usage ------------ - -Recording votes and retrieving scores -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Votes are recorded using the ``record_vote`` helper function:: - - >>> from django.contrib.auth.models import User - >>> from shop.apps.products.models import Widget - >>> from voting.models import Vote - >>> user = User.objects.get(pk=1) - >>> widget = Widget.objects.get(pk=1) - >>> Vote.objects.record_vote(widget, user, +1) - -The score for an object can be retrieved using the ``get_score`` -helper function:: - - >>> Vote.objects.get_score(widget) - {'score': 1, 'num_votes': 1} - -If the same user makes another vote on the same object, their vote -is either modified or deleted, as appropriate:: - - >>> Vote.objects.record_vote(widget, user, -1) - >>> Vote.objects.get_score(widget) - {'score': -1, 'num_votes': 1} - >>> Vote.objects.record_vote(widget, user, 0) - >>> Vote.objects.get_score(widget) - {'score': 0, 'num_votes': 0} - - -Generic Views -============= - -The ``voting.views`` module contains views to handle a couple of -common cases: displaying a page to confirm a vote when it is requested -via ``GET`` and making the vote itself via ``POST``, or voting via -XMLHttpRequest ``POST``. - -The following sample URLconf demonstrates using a generic view for -voting on a model, allowing for regular voting and XMLHttpRequest -voting at the same URL:: - - from django.conf.urls.defaults import * - from voting.views import vote_on_object - from shop.apps.products.models import Widget - - widget_dict = { - 'model': Widget, - 'template_object_name': 'widget', - 'allow_xmlhttprequest': True, - } - - urlpatterns = patterns('', - (r'^widgets/(?P\d+)/(?Pup|down|clear)vote/?$', vote_on_object, widget_dict), - ) - -``voting.views.vote_on_object`` --------------------------------- - -**Description:** - -A view that displays a confirmation page and votes on an object. The -given object will only be voted on if the request method is ``POST``. -If this view is fetched via ``GET``, it will display a confirmation -page that should contain a form that ``POST``\s to the same URL. - -**Required arguments:** - - * ``model``: The Django model class of the object that will be - voted on. - - * Either ``object_id`` or (``slug`` *and* ``slug_field``) is - required. - - If you provide ``object_id``, it should be the value of the - primary-key field for the object being voted on. - - Otherwise, ``slug`` should be the slug of the given object, and - ``slug_field`` should be the name of the slug field in the - ``QuerySet``'s model. - - * ``direction``: The kind of vote to be made, must be one of - ``'up'``, ``'down'`` or ``'clear'``. - - * Either a ``post_vote_redirect`` argument defining a URL must - be supplied, or a ``next`` parameter must supply a URL in the - request when the vote is ``POST``\ed, or the object being voted - on must define a ``get_absolute_url`` method or property. - - The view checks for these in the order given above. - -**Optional arguments:** - - * ``allow_xmlhttprequest``: A boolean that designates whether this - view should also allow votes to be made via XMLHttpRequest. - - If this is ``True``, the request headers will be check for an - ``HTTP_X_REQUESTED_WITH`` header which has a value of - ``XMLHttpRequest``. If this header is found, processing of the - current request is delegated to - ``voting.views.xmlhttprequest_vote_on_object``. - - * ``template_name``: The full name of a template to use in - rendering the page. This lets you override the default template - name (see below). - - * ``template_loader``: The template loader to use when loading the - template. By default, it's ``django.template.loader``. - - * ``extra_context``: A dictionary of values to add to the template - context. By default, this is an empty dictionary. If a value in - the dictionary is callable, the generic view will call it just - before rendering the template. - - * ``context_processors``: A list of template-context processors to - apply to the view's template. - - * ``template_object_name``: Designates the name of the template - variable to use in the template context. By default, this is - ``'object'``. - -**Template name:** - -If ``template_name`` isn't specified, this view will use the template -``/_confirm_vote.html`` by default. - -**Template context:** - -In addition to ``extra_context``, the template's context will be: - - * ``object``: The original object that's about to be voted on. - This variable's name depends on the ``template_object_name`` - parameter, which is ``'object'`` by default. If - ``template_object_name`` is ``'foo'``, this variable's name will - be ``foo``. - - * ``direction``: The argument which was given for the vote's - ``direction`` (see above). - -``voting.views.xmlhttprequest_vote_on_object`` ------------------------------------------------ - -**Description:** - -A view for use in voting on objects via XMLHttpRequest. The given -object will only be voted on if the request method is ``POST``. This -view will respond with JSON text instead of rendering a template or -redirecting. - -**Required arguments:** - - * ``model``: The Django model class of the object that will be - voted on. - - * Either ``object_id`` or (``slug`` *and* ``slug_field``) is - required. - - If you provide ``object_id``, it should be the value of the - primary-key field for the object being voted on. - - Otherwise, ``slug`` should be the slug of the given object, and - ``slug_field`` should be the name of the slug field in the - ``QuerySet``'s model. - - * ``direction``: The kind of vote to be made, must be one of - ``'up'``, ``'down'`` or ``'clear'``. - -**JSON text context:** - -The context provided by the JSON text returned will be: - - * ``success``: ``true`` if the vote was successfully processed, - ``false`` otherwise. - - * ``score``: an object containing a ``score`` property, which - holds the object's updated score, and a ``num_votes`` property, - which holds the total number of votes cast for the object. - - * ``error_message``: if the vote was not successfully processed, - this property will contain an error message. - - -Template tags -============= - -The ``voting.templatetags.voting_tags`` module defines a number of -template tags which may be used to retrieve and display voting -details. - -Tag reference -------------- - -score_for_object -~~~~~~~~~~~~~~~~ - -Retrieves the total score for an object and the number of votes -it's received, storing them in a context variable which has ``score`` -and ``num_votes`` properties. - -Example usage:: - - {% score_for_object widget as score %} - - {{ score.score }} point{{ score.score|pluralize }} - after {{ score.num_votes }} vote{{ score.num_votes|pluralize }} - -scores_for_objects -~~~~~~~~~~~~~~~~~~ - -Retrieves the total scores and number of votes cast for a list of -objects as a dictionary keyed with the objects' ids and stores it in a -context variable. - -Example usage:: - - {% scores_for_objects widget_list as scores %} - -vote_by_user -~~~~~~~~~~~~ - -Retrieves the ``Vote`` cast by a user on a particular object and -stores it in a context variable. If the user has not voted, the -context variable will be ``None``. - -Example usage:: - - {% vote_by_user user on widget as vote %} - -votes_by_user -~~~~~~~~~~~~~ - -Retrieves the votes cast by a user on a list of objects as a -dictionary keyed with object ids and stores it in a context -variable. - -Example usage:: - - {% votes_by_user user on widget_list as vote_dict %} - -dict_entry_for_item -~~~~~~~~~~~~~~~~~~~ - -Given an object and a dictionary keyed with object ids - as returned -by the ``votes_by_user`` and ``scores_for_objects`` template tags - -retrieves the value for the given object and stores it in a context -variable, storing ``None`` if no value exists for the given object. - -Example usage:: - - {% dict_entry_for_item widget from vote_dict as vote %} - -confirm_vote_message -~~~~~~~~~~~~~~~~~~~~ - -Intended for use in vote confirmatio templates, creates an appropriate -message asking the user to confirm the given vote for the given object -description. - -Example usage:: - - {% confirm_vote_message widget.title direction %} - -Filter reference ----------------- - -vote_display -~~~~~~~~~~~~ - -Given a string mapping values for up and down votes, returns one -of the strings according to the given ``Vote``: - -========= ===================== ============= -Vote type Argument Outputs -========= ===================== ============= -``+1`` ``'Bodacious,Bogus'`` ``Bodacious`` -``-1`` ``'Bodacious,Bogus'`` ``Bogus`` -========= ===================== ============= - -If no string mapping is given, ``'Up'`` and ``'Down'`` will be used. - -Example usage:: - - {{ vote|vote_display:"Bodacious,Bogus" }} From 84b12f615acecdea4db479bb3afd2399f49c3bf7 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 10 Mar 2021 22:04:08 +0100 Subject: [PATCH 18/18] Update manifest template. --- MANIFEST.in | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 2479736..3e70b0c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1 @@ -include LICENSE.rst README.rst tox.ini CHANGELOG.rst -recursive-include docs * +include LICENSE.rst README.rst tox.ini