Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -q Django==${{ matrix.django-version }} flake8 coverage djangorestframework
pip install -q Django==${{ matrix.django-version }} flake8 coverage djangorestframework swapper tox
- name: Lint with flake8
run: |
flake8 --exclude vote/migrations/* vote
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ __pycache__/
htmlcov
.coverage
.eggs/
.tox/
72 changes: 72 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,75 @@ POST /api/comments/{id}/vote/
POST /api/comments/{id}/vote/ {"action":"down"}
DELETE /api/comments/{id}/vote/
```

### Swapping Vote Model

To swap Vote model with your own model:

1. Declare your own Vote model:

```
# myvote/models.py

from vote.base_models import AbstractVote
from vote.models import VoteModel

class MyVote(AbstractVote):
'''To test model swapping'''
modified = models.DateTimeField(auto_now=True)

class Meta:
abstract = False
unique_together = ('user_id', 'content_type', 'object_id', 'action')
index_together = ('content_type', 'object_id')
```

2. In your votable model:

```
from vote.models import VotableManager
from myvote.models import MyVote

class Comment(VoteModel):
user_id = models.BigIntegerField()
content = models.TextField()
num_vote = models.IntegerField(default=0)
create_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True)

votes = VotableManager(MyVote)
```

3. Update settings, swapping the Vote model with yours:

```
VOTE_VOTE_MODEL = 'myvote.MyVote
```

Alternatively, declare your own VotableManager, which overrides the constructor
specifying `MyVote` as the `through` argument.

```
class MyVotableManager(VotableMangaer):
def __init__(self, **kwargs):
super().__init__(MyVote, **kwargs)


class MyVoteModel(VoteModel):
votes = MyVotableManager()

class Meta:
abstract = True
```

Then you can use `MyVoteModel` instead of the default `VoteModel` in your
votable models:

```
class Comment(MyVoteModel):
user_id = models.BigIntegerField()
content = models.TextField()
num_vote = models.IntegerField(default=0)
create_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True)
```
10 changes: 9 additions & 1 deletion runtests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@

from django.conf import settings
from django.core.management import execute_from_command_line
from django.utils.functional import empty

if not settings.configured:

def configure(**options):
if settings._wrapped is not empty:
settings._wrapped = empty
settings.configure(
SECRET_KEY="secret key",
DATABASES={
Expand Down Expand Up @@ -40,8 +44,12 @@
},
},
],
**options
)

if not settings.configured:
configure()


def runtests():
argv = sys.argv[:1] + ["test"] + sys.argv[1:]
Expand Down
27 changes: 27 additions & 0 deletions runtests_swapped.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env python
import sys

from django.conf import settings
from django.core.management import execute_from_command_line
from runtests import configure

if not settings.configured:
configure()


def runtests_swapped():
argv = sys.argv[:1] + ["test", "test.tests.VoteTest.test_vote_up"] + sys.argv[1:]
execute_from_command_line(argv)
configure(VOTE_VOTE_MODEL='test.MyVote')
from test.models import Comment, MyVote
from vote.models import VotableManager
from vote.utils import _reset_vote_model
_reset_vote_model()
# The VoteModel's votes manager has to be updated for the new Vote model
Comment.votes = VotableManager(MyVote)
argv = sys.argv[:1] + ["test"] + sys.argv[1:]
execute_from_command_line(argv)


if __name__ == "__main__":
runtests_swapped()
11 changes: 11 additions & 0 deletions test/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
from django.db import models
from vote.base_models import AbstractVote
from vote.models import VoteModel


# Create your models here.
class MyVote(AbstractVote):
'''To test model swapping'''
modified = models.DateTimeField(auto_now=True)

class Meta:
abstract = False
unique_together = ('user_id', 'content_type', 'object_id', 'action')
index_together = ('content_type', 'object_id')


class Comment(VoteModel):
user_id = models.BigIntegerField()
content = models.TextField()
Expand Down
12 changes: 8 additions & 4 deletions test/tests.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import absolute_import
from django.test import TestCase
from django.contrib.auth.models import User
from vote.models import Vote, UP, DOWN
from test.models import Comment
from vote.base_models import UP, DOWN
from vote.models import Vote
from test.models import Comment, MyVote
from vote.utils import _get_vote_model


class VoteTest(TestCase):
Expand All @@ -23,7 +25,8 @@ def setUp(self):

def tearDown(self):
self.model.objects.all().delete()
self.through.objects.all().delete()
vote_model = _get_vote_model()
vote_model.objects.all().delete()
User.objects.all().delete()

def call_api(self, api_name, model=None, *args, **kwargs):
Expand Down Expand Up @@ -81,7 +84,8 @@ def test_vote_get(self):
content="I'm a comment")
self.assertIsNone(self.call_api('get', comment, self.user2.pk))
self.call_api('up', comment, self.user2.pk)
vote = Vote.objects.first()
vote_model = _get_vote_model()
vote = vote_model.objects.first()
self.assertEqual(vote, self.call_api('get', comment, self.user2.pk))

def test_vote_all(self):
Expand Down
19 changes: 19 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[tox]
envlist = py39-django{22,32,40},py39-django{22,32,40}-swapper

[testenv-django{22,32,40}]
deps =
django22: Django==2.2
django32: Django==3.2
django40: Django==4.0
djangorestframework
commands = ./runtests.py

[testenv:py39-django{22,32,40}-swapper]
deps =
django22: Django==2.2
django32: Django==3.2
django40: Django==4.0
djangorestframework
swapper: swapper==1.3.0
commands = ./runtests_swapped.py
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems there are many duplications with the current Github CI setup, could we use GitHub action to do this?

56 changes: 56 additions & 0 deletions vote/base_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey

UP = 0
DOWN = 1


class VoteManager(models.Manager):

def filter(self, *args, **kwargs):
if 'content_object' in kwargs:
content_object = kwargs.pop('content_object')
content_type = ContentType.objects.get_for_model(content_object)
kwargs.update({
'content_type': content_type,
'object_id': content_object.pk
})

return super(VoteManager, self).filter(*args, **kwargs)


class AbstractVote(models.Model):
ACTION_FIELD = {
UP: 'num_vote_up',
DOWN: 'num_vote_down'
}

user_id = models.BigIntegerField()
content_type = models.ForeignKey(
ContentType,
related_name='+',
on_delete=models.CASCADE
)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey()
action = models.PositiveSmallIntegerField(default=UP)
create_at = models.DateTimeField(auto_now_add=True)

objects = VoteManager()

class Meta:
abstract = True
index_together = ('content_type', 'object_id')

@classmethod
def votes_for(cls, model, instance=None, action=UP):
ct = ContentType.objects.get_for_model(model)
kwargs = {
"content_type": ct,
"action": action
}
if instance is not None:
kwargs["object_id"] = instance.pk

return cls.objects.filter(**kwargs)
16 changes: 10 additions & 6 deletions vote/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ def __init__(self, through, model, instance, field_name='votes'):
self.field_name = field_name

def vote(self, user_id, action):
'''Returns the Vote object on success. None on failure.'''
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change for users who are using this API, why do you change this?

try:
vote = None
with transaction.atomic():
self.instance = self.model.objects.select_for_update().get(
pk=self.instance.pk)
Expand All @@ -75,20 +77,22 @@ def vote(self, user_id, action):
setattr(self.instance, voted_field,
getattr(self.instance, voted_field) - 1)
except self.through.DoesNotExist:
self.through.objects.create(user_id=user_id,
content_type=content_type,
object_id=self.instance.pk,
action=action)
vote = self.through.objects.create(
user_id=user_id,
content_type=content_type,
object_id=self.instance.pk,
action=action
)

statistics_field = self.through.ACTION_FIELD.get(action)
setattr(self.instance, statistics_field,
getattr(self.instance, statistics_field) + 1)

self.instance.save()

return True
return vote
except (OperationalError, IntegrityError):
return False
return None

@instance_required
def up(self, user_id):
Expand Down
55 changes: 10 additions & 45 deletions vote/models.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,21 @@
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from vote.base_models import AbstractVote
from vote.managers import VotableManager
try:
from swapper import swappable_setting
except ImportError:
swappable_setting = None

UP = 0
DOWN = 1
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is also a breaking change that users' code could be broken after upgrading if they use this variable.



class VoteManager(models.Manager):

def filter(self, *args, **kwargs):
if 'content_object' in kwargs:
content_object = kwargs.pop('content_object')
content_type = ContentType.objects.get_for_model(content_object)
kwargs.update({
'content_type': content_type,
'object_id': content_object.pk
})

return super(VoteManager, self).filter(*args, **kwargs)


class Vote(models.Model):
ACTION_FIELD = {
UP: 'num_vote_up',
DOWN: 'num_vote_down'
}

user_id = models.BigIntegerField()
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey()
action = models.PositiveSmallIntegerField(default=UP)
create_at = models.DateTimeField(auto_now_add=True)

objects = VoteManager()
class Vote(AbstractVote):

class Meta:
abstract = False
unique_together = ('user_id', 'content_type', 'object_id', 'action')
index_together = ('content_type', 'object_id')

@classmethod
def votes_for(cls, model, instance=None, action=UP):
ct = ContentType.objects.get_for_model(model)
kwargs = {
"content_type": ct,
"action": action
}
if instance is not None:
kwargs["object_id"] = instance.pk

return cls.objects.filter(**kwargs)
swappable = None
if swappable_setting:
swappable = swappable_setting('vote', 'Vote')


class VoteModel(models.Model):
Expand Down
2 changes: 1 addition & 1 deletion vote/templatetags/vote.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django import template
from django.contrib.auth.models import AnonymousUser

from vote.models import UP
from vote.base_models import UP

register = template.Library()

Expand Down
Loading