Generic voting application

The stars application has been removed in favor of a more generic voting
application that works with any model. Starring a project is just a
special case of voting a project.

Usage.

Add a vote:

    votes.add_vote(<model instance>, user)

Remove a vote:

    votes.remove_vote(<model instance>, user)

Get the queryset of users that voted an object:

    votes.get_voters(<model instance>)

Get the number of votes an object has:

    votes.get_votes(<model instance>)

Get the objects of type <model> voted by an user:

    votes.get_voted(user, <model>)

The issues application is already making use of the votes application
through the following urls:

        /api/v1/issues/<id>/upvote      <- url name is "issues-upvote"
        /api/v1/issues/<id>/downvote    <- url name is "issues-downvote"
remotes/origin/enhancement/email-actions
Anler Hp 2014-06-02 11:53:59 +02:00
parent fcf4747e93
commit 9923e50603
21 changed files with 319 additions and 530 deletions

View File

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

View File

@ -40,7 +40,7 @@ from . import serializers
from . import models from . import models
from . import permissions from . import permissions
from . import services from . import services
from . import stars from . import votes
class ProjectAdminViewSet(ModelCrudViewSet): class ProjectAdminViewSet(ModelCrudViewSet):
@ -50,7 +50,7 @@ class ProjectAdminViewSet(ModelCrudViewSet):
def get_queryset(self): def get_queryset(self):
qs = models.Project.objects.all() 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 return qs
def pre_save(self, obj): def pre_save(self, obj):
@ -71,7 +71,7 @@ class ProjectViewSet(ModelCrudViewSet):
def get_queryset(self): def get_queryset(self):
qs = models.Project.objects.all() 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) | qs = qs.filter(Q(owner=self.request.user) |
Q(members=self.request.user)) Q(members=self.request.user))
return qs.distinct() return qs.distinct()
@ -84,13 +84,13 @@ class ProjectViewSet(ModelCrudViewSet):
@detail_route(methods=['post'], permission_classes=(IsAuthenticated,)) @detail_route(methods=['post'], permission_classes=(IsAuthenticated,))
def star(self, request, pk=None): def star(self, request, pk=None):
project = self.get_object() project = self.get_object()
stars.star(project, user=request.user) votes.add_vote(project, user=request.user)
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_200_OK)
@detail_route(methods=['post'], permission_classes=(IsAuthenticated,)) @detail_route(methods=['post'], permission_classes=(IsAuthenticated,))
def unstar(self, request, pk=None): def unstar(self, request, pk=None):
project = self.get_object() project = self.get_object()
stars.unstar(project, user=request.user) votes.remove_vote(project, user=request.user)
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_200_OK)
@detail_route(methods=['get']) @detail_route(methods=['get'])
@ -328,12 +328,13 @@ class ProjectTemplateViewSet(ModelCrudViewSet):
class FansViewSet(ModelCrudViewSet): class FansViewSet(ModelCrudViewSet):
serializer_class = serializers.FanSerializer serializer_class = votes.serializers.VoterSerializer
list_serializer_class = serializers.FanSerializer list_serializer_class = votes.serializers.VoterSerializer
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
def get_queryset(self): 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): class StarredViewSet(ModelCrudViewSet):
@ -342,4 +343,4 @@ class StarredViewSet(ModelCrudViewSet):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
def get_queryset(self): 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 filters
from taiga.base import exceptions as exc 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.base.api import ModelCrudViewSet
from taiga.projects.mixins.notifications import NotificationSenderMixin from taiga.projects.mixins.notifications import NotificationSenderMixin
from .. import votes
from . import models from . import models
from . import permissions from . import permissions
from . import serializers from . import serializers
@ -113,6 +114,11 @@ class IssueViewSet(NotificationSenderMixin, ModelCrudViewSet):
update_notification_template = "update_issue_notification" update_notification_template = "update_issue_notification"
destroy_notification_template = "destroy_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): def pre_save(self, obj):
if not obj.id: if not obj.id:
obj.owner = self.request.user obj.owner = self.request.user
@ -139,3 +145,25 @@ class IssueViewSet(NotificationSenderMixin, ModelCrudViewSet):
if obj.type and obj.type.project != obj.project: if obj.type and obj.type.project != obj.project:
raise exc.PermissionDenied(_("You don't have permissions for add/modify this issue.")) 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") generated_user_stories = serializers.SerializerMethodField("get_generated_user_stories")
blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html") blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
description_html = serializers.SerializerMethodField("get_description_html") description_html = serializers.SerializerMethodField("get_description_html")
votes = serializers.SerializerMethodField("get_votes_number")
class Meta: class Meta:
model = models.Issue model = models.Issue
@ -54,6 +55,10 @@ class IssueSerializer(WatcherValidationSerializerMixin, serializers.ModelSeriali
def get_description_html(self, obj): def get_description_html(self, obj):
return mdrender(obj.project, obj.description) 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): class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer):

View File

@ -84,17 +84,16 @@ class ProjectMembershipSerializer(serializers.ModelSerializer):
class ProjectSerializer(serializers.ModelSerializer): class ProjectSerializer(serializers.ModelSerializer):
tags = PickleField(required=False) tags = PickleField(required=False)
stars = serializers.SerializerMethodField("get_starts_number") stars = serializers.SerializerMethodField("get_stars_number")
class Meta: class Meta:
model = models.Project model = models.Project
read_only_fields = ("created_date", "modified_date", "owner") read_only_fields = ("created_date", "modified_date", "owner")
exclude = ("last_us_ref", "last_task_ref", "last_issue_ref") exclude = ("last_us_ref", "last_task_ref", "last_issue_ref")
def get_starts_number(self, obj): def get_stars_number(self, obj):
# The "starts_count" attribute is attached by # The "stars_count" attribute is attached in the get_queryset of the viewset.
# starts app service methods return getattr(obj, "stars_count", 0)
return getattr(obj, "starts_count", 0)
class ProjectDetailSerializer(ProjectSerializer): class ProjectDetailSerializer(ProjectSerializer):
@ -163,14 +162,6 @@ class ProjectTemplateSerializer(serializers.ModelSerializer):
model = models.ProjectTemplate 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 StarredSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.Project 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,3 +0,0 @@
from django.contrib import admin
# Register your models here.

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

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

@ -107,11 +107,13 @@ from taiga.projects.milestones.api import MilestoneViewSet
from taiga.projects.userstories.api import UserStoryViewSet from taiga.projects.userstories.api import UserStoryViewSet
from taiga.projects.tasks.api import TaskViewSet from taiga.projects.tasks.api import TaskViewSet
from taiga.projects.issues.api import IssueViewSet from taiga.projects.issues.api import IssueViewSet
from taiga.projects.issues.api import VotersViewSet
from taiga.projects.wiki.api import WikiViewSet, WikiLinkViewSet from taiga.projects.wiki.api import WikiViewSet, WikiLinkViewSet
router.register(r"milestones", MilestoneViewSet, base_name="milestones") router.register(r"milestones", MilestoneViewSet, base_name="milestones")
router.register(r"userstories", UserStoryViewSet, base_name="userstories") router.register(r"userstories", UserStoryViewSet, base_name="userstories")
router.register(r"tasks", TaskViewSet, base_name="tasks") router.register(r"tasks", TaskViewSet, base_name="tasks")
router.register(r"issues", IssueViewSet, base_name="issues") 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", WikiViewSet, base_name="wiki")
router.register(r"wiki-links", WikiLinkViewSet, base_name="wiki-links") router.register(r"wiki-links", WikiLinkViewSet, base_name="wiki-links")

View File

@ -192,3 +192,25 @@ class VotesFactory(Factory):
class ContentTypeFactory(Factory): class ContentTypeFactory(Factory):
FACTORY_FOR = get_model("contenttypes", "ContentType") 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

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

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