Merge pull request #44 from taigaio/votes

Generic voting application
remotes/origin/enhancement/email-actions
Andrey Antukh 2014-06-03 09:53:57 +02:00
commit 1a1114a8dd
27 changed files with 554 additions and 523 deletions

View File

@ -182,7 +182,7 @@ INSTALLED_APPS = [
"taiga.projects.wiki",
"taiga.projects.history",
"taiga.projects.notifications",
"taiga.projects.stars",
"taiga.projects.votes",
"south",
"reversion",

View File

@ -40,7 +40,7 @@ from . import serializers
from . import models
from . import permissions
from . import services
from . import stars
from . import votes
class ProjectAdminViewSet(ModelCrudViewSet):
@ -50,7 +50,7 @@ class ProjectAdminViewSet(ModelCrudViewSet):
def get_queryset(self):
qs = models.Project.objects.all()
qs = stars.attach_startscount_to_queryset(qs)
qs = votes.attach_votescount_to_queryset(qs, as_field="stars_count")
return qs
def pre_save(self, obj):
@ -71,7 +71,7 @@ class ProjectViewSet(ModelCrudViewSet):
def get_queryset(self):
qs = models.Project.objects.all()
qs = stars.attach_startscount_to_queryset(qs)
qs = votes.attach_votescount_to_queryset(qs, as_field="stars_count")
qs = qs.filter(Q(owner=self.request.user) |
Q(members=self.request.user))
return qs.distinct()
@ -84,13 +84,13 @@ class ProjectViewSet(ModelCrudViewSet):
@detail_route(methods=['post'], permission_classes=(IsAuthenticated,))
def star(self, request, pk=None):
project = self.get_object()
stars.star(project, user=request.user)
votes.add_vote(project, user=request.user)
return Response(status=status.HTTP_200_OK)
@detail_route(methods=['post'], permission_classes=(IsAuthenticated,))
def unstar(self, request, pk=None):
project = self.get_object()
stars.unstar(project, user=request.user)
votes.remove_vote(project, user=request.user)
return Response(status=status.HTTP_200_OK)
@detail_route(methods=['get'])
@ -328,12 +328,13 @@ class ProjectTemplateViewSet(ModelCrudViewSet):
class FansViewSet(ModelCrudViewSet):
serializer_class = serializers.FanSerializer
list_serializer_class = serializers.FanSerializer
serializer_class = votes.serializers.VoterSerializer
list_serializer_class = votes.serializers.VoterSerializer
permission_classes = (IsAuthenticated,)
def get_queryset(self):
return stars.get_fans(self.kwargs.get("project_id"))
project = models.Project.objects.get(pk=self.kwargs.get("project_id"))
return votes.get_voters(project)
class StarredViewSet(ModelCrudViewSet):
@ -342,4 +343,4 @@ class StarredViewSet(ModelCrudViewSet):
permission_classes = (IsAuthenticated,)
def get_queryset(self):
return stars.get_starred(self.kwargs.get("user_id"))
return votes.get_voted(self.kwargs.get("user_id"), model=models.Project)

View File

@ -24,11 +24,12 @@ from rest_framework import filters
from taiga.base import filters
from taiga.base import exceptions as exc
from taiga.base.decorators import list_route
from taiga.base.decorators import list_route, detail_route
from taiga.base.api import ModelCrudViewSet
from taiga.projects.mixins.notifications import NotificationSenderMixin
from .. import votes
from . import models
from . import permissions
from . import serializers
@ -113,6 +114,11 @@ class IssueViewSet(NotificationSenderMixin, ModelCrudViewSet):
update_notification_template = "update_issue_notification"
destroy_notification_template = "destroy_issue_notification"
def get_queryset(self):
qs = self.model.objects.all()
qs = votes.attach_votescount_to_queryset(qs, as_field="votes_count")
return qs
def pre_save(self, obj):
if not obj.id:
obj.owner = self.request.user
@ -139,3 +145,25 @@ class IssueViewSet(NotificationSenderMixin, ModelCrudViewSet):
if obj.type and obj.type.project != obj.project:
raise exc.PermissionDenied(_("You don't have permissions for add/modify this issue."))
@detail_route(methods=['post'], permission_classes=(IsAuthenticated,))
def upvote(self, request, pk=None):
issue = self.get_object()
votes.add_vote(issue, user=request.user)
return Response(status=status.HTTP_200_OK)
@detail_route(methods=['post'], permission_classes=(IsAuthenticated,))
def downvote(self, request, pk=None):
issue = self.get_object()
votes.remove_vote(issue, user=request.user)
return Response(status=status.HTTP_200_OK)
class VotersViewSet(ModelCrudViewSet):
serializer_class = votes.serializers.VoterSerializer
list_serializer_class = votes.serializers.VoterSerializer
permission_classes = (IsAuthenticated,)
def get_queryset(self):
issue = models.Issue.objects.get(pk=self.kwargs.get("issue_id"))
return votes.get_voters(issue)

View File

@ -37,6 +37,7 @@ class IssueSerializer(WatcherValidationSerializerMixin, serializers.ModelSeriali
generated_user_stories = serializers.SerializerMethodField("get_generated_user_stories")
blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
description_html = serializers.SerializerMethodField("get_description_html")
votes = serializers.SerializerMethodField("get_votes_number")
class Meta:
model = models.Issue
@ -54,6 +55,10 @@ class IssueSerializer(WatcherValidationSerializerMixin, serializers.ModelSeriali
def get_description_html(self, obj):
return mdrender(obj.project, obj.description)
def get_votes_number(self, obj):
# The "votes_count" attribute is attached in the get_queryset of the viewset.
return getattr(obj, "votes_count", 0)
class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer):

View File

@ -84,17 +84,16 @@ class ProjectMembershipSerializer(serializers.ModelSerializer):
class ProjectSerializer(serializers.ModelSerializer):
tags = PickleField(required=False)
stars = serializers.SerializerMethodField("get_starts_number")
stars = serializers.SerializerMethodField("get_stars_number")
class Meta:
model = models.Project
read_only_fields = ("created_date", "modified_date", "owner")
exclude = ("last_us_ref", "last_task_ref", "last_issue_ref")
def get_starts_number(self, obj):
# The "starts_count" attribute is attached by
# starts app service methods
return getattr(obj, "starts_count", 0)
def get_stars_number(self, obj):
# The "stars_count" attribute is attached in the get_queryset of the viewset.
return getattr(obj, "stars_count", 0)
class ProjectDetailSerializer(ProjectSerializer):
@ -163,14 +162,6 @@ class ProjectTemplateSerializer(serializers.ModelSerializer):
model = models.ProjectTemplate
class FanSerializer(serializers.ModelSerializer):
full_name = serializers.CharField(source='get_full_name', required=False)
class Meta:
model = User
fields = ('id', 'username', 'first_name', 'last_name', 'full_name')
class StarredSerializer(serializers.ModelSerializer):
class Meta:
model = models.Project

View File

@ -1,12 +0,0 @@
from .services import star
from .services import unstar
from .services import get_fans
from .services import get_starred
from .services import attach_startscount_to_queryset
__all__ = ("star",
"unstar",
"get_fans",
"get_starred",
"attach_startscount_to_queryset",)

View File

@ -1,248 +0,0 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as 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 'Fan'
db.create_table('stars_fan', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('project', self.gf('django.db.models.fields.related.ForeignKey')(related_name='fans', to=orm['projects.Project'])),
('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='fans', to=orm['users.User'])),
))
db.send_create_signal('stars', ['Fan'])
# Adding unique constraint on 'Fan', fields ['project', 'user']
db.create_unique('stars_fan', ['project_id', 'user_id'])
# Adding model 'Stars'
db.create_table('stars_stars', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('project', self.gf('django.db.models.fields.related.OneToOneField')(unique=True, to=orm['projects.Project'])),
('count', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
))
db.send_create_signal('stars', ['Stars'])
def backwards(self, orm):
# Removing unique constraint on 'Fan', fields ['project', 'user']
db.delete_unique('stars_fan', ['project_id', 'user_id'])
# Deleting model 'Fan'
db.delete_table('stars_fan')
# Deleting model 'Stars'
db.delete_table('stars_stars')
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', [], {'blank': 'True', 'to': "orm['auth.Permission']", 'symmetrical': 'False'})
},
'auth.permission': {
'Meta': {'unique_together': "(('content_type', 'codename'),)", 'ordering': "('content_type__app_label', 'content_type__model', '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'})
},
'contenttypes.contenttype': {
'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'ordering': "('name',)", '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'})
},
'domains.domain': {
'Meta': {'ordering': "('domain',)", 'object_name': 'Domain'},
'alias_of': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'null': 'True', 'default': 'None', 'related_name': "'+'", 'to': "orm['domains.Domain']"}),
'default_language': ('django.db.models.fields.CharField', [], {'blank': 'True', 'default': "''", 'max_length': '20'}),
'domain': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'public_register': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'scheme': ('django.db.models.fields.CharField', [], {'null': 'True', 'default': 'None', 'max_length': '60'})
},
'projects.issuestatus': {
'Meta': {'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']", 'object_name': 'IssueStatus'},
'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'issue_statuses'", 'to': "orm['projects.Project']"})
},
'projects.issuetype': {
'Meta': {'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']", 'object_name': 'IssueType'},
'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'issue_types'", 'to': "orm['projects.Project']"})
},
'projects.membership': {
'Meta': {'unique_together': "(('user', 'project'),)", 'ordering': "['project', 'role']", 'object_name': 'Membership'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'default': 'datetime.datetime.now', 'auto_now_add': 'True'}),
'email': ('django.db.models.fields.EmailField', [], {'blank': 'True', 'null': 'True', 'default': 'None', 'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'memberships'", 'to': "orm['projects.Project']"}),
'role': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'memberships'", 'to': "orm['users.Role']"}),
'token': ('django.db.models.fields.CharField', [], {'blank': 'True', 'null': 'True', 'default': 'None', 'max_length': '60'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'null': 'True', 'default': 'None', 'related_name': "'memberships'", 'to': "orm['users.User']"})
},
'projects.points': {
'Meta': {'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']", 'object_name': 'Points'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'points'", 'to': "orm['projects.Project']"}),
'value': ('django.db.models.fields.FloatField', [], {'blank': 'True', 'null': 'True', 'default': 'None'})
},
'projects.priority': {
'Meta': {'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']", 'object_name': 'Priority'},
'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'priorities'", 'to': "orm['projects.Project']"})
},
'projects.project': {
'Meta': {'ordering': "['name']", 'object_name': 'Project'},
'created_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now_add': 'True'}),
'creation_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'null': 'True', 'default': 'None', 'related_name': "'projects'", 'to': "orm['projects.ProjectTemplate']"}),
'default_issue_status': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'on_delete': 'models.SET_NULL', 'unique': 'True', 'null': 'True', 'related_name': "'+'", 'to': "orm['projects.IssueStatus']"}),
'default_issue_type': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'on_delete': 'models.SET_NULL', 'unique': 'True', 'null': 'True', 'related_name': "'+'", 'to': "orm['projects.IssueType']"}),
'default_points': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'on_delete': 'models.SET_NULL', 'unique': 'True', 'null': 'True', 'related_name': "'+'", 'to': "orm['projects.Points']"}),
'default_priority': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'on_delete': 'models.SET_NULL', 'unique': 'True', 'null': 'True', 'related_name': "'+'", 'to': "orm['projects.Priority']"}),
'default_severity': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'on_delete': 'models.SET_NULL', 'unique': 'True', 'null': 'True', 'related_name': "'+'", 'to': "orm['projects.Severity']"}),
'default_task_status': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'on_delete': 'models.SET_NULL', 'unique': 'True', 'null': 'True', 'related_name': "'+'", 'to': "orm['projects.TaskStatus']"}),
'default_us_status': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'on_delete': 'models.SET_NULL', 'unique': 'True', 'null': 'True', 'related_name': "'+'", 'to': "orm['projects.UserStoryStatus']"}),
'description': ('django.db.models.fields.TextField', [], {}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'null': 'True', 'default': 'None', 'related_name': "'projects'", 'to': "orm['domains.Domain']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_backlog_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_issues_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_kanban_activated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_wiki_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'members': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'projects'", 'to': "orm['users.User']", 'through': "orm['projects.Membership']"}),
'modified_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '250'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'owned_projects'", 'to': "orm['users.User']"}),
'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'blank': 'True', 'max_length': '250'}),
'tags': ('picklefield.fields.PickledObjectField', [], {'blank': 'True'}),
'total_milestones': ('django.db.models.fields.IntegerField', [], {'blank': 'True', 'null': 'True', 'default': '0'}),
'total_story_points': ('django.db.models.fields.FloatField', [], {'null': 'True', 'default': 'None'}),
'videoconferences': ('django.db.models.fields.CharField', [], {'blank': 'True', 'null': 'True', 'max_length': '250'}),
'videoconferences_salt': ('django.db.models.fields.CharField', [], {'blank': 'True', 'null': 'True', 'max_length': '250'})
},
'projects.projecttemplate': {
'Meta': {'unique_together': "(['slug', 'domain'],)", 'ordering': "['name']", 'object_name': 'ProjectTemplate'},
'created_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now_add': 'True'}),
'default_options': ('django_pgjson.fields.JsonField', [], {}),
'default_owner_role': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'description': ('django.db.models.fields.TextField', [], {}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'null': 'True', 'default': 'None', 'related_name': "'templates'", 'to': "orm['domains.Domain']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_backlog_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_issues_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_kanban_activated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_wiki_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'issue_statuses': ('django_pgjson.fields.JsonField', [], {}),
'issue_types': ('django_pgjson.fields.JsonField', [], {}),
'modified_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '250'}),
'points': ('django_pgjson.fields.JsonField', [], {}),
'priorities': ('django_pgjson.fields.JsonField', [], {}),
'roles': ('django_pgjson.fields.JsonField', [], {}),
'severities': ('django_pgjson.fields.JsonField', [], {}),
'slug': ('django.db.models.fields.SlugField', [], {'blank': 'True', 'max_length': '250'}),
'task_statuses': ('django_pgjson.fields.JsonField', [], {}),
'us_statuses': ('django_pgjson.fields.JsonField', [], {}),
'videoconferences': ('django.db.models.fields.CharField', [], {'blank': 'True', 'null': 'True', 'max_length': '250'}),
'videoconferences_salt': ('django.db.models.fields.CharField', [], {'blank': 'True', 'null': 'True', 'max_length': '250'})
},
'projects.severity': {
'Meta': {'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']", 'object_name': 'Severity'},
'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'severities'", 'to': "orm['projects.Project']"})
},
'projects.taskstatus': {
'Meta': {'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']", 'object_name': 'TaskStatus'},
'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'task_statuses'", 'to': "orm['projects.Project']"})
},
'projects.userstorystatus': {
'Meta': {'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']", 'object_name': 'UserStoryStatus'},
'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'us_statuses'", 'to': "orm['projects.Project']"}),
'wip_limit': ('django.db.models.fields.IntegerField', [], {'blank': 'True', 'null': 'True', 'default': 'None'})
},
'stars.fan': {
'Meta': {'unique_together': "(('project', 'user'),)", 'object_name': 'Fan'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'fans'", 'to': "orm['projects.Project']"}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'fans'", 'to': "orm['users.User']"})
},
'stars.stars': {
'Meta': {'object_name': 'Stars'},
'count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'project': ('django.db.models.fields.related.OneToOneField', [], {'unique': 'True', 'to': "orm['projects.Project']"})
},
'users.role': {
'Meta': {'unique_together': "(('slug', 'project'),)", 'ordering': "['order', 'slug']", 'object_name': 'Role'},
'computable': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'roles'", 'to': "orm['auth.Permission']"}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'roles'", 'to': "orm['projects.Project']"}),
'slug': ('django.db.models.fields.SlugField', [], {'blank': 'True', 'max_length': '250'})
},
'users.user': {
'Meta': {'ordering': "['username']", 'object_name': 'User'},
'color': ('django.db.models.fields.CharField', [], {'blank': 'True', 'default': "'#55c1f6'", 'max_length': '9'}),
'colorize_tags': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'default_language': ('django.db.models.fields.CharField', [], {'blank': 'True', 'default': "''", 'max_length': '20'}),
'default_timezone': ('django.db.models.fields.CharField', [], {'blank': 'True', 'default': "''", 'max_length': '20'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'email': ('django.db.models.fields.EmailField', [], {'blank': 'True', 'max_length': '75'}),
'first_name': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '30'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'user_set'", 'to': "orm['auth.Group']", 'symmetrical': 'False'}),
'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', [], {'blank': 'True', 'max_length': '30'}),
'notify_changes_by_me': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'notify_level': ('django.db.models.fields.CharField', [], {'default': "'all_owned_projects'", 'max_length': '32'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'photo': ('django.db.models.fields.files.FileField', [], {'blank': 'True', 'null': 'True', 'max_length': '500'}),
'token': ('django.db.models.fields.CharField', [], {'blank': 'True', 'null': 'True', 'default': 'None', 'max_length': '200'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'user_set'", 'to': "orm['auth.Permission']", 'symmetrical': 'False'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
}
}
complete_apps = ['stars']

View File

@ -1,36 +0,0 @@
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.db import models
class Fan(models.Model):
project = models.ForeignKey("projects.Project", null=False, blank=False, related_name="fans",
verbose_name=_("project"))
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
related_name="fans", verbose_name=_("fans"))
class Meta:
verbose_name = _("Star")
verbose_name_plural = _("Stars")
unique_together = ("project", "user")
def __unicode__(self):
return self.user
@classmethod
def create(cls, *args, **kwargs):
return cls.objects.create(*args, **kwargs)
class Stars(models.Model):
project = models.OneToOneField("projects.Project", null=False, blank=False,
related_name="stars", verbose_name=_("project"))
count = models.PositiveIntegerField(null=False, blank=False, default=0,
verbose_name=_("count"))
class Meta:
verbose_name = _("Stars")
verbose_name_plural = _("Stars")
def __unicode__(self):
return "{} stars".format(self.count)

View File

@ -1,100 +0,0 @@
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 Fan, Stars
def star(project, user):
"""Star a project for an user.
If the user has already starred the project nothing happends so this function can be considered
idempotent.
:param project: :class:`~taiga.projects.models.Project` instance.
:param user: :class:`~taiga.users.models.User` instance.
"""
with atomic():
fan, created = Fan.objects.get_or_create(project=project,
user=user)
if not created:
return
stars, _ = Stars.objects.get_or_create(project=project)
stars.count = F('count') + 1
stars.save()
def unstar(project, user):
"""
Unstar a project for an user.
If the user has not starred the project nothing happens so this function can be considered
idempotent.
:param project: :class:`~taiga.projects.models.Project` instance.
:param user: :class:`~taiga.users.models.User` instance.
"""
with atomic():
qs = Fan.objects.filter(project=project, user=user)
if not qs.exists():
return
qs.delete()
stars, _ = Stars.objects.get_or_create(project=project)
stars.count = F('count') - 1
stars.save()
def get_stars(project):
"""
Get the count of stars a project have.
"""
instance, _ = Stars.objects.get_or_create(project=project)
return instance.count
def get_fans(project_or_id):
"""Get the fans a project have."""
qs = get_user_model().objects.get_queryset()
if isinstance(project_or_id, int):
qs = qs.filter(fans__project_id=project_or_id)
else:
qs = qs.filter(fans__project=project_or_id)
return qs
def get_starred(user_or_id):
"""Get the projects an user has starred."""
project_model = get_model("projects", "Project")
qs = project_model.objects.get_queryset()
if isinstance(user_or_id, int):
qs = qs.filter(fans__user_id=user_or_id)
else:
qs = qs.filter(fans__user=user_or_id)
return qs
def attach_startscount_to_queryset(queryset):
"""
Attach stars count to each object of projects queryset.
Because of lazynes of starts objects creation, this makes
much simple and more efficient way to access to project
starts number.
(The other way was be do it on serializer with some try/except
blocks and additional queryes)
"""
sql = ("SELECT coalesce(stars_stars.count, 0) FROM stars_stars "
"WHERE stars_stars.project_id = projects_project.id ")
qs = queryset.extra(select={"starts_count": sql})
return qs

View File

@ -0,0 +1,7 @@
from .services import add_vote
from .services import remove_vote
from .services import get_voters
from .services import get_votes
from .services import get_voted
from .utils import attach_votescount_to_queryset
from . import serializers

View File

@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as 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 'Votes'
db.create_table('votes_votes', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])),
('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()),
('count', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
))
db.send_create_signal('votes', ['Votes'])
# Adding unique constraint on 'Votes', fields ['content_type', 'object_id']
db.create_unique('votes_votes', ['content_type_id', 'object_id'])
# Adding model 'Vote'
db.create_table('votes_vote', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])),
('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['users.User'], related_name='votes')),
))
db.send_create_signal('votes', ['Vote'])
# Adding unique constraint on 'Vote', fields ['content_type', 'object_id', 'user']
db.create_unique('votes_vote', ['content_type_id', 'object_id', 'user_id'])
def backwards(self, orm):
# Removing unique constraint on 'Vote', fields ['content_type', 'object_id', 'user']
db.delete_unique('votes_vote', ['content_type_id', 'object_id', 'user_id'])
# Removing unique constraint on 'Votes', fields ['content_type', 'object_id']
db.delete_unique('votes_votes', ['content_type_id', 'object_id'])
# Deleting model 'Votes'
db.delete_table('votes_votes')
# Deleting model 'Vote'
db.delete_table('votes_vote')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['auth.Permission']", 'blank': 'True'})
},
'auth.permission': {
'Meta': {'object_name': 'Permission', 'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)"},
'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'})
},
'contenttypes.contenttype': {
'Meta': {'object_name': 'ContentType', 'db_table': "'django_content_type'", 'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)"},
'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'})
},
'users.user': {
'Meta': {'object_name': 'User', 'ordering': "['username']"},
'color': ('django.db.models.fields.CharField', [], {'default': "'#4f52fe'", 'max_length': '9', 'blank': 'True'}),
'colorize_tags': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'default_language': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20', 'blank': 'True'}),
'default_timezone': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'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', [], {'symmetrical': 'False', 'to': "orm['auth.Group']", 'related_name': "'user_set'", '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'}),
'notify_changes_by_me': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'notify_level': ('django.db.models.fields.CharField', [], {'default': "'all_owned_projects'", 'max_length': '32'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'photo': ('django.db.models.fields.files.FileField', [], {'max_length': '500', 'blank': 'True', 'null': 'True'}),
'token': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '200', 'blank': 'True', 'null': 'True'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['auth.Permission']", 'related_name': "'user_set'", 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'})
},
'votes.vote': {
'Meta': {'object_name': 'Vote', 'unique_together': "(('content_type', 'object_id', 'user'),)"},
'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['users.User']", 'related_name': "'votes'"})
},
'votes.votes': {
'Meta': {'object_name': 'Votes', 'unique_together': "(('content_type', 'object_id'),)"},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
}
}
complete_apps = ['votes']

View File

@ -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

View File

@ -0,0 +1,11 @@
from django.contrib.auth import get_user_model
from rest_framework import serializers
class VoterSerializer(serializers.ModelSerializer):
full_name = serializers.CharField(source='get_full_name', required=False)
class Meta:
model = get_user_model()
fields = ('id', 'username', 'first_name', 'last_name', 'full_name')

View File

@ -0,0 +1,98 @@
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_or_id, model):
"""Get the objects voted by an user.
:param user_or_id: :class:`~taiga.users.models.User` instance or id.
:param model: Show only objects of this kind. Can be any Django model class.
:return: Queryset of objects representing the votes of the user.
"""
obj_type = get_model("contenttypes", "ContentType").objects.get_for_model(model)
conditions = ('votes_vote.content_type_id = %s',
'%s.id = votes_vote.object_id' % model._meta.db_table,
'votes_vote.user_id = %s')
if isinstance(user_or_id, get_user_model()):
user_id = user_or_id.id
else:
user_id = user_or_id
return model.objects.extra(where=conditions, tables=('votes_vote',),
params=(obj_type.id, user_id))

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,24 @@
from django.db.models.loading import get_model
def attach_votescount_to_queryset(queryset, as_field="votes_count"):
"""Attach votes count to each object of the queryset.
Because of laziness of vote objects creation, this makes much simpler and more efficient to
access to voted-object number of votes.
(The other way was to do it in the serializer with some try/except blocks and additional
queries)
:param queryset: A Django queryset object.
:param as_field: Attach the votes-count as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
type = get_model("contenttypes", "ContentType").objects.get_for_model(model)
sql = ("SELECT coalesce(votes_votes.count, 0) FROM votes_votes "
"WHERE votes_votes.content_type_id = {type_id} AND votes_votes.object_id = {tbl}.id")
sql = sql.format(type_id=type.id, tbl=model._meta.db_table)
qs = queryset.extra(select={as_field: sql})
return qs

View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@ -107,11 +107,13 @@ from taiga.projects.milestones.api import MilestoneViewSet
from taiga.projects.userstories.api import UserStoryViewSet
from taiga.projects.tasks.api import TaskViewSet
from taiga.projects.issues.api import IssueViewSet
from taiga.projects.issues.api import VotersViewSet
from taiga.projects.wiki.api import WikiViewSet, WikiLinkViewSet
router.register(r"milestones", MilestoneViewSet, base_name="milestones")
router.register(r"userstories", UserStoryViewSet, base_name="userstories")
router.register(r"tasks", TaskViewSet, base_name="tasks")
router.register(r"issues", IssueViewSet, base_name="issues")
router.register(r"issues/(?P<issue_id>\d+)/voters", VotersViewSet, base_name="issue-voters")
router.register(r"wiki", WikiViewSet, base_name="wiki")
router.register(r"wiki-links", WikiLinkViewSet, base_name="wiki-links")

View File

@ -173,3 +173,44 @@ class StarsFactory(Factory):
project = factory.SubFactory("tests.factories.ProjectFactory")
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")
FACTORY_DJANGO_GET_OR_CREATE = ("app_label", "model")
app_label = factory.LazyAttribute(lambda obj: ContentTypeFactory.FACTORY_FOR._meta.app_label)
model = factory.LazyAttribute(lambda obj: ContentTypeFactory.FACTORY_FOR._meta.model_name)
def create_issue(**kwargs):
"Create an issue and its dependencies in an appropriate way."
owner = kwargs.pop("owner") if "owner" in kwargs else UserFactory()
project = ProjectFactory.create(owner=owner)
defaults = {
"project": project,
"owner": owner,
"status": IssueStatusFactory.create(project=project),
"milestone": MilestoneFactory.create(project=project),
"priority": PriorityFactory.create(project=project),
"severity": SeverityFactory.create(project=project),
"type": IssueTypeFactory.create(project=project),
}
defaults.update(kwargs)
return IssueFactory.create(**defaults)

View File

@ -9,13 +9,17 @@ from taiga.base.utils.db import filter_by_tags
from taiga.base import neighbors as n
from .. import factories as f
from ..utils import disconnect_signals
from ..utils import disconnect_signals, reconnect_signals
def setup_module():
disconnect_signals()
def teardown_module():
reconnect_signals()
class TestGetAttribute:
def test_no_attribute(self, object):
object.first_name = "name"

View File

@ -57,7 +57,7 @@ def test_project_member_unstar_project(client):
def test_list_project_fans(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
fan = f.FanFactory.create(project=project)
fan = f.VoteFactory.create(content_object=project)
url = reverse("project-fans-list", args=(project.id,))
client.login(user)
@ -70,7 +70,7 @@ def test_list_project_fans(client):
def test_get_project_fan(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
fan = f.FanFactory.create(project=project)
fan = f.VoteFactory.create(content_object=project)
url = reverse("project-fans-detail", args=(project.id, fan.user.id))
client.login(user)
@ -82,33 +82,36 @@ def test_get_project_fan(client):
def test_list_user_starred_projects(client):
user = f.UserFactory.create()
fan = f.FanFactory.create(user=user)
project = f.ProjectFactory()
url = reverse("user-starred-list", args=(user.id,))
f.VoteFactory.create(user=user, content_object=project)
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data[0]['id'] == fan.project.id
assert response.data[0]['id'] == project.id
def test_get_user_starred_project(client):
user = f.UserFactory.create()
fan = f.FanFactory.create(user=user)
url = reverse("user-starred-detail", args=(user.id, fan.project.id))
project = f.ProjectFactory()
url = reverse("user-starred-detail", args=(user.id, project.id))
f.VoteFactory.create(user=user, content_object=project)
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data['id'] == fan.project.id
assert response.data['id'] == project.id
def test_get_project_stars(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
f.StarsFactory.create(project=project, count=5)
url = reverse("projects-detail", args=(project.id,))
f.VotesFactory.create(content_object=project, count=5)
f.VotesFactory.create(count=3)
client.login(user)
response = client.get(url)

View File

@ -0,0 +1,68 @@
import pytest
from django.core.urlresolvers import reverse
from django.contrib.contenttypes.models import ContentType
from .. import factories as f
pytestmark = pytest.mark.django_db
def test_upvote_issue(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
url = reverse("issues-upvote", args=(issue.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_downvote_issue(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
url = reverse("issues-downvote", args=(issue.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_list_issue_voters(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
url = reverse("issue-voters-list", args=(issue.id,))
f.VoteFactory.create(content_object=issue, user=user)
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data[0]['id'] == user.id
def test_get_issue_voter(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
vote = f.VoteFactory.create(content_object=issue, user=user)
url = reverse("issue-voters-detail", args=(issue.id, vote.user.id))
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data['id'] == vote.user.id
def test_get_issue_votes(client):
user = f.UserFactory.create()
issue = f.create_issue(owner=user)
url = reverse("issues-detail", args=(issue.id,))
f.VotesFactory.create(content_object=issue, count=5)
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data['votes'] == 5

View File

@ -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]

View File

@ -1,91 +0,0 @@
from unittest import mock
from taiga.projects.stars import services as stars
from .. import factories as f
def setup_module(module):
module.patcher = mock.patch.object(stars, "atomic", mock.MagicMock())
module.patcher.start()
def teardown_module(module):
module.patcher.stop()
def test_user_star_project():
"An user can star a project"
user = f.UserFactory.build()
project = f.ProjectFactory.build()
with mock.patch("taiga.projects.stars.services.Fan") as Fan:
with mock.patch("taiga.projects.stars.services.Stars") as Stars:
stars_instance = mock.Mock()
Fan.objects.get_or_create = mock.MagicMock(return_value=(mock.Mock(), True))
Stars.objects.get_or_create = mock.MagicMock(return_value=(stars_instance, True))
stars.star(project, user=user)
assert stars_instance.count.connector == '+'
assert stars_instance.count.children[1] == 1
assert stars_instance.save.called
def test_idempotence_user_star_project():
"An user can star a project many times but only one star is counted"
user = f.UserFactory.build()
project = f.ProjectFactory.build()
with mock.patch("taiga.projects.stars.services.Fan") as Fan:
with mock.patch("taiga.projects.stars.services.Stars") as Stars:
stars_instance = mock.Mock()
Fan.objects.get_or_create = mock.MagicMock(return_value=(mock.Mock(), False))
Stars.objects.get_or_create = mock.MagicMock(return_value=(stars_instance, True))
stars.star(project, user=user)
assert not Fan.objects.create.called
assert not stars_instance.save.called
def test_user_unstar_project():
"An user can unstar a project"
fan = f.FanFactory.build()
with mock.patch("taiga.projects.stars.services.Fan") as Fan:
with mock.patch("taiga.projects.stars.services.Stars") as Stars:
delete_mock = mock.Mock()
stars_instance = mock.Mock()
Fan.objects.filter(project=fan.project, user=fan.user).exists = mock.Mock(
return_value=True)
Fan.objects.filter(project=fan.project, user=fan.user).delete = delete_mock
Stars.objects.get_or_create = mock.MagicMock(return_value=(stars_instance, True))
stars.unstar(fan.project, user=fan.user)
assert delete_mock.called
assert stars_instance.count.connector == '-'
assert stars_instance.count.children[1] == 1
assert stars_instance.save.called
def test_idempotence_user_unstar_project():
"An user can unstar a project many times but only one star is discounted"
fan = f.FanFactory.build()
with mock.patch("taiga.projects.stars.services.Fan") as Fan:
with mock.patch("taiga.projects.stars.services.Stars") as Stars:
delete_mock = mock.Mock()
stars_instance = mock.Mock()
Fan.objects.filter(project=fan.project, user=fan.user).exists = mock.Mock(
return_value=False)
Fan.objects.filter(project=fan.project, user=fan.user).delete = delete_mock
Stars.objects.get_or_create = mock.MagicMock(return_value=(stars_instance, True))
stars.unstar(fan.project, user=fan.user)
assert not delete_mock.called
assert not stars_instance.save.called

View File

@ -1,6 +1,19 @@
from django.db.models import signals
def disconnect_signals():
signals.pre_save.receivers = []
signals.post_save.receivers = []
def signals_switch():
pre_save = signals.pre_save.receivers
post_save = signals.post_save.receivers
def disconnect():
signals.pre_save.receivers = []
signals.post_save.receivers = []
def reconnect():
signals.pre_save.receivers = pre_save
signals.post_save.receivers = post_save
return disconnect, reconnect
disconnect_signals, reconnect_signals = signals_switch()