Star/Unstar projects
parent
a4bb6e7eee
commit
b56dfe7cf5
|
@ -8,3 +8,4 @@ pytest-pythonpath==0.3
|
|||
|
||||
coverage==3.7.1
|
||||
coveralls==0.4.2
|
||||
django-testclient-extensions==0.1.1
|
||||
|
|
|
@ -186,6 +186,7 @@ INSTALLED_APPS = [
|
|||
"taiga.projects.wiki",
|
||||
"taiga.projects.history",
|
||||
"taiga.projects.notifications",
|
||||
"taiga.projects.stars",
|
||||
|
||||
"south",
|
||||
"reversion",
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
from .services import star, unstar
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -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']
|
|
@ -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)
|
|
@ -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)
|
|
@ -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,36 @@ 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")
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue