Merge pull request #43 from taigaio/stars

Star projects
remotes/origin/enhancement/email-actions
Jesús Espino 2014-05-27 18:05:17 +02:00
commit 13ec5c3d0b
21 changed files with 707 additions and 36 deletions

View File

@ -8,3 +8,4 @@ pytest-pythonpath==0.3
coverage==3.7.1
coveralls==0.4.2
django-testclient-extensions==0.1.1

View File

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

View File

@ -40,6 +40,7 @@ from . import serializers
from . import models
from . import permissions
from . import services
from . import stars
class ProjectAdminViewSet(ModelCrudViewSet):
@ -73,6 +74,18 @@ class ProjectViewSet(ModelCrudViewSet):
project = self.get_object()
return Response(services.get_stats_for_project(project))
@detail_route(methods=['post'], permission_classes=(IsAuthenticated,))
def star(self, request, pk=None):
project = self.get_object()
stars.star(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)
return Response(status=status.HTTP_200_OK)
@detail_route(methods=['get'])
def issues_stats(self, request, pk=None):
project = self.get_object()
@ -311,3 +324,21 @@ class ProjectTemplateViewSet(ModelCrudViewSet):
def get_queryset(self):
return models.ProjectTemplate.objects.all()
class FansViewSet(ModelCrudViewSet):
serializer_class = serializers.FanSerializer
list_serializer_class = serializers.FanSerializer
permission_classes = (IsAuthenticated,)
def get_queryset(self):
return stars.get_fans(self.kwargs.get("project_id"))
class StarredViewSet(ModelCrudViewSet):
serializer_class = serializers.StarredSerializer
list_serializer_class = serializers.StarredSerializer
permission_classes = (IsAuthenticated,)
def get_queryset(self):
return stars.get_starred(self.kwargs.get("user_id"))

View File

@ -19,7 +19,7 @@ from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from taiga.base.serializers import PickleField, JsonField
from taiga.users.models import Role
from taiga.users.models import Role, User
from . import models
@ -84,6 +84,7 @@ class ProjectMembershipSerializer(serializers.ModelSerializer):
class ProjectSerializer(serializers.ModelSerializer):
tags = PickleField(required=False)
stars = serializers.IntegerField(source="stars.count")
class Meta:
model = models.Project
@ -150,3 +151,17 @@ class ProjectTemplateSerializer(serializers.ModelSerializer):
class Meta:
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
fields = ['id', 'name', 'slug']

View File

@ -0,0 +1 @@
from .services import star, unstar, get_fans, get_starred

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,248 @@
# -*- 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

@ -0,0 +1,36 @@
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

@ -0,0 +1,67 @@
from django.db.models import F
from django.db.transaction import atomic
from django.contrib.auth import get_user_model
from ..models import Project
from .models import Fan, Stars
@atomic
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.
"""
if not Fan.objects.filter(project=project, user=user).exists():
Fan.objects.create(project=project, user=user)
stars, _ = Stars.objects.get_or_create(project=project)
stars.count = F('count') + 1
stars.save()
@atomic
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.
"""
if Fan.objects.filter(project=project, user=user).exists():
Fan.objects.filter(project=project, user=user).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."""
return Stars.objects.filter(project=project).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."""
qs = Project.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

View File

@ -59,9 +59,13 @@ from taiga.projects.api import IssueTypeViewSet
from taiga.projects.api import PriorityViewSet
from taiga.projects.api import SeverityViewSet
from taiga.projects.api import ProjectTemplateViewSet
from taiga.projects.api import FansViewSet
from taiga.projects.api import StarredViewSet
router.register(r"roles", RolesViewSet, base_name="roles")
router.register(r"projects", ProjectViewSet, base_name="projects")
router.register(r"projects/(?P<project_id>\d+)/fans", FansViewSet, base_name="project-fans")
router.register(r"users/(?P<user_id>\d+)/starred", StarredViewSet, base_name="user-starred")
router.register(r"project-templates", ProjectTemplateViewSet, base_name="project-templates")
router.register(r"memberships", MembershipViewSet, base_name="memberships")
router.register(r"invitations", InvitationViewSet, base_name="invitations")

View File

@ -18,6 +18,8 @@ from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import Permission
from rest_framework import serializers
from taiga.projects.models import Project
from .models import User, Role

View File

@ -1,13 +1,6 @@
import pytest
class Object:
pass
@pytest.fixture
def object():
return Object()
from .fixtures import *
def pytest_addoption(parser):

View File

@ -1,4 +1,5 @@
import uuid
import threading
import factory
from django.conf import settings
@ -7,11 +8,25 @@ import taiga.projects.models
import taiga.projects.userstories.models
import taiga.projects.issues.models
import taiga.projects.milestones.models
import taiga.projects.stars.models
import taiga.users.models
import taiga.userstorage.models
class ProjectTemplateFactory(factory.DjangoModelFactory):
class Factory(factory.DjangoModelFactory):
FACTORY_STRATEGY = factory.CREATE_STRATEGY
_SEQUENCE = 1
_SEQUENCE_LOCK = threading.Lock()
@classmethod
def _setup_next_sequence(cls):
with cls._SEQUENCE_LOCK:
cls._SEQUENCE += 1
return cls._SEQUENCE
class ProjectTemplateFactory(Factory):
FACTORY_FOR = taiga.projects.models.ProjectTemplate
FACTORY_DJANGO_GET_OR_CREATE = ("slug", )
@ -28,7 +43,7 @@ class ProjectTemplateFactory(factory.DjangoModelFactory):
roles = []
class ProjectFactory(factory.DjangoModelFactory):
class ProjectFactory(Factory):
FACTORY_FOR = taiga.projects.models.Project
name = factory.Sequence(lambda n: "Project {}".format(n))
@ -38,14 +53,30 @@ class ProjectFactory(factory.DjangoModelFactory):
creation_template = factory.SubFactory("tests.factories.ProjectTemplateFactory")
class RoleFactory(factory.DjangoModelFactory):
class RoleFactory(Factory):
FACTORY_FOR = taiga.users.models.Role
name = "Tester"
project = factory.SubFactory("tests.factories.ProjectFactory")
class UserFactory(factory.DjangoModelFactory):
class PointsFactory(Factory):
FACTORY_FOR = taiga.projects.models.Points
name = factory.Sequence(lambda n: "Points {}".format(n))
value = 2
project = factory.SubFactory("tests.factories.ProjectFactory")
class RolePointsFactory(Factory):
FACTORY_FOR = taiga.projects.userstories.models.RolePoints
user_story = factory.SubFactory("tests.factories.UserStoryFactory")
role = factory.SubFactory("tests.factories.RoleFactory")
points = factory.SubFactory("tests.factories.PointsFactory")
class UserFactory(Factory):
FACTORY_FOR = taiga.users.models.User
username = factory.Sequence(lambda n: "user{}".format(n))
@ -53,7 +84,7 @@ class UserFactory(factory.DjangoModelFactory):
password = factory.PostGeneration(lambda obj, *args, **kwargs: obj.set_password(obj.username))
class MembershipFactory(factory.DjangoModelFactory):
class MembershipFactory(Factory):
FACTORY_FOR = taiga.projects.models.Membership
token = factory.LazyAttribute(lambda obj: str(uuid.uuid1()))
@ -62,7 +93,7 @@ class MembershipFactory(factory.DjangoModelFactory):
user = factory.SubFactory("tests.factories.UserFactory")
class StorageEntryFactory(factory.DjangoModelFactory):
class StorageEntryFactory(Factory):
FACTORY_FOR = taiga.userstorage.models.StorageEntry
owner = factory.SubFactory("tests.factories.UserFactory")
@ -70,7 +101,7 @@ class StorageEntryFactory(factory.DjangoModelFactory):
value = factory.Sequence(lambda n: "value {}".format(n))
class UserStoryFactory(factory.DjangoModelFactory):
class UserStoryFactory(Factory):
FACTORY_FOR = taiga.projects.userstories.models.UserStory
ref = factory.Sequence(lambda n: n)
@ -79,7 +110,7 @@ class UserStoryFactory(factory.DjangoModelFactory):
subject = factory.Sequence(lambda n: "User Story {}".format(n))
class MilestoneFactory(factory.DjangoModelFactory):
class MilestoneFactory(Factory):
FACTORY_FOR = taiga.projects.milestones.models.Milestone
name = factory.Sequence(lambda n: "Milestone {}".format(n))
@ -87,7 +118,7 @@ class MilestoneFactory(factory.DjangoModelFactory):
project = factory.SubFactory("tests.factories.ProjectFactory")
class IssueFactory(factory.DjangoModelFactory):
class IssueFactory(Factory):
FACTORY_FOR = taiga.projects.issues.models.Issue
subject = factory.Sequence(lambda n: "Issue {}".format(n))
@ -100,29 +131,43 @@ class IssueFactory(factory.DjangoModelFactory):
milestone = factory.SubFactory("tests.factories.MilestoneFactory")
class IssueStatusFactory(factory.DjangoModelFactory):
class IssueStatusFactory(Factory):
FACTORY_FOR = taiga.projects.models.IssueStatus
name = factory.Sequence(lambda n: "Issue Status {}".format(n))
project = factory.SubFactory("tests.factories.ProjectFactory")
class SeverityFactory(factory.DjangoModelFactory):
class SeverityFactory(Factory):
FACTORY_FOR = taiga.projects.models.Severity
name = factory.Sequence(lambda n: "Severity {}".format(n))
project = factory.SubFactory("tests.factories.ProjectFactory")
class PriorityFactory(factory.DjangoModelFactory):
class PriorityFactory(Factory):
FACTORY_FOR = taiga.projects.models.Priority
name = factory.Sequence(lambda n: "Priority {}".format(n))
project = factory.SubFactory("tests.factories.ProjectFactory")
class IssueTypeFactory(factory.DjangoModelFactory):
class IssueTypeFactory(Factory):
FACTORY_FOR = taiga.projects.models.IssueType
name = factory.Sequence(lambda n: "Issue Type {}".format(n))
project = factory.SubFactory("tests.factories.ProjectFactory")
class FanFactory(Factory):
FACTORY_FOR = taiga.projects.stars.models.Fan
project = factory.SubFactory("tests.factories.ProjectFactory")
user = factory.SubFactory("tests.factories.UserFactory")
class StarsFactory(Factory):
FACTORY_FOR = taiga.projects.stars.models.Stars
project = factory.SubFactory("tests.factories.ProjectFactory")
count = 0

17
tests/fixtures.py Normal file
View File

@ -0,0 +1,17 @@
import pytest
class Object:
pass
@pytest.fixture
def object():
return Object()
@pytest.fixture
def client():
from testclient_extensions import Client
return Client()

View File

@ -5,14 +5,15 @@ from .. import factories
pytestmark = pytest.mark.django_db
@pytest.fixture
def register_form():
return {"username": "username",
"password": "password",
"first_name": "fname",
"last_name": "lname",
"email": "user@email.com",
"type": "public"}
return {"username": "username",
"password": "password",
"first_name": "fname",
"last_name": "lname",
"email": "user@email.com",
"type": "public"}
def test_respond_201_if_domain_allows_public_registration(client, register_form):

View File

@ -7,7 +7,13 @@ from taiga.projects.userstories.models import UserStory
from taiga.projects.issues.models import Issue
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
def setup_module():
disconnect_signals()
class TestGetAttribute:
@ -53,7 +59,6 @@ def test_disjunction_filters():
@pytest.mark.django_db
@pytest.mark.slow
class TestUserStories:
def test_no_filters(self):
project = f.ProjectFactory.create()
@ -99,7 +104,6 @@ class TestUserStories:
@pytest.mark.django_db
@pytest.mark.slow
class TestIssues:
def test_no_filters(self):
project = f.ProjectFactory.create()

View File

@ -0,0 +1,117 @@
import pytest
from django.core.urlresolvers import reverse
from .. import factories as f
pytestmark = pytest.mark.django_db
def test_project_owner_star_project(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
url = reverse("projects-star", args=(project.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_project_owner_unstar_project(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
url = reverse("projects-unstar", args=(project.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_project_member_star_project(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create()
role = f.RoleFactory.create(project=project)
f.MembershipFactory.create(project=project, user=user, role=role)
url = reverse("projects-star", args=(project.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_project_member_unstar_project(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create()
role = f.RoleFactory.create(project=project)
f.MembershipFactory.create(project=project, user=user, role=role)
url = reverse("projects-unstar", args=(project.id,))
client.login(user)
response = client.post(url)
assert response.status_code == 200
def test_list_project_fans(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
fan = f.FanFactory.create(project=project)
url = reverse("project-fans-list", args=(project.id,))
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data[0]['id'] == fan.user.id
def test_get_project_fan(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
fan = f.FanFactory.create(project=project)
url = reverse("project-fans-detail", args=(project.id, fan.user.id))
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data['id'] == fan.user.id
def test_list_user_starred_projects(client):
user = f.UserFactory.create()
fan = f.FanFactory.create(user=user)
url = reverse("user-starred-list", args=(user.id,))
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data[0]['id'] == fan.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))
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data['id'] == fan.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,))
client.login(user)
response = client.get(url)
assert response.status_code == 200
assert response.data['stars'] == 5

View File

@ -1,9 +1,4 @@
from django.db.models import signals
def disconnect_signals():
signals.pre_save.receivers = []
signals.post_save.receivers = []
from ..utils import disconnect_signals
def pytest_runtest_setup(item):

84
tests/unit/test_stars.py Normal file
View File

@ -0,0 +1,84 @@
from unittest import mock
from taiga.projects.stars import services as stars
from .. import factories as f
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()
stars_instance.save = mock.Mock()
Fan.objects.filter(project=project, user=user).exists = mock.Mock(return_value=False)
Stars.objects.get_or_create = mock.MagicMock(return_value=(stars_instance, True))
stars.star(project, user=user)
assert Fan.objects.create.called
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()
stars_instance.save = mock.Mock()
Fan.objects.filter(project=project, user=user).exists = mock.Mock(return_value=True)
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()
stars_instance.save = 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()
stars_instance.save = 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

6
tests/utils.py Normal file
View File

@ -0,0 +1,6 @@
from django.db.models import signals
def disconnect_signals():
signals.pre_save.receivers = []
signals.post_save.receivers = []