From 9cda1b57007686d71c404a54ae8ab99ab9e9fd13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 24 Apr 2014 10:51:29 +0200 Subject: [PATCH] US#45: Testing and adding migrations on project templates --- .travis.yml | 2 + requirements.txt | 1 + taiga/base/serializers.py | 5 +- ...te__add_field_project_creation_template.py | 269 ++++++++++++++++++ taiga/projects/models.py | 40 +-- taiga/projects/permissions.py | 13 +- taiga/projects/serializers.py | 21 +- taiga/projects/tests/tests_api.py | 240 +++++++++------- taiga/projects/tests/tests_model.py | 57 ++++ 9 files changed, 517 insertions(+), 131 deletions(-) create mode 100644 taiga/projects/migrations/0014_auto__add_projecttemplate__add_field_project_creation_template.py create mode 100644 taiga/projects/tests/tests_model.py diff --git a/.travis.yml b/.travis.yml index f9efebd5..79330959 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,8 @@ python: - "3.3" services: - rabbitmq # will start rabbitmq-server +addons: + postgresql: "9.3" before_script: - psql -c 'create database taiga;' -U postgres install: diff --git a/requirements.txt b/requirements.txt index e9167e59..39d25f08 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ psycopg2==2.5.2 pytz>=2013.9 six>=1.4.1 djmail>=0.4 +django-pgjson==0.1.2 django-jinja>=0.23 jinja2==2.7.1 pygments>=1.6 diff --git a/taiga/base/serializers.py b/taiga/base/serializers.py index eb5b3580..898bf9df 100644 --- a/taiga/base/serializers.py +++ b/taiga/base/serializers.py @@ -22,6 +22,7 @@ from reversion.models import Version import reversion from taiga.domains.base import get_active_domain +from taiga.domains.models import Domain class PickleField(serializers.WritableField): @@ -50,11 +51,13 @@ class AutoDomainField(serializers.WritableField): Automatically set domain field serializer. """ def to_native(self, obj): + if obj: + return obj.id return obj def from_native(self, data): domain = get_active_domain() - return domain.id + return domain class VersionSerializer(serializers.ModelSerializer): created_date = serializers.SerializerMethodField("get_created_date") diff --git a/taiga/projects/migrations/0014_auto__add_projecttemplate__add_field_project_creation_template.py b/taiga/projects/migrations/0014_auto__add_projecttemplate__add_field_project_creation_template.py new file mode 100644 index 00000000..a7eef861 --- /dev/null +++ b/taiga/projects/migrations/0014_auto__add_projecttemplate__add_field_project_creation_template.py @@ -0,0 +1,269 @@ +# -*- 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 'ProjectTemplate' + db.create_table('projects_projecttemplate', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=250, unique=True)), + ('slug', self.gf('django.db.models.fields.SlugField')(max_length=250, unique=True, blank=True)), + ('description', self.gf('django.db.models.fields.TextField')()), + ('created_date', self.gf('django.db.models.fields.DateTimeField')(blank=True, auto_now_add=True)), + ('modified_date', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + ('domain', self.gf('django.db.models.fields.related.ForeignKey')(related_name='templates', to=orm['domains.Domain'], blank=True, null=True, default=None)), + ('is_backlog_activated', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('is_kanban_activated', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('is_wiki_activated', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('is_issues_activated', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('videoconferences', self.gf('django.db.models.fields.CharField')(max_length=250, null=True, blank=True)), + ('videoconferences_salt', self.gf('django.db.models.fields.CharField')(max_length=250, null=True, blank=True)), + ('default_options', self.gf('django_pgjson.fields.JsonField')()), + ('us_statuses', self.gf('django_pgjson.fields.JsonField')()), + ('points', self.gf('django_pgjson.fields.JsonField')()), + ('task_statuses', self.gf('django_pgjson.fields.JsonField')()), + ('issue_statuses', self.gf('django_pgjson.fields.JsonField')()), + ('issue_types', self.gf('django_pgjson.fields.JsonField')()), + ('priorities', self.gf('django_pgjson.fields.JsonField')()), + ('severities', self.gf('django_pgjson.fields.JsonField')()), + ('roles', self.gf('django_pgjson.fields.JsonField')()), + )) + db.send_create_signal('projects', ['ProjectTemplate']) + + # Adding field 'Project.creation_template' + db.add_column('projects_project', 'creation_template', + self.gf('django.db.models.fields.related.ForeignKey')(related_name='projects', to=orm['projects.ProjectTemplate'], blank=True, null=True, default=None), + keep_default=False) + + + def backwards(self, orm): + # Deleting model 'ProjectTemplate' + db.delete_table('projects_projecttemplate') + + # Deleting field 'Project.creation_template' + db.delete_column('projects_project', 'creation_template_id') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['auth.Permission']", 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission', 'ordering': "('content_type__app_label', 'content_type__model', 'codename')"}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'db_table': "'django_content_type'", 'object_name': 'ContentType', 'unique_together': "(('app_label', 'model'),)"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'domains.domain': { + 'Meta': {'object_name': 'Domain', 'ordering': "('domain',)"}, + 'alias_of': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['domains.Domain']", 'blank': 'True', 'null': 'True', 'default': 'None'}), + 'default_language': ('django.db.models.fields.CharField', [], {'max_length': '20', 'blank': 'True', 'default': "''"}), + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True'}), + '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', [], {'max_length': '60', 'null': 'True', 'default': 'None'}) + }, + 'projects.attachment': { + 'Meta': {'object_name': 'Attachment', 'ordering': "['project', 'created_date']"}, + 'attached_file': ('django.db.models.fields.files.FileField', [], {'max_length': '500', 'null': 'True', 'blank': 'True'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'created_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now_add': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_attachments'", 'to': "orm['users.User']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attachments'", 'to': "orm['projects.Project']"}) + }, + 'projects.issuestatus': { + 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'IssueStatus', 'ordering': "['project', 'order', 'name']"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + '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'),)", 'object_name': 'IssueType', 'ordering': "['project', 'order', 'name']"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + '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': {'object_name': 'Membership', 'ordering': "['project', 'role']"}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now_add': 'True', 'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'null': 'True', 'blank': 'True', 'default': 'None'}), + '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', [], {'max_length': '60', 'null': 'True', 'blank': 'True', 'default': 'None'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'memberships'", 'to': "orm['users.User']", 'blank': 'True', 'null': 'True', 'default': 'None'}) + }, + 'projects.points': { + 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'Points', 'ordering': "['project', 'order', 'name']"}, + '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', [], {'null': 'True', 'blank': 'True', 'default': 'None'}) + }, + 'projects.priority': { + 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'Priority', 'ordering': "['project', 'order', 'name']"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + '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': {'object_name': 'Project', 'ordering': "['name']"}, + 'created_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now_add': 'True'}), + 'creation_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects'", 'to': "orm['projects.ProjectTemplate']", 'blank': 'True', 'null': 'True', 'default': 'None'}), + 'default_issue_status': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'unique': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['projects.IssueStatus']", 'blank': 'True', 'null': 'True'}), + 'default_issue_type': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'unique': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['projects.IssueType']", 'blank': 'True', 'null': 'True'}), + 'default_points': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'unique': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['projects.Points']", 'blank': 'True', 'null': 'True'}), + 'default_priority': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'unique': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['projects.Priority']", 'blank': 'True', 'null': 'True'}), + 'default_question_status': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'unique': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['projects.QuestionStatus']", 'blank': 'True', 'null': 'True'}), + 'default_severity': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'unique': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['projects.Severity']", 'blank': 'True', 'null': 'True'}), + 'default_task_status': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'unique': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['projects.TaskStatus']", 'blank': 'True', 'null': 'True'}), + 'default_us_status': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'unique': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['projects.UserStoryStatus']", 'blank': 'True', 'null': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects'", 'to': "orm['domains.Domain']", 'blank': 'True', 'null': 'True', 'default': 'None'}), + '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'}), + 'last_issue_ref': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'default': '0'}), + 'last_task_ref': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'default': '0'}), + 'last_us_ref': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'default': '0'}), + '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', [], {'auto_now': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '250', 'unique': 'True'}), + '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', [], {'max_length': '250', 'unique': 'True', 'blank': 'True'}), + 'tags': ('picklefield.fields.PickledObjectField', [], {'blank': 'True'}), + 'total_milestones': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True', 'default': '0'}), + 'total_story_points': ('django.db.models.fields.FloatField', [], {'null': 'True', 'default': 'None'}), + 'videoconferences': ('django.db.models.fields.CharField', [], {'max_length': '250', 'null': 'True', 'blank': 'True'}), + 'videoconferences_salt': ('django.db.models.fields.CharField', [], {'max_length': '250', 'null': 'True', 'blank': 'True'}) + }, + 'projects.projecttemplate': { + 'Meta': {'object_name': 'ProjectTemplate', 'ordering': "['name']"}, + 'created_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now_add': 'True'}), + 'default_options': ('django_pgjson.fields.JsonField', [], {}), + 'description': ('django.db.models.fields.TextField', [], {}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'templates'", 'to': "orm['domains.Domain']", 'blank': 'True', 'null': 'True', 'default': 'None'}), + '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', [], {'auto_now': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '250', 'unique': 'True'}), + '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', [], {'max_length': '250', 'unique': 'True', 'blank': 'True'}), + 'task_statuses': ('django_pgjson.fields.JsonField', [], {}), + 'us_statuses': ('django_pgjson.fields.JsonField', [], {}), + 'videoconferences': ('django.db.models.fields.CharField', [], {'max_length': '250', 'null': 'True', 'blank': 'True'}), + 'videoconferences_salt': ('django.db.models.fields.CharField', [], {'max_length': '250', 'null': 'True', 'blank': 'True'}) + }, + 'projects.questionstatus': { + 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'QuestionStatus', 'ordering': "['project', 'order', 'name']"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + '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': "'question_status'", 'to': "orm['projects.Project']"}) + }, + 'projects.severity': { + 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'Severity', 'ordering': "['project', 'order', 'name']"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + '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'),)", 'object_name': 'TaskStatus', 'ordering': "['project', 'order', 'name']"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + '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'),)", 'object_name': 'UserStoryStatus', 'ordering': "['project', 'order', 'name']"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + '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', [], {'null': 'True', 'blank': 'True', 'default': 'None'}) + }, + 'users.role': { + 'Meta': {'unique_together': "(('slug', 'project'),)", 'object_name': 'Role', 'ordering': "['order', 'slug']"}, + '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', [], {'max_length': '250', 'blank': 'True'}) + }, + 'users.user': { + 'Meta': {'object_name': 'User', 'ordering': "['username']"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '9', 'blank': 'True', 'default': "'#5429db'"}), + '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', [], {'max_length': '20', 'blank': 'True', 'default': "''"}), + 'default_timezone': ('django.db.models.fields.CharField', [], {'max_length': '20', 'blank': 'True', 'default': "''"}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'to': "orm['auth.Group']", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'notify_changes_by_me': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'notify_level': ('django.db.models.fields.CharField', [], {'max_length': '32', 'default': "'all_owned_projects'"}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'photo': ('django.db.models.fields.files.FileField', [], {'max_length': '500', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True', 'default': 'None'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'to': "orm['auth.Permission']", 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'}) + } + } + + complete_apps = ['projects'] \ No newline at end of file diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 6553d412..4a28d5ba 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -161,6 +161,11 @@ class Project(ProjectDefaults, models.Model): videoconferences_salt = models.CharField(max_length=250, null=True, blank=True, verbose_name=_("videoconference room salt")) + creation_template = models.ForeignKey("projects.ProjectTemplate", + related_name="projects", null=True, + blank=True, default=None, + verbose_name=_("creation template")) + domain = models.ForeignKey("domains.Domain", related_name="projects", null=True, blank=True, default=None, verbose_name=_("domain")) @@ -549,15 +554,15 @@ class ProjectTemplate(models.Model): videoconferences_salt = models.CharField(max_length=250, null=True, blank=True, verbose_name=_("videoconference room salt")) - default_options = JsonField(null=True, blank=True, default=None, verbose_name=_("default options")) - us_statuses = JsonField(null=True, blank=True, default=None, verbose_name=_("us statuses")) - points = JsonField(null=True, blank=True, default=None, verbose_name=_("us points")) - task_statuses = JsonField(null=True, blank=True, default=None, verbose_name=_("task statuses")) - issue_statuses = JsonField(null=True, blank=True, default=None, verbose_name=_("issue statuses")) - issue_types = JsonField(null=True, blank=True, default=None, verbose_name=_("issue types")) - priorities = JsonField(null=True, blank=True, default=None, verbose_name=_("issue types")) - severities = JsonField(null=True, blank=True, default=None, verbose_name=_("issue types")) - roles = JsonField(null=True, blank=True, default=None, verbose_name=_("roles")) + default_options = JsonField(null=True, blank=True, verbose_name=_("default options")) + us_statuses = JsonField(null=True, blank=True, verbose_name=_("us statuses")) + points = JsonField(null=True, blank=True, verbose_name=_("us points")) + task_statuses = JsonField(null=True, blank=True, verbose_name=_("task statuses")) + issue_statuses = JsonField(null=True, blank=True, verbose_name=_("issue statuses")) + issue_types = JsonField(null=True, blank=True, verbose_name=_("issue types")) + priorities = JsonField(null=True, blank=True, verbose_name=_("issue types")) + severities = JsonField(null=True, blank=True, verbose_name=_("issue types")) + roles = JsonField(null=True, blank=True, verbose_name=_("roles")) class Meta: verbose_name = "project template" @@ -582,13 +587,13 @@ class ProjectTemplate(models.Model): self.videoconferences_salt = project.videoconferences_salt self.default_options = { - "points": project.default_points.name, - "us_status": project.default_us_status.name, - "task_status": project.default_task_status.name, - "issue_status": project.default_issue_status.name, - "issue_type": project.default_issue_type.name, - "priority": project.default_priority.name, - "severity": project.default_severity.name + "points": getattr(project.default_points, "name", None), + "us_status": getattr(project.default_us_status, "name", None), + "task_status": getattr(project.default_task_status, "name", None), + "issue_status": getattr(project.default_issue_status, "name", None), + "issue_type": getattr(project.default_issue_type, "name", None), + "priority": getattr(project.default_priority, "name", None), + "severity": getattr(project.default_severity, "name", None) } @@ -665,8 +670,9 @@ class ProjectTemplate(models.Model): def apply_to_project(self, project): if project.id is None: - raise "Project need an id (must be a saved project)" + raise Exception("Project need an id (must be a saved project)") + project.creation_template = self project.is_backlog_activated = self.is_backlog_activated project.is_kanban_activated = self.is_kanban_activated project.is_wiki_activated = self.is_wiki_activated diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index 12ed9455..b56c8a1e 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -1,5 +1,4 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 Andrey Antukh # Copyright (C) 2014 Jesús Espino # Copyright (C) 2014 David Barragán # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -175,5 +174,11 @@ class ProjectTemplatePermission(BasePermission): return domain.user_is_owner(request.user) def has_object_permission(self, request, view, obj): - domain = get_active_domain() - return domain.user_is_owner(request.user) + current_domain = get_active_domain() + if obj.domain: + return obj.domain == current_domain and current_domain.user_is_owner(request.user) + else: + if request.method == "GET": + return current_domain.user_is_owner(request.user) + else: + False diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index c465d55c..2c3a3f44 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -16,6 +16,7 @@ from os import path from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ from taiga.base.serializers import PickleField, JsonField, AutoDomainField from taiga.users.models import Role @@ -185,17 +186,17 @@ class RoleSerializer(serializers.ModelSerializer): class ProjectTemplateSerializer(serializers.ModelSerializer): - domain = AutoDomainField() + domain = AutoDomainField(required=False, label=_("Domain")) - default_options = JsonField() - us_statuses = JsonField() - points = JsonField() - task_statuses = JsonField() - issue_statuses = JsonField() - issue_types = JsonField() - priorities = JsonField() - severities = JsonField() - roles = JsonField() + default_options = JsonField(required=False, label=_("Default options")) + us_statuses = JsonField(required=False, label=_("User story's statuses")) + points = JsonField(required=False, label=_("Points")) + task_statuses = JsonField(required=False, label=_("Task's statuses")) + issue_statuses = JsonField(required=False, label=_("Issue's statuses")) + issue_types = JsonField(required=False, label=_("Issue's types")) + priorities = JsonField(required=False, label=_("Priorities")) + severities = JsonField(required=False, label=_("Severities")) + roles = JsonField(required=False, label=_("Roles")) class Meta: model = models.ProjectTemplate diff --git a/taiga/projects/tests/tests_api.py b/taiga/projects/tests/tests_api.py index 37bba1f2..7e58034c 100644 --- a/taiga/projects/tests/tests_api.py +++ b/taiga/projects/tests/tests_api.py @@ -584,7 +584,8 @@ class ProjectTemplatesTestCase(test.TestCase): self.client.logout() - ProjectTemplate.objects.order_by("created_date")[0:2].delete() + ProjectTemplate.objects.order_by("created_date")[0].delete() + ProjectTemplate.objects.order_by("created_date")[1].delete() def test_create_project_template_by_not_domain_owner(self): data = { @@ -594,14 +595,14 @@ class ProjectTemplatesTestCase(test.TestCase): } self.assertEqual(ProjectTemplate.objects.all().count(), 2) - response = self.client.login(username=self.user1.username, - password=self.user1.username) + response = self.client.login(username=self.user2.username, + password=self.user2.username) self.assertTrue(response) response = self.client.post( reverse("project-templates-list"), json.dumps(data), content_type="application/json") - self.assertEqual(response.status_code, 401) + self.assertEqual(response.status_code, 403) self.assertEqual(ProjectTemplate.objects.all().count(), 2) data = { @@ -615,112 +616,153 @@ class ProjectTemplatesTestCase(test.TestCase): reverse("project-templates-list"), json.dumps(data), content_type="application/json") - self.assertEqual(response.status_code, 401) + self.assertEqual(response.status_code, 403) self.assertEqual(ProjectTemplate.objects.all().count(), 2) self.client.logout() - # def test_edit_project_by_anon(self): - # data = { - # "description": "Edited project description", - # } + def test_edit_project_template_by_anon(self): + template = ProjectTemplate.objects.create( + name="Test Project Template with domain", + slug="test-project-template-with-domain", + description="A new Test Project Template with domain", + domain_id=1 + ) - # self.assertEqual(Project.objects.all().count(), 4) - # self.assertNotEqual(data["description"], self.project1.description) - # response = self.client.patch( - # reverse("projects-detail", args=(self.project1.id,)), - # json.dumps(data), - # content_type="application/json") - # self.assertEqual(response.status_code, 401) - # self.assertEqual(Project.objects.all().count(), 4) + data = {"description": "A new Test Project Template", } - # def test_edit_project_by_owner(self): - # data = { - # "description": "Modified project description", - # } + self.assertEqual(ProjectTemplate.objects.all().count(), 3) + response = self.client.patch( + reverse("project-templates-detail", args=(1,)), + json.dumps(data), + content_type="application/json") + self.assertEqual(response.status_code, 401) - # self.assertEqual(Project.objects.all().count(), 4) - # self.assertNotEqual(data["description"], self.project1.description) - # response = self.client.login(username=self.user1.username, - # password=self.user1.username) - # self.assertTrue(response) - # response = self.client.patch( - # reverse("projects-detail", args=(self.project1.id,)), - # json.dumps(data), - # content_type="application/json") - # self.assertEqual(response.status_code, 200) - # self.assertEqual(data["description"], response.data["description"]) - # self.assertEqual(Project.objects.all().count(), 4) - # self.client.logout() + response = self.client.patch( + reverse("project-templates-detail", args=(template.id,)), + json.dumps(data), + content_type="application/json") + self.assertEqual(response.status_code, 401) - # def test_edit_project_by_membership(self): - # data = { - # "description": "Edited project description", - # } + template.delete() - # self.assertEqual(Project.objects.all().count(), 4) - # self.assertNotEqual(data["description"], self.project1.description) - # response = self.client.login(username=self.user3.username, - # password=self.user3.username) - # self.assertTrue(response) - # response = self.client.patch( - # reverse("projects-detail", args=(self.project1.id,)), - # json.dumps(data), - # content_type="application/json") - # self.assertEqual(response.status_code, 200) - # self.assertEqual(data["description"], response.data["description"]) - # self.assertEqual(Project.objects.all().count(), 4) - # self.client.logout() + def test_edit_project_template_by_domain_owner(self): + template = ProjectTemplate.objects.create( + name="Test Project Template with domain", + slug="test-project-template-with-domain", + description="A new Test Project Template with domain", + domain_id=1 + ) - # def test_edit_project_by_not_membership(self): - # data = { - # "description": "Edited project description", - # } + data = {"description": "A new Test Project Template", } - # self.assertEqual(Project.objects.all().count(), 4) - # self.assertNotEqual(data["description"], self.project1.description) - # response = self.client.login(username=self.user2.username, - # password=self.user2.username) - # self.assertTrue(response) - # response = self.client.patch( - # reverse("projects-detail", args=(self.project1.id,)), - # json.dumps(data), - # content_type="application/json") - # self.assertEqual(response.status_code, 404) - # self.assertEqual(Project.objects.all().count(), 4) - # self.client.logout() + response = self.client.login(username=self.user1.username, + password=self.user1.username) + self.assertTrue(response) - # def test_delete_project_by_anon(self): - # self.assertEqual(Project.objects.all().count(), 4) - # response = self.client.delete(reverse("projects-detail", args=(self.project1.id,))) - # self.assertEqual(response.status_code, 401) - # self.assertEqual(Project.objects.all().count(), 4) + self.assertEqual(ProjectTemplate.objects.all().count(), 3) + response = self.client.patch( + reverse("project-templates-detail", args=(1,)), + json.dumps(data), + content_type="application/json") + self.assertEqual(response.status_code, 403) - # def test_delete_project_by_owner(self): - # self.assertEqual(Project.objects.all().count(), 4) - # response = self.client.login(username=self.user1.username, - # password=self.user1.username) - # self.assertTrue(response) - # response = self.client.delete(reverse("projects-detail", args=(self.project1.id,))) - # self.assertEqual(response.status_code, 204) - # self.assertEqual(Project.objects.all().count(), 3) - # self.client.logout() + response = self.client.patch( + reverse("project-templates-detail", args=(template.id,)), + json.dumps(data), + content_type="application/json") + self.assertEqual(response.status_code, 200) - # def test_delete_project_by_membership(self): - # self.assertEqual(Project.objects.all().count(), 4) - # response = self.client.login(username=self.user3.username, - # password=self.user3.username) - # self.assertTrue(response) - # response = self.client.delete(reverse("projects-detail", args=(self.project1.id,))) - # self.assertEqual(response.status_code, 403) - # self.assertEqual(Project.objects.all().count(), 4) - # self.client.logout() + template.delete() + self.client.logout() - # def test_delete_project_by_not_membership(self): - # self.assertEqual(Project.objects.all().count(), 4) - # response = self.client.login(username=self.user1.username, - # password=self.user1.username) - # self.assertTrue(response) - # response = self.client.delete(reverse("projects-detail", args=(self.project3.id,))) - # self.assertEqual(response.status_code, 404) - # self.assertEqual(Project.objects.all().count(), 4) - # self.client.logout() + def test_edit_project_template_by_not_domain_owner(self): + template = ProjectTemplate.objects.create( + name="Test Project Template with domain", + slug="test-project-template-with-domain", + description="A new Test Project Template with domain", + domain_id=1 + ) + + data = {"description": "A new Test Project Template", } + + response = self.client.login(username=self.user2.username, + password=self.user2.username) + self.assertTrue(response) + + self.assertEqual(ProjectTemplate.objects.all().count(), 3) + response = self.client.patch( + reverse("project-templates-detail", args=(1,)), + json.dumps(data), + content_type="application/json") + self.assertEqual(response.status_code, 403) + + response = self.client.patch( + reverse("project-templates-detail", args=(template.id,)), + json.dumps(data), + content_type="application/json") + self.assertEqual(response.status_code, 403) + + template.delete() + self.client.logout() + + def test_delete_project_template_by_anon(self): + template = ProjectTemplate.objects.create( + name="Test Project Template with domain", + slug="test-project-template-with-domain", + description="A new Test Project Template with domain", + domain_id=1 + ) + self.assertEqual(ProjectTemplate.objects.all().count(), 3) + response = self.client.delete(reverse("projects-detail", args=(1,))) + self.assertEqual(response.status_code, 401) + self.assertEqual(ProjectTemplate.objects.all().count(), 3) + + response = self.client.delete(reverse("projects-detail", args=(template.id,))) + self.assertEqual(response.status_code, 401) + self.assertEqual(ProjectTemplate.objects.all().count(), 3) + + template.delete() + + def test_delete_project_template_by_domain_owner(self): + template = ProjectTemplate.objects.create( + name="Test Project Template with domain", + slug="test-project-template-with-domain", + description="A new Test Project Template with domain", + domain_id=1 + ) + self.assertEqual(ProjectTemplate.objects.all().count(), 3) + response = self.client.login(username=self.user1.username, + password=self.user1.username) + self.assertTrue(response) + response = self.client.delete(reverse("project-templates-detail", args=(1,))) + self.assertEqual(response.status_code, 403) + self.assertEqual(ProjectTemplate.objects.all().count(), 3) + + response = self.client.delete(reverse("project-templates-detail", args=(template.id,))) + self.assertEqual(response.status_code, 204) + self.assertEqual(ProjectTemplate.objects.all().count(), 2) + + template.delete() + self.client.logout() + + def test_delete_project_template_by_not_domain_owner(self): + template = ProjectTemplate.objects.create( + name="Test Project Template with domain", + slug="test-project-template-with-domain", + description="A new Test Project Template with domain", + domain_id=1 + ) + self.assertEqual(ProjectTemplate.objects.all().count(), 3) + response = self.client.login(username=self.user2.username, + password=self.user2.username) + self.assertTrue(response) + response = self.client.delete(reverse("project-templates-detail", args=(1,))) + self.assertEqual(response.status_code, 403) + self.assertEqual(ProjectTemplate.objects.all().count(), 3) + + response = self.client.delete(reverse("project-templates-detail", args=(template.id,))) + self.assertEqual(response.status_code, 403) + self.assertEqual(ProjectTemplate.objects.all().count(), 3) + + template.delete() + self.client.logout() diff --git a/taiga/projects/tests/tests_model.py b/taiga/projects/tests/tests_model.py new file mode 100644 index 00000000..c13f2c8b --- /dev/null +++ b/taiga/projects/tests/tests_model.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- + +import json + +from django import test +from django.core.urlresolvers import reverse +from django.core import mail +from django.db.models import get_model +from django.conf import settings + +from taiga.users.tests import create_user +from taiga.projects.models import Project, Membership, ProjectTemplate +from taiga.domains.models import Domain + +from . import create_project +from . import add_membership + + +class ProjectTemplateModelTestCase(test.TestCase): + fixtures = ["initial_domains.json", "initial_project_templates.json"] + + def setUp(self): + self.user = create_user(1) + self.domain = Domain.objects.all()[0] + self.template = ProjectTemplate.objects.get(slug="scrum") + + def test_apply_to_not_saved_project(self): + not_saved_project = Project() + self.assertRaises(Exception, self.template.apply_to_project, (not_saved_project,)) + + def test_apply_to_saved_project(self): + # Post-save apply the default template + project = Project.objects.create(name="Test", slug="test", owner_id=1) + self.assertEqual(project.creation_template.slug, settings.DEFAULT_PROJECT_TEMPLATE) + + def test_load_data_from_project_with_invalid_object(self): + self.assertRaises(Exception, self.template.load_data_from_project, (None,)) + + def test_load_data_from_project_not_defaults(self): + project = Project.objects.create(name="Test", slug="test", owner_id=1) + project.default_points = None + project.default_us_status = None + project.default_task_status = None + project.default_issue_status = None + project.default_issue_type = None + project.default_priority = None + project.default_severity = None + + template = ProjectTemplate() + template.load_data_from_project(project) + self.assertIsNone(template.default_options["points"]) + self.assertIsNone(template.default_options["us_status"]) + self.assertIsNone(template.default_options["task_status"]) + self.assertIsNone(template.default_options["issue_status"]) + self.assertIsNone(template.default_options["issue_type"]) + self.assertIsNone(template.default_options["priority"]) + self.assertIsNone(template.default_options["severity"])