Service for adding, removing and listing votes
parent
baeab00e28
commit
fcf4747e93
|
@ -183,6 +183,7 @@ INSTALLED_APPS = [
|
||||||
"taiga.projects.history",
|
"taiga.projects.history",
|
||||||
"taiga.projects.notifications",
|
"taiga.projects.notifications",
|
||||||
"taiga.projects.stars",
|
"taiga.projects.stars",
|
||||||
|
"taiga.projects.votes",
|
||||||
|
|
||||||
"south",
|
"south",
|
||||||
"reversion",
|
"reversion",
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
|
@ -0,0 +1,35 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.contrib.contenttypes import generic
|
||||||
|
|
||||||
|
|
||||||
|
class Votes(models.Model):
|
||||||
|
content_type = models.ForeignKey("contenttypes.ContentType")
|
||||||
|
object_id = models.PositiveIntegerField()
|
||||||
|
content_object = generic.GenericForeignKey("content_type", "object_id")
|
||||||
|
count = models.PositiveIntegerField(default=0)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Votes")
|
||||||
|
verbose_name_plural = _("Votes")
|
||||||
|
unique_together = ("content_type", "object_id")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.count
|
||||||
|
|
||||||
|
|
||||||
|
class Vote(models.Model):
|
||||||
|
content_type = models.ForeignKey("contenttypes.ContentType")
|
||||||
|
object_id = models.PositiveIntegerField(null=False)
|
||||||
|
content_object = generic.GenericForeignKey("content_type", "object_id")
|
||||||
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
|
||||||
|
related_name="votes", verbose_name=_("votes"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Vote")
|
||||||
|
verbose_name_plural = _("Votes")
|
||||||
|
unique_together = ("content_type", "object_id", "user")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.user
|
|
@ -0,0 +1,92 @@
|
||||||
|
from django.db.models import F
|
||||||
|
from django.db.transaction import atomic
|
||||||
|
from django.db.models.loading import get_model
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from .models import Votes, Vote
|
||||||
|
|
||||||
|
|
||||||
|
def add_vote(obj, user):
|
||||||
|
"""Add a vote to an object.
|
||||||
|
|
||||||
|
If the user has already voted the object nothing happends, so this function can be considered
|
||||||
|
idempotent.
|
||||||
|
|
||||||
|
:param obj: Any Django model instance.
|
||||||
|
:param user: User adding the vote. :class:`~taiga.users.models.User` instance.
|
||||||
|
"""
|
||||||
|
obj_type = get_model("contenttypes", "ContentType").objects.get_for_model(obj)
|
||||||
|
with atomic():
|
||||||
|
_, created = Vote.objects.get_or_create(content_type=obj_type, object_id=obj.id, user=user)
|
||||||
|
|
||||||
|
if not created:
|
||||||
|
return
|
||||||
|
|
||||||
|
votes, _ = Votes.objects.get_or_create(content_type=obj_type, object_id=obj.id)
|
||||||
|
votes.count = F('count') + 1
|
||||||
|
votes.save()
|
||||||
|
|
||||||
|
|
||||||
|
def remove_vote(obj, user):
|
||||||
|
"""Remove an user vote from an object.
|
||||||
|
|
||||||
|
If the user has not voted the object nothing happens so this function can be considered
|
||||||
|
idempotent.
|
||||||
|
|
||||||
|
:param obj: Any Django model instance.
|
||||||
|
:param user: User removing her vote. :class:`~taiga.users.models.User` instance.
|
||||||
|
"""
|
||||||
|
obj_type = get_model("contenttypes", "ContentType").objects.get_for_model(obj)
|
||||||
|
with atomic():
|
||||||
|
qs = Vote.objects.filter(content_type=obj_type, object_id=obj.id, user=user)
|
||||||
|
if not qs.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
qs.delete()
|
||||||
|
|
||||||
|
votes, _ = Votes.objects.get_or_create(content_type=obj_type, object_id=obj.id)
|
||||||
|
votes.count = F('count') - 1
|
||||||
|
votes.save()
|
||||||
|
|
||||||
|
|
||||||
|
def get_voters(obj):
|
||||||
|
"""Get the voters of an object.
|
||||||
|
|
||||||
|
:param obj: Any Django model instance.
|
||||||
|
|
||||||
|
:return: User queryset object representing the users that voted the object.
|
||||||
|
"""
|
||||||
|
obj_type = get_model("contenttypes", "ContentType").objects.get_for_model(obj)
|
||||||
|
|
||||||
|
return get_user_model().objects.filter(votes__content_type=obj_type, votes__object_id=obj.id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_votes(obj):
|
||||||
|
"""Get the number of votes an object has.
|
||||||
|
|
||||||
|
:param obj: Any Django model instance.
|
||||||
|
|
||||||
|
:return: Number of votes or `0` if the object has no votes at all.
|
||||||
|
"""
|
||||||
|
obj_type = get_model("contenttypes", "ContentType").objects.get_for_model(obj)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return Votes.objects.get(content_type=obj_type, object_id=obj.id).count
|
||||||
|
except Votes.DoesNotExist:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_voted(user, obj_class):
|
||||||
|
"""Get the objects voted by an user.
|
||||||
|
|
||||||
|
:param user: :class:`~taiga.users.models.User` instance.
|
||||||
|
:param obj_class: Show only objects of this kind. Can be any Django model class.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
obj_type = get_model("contenttypes", "ContentType").objects.get_for_model(obj_class)
|
||||||
|
conditions = ('votes_vote.content_type_id = %s',
|
||||||
|
'%s.id = votes_vote.object_id' % obj_class._meta.db_table,
|
||||||
|
'votes_vote.user_id = %s')
|
||||||
|
return obj_class.objects.extra(where=conditions, tables=('votes_vote',),
|
||||||
|
params=(obj_type.id, user.id))
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
|
@ -173,3 +173,22 @@ class StarsFactory(Factory):
|
||||||
|
|
||||||
project = factory.SubFactory("tests.factories.ProjectFactory")
|
project = factory.SubFactory("tests.factories.ProjectFactory")
|
||||||
count = 0
|
count = 0
|
||||||
|
|
||||||
|
|
||||||
|
class VoteFactory(Factory):
|
||||||
|
FACTORY_FOR = get_model("votes", "Vote")
|
||||||
|
|
||||||
|
content_type = factory.SubFactory("tests.factories.ContentTypeFactory")
|
||||||
|
object_id = factory.Sequence(lambda n: n)
|
||||||
|
user = factory.SubFactory("tests.factories.UserFactory")
|
||||||
|
|
||||||
|
|
||||||
|
class VotesFactory(Factory):
|
||||||
|
FACTORY_FOR = get_model("votes", "Votes")
|
||||||
|
|
||||||
|
content_type = factory.SubFactory("tests.factories.ContentTypeFactory")
|
||||||
|
object_id = factory.Sequence(lambda n: n)
|
||||||
|
|
||||||
|
|
||||||
|
class ContentTypeFactory(Factory):
|
||||||
|
FACTORY_FOR = get_model("contenttypes", "ContentType")
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
from taiga.projects.votes import services as votes, models
|
||||||
|
|
||||||
|
from .. import factories as f
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_vote():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
project_type = ContentType.objects.get_for_model(project)
|
||||||
|
user = f.UserFactory()
|
||||||
|
votes_qs = models.Votes.objects.filter(content_type=project_type, object_id=project.id)
|
||||||
|
|
||||||
|
votes.add_vote(project, user)
|
||||||
|
|
||||||
|
assert votes_qs.get().count == 1
|
||||||
|
|
||||||
|
votes.add_vote(project, user) # add_vote must be idempotent
|
||||||
|
|
||||||
|
assert votes_qs.get().count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_vote():
|
||||||
|
user = f.UserFactory()
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
project_type = ContentType.objects.get_for_model(project)
|
||||||
|
votes_qs = models.Votes.objects.filter(content_type=project_type, object_id=project.id)
|
||||||
|
f.VotesFactory(content_type=project_type, object_id=project.id, count=1)
|
||||||
|
f.VoteFactory(content_type=project_type, object_id=project.id, user=user)
|
||||||
|
|
||||||
|
assert votes_qs.get().count == 1
|
||||||
|
|
||||||
|
votes.remove_vote(project, user)
|
||||||
|
|
||||||
|
assert votes_qs.get().count == 0
|
||||||
|
|
||||||
|
votes.remove_vote(project, user) # remove_vote must be idempotent
|
||||||
|
|
||||||
|
assert votes_qs.get().count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_votes():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
project_type = ContentType.objects.get_for_model(project)
|
||||||
|
f.VotesFactory(content_type=project_type, object_id=project.id, count=4)
|
||||||
|
|
||||||
|
assert votes.get_votes(project) == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_voters():
|
||||||
|
f.UserFactory()
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
project_type = ContentType.objects.get_for_model(project)
|
||||||
|
vote = f.VoteFactory(content_type=project_type, object_id=project.id)
|
||||||
|
|
||||||
|
assert list(votes.get_voters(project)) == [vote.user]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_voted():
|
||||||
|
f.ProjectFactory()
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
project_type = ContentType.objects.get_for_model(project)
|
||||||
|
vote = f.VoteFactory(content_type=project_type, object_id=project.id)
|
||||||
|
|
||||||
|
assert list(votes.get_voted(vote.user, type(project))) == [project]
|
Loading…
Reference in New Issue