Star/Unstar projects

remotes/origin/enhancement/email-actions
Anler Hp 2014-05-27 13:31:28 +02:00
parent a4bb6e7eee
commit b56dfe7cf5
13 changed files with 554 additions and 19 deletions

View File

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

View File

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

View File

@ -40,6 +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
class ProjectAdminViewSet(ModelCrudViewSet): class ProjectAdminViewSet(ModelCrudViewSet):
@ -73,6 +74,18 @@ class ProjectViewSet(ModelCrudViewSet):
project = self.get_object() project = self.get_object()
return Response(services.get_stats_for_project(project)) 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']) @detail_route(methods=['get'])
def issues_stats(self, request, pk=None): def issues_stats(self, request, pk=None):
project = self.get_object() project = self.get_object()

View File

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

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,
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,55 @@
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):
"""Get the fans a project have."""
return get_user_model().objects.filter(fans__project=project)
def get_starred_projects(user):
"""Get the projects an user has starred."""
return Project.objects.filter(fans__user=user)

View File

@ -1,4 +1,5 @@
import uuid import uuid
import threading
import factory import factory
from django.conf import settings from django.conf import settings
@ -7,11 +8,25 @@ import taiga.projects.models
import taiga.projects.userstories.models import taiga.projects.userstories.models
import taiga.projects.issues.models import taiga.projects.issues.models
import taiga.projects.milestones.models import taiga.projects.milestones.models
import taiga.projects.stars.models
import taiga.users.models import taiga.users.models
import taiga.userstorage.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_FOR = taiga.projects.models.ProjectTemplate
FACTORY_DJANGO_GET_OR_CREATE = ("slug", ) FACTORY_DJANGO_GET_OR_CREATE = ("slug", )
@ -28,7 +43,7 @@ class ProjectTemplateFactory(factory.DjangoModelFactory):
roles = [] roles = []
class ProjectFactory(factory.DjangoModelFactory): class ProjectFactory(Factory):
FACTORY_FOR = taiga.projects.models.Project FACTORY_FOR = taiga.projects.models.Project
name = factory.Sequence(lambda n: "Project {}".format(n)) name = factory.Sequence(lambda n: "Project {}".format(n))
@ -38,14 +53,30 @@ class ProjectFactory(factory.DjangoModelFactory):
creation_template = factory.SubFactory("tests.factories.ProjectTemplateFactory") creation_template = factory.SubFactory("tests.factories.ProjectTemplateFactory")
class RoleFactory(factory.DjangoModelFactory): class RoleFactory(Factory):
FACTORY_FOR = taiga.users.models.Role FACTORY_FOR = taiga.users.models.Role
name = "Tester" name = "Tester"
project = factory.SubFactory("tests.factories.ProjectFactory") 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 FACTORY_FOR = taiga.users.models.User
username = factory.Sequence(lambda n: "user{}".format(n)) 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)) 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 FACTORY_FOR = taiga.projects.models.Membership
token = factory.LazyAttribute(lambda obj: str(uuid.uuid1())) token = factory.LazyAttribute(lambda obj: str(uuid.uuid1()))
@ -62,7 +93,7 @@ class MembershipFactory(factory.DjangoModelFactory):
user = factory.SubFactory("tests.factories.UserFactory") user = factory.SubFactory("tests.factories.UserFactory")
class StorageEntryFactory(factory.DjangoModelFactory): class StorageEntryFactory(Factory):
FACTORY_FOR = taiga.userstorage.models.StorageEntry FACTORY_FOR = taiga.userstorage.models.StorageEntry
owner = factory.SubFactory("tests.factories.UserFactory") owner = factory.SubFactory("tests.factories.UserFactory")
@ -70,7 +101,7 @@ class StorageEntryFactory(factory.DjangoModelFactory):
value = factory.Sequence(lambda n: "value {}".format(n)) value = factory.Sequence(lambda n: "value {}".format(n))
class UserStoryFactory(factory.DjangoModelFactory): class UserStoryFactory(Factory):
FACTORY_FOR = taiga.projects.userstories.models.UserStory FACTORY_FOR = taiga.projects.userstories.models.UserStory
ref = factory.Sequence(lambda n: n) ref = factory.Sequence(lambda n: n)
@ -79,7 +110,7 @@ class UserStoryFactory(factory.DjangoModelFactory):
subject = factory.Sequence(lambda n: "User Story {}".format(n)) subject = factory.Sequence(lambda n: "User Story {}".format(n))
class MilestoneFactory(factory.DjangoModelFactory): class MilestoneFactory(Factory):
FACTORY_FOR = taiga.projects.milestones.models.Milestone FACTORY_FOR = taiga.projects.milestones.models.Milestone
name = factory.Sequence(lambda n: "Milestone {}".format(n)) name = factory.Sequence(lambda n: "Milestone {}".format(n))
@ -87,7 +118,7 @@ class MilestoneFactory(factory.DjangoModelFactory):
project = factory.SubFactory("tests.factories.ProjectFactory") project = factory.SubFactory("tests.factories.ProjectFactory")
class IssueFactory(factory.DjangoModelFactory): class IssueFactory(Factory):
FACTORY_FOR = taiga.projects.issues.models.Issue FACTORY_FOR = taiga.projects.issues.models.Issue
subject = factory.Sequence(lambda n: "Issue {}".format(n)) subject = factory.Sequence(lambda n: "Issue {}".format(n))
@ -100,29 +131,36 @@ class IssueFactory(factory.DjangoModelFactory):
milestone = factory.SubFactory("tests.factories.MilestoneFactory") milestone = factory.SubFactory("tests.factories.MilestoneFactory")
class IssueStatusFactory(factory.DjangoModelFactory): class IssueStatusFactory(Factory):
FACTORY_FOR = taiga.projects.models.IssueStatus FACTORY_FOR = taiga.projects.models.IssueStatus
name = factory.Sequence(lambda n: "Issue Status {}".format(n)) name = factory.Sequence(lambda n: "Issue Status {}".format(n))
project = factory.SubFactory("tests.factories.ProjectFactory") project = factory.SubFactory("tests.factories.ProjectFactory")
class SeverityFactory(factory.DjangoModelFactory): class SeverityFactory(Factory):
FACTORY_FOR = taiga.projects.models.Severity FACTORY_FOR = taiga.projects.models.Severity
name = factory.Sequence(lambda n: "Severity {}".format(n)) name = factory.Sequence(lambda n: "Severity {}".format(n))
project = factory.SubFactory("tests.factories.ProjectFactory") project = factory.SubFactory("tests.factories.ProjectFactory")
class PriorityFactory(factory.DjangoModelFactory): class PriorityFactory(Factory):
FACTORY_FOR = taiga.projects.models.Priority FACTORY_FOR = taiga.projects.models.Priority
name = factory.Sequence(lambda n: "Priority {}".format(n)) name = factory.Sequence(lambda n: "Priority {}".format(n))
project = factory.SubFactory("tests.factories.ProjectFactory") project = factory.SubFactory("tests.factories.ProjectFactory")
class IssueTypeFactory(factory.DjangoModelFactory): class IssueTypeFactory(Factory):
FACTORY_FOR = taiga.projects.models.IssueType FACTORY_FOR = taiga.projects.models.IssueType
name = factory.Sequence(lambda n: "Issue Type {}".format(n)) name = factory.Sequence(lambda n: "Issue Type {}".format(n))
project = factory.SubFactory("tests.factories.ProjectFactory") 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")

View File

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

View File

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

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