diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py
index dda77a44..1f6e4edd 100644
--- a/taiga/projects/admin.py
+++ b/taiga/projects/admin.py
@@ -15,26 +15,11 @@
# along with this program. If not, see .
from django.contrib import admin
-from django.contrib.contenttypes import generic
from taiga.projects.milestones.admin import MilestoneInline
from taiga.users.admin import RoleInline
from . import models
-import reversion
-
-
-class AttachmentAdmin(reversion.VersionAdmin):
- list_display = ["id", "project", "attached_file", "owner", "content_type", "content_object"]
- list_display_links = ["id", "attached_file",]
- list_filter = ["project", "content_type"]
-
-
-class AttachmentInline(generic.GenericTabularInline):
- model = models.Attachment
- fields = ("attached_file", "owner")
- extra = 0
-
class MembershipAdmin(admin.ModelAdmin):
list_display = ['project', 'role', 'user']
@@ -129,7 +114,6 @@ admin.site.register(models.TaskStatus, TaskStatusAdmin)
admin.site.register(models.UserStoryStatus, UserStoryStatusAdmin)
admin.site.register(models.Points, PointsAdmin)
admin.site.register(models.Project, ProjectAdmin)
-admin.site.register(models.Attachment, AttachmentAdmin)
admin.site.register(models.Membership, MembershipAdmin)
admin.site.register(models.Severity, SeverityAdmin)
admin.site.register(models.Priority, PriorityAdmin)
diff --git a/taiga/projects/attachments/__init__.py b/taiga/projects/attachments/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/taiga/projects/attachments/admin.py b/taiga/projects/attachments/admin.py
new file mode 100644
index 00000000..e8d78de6
--- /dev/null
+++ b/taiga/projects/attachments/admin.py
@@ -0,0 +1,35 @@
+# 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
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.contrib import admin
+from django.contrib.contenttypes import generic
+
+from . import models
+
+
+class AttachmentAdmin(admin.ModelAdmin):
+ list_display = ["id", "project", "attached_file", "owner", "content_type", "content_object"]
+ list_display_links = ["id", "attached_file",]
+ list_filter = ["project", "content_type"]
+
+
+class AttachmentInline(generic.GenericTabularInline):
+ model = models.Attachment
+ fields = ("attached_file", "owner")
+ extra = 0
+
+
+admin.site.register(models.Attachment, AttachmentAdmin)
diff --git a/taiga/projects/attachments/api.py b/taiga/projects/attachments/api.py
new file mode 100644
index 00000000..54dccfc1
--- /dev/null
+++ b/taiga/projects/attachments/api.py
@@ -0,0 +1,110 @@
+# 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
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.utils.translation import ugettext as _
+from django.contrib.contenttypes.models import ContentType
+from django.shortcuts import get_object_or_404
+from rest_framework.permissions import IsAuthenticated
+
+from taiga.base.api import ModelCrudViewSet
+from taiga.base import filters
+from taiga.base import exceptions as exc
+from taiga.projects.mixins.notifications import NotificationSenderMixin
+from taiga.projects.history.services import take_snapshot
+
+from . import permissions
+from . import serializers
+from . import models
+
+
+class BaseAttachmentViewSet(NotificationSenderMixin, ModelCrudViewSet):
+ model = models.Attachment
+ serializer_class = serializers.AttachmentSerializer
+ permission_classes = (IsAuthenticated, permissions.AttachmentPermission,)
+
+ filter_backends = (filters.IsProjectMemberFilterBackend,)
+ filter_fields = ["project", "object_id"]
+
+ content_type = None
+
+ def get_content_type(self):
+ app_name, model = self.content_type.split(".", 1)
+ return get_object_or_404(ContentType, app_label=app_name, model=model)
+
+ def get_queryset(self):
+ ct = self.get_content_type()
+ qs = super().get_queryset()
+ qs = qs.filter(content_type=ct)
+ return qs.distinct()
+
+ def pre_save(self, obj):
+ if not obj.id:
+ obj.content_type = self.get_content_type()
+ obj.owner = self.request.user
+
+ super().pre_save(obj)
+
+ def pre_conditions_on_save(self, obj):
+ super().pre_conditions_on_save(obj)
+
+ if (obj.project.owner != self.request.user and
+ obj.project.memberships.filter(user=self.request.user).count() == 0):
+ raise exc.PermissionDenied(_("You don't have permissions for "
+ "add attachments to this user story"))
+
+ def _get_object_for_snapshot(self, obj):
+ return obj.content_object
+
+ def pre_destroy(self, obj):
+ pass
+
+ def post_destroy(self, obj):
+ user = self.request.user
+ comment = self.request.DATA.get("comment", "")
+
+ obj = self._get_object_for_snapshot(obj)
+ history = take_snapshot(obj, comment=comment, user=user)
+
+ if history:
+ self._post_save_notification_sender(obj, history)
+
+
+class UserStoryAttachmentViewSet(BaseAttachmentViewSet):
+ content_type = "userstories.userstory"
+ create_notification_template = "create_userstory_notification"
+ update_notification_template = "update_userstory_notification"
+ destroy_notification_template = "destroy_userstory_notification"
+
+
+class IssueAttachmentViewSet(BaseAttachmentViewSet):
+ content_type = "issues.issue"
+ create_notification_template = "create_issue_notification"
+ update_notification_template = "update_issue_notification"
+ destroy_notification_template = "destroy_issue_notification"
+
+
+class TaskAttachmentViewSet(BaseAttachmentViewSet):
+ content_type = "tasks.task"
+ create_notification_template = "create_task_notification"
+ update_notification_template = "update_task_notification"
+ destroy_notification_template = "destroy_task_notification"
+
+
+class WikiAttachmentViewSet(BaseAttachmentViewSet):
+ content_type = "wiki.wiki"
+ create_notification_template = "create_wiki_notification"
+ update_notification_template = "update_wiki_notification"
+ destroy_notification_template = "destroy_wiki_notification"
diff --git a/taiga/projects/attachments/migrations/0001_create_attachment.py b/taiga/projects/attachments/migrations/0001_create_attachment.py
new file mode 100644
index 00000000..7989a733
--- /dev/null
+++ b/taiga/projects/attachments/migrations/0001_create_attachment.py
@@ -0,0 +1,246 @@
+# -*- 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):
+ db.rename_table('projects_attachment', 'attachments_attachment')
+
+ if not db.dry_run:
+ # For permissions to work properly after migrating
+ orm['contenttypes.contenttype'].objects.filter(app_label='projects', model='attachment').update(app_label='attachments')
+
+ def backwards(self, orm):
+ db.rename_table('attachments_attachment', 'projects_attachment', )
+
+ if not db.dry_run:
+ # For permissions to work properly after migrating
+ orm['contenttypes.contenttype'].objects.filter(app_label='attachments', model='attachment').update(app_label='projects')
+
+
+ models = {
+ 'attachments.attachment': {
+ 'Meta': {'object_name': 'Attachment', 'ordering': "['project', 'created_date']"},
+ 'attached_file': ('django.db.models.fields.files.FileField', [], {'null': 'True', 'max_length': '500', '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'}),
+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_deprecated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'modified_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['users.User']", 'related_name': "'change_attachments'"}),
+ 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']", 'related_name': "'attachments'"})
+ },
+ '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', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', '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': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'", 'ordering': "('name',)"},
+ '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', [], {'to': "orm['domains.Domain']", 'null': 'True', 'default': 'None', 'blank': 'True', 'related_name': "'+'"}),
+ 'default_language': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20', 'blank': 'True'}),
+ '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', [], {'default': 'None', 'max_length': '60', 'null': 'True'})
+ },
+ 'projects.issuestatus': {
+ 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'IssueStatus', 'ordering': "['project', 'order', 'name']"},
+ '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', [], {'to': "orm['projects.Project']", 'related_name': "'issue_statuses'"})
+ },
+ 'projects.issuetype': {
+ 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'IssueType', 'ordering': "['project', 'order', 'name']"},
+ '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', [], {'to': "orm['projects.Project']", 'related_name': "'issue_types'"})
+ },
+ 'projects.membership': {
+ 'Meta': {'unique_together': "(('user', 'project', 'email'),)", 'object_name': 'Membership', 'ordering': "['project', 'role']"},
+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'blank': 'True', 'auto_now_add': 'True'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'null': 'True', 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']", 'related_name': "'memberships'"}),
+ 'role': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['users.Role']", 'related_name': "'memberships'"}),
+ 'token': ('django.db.models.fields.CharField', [], {'default': 'None', 'null': 'True', 'max_length': '60', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['users.User']", 'null': 'True', 'default': 'None', 'blank': 'True', 'related_name': "'memberships'"})
+ },
+ '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', [], {'to': "orm['projects.Project']", 'related_name': "'points'"}),
+ 'value': ('django.db.models.fields.FloatField', [], {'default': 'None', 'blank': 'True', 'null': 'True'})
+ },
+ 'projects.priority': {
+ 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'Priority', 'ordering': "['project', 'order', 'name']"},
+ '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', [], {'to': "orm['projects.Project']", 'related_name': "'priorities'"})
+ },
+ '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', [], {'to': "orm['projects.ProjectTemplate']", 'null': 'True', 'default': 'None', 'blank': 'True', 'related_name': "'projects'"}),
+ 'default_issue_status': ('django.db.models.fields.related.OneToOneField', [], {'unique': 'True', 'related_name': "'+'", 'on_delete': 'models.SET_NULL', 'to': "orm['projects.IssueStatus']", 'blank': 'True', 'null': 'True'}),
+ 'default_issue_type': ('django.db.models.fields.related.OneToOneField', [], {'unique': 'True', 'related_name': "'+'", 'on_delete': 'models.SET_NULL', 'to': "orm['projects.IssueType']", 'blank': 'True', 'null': 'True'}),
+ 'default_points': ('django.db.models.fields.related.OneToOneField', [], {'unique': 'True', 'related_name': "'+'", 'on_delete': 'models.SET_NULL', 'to': "orm['projects.Points']", 'blank': 'True', 'null': 'True'}),
+ 'default_priority': ('django.db.models.fields.related.OneToOneField', [], {'unique': 'True', 'related_name': "'+'", 'on_delete': 'models.SET_NULL', 'to': "orm['projects.Priority']", 'blank': 'True', 'null': 'True'}),
+ 'default_question_status': ('django.db.models.fields.related.OneToOneField', [], {'unique': 'True', 'related_name': "'+'", 'on_delete': 'models.SET_NULL', 'to': "orm['projects.QuestionStatus']", 'blank': 'True', 'null': 'True'}),
+ 'default_severity': ('django.db.models.fields.related.OneToOneField', [], {'unique': 'True', 'related_name': "'+'", 'on_delete': 'models.SET_NULL', 'to': "orm['projects.Severity']", 'blank': 'True', 'null': 'True'}),
+ 'default_task_status': ('django.db.models.fields.related.OneToOneField', [], {'unique': 'True', 'related_name': "'+'", 'on_delete': 'models.SET_NULL', 'to': "orm['projects.TaskStatus']", 'blank': 'True', 'null': 'True'}),
+ 'default_us_status': ('django.db.models.fields.related.OneToOneField', [], {'unique': 'True', 'related_name': "'+'", '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', [], {'to': "orm['domains.Domain']", 'null': 'True', 'default': 'None', 'blank': 'True', 'related_name': "'projects'"}),
+ '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', [], {'default': '0', 'null': 'True'}),
+ 'last_task_ref': ('django.db.models.fields.BigIntegerField', [], {'default': '0', 'null': 'True'}),
+ 'last_us_ref': ('django.db.models.fields.BigIntegerField', [], {'default': '0', 'null': 'True'}),
+ 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['users.User']", 'through': "orm['projects.Membership']", 'symmetrical': 'False', 'related_name': "'projects'"}),
+ 'modified_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '250'}),
+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['users.User']", 'related_name': "'owned_projects'"}),
+ 'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '250', 'blank': 'True'}),
+ 'tags': ('picklefield.fields.PickledObjectField', [], {'blank': 'True'}),
+ 'total_milestones': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True', 'null': 'True'}),
+ 'total_story_points': ('django.db.models.fields.FloatField', [], {'default': 'None', 'null': 'True'}),
+ 'videoconferences': ('django.db.models.fields.CharField', [], {'null': 'True', 'max_length': '250', 'blank': 'True'}),
+ 'videoconferences_salt': ('django.db.models.fields.CharField', [], {'null': 'True', 'max_length': '250', 'blank': 'True'})
+ },
+ 'projects.projecttemplate': {
+ 'Meta': {'unique_together': "(['slug', 'domain'],)", 'object_name': 'ProjectTemplate', 'ordering': "['name']"},
+ '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', [], {'to': "orm['domains.Domain']", 'null': 'True', 'default': 'None', 'blank': 'True', 'related_name': "'templates'"}),
+ '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'}),
+ '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', 'blank': 'True'}),
+ 'task_statuses': ('django_pgjson.fields.JsonField', [], {}),
+ 'us_statuses': ('django_pgjson.fields.JsonField', [], {}),
+ 'videoconferences': ('django.db.models.fields.CharField', [], {'null': 'True', 'max_length': '250', 'blank': 'True'}),
+ 'videoconferences_salt': ('django.db.models.fields.CharField', [], {'null': 'True', 'max_length': '250', 'blank': 'True'})
+ },
+ 'projects.questionstatus': {
+ 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'QuestionStatus', 'ordering': "['project', 'order', 'name']"},
+ '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', [], {'to': "orm['projects.Project']", 'related_name': "'question_status'"})
+ },
+ 'projects.severity': {
+ 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'Severity', 'ordering': "['project', 'order', 'name']"},
+ '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', [], {'to': "orm['projects.Project']", 'related_name': "'severities'"})
+ },
+ 'projects.taskstatus': {
+ 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'TaskStatus', 'ordering': "['project', 'order', 'name']"},
+ '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', [], {'to': "orm['projects.Project']", 'related_name': "'task_statuses'"})
+ },
+ 'projects.userstorystatus': {
+ 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'UserStoryStatus', 'ordering': "['project', 'order', 'name']"},
+ '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', [], {'to': "orm['projects.Project']", 'related_name': "'us_statuses'"}),
+ 'wip_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'blank': 'True', 'null': 'True'})
+ },
+ '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', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'related_name': "'roles'"}),
+ 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']", 'related_name': "'roles'"}),
+ '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', [], {'default': "'#7da64d'", 'max_length': '9', 'blank': 'True'}),
+ 'colorize_tags': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'default_language': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20', 'blank': 'True'}),
+ 'default_timezone': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20', 'blank': 'True'}),
+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True', 'related_name': "'user_set'"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'notify_changes_by_me': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'notify_level': ('django.db.models.fields.CharField', [], {'default': "'all_owned_projects'", 'max_length': '32'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'photo': ('django.db.models.fields.files.FileField', [], {'null': 'True', 'max_length': '500', 'blank': 'True'}),
+ 'token': ('django.db.models.fields.CharField', [], {'default': 'None', 'null': 'True', 'max_length': '200', 'blank': 'True'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True', 'related_name': "'user_set'"}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ }
+ }
+
+ complete_apps = ['attachments']
diff --git a/taiga/projects/attachments/migrations/__init__.py b/taiga/projects/attachments/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/taiga/projects/attachments/models.py b/taiga/projects/attachments/models.py
new file mode 100644
index 00000000..55a00d74
--- /dev/null
+++ b/taiga/projects/attachments/models.py
@@ -0,0 +1,70 @@
+# 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
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import time
+
+from django.db import models
+from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes import generic
+from django.utils.translation import ugettext_lazy as _
+from django.utils import timezone
+
+
+def get_attachment_file_path(instance, filename):
+ template = "attachment-files/{project}/{model}/{stamp}/{filename}"
+ current_timestamp = int(time.mktime(timezone.now().timetuple()))
+
+ upload_to_path = template.format(stamp=current_timestamp,
+ project=instance.project.slug,
+ model=instance.content_type.model,
+ filename=filename)
+ return upload_to_path
+
+
+class Attachment(models.Model):
+ owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
+ related_name="change_attachments",
+ verbose_name=_("owner"))
+ project = models.ForeignKey("projects.Project", null=False, blank=False,
+ related_name="attachments", verbose_name=_("project"))
+ content_type = models.ForeignKey(ContentType, null=False, blank=False,
+ verbose_name=_("content type"))
+ object_id = models.PositiveIntegerField(null=False, blank=False,
+ verbose_name=_("object id"))
+ content_object = generic.GenericForeignKey("content_type", "object_id")
+ created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
+ verbose_name=_("created date"))
+ modified_date = models.DateTimeField(auto_now=True, null=False, blank=False,
+ verbose_name=_("modified date"))
+
+ attached_file = models.FileField(max_length=500, null=True, blank=True,
+ upload_to=get_attachment_file_path,
+ verbose_name=_("attached file"))
+ is_deprecated = models.BooleanField(default=False, verbose_name=_("is deprecated"))
+ description = models.TextField(null=False, blank=True, verbose_name=_("description"))
+ order = models.IntegerField(default=0, null=False, blank=False, verbose_name=_("order"))
+
+ class Meta:
+ verbose_name = "attachment"
+ verbose_name_plural = "attachments"
+ ordering = ["project", "created_date"]
+ permissions = (
+ ("view_attachment", "Can view attachment"),
+ )
+
+ def __str__(self):
+ return "Attachment: {}".format(self.id)
diff --git a/taiga/projects/attachments/permissions.py b/taiga/projects/attachments/permissions.py
new file mode 100644
index 00000000..a28ac3be
--- /dev/null
+++ b/taiga/projects/attachments/permissions.py
@@ -0,0 +1,28 @@
+# 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
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+
+from taiga.base.permissions import BasePermission
+
+
+class AttachmentPermission(BasePermission):
+ get_permission = "view_attachment"
+ post_permission = "add_attachment"
+ put_permission = "change_attachment"
+ patch_permission = "change_attachment"
+ delete_permission = "delete_attachment"
+ safe_methods = ["HEAD", "OPTIONS"]
+ path_to_project = ["project"]
diff --git a/taiga/projects/attachments/serializers.py b/taiga/projects/attachments/serializers.py
new file mode 100644
index 00000000..eb74043d
--- /dev/null
+++ b/taiga/projects/attachments/serializers.py
@@ -0,0 +1,50 @@
+# 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
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from rest_framework import serializers
+
+from . import models
+
+from os import path
+
+
+class AttachmentSerializer(serializers.ModelSerializer):
+ name = serializers.SerializerMethodField("get_name")
+ url = serializers.SerializerMethodField("get_url")
+ size = serializers.SerializerMethodField("get_size")
+
+ class Meta:
+ model = models.Attachment
+ fields = ("id", "project", "owner", "name", "attached_file", "size", "url",
+ "description", "is_deprecated", "created_date", "modified_date",
+ "object_id", "order")
+ read_only_fields = ("owner",)
+
+ def get_name(self, obj):
+ if obj.attached_file:
+ return path.basename(obj.attached_file.path)
+ return ""
+
+ def get_url(self, obj):
+ return obj.attached_file.url if obj and obj.attached_file else ""
+
+ def get_size(self, obj):
+ if obj.attached_file:
+ try:
+ return obj.attached_file.size
+ except FileNotFoundError:
+ pass
+ return 0
diff --git a/taiga/projects/issues/admin.py b/taiga/projects/issues/admin.py
index 562fbe09..c45ad8f6 100644
--- a/taiga/projects/issues/admin.py
+++ b/taiga/projects/issues/admin.py
@@ -16,7 +16,7 @@
from django.contrib import admin
-from taiga.projects.admin import AttachmentInline
+from taiga.projects.attachments.admin import AttachmentInline
from . import models
diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py
index ffea1077..e218af61 100644
--- a/taiga/projects/issues/api.py
+++ b/taiga/projects/issues/api.py
@@ -25,9 +25,6 @@ from rest_framework import filters
from taiga.base import filters
from taiga.base import exceptions as exc
from taiga.base.decorators import list_route
-from taiga.projects.permissions import AttachmentPermission
-from taiga.projects.serializers import AttachmentSerializer
-from taiga.projects.models import Attachment
from taiga.base.api import ModelCrudViewSet
from taiga.base.api import NeighborsApiMixin
from taiga.projects.mixins.notifications import NotificationSenderMixin
@@ -141,29 +138,3 @@ class IssueViewSet(NeighborsApiMixin, NotificationSenderMixin, ModelCrudViewSet)
if obj.type and obj.type.project != obj.project:
raise exc.PermissionDenied(_("You don't have permissions for add/modify this issue."))
-class IssueAttachmentViewSet(ModelCrudViewSet):
- model = Attachment
- serializer_class = AttachmentSerializer
- permission_classes = (IsAuthenticated, AttachmentPermission)
- filter_backends = (filters.IsProjectMemberFilterBackend,)
- filter_fields = ["project", "object_id"]
-
- def get_queryset(self):
- ct = ContentType.objects.get_for_model(models.Issue)
- qs = super().get_queryset()
- qs = qs.filter(content_type=ct)
- return qs.distinct()
-
- def pre_save(self, obj):
- if not obj.id:
- obj.content_type = ContentType.objects.get_for_model(models.Issue)
- obj.owner = self.request.user
- super().pre_save(obj)
-
- def pre_conditions_on_save(self, obj):
- super().pre_conditions_on_save(obj)
-
- if (obj.project.owner != self.request.user and
- obj.project.memberships.filter(user=self.request.user).count() == 0):
- raise exc.PermissionDenied(_("You don't have permissions for add attachments "
- "to this issue"))
diff --git a/taiga/projects/issues/models.py b/taiga/projects/issues/models.py
index e79e4499..3587225c 100644
--- a/taiga/projects/issues/models.py
+++ b/taiga/projects/issues/models.py
@@ -64,7 +64,7 @@ class Issue(NeighborsMixin, WatchedMixin, BlockedMixin):
related_name="watched_issues",
verbose_name=_("watchers"))
tags = PickledObjectField(null=False, blank=True, verbose_name=_("tags"))
- attachments = generic.GenericRelation("projects.Attachment")
+ attachments = generic.GenericRelation("attachments.Attachment")
notifiable_fields = [
"subject",
diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py
index ed29c340..31c464d5 100644
--- a/taiga/projects/issues/serializers.py
+++ b/taiga/projects/issues/serializers.py
@@ -17,7 +17,7 @@
from rest_framework import serializers
from taiga.base.serializers import PickleField, NeighborsSerializerMixin
-from taiga.projects.serializers import AttachmentSerializer
+from taiga.projects.attachments.serializers import AttachmentSerializer
from taiga.projects.mixins.notifications import WatcherValidationSerializerMixin
from . import models
diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py
index 08e5113c..70faa0aa 100644
--- a/taiga/projects/management/commands/sample_data.py
+++ b/taiga/projects/management/commands/sample_data.py
@@ -30,6 +30,7 @@ from taiga.projects.userstories.models import *
from taiga.projects.tasks.models import *
from taiga.projects.issues.models import *
from taiga.projects.wiki.models import *
+from taiga.projects.attachments.models import *
import random
import datetime
diff --git a/taiga/projects/migrations/0020_drop_attachment.py b/taiga/projects/migrations/0020_drop_attachment.py
new file mode 100644
index 00000000..9dc860f3
--- /dev/null
+++ b/taiga/projects/migrations/0020_drop_attachment.py
@@ -0,0 +1,226 @@
+# -*- 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):
+ depends_on = (
+ ('attachments', '0001_create_attachment'),
+ )
+
+ def forwards(self, orm):
+ pass
+
+ def backwards(self, orm):
+ pass
+
+ 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': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'object_name': 'Permission', 'unique_together': "(('content_type', 'codename'),)"},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'object_name': 'ContentType', 'db_table': "'django_content_type'", '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': {'ordering': "('domain',)", 'object_name': 'Domain'},
+ 'alias_of': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['domains.Domain']", 'blank': 'True', 'null': 'True', 'related_name': "'+'"}),
+ 'default_language': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20', 'blank': 'True'}),
+ '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', [], {'default': 'None', 'max_length': '60', 'null': 'True'})
+ },
+ 'projects.issuestatus': {
+ 'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'IssueStatus', 'unique_together': "(('project', 'name'),)"},
+ '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', [], {'to': "orm['projects.Project']", 'related_name': "'issue_statuses'"})
+ },
+ 'projects.issuetype': {
+ 'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'IssueType', 'unique_together': "(('project', 'name'),)"},
+ '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', [], {'to': "orm['projects.Project']", 'related_name': "'issue_types'"})
+ },
+ 'projects.membership': {
+ 'Meta': {'ordering': "['project', 'role']", 'object_name': 'Membership', 'unique_together': "(('user', 'project', 'email'),)"},
+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now_add': 'True', 'blank': 'True'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '255', 'blank': 'True', 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']", 'related_name': "'memberships'"}),
+ 'role': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['users.Role']", 'related_name': "'memberships'"}),
+ 'token': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '60', 'blank': 'True', 'null': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['users.User']", 'blank': 'True', 'null': 'True', 'related_name': "'memberships'"})
+ },
+ 'projects.points': {
+ 'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'Points', 'unique_together': "(('project', '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', [], {'to': "orm['projects.Project']", 'related_name': "'points'"}),
+ 'value': ('django.db.models.fields.FloatField', [], {'default': 'None', 'blank': 'True', 'null': 'True'})
+ },
+ 'projects.priority': {
+ 'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'Priority', 'unique_together': "(('project', 'name'),)"},
+ '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', [], {'to': "orm['projects.Project']", 'related_name': "'priorities'"})
+ },
+ 'projects.project': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'Project'},
+ 'created_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'creation_template': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['projects.ProjectTemplate']", 'blank': 'True', 'null': 'True', 'related_name': "'projects'"}),
+ 'default_issue_status': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['projects.IssueStatus']", 'null': 'True', 'related_name': "'+'", 'unique': 'True', 'blank': 'True', 'on_delete': 'models.SET_NULL'}),
+ 'default_issue_type': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['projects.IssueType']", 'null': 'True', 'related_name': "'+'", 'unique': 'True', 'blank': 'True', 'on_delete': 'models.SET_NULL'}),
+ 'default_points': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['projects.Points']", 'null': 'True', 'related_name': "'+'", 'unique': 'True', 'blank': 'True', 'on_delete': 'models.SET_NULL'}),
+ 'default_priority': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['projects.Priority']", 'null': 'True', 'related_name': "'+'", 'unique': 'True', 'blank': 'True', 'on_delete': 'models.SET_NULL'}),
+ 'default_question_status': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['projects.QuestionStatus']", 'null': 'True', 'related_name': "'+'", 'unique': 'True', 'blank': 'True', 'on_delete': 'models.SET_NULL'}),
+ 'default_severity': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['projects.Severity']", 'null': 'True', 'related_name': "'+'", 'unique': 'True', 'blank': 'True', 'on_delete': 'models.SET_NULL'}),
+ 'default_task_status': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['projects.TaskStatus']", 'null': 'True', 'related_name': "'+'", 'unique': 'True', 'blank': 'True', 'on_delete': 'models.SET_NULL'}),
+ 'default_us_status': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['projects.UserStoryStatus']", 'null': 'True', 'related_name': "'+'", 'unique': 'True', 'blank': 'True', 'on_delete': 'models.SET_NULL'}),
+ 'description': ('django.db.models.fields.TextField', [], {}),
+ 'domain': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['domains.Domain']", 'blank': 'True', 'null': 'True', 'related_name': "'projects'"}),
+ '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', [], {'default': '0', 'null': 'True'}),
+ 'last_task_ref': ('django.db.models.fields.BigIntegerField', [], {'default': '0', 'null': 'True'}),
+ 'last_us_ref': ('django.db.models.fields.BigIntegerField', [], {'default': '0', 'null': 'True'}),
+ 'members': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['users.User']", 'through': "orm['projects.Membership']", 'related_name': "'projects'"}),
+ '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', [], {'to': "orm['users.User']", 'related_name': "'owned_projects'"}),
+ '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', [], {'default': '0', 'blank': 'True', 'null': 'True'}),
+ 'total_story_points': ('django.db.models.fields.FloatField', [], {'default': 'None', 'null': 'True'}),
+ 'videoconferences': ('django.db.models.fields.CharField', [], {'max_length': '250', 'blank': 'True', 'null': 'True'}),
+ 'videoconferences_salt': ('django.db.models.fields.CharField', [], {'max_length': '250', 'blank': 'True', 'null': 'True'})
+ },
+ 'projects.projecttemplate': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'ProjectTemplate', 'unique_together': "(['slug', 'domain'],)"},
+ 'created_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': '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', [], {'default': 'None', 'to': "orm['domains.Domain']", 'blank': 'True', 'null': 'True', 'related_name': "'templates'"}),
+ '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'}),
+ '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', 'blank': 'True'}),
+ 'task_statuses': ('django_pgjson.fields.JsonField', [], {}),
+ 'us_statuses': ('django_pgjson.fields.JsonField', [], {}),
+ 'videoconferences': ('django.db.models.fields.CharField', [], {'max_length': '250', 'blank': 'True', 'null': 'True'}),
+ 'videoconferences_salt': ('django.db.models.fields.CharField', [], {'max_length': '250', 'blank': 'True', 'null': 'True'})
+ },
+ 'projects.questionstatus': {
+ 'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'QuestionStatus', 'unique_together': "(('project', 'name'),)"},
+ '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', [], {'to': "orm['projects.Project']", 'related_name': "'question_status'"})
+ },
+ 'projects.severity': {
+ 'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'Severity', 'unique_together': "(('project', 'name'),)"},
+ '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', [], {'to': "orm['projects.Project']", 'related_name': "'severities'"})
+ },
+ 'projects.taskstatus': {
+ 'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'TaskStatus', 'unique_together': "(('project', 'name'),)"},
+ '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', [], {'to': "orm['projects.Project']", 'related_name': "'task_statuses'"})
+ },
+ 'projects.userstorystatus': {
+ 'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'UserStoryStatus', 'unique_together': "(('project', 'name'),)"},
+ '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', [], {'to': "orm['projects.Project']", 'related_name': "'us_statuses'"}),
+ 'wip_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'blank': 'True', 'null': 'True'})
+ },
+ 'users.role': {
+ 'Meta': {'ordering': "['order', 'slug']", 'object_name': 'Role', 'unique_together': "(('slug', 'project'),)"},
+ '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', 'to': "orm['auth.Permission']", 'related_name': "'roles'"}),
+ 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']", 'related_name': "'roles'"}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '250', 'blank': 'True'})
+ },
+ 'users.user': {
+ 'Meta': {'ordering': "['username']", 'object_name': 'User'},
+ 'color': ('django.db.models.fields.CharField', [], {'default': "'#910f4d'", 'max_length': '9', 'blank': 'True'}),
+ 'colorize_tags': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'default_language': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20', 'blank': 'True'}),
+ 'default_timezone': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20', 'blank': 'True'}),
+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['auth.Group']", 'blank': 'True', 'related_name': "'user_set'"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'notify_changes_by_me': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'notify_level': ('django.db.models.fields.CharField', [], {'default': "'all_owned_projects'", 'max_length': '32'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'photo': ('django.db.models.fields.files.FileField', [], {'max_length': '500', 'blank': 'True', 'null': 'True'}),
+ 'token': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '200', 'blank': 'True', 'null': 'True'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['auth.Permission']", 'blank': 'True', 'related_name': "'user_set'"}),
+ 'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'})
+ }
+ }
+
+ complete_apps = ['projects']
diff --git a/taiga/projects/models.py b/taiga/projects/models.py
index 89bd3585..0570b65b 100644
--- a/taiga/projects/models.py
+++ b/taiga/projects/models.py
@@ -15,16 +15,12 @@
# along with this program. If not, see .
import itertools
-import time
-import reversion
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models.loading import get_model
from django.conf import settings
from django.dispatch import receiver
-from django.contrib.contenttypes.models import ContentType
-from django.contrib.contenttypes import generic
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.utils.translation import ugettext_lazy as _
@@ -279,52 +275,6 @@ class Project(ProjectDefaults, models.Model):
return self._get_user_stories_points(self.user_stories.filter(milestone__isnull=False))
-def get_attachment_file_path(instance, filename):
- template = "attachment-files/{project}/{model}/{stamp}/{filename}"
- current_timestamp = int(time.mktime(timezone.now().timetuple()))
-
- upload_to_path = template.format(stamp=current_timestamp,
- project=instance.project.slug,
- model=instance.content_type.model,
- filename=filename)
- return upload_to_path
-
-
-class Attachment(models.Model):
- owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
- related_name="change_attachments",
- verbose_name=_("owner"))
- project = models.ForeignKey("Project", null=False, blank=False,
- related_name="attachments", verbose_name=_("project"))
- content_type = models.ForeignKey(ContentType, null=False, blank=False,
- verbose_name=_("content type"))
- object_id = models.PositiveIntegerField(null=False, blank=False,
- verbose_name=_("object id"))
- content_object = generic.GenericForeignKey("content_type", "object_id")
- created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
- verbose_name=_("created date"))
- modified_date = models.DateTimeField(auto_now=True, null=False, blank=False,
- verbose_name=_("modified date"))
-
- attached_file = models.FileField(max_length=500, null=True, blank=True,
- upload_to=get_attachment_file_path,
- verbose_name=_("attached file"))
- is_deprecated = models.BooleanField(default=False, verbose_name=_("is deprecated"))
- description = models.TextField(null=False, blank=True, verbose_name=_("description"))
- order = models.IntegerField(default=0, null=False, blank=False, verbose_name=_("order"))
-
- class Meta:
- verbose_name = "attachment"
- verbose_name_plural = "attachments"
- ordering = ["project", "created_date"]
- permissions = (
- ("view_attachment", "Can view attachment"),
- )
-
- def __str__(self):
- return "Attachment: {}".format(self.id)
-
-
# User Stories common Models
class UserStoryStatus(models.Model):
name = models.CharField(max_length=255, null=False, blank=False,
diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py
index dfbb5735..cde08d85 100644
--- a/taiga/projects/permissions.py
+++ b/taiga/projects/permissions.py
@@ -61,16 +61,6 @@ class MembershipPermission(BasePermission):
path_to_project = ["project"]
-class AttachmentPermission(BasePermission):
- get_permission = "view_attachment"
- post_permission = "add_attachment"
- put_permission = "change_attachment"
- patch_permission = "change_attachment"
- delete_permission = "delete_attachment"
- safe_methods = ["HEAD", "OPTIONS"]
- path_to_project = ["project"]
-
-
# User Stories
class PointsPermission(BasePermission):
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index 57e6d097..57e8ec57 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -24,35 +24,6 @@ from taiga.users.models import Role
from . import models
-class AttachmentSerializer(serializers.ModelSerializer):
- name = serializers.SerializerMethodField("get_name")
- url = serializers.SerializerMethodField("get_url")
- size = serializers.SerializerMethodField("get_size")
-
- class Meta:
- model = models.Attachment
- fields = ("id", "project", "owner", "name", "attached_file", "size", "url",
- "description", "is_deprecated", "created_date", "modified_date",
- "object_id", "order")
- read_only_fields = ("owner",)
-
- def get_name(self, obj):
- if obj.attached_file:
- return path.basename(obj.attached_file.path)
- return ""
-
- def get_url(self, obj):
- return obj.attached_file.url if obj and obj.attached_file else ""
-
- def get_size(self, obj):
- if obj.attached_file:
- try:
- return obj.attached_file.size
- except FileNotFoundError:
- pass
- return 0
-
-
# User Stories common serializers
class PointsSerializer(serializers.ModelSerializer):
diff --git a/taiga/projects/tasks/admin.py b/taiga/projects/tasks/admin.py
index 5fb79345..578e4dea 100644
--- a/taiga/projects/tasks/admin.py
+++ b/taiga/projects/tasks/admin.py
@@ -15,9 +15,8 @@
# along with this program. If not, see .
from django.contrib import admin
-import reversion
-from taiga.projects.admin import AttachmentInline
+from taiga.projects.attachments.admin import AttachmentInline
from . import models
diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py
index 752ac9c5..21933636 100644
--- a/taiga/projects/tasks/api.py
+++ b/taiga/projects/tasks/api.py
@@ -15,7 +15,6 @@
# along with this program. If not, see .
from django.utils.translation import ugettext_lazy as _
-from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404
from rest_framework.permissions import IsAuthenticated
@@ -27,41 +26,13 @@ from taiga.base import exceptions as exc
from taiga.base.decorators import list_route
from taiga.base.permissions import has_project_perm
from taiga.base.api import ModelCrudViewSet
-from taiga.projects.permissions import AttachmentPermission
-from taiga.projects.serializers import AttachmentSerializer
-from taiga.projects.models import Attachment, Project
from taiga.projects.mixins.notifications import NotificationSenderMixin
+from taiga.projects.models import Project
from taiga.projects.userstories.models import UserStory
from . import models
from . import permissions
from . import serializers
-class TaskAttachmentViewSet(ModelCrudViewSet):
- model = Attachment
- serializer_class = AttachmentSerializer
- permission_classes = (IsAuthenticated, AttachmentPermission,)
- filter_backends = (filters.IsProjectMemberFilterBackend,)
- filter_fields = ["project", "object_id"]
-
- def get_queryset(self):
- ct = ContentType.objects.get_for_model(models.Task)
- qs = super().get_queryset()
- qs = qs.filter(content_type=ct)
- return qs.distinct()
-
- def pre_save(self, obj):
- if not obj.id:
- obj.content_type = ContentType.objects.get_for_model(models.Task)
- obj.owner = self.request.user
- super().pre_save(obj)
-
- def pre_conditions_on_save(self, obj):
- super().pre_conditions_on_save(obj)
-
- if (obj.project.owner != self.request.user and
- obj.project.memberships.filter(user=self.request.user).count() == 0):
- raise exc.PermissionDenied(_("You don't have permissions for add "
- "attachments to this task."))
from . import services
diff --git a/taiga/projects/tasks/models.py b/taiga/projects/tasks/models.py
index b6e3566f..3503253d 100644
--- a/taiga/projects/tasks/models.py
+++ b/taiga/projects/tasks/models.py
@@ -59,9 +59,9 @@ class Task(WatchedMixin, BlockedMixin):
default=None, related_name="tasks_assigned_to_me",
verbose_name=_("assigned to"))
watchers = models.ManyToManyField(settings.AUTH_USER_MODEL, null=True, blank=True,
- related_name="watched_tasks", verbose_name=_("watchers"))
+ related_name="watched_tasks", verbose_name=_("watchers"))
tags = PickledObjectField(null=False, blank=True, verbose_name=_("tags"))
- attachments = generic.GenericRelation("projects.Attachment")
+ attachments = generic.GenericRelation("attachments.Attachment")
is_iocaine = models.BooleanField(default=False, null=False, blank=True,
verbose_name=_("is iocaine"))
diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py
index ed4857fe..fabee54a 100644
--- a/taiga/projects/tasks/serializers.py
+++ b/taiga/projects/tasks/serializers.py
@@ -20,8 +20,6 @@ from taiga.base.serializers import PickleField
from . import models
-import reversion
-
class TaskSerializer(serializers.ModelSerializer):
tags = PickleField(required=False, default=[])
diff --git a/taiga/projects/userstories/admin.py b/taiga/projects/userstories/admin.py
index 3e47aafa..1a6bbf3a 100644
--- a/taiga/projects/userstories/admin.py
+++ b/taiga/projects/userstories/admin.py
@@ -16,7 +16,7 @@
from django.contrib import admin
-from taiga.projects.admin import AttachmentInline
+from taiga.projects.attachments.admin import AttachmentInline
from . import models
diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py
index 4392edf0..0c06cd2d 100644
--- a/taiga/projects/userstories/api.py
+++ b/taiga/projects/userstories/api.py
@@ -15,8 +15,7 @@
# along with this program. If not, see .
from django.db import transaction
-from django.utils.translation import ugettext_lazy as _
-from django.contrib.contenttypes.models import ContentType
+from django.utils.translation import ugettext as _
from django.shortcuts import get_object_or_404
from rest_framework.permissions import IsAuthenticated
@@ -28,12 +27,10 @@ from taiga.base import exceptions as exc
from taiga.base.decorators import list_route
from taiga.base.decorators import action
from taiga.base.permissions import has_project_perm
-from taiga.projects.permissions import AttachmentPermission
-from taiga.projects.serializers import AttachmentSerializer
-from taiga.projects.models import Attachment, Project
from taiga.base.api import ModelCrudViewSet
from taiga.base.api import NeighborsApiMixin
from taiga.projects.mixins.notifications import NotificationSenderMixin
+from taiga.projects.models import Project
from taiga.projects.history.services import take_snapshot
from . import models
@@ -42,35 +39,6 @@ from . import serializers
from . import services
-class UserStoryAttachmentViewSet(ModelCrudViewSet):
- model = Attachment
- serializer_class = AttachmentSerializer
- permission_classes = (IsAuthenticated, AttachmentPermission,)
- filter_backends = (filters.IsProjectMemberFilterBackend,)
- filter_fields = ["project", "object_id"]
-
- def get_queryset(self):
- ct = ContentType.objects.get_for_model(models.UserStory)
- qs = super().get_queryset()
- qs = qs.filter(content_type=ct)
- return qs.distinct()
-
- def pre_save(self, obj):
- if not obj.id:
- obj.content_type = ContentType.objects.get_for_model(models.UserStory)
- obj.owner = self.request.user
-
- super().pre_save(obj)
-
- def pre_conditions_on_save(self, obj):
- super().pre_conditions_on_save(obj)
-
- if (obj.project.owner != self.request.user and
- obj.project.memberships.filter(user=self.request.user).count() == 0):
- raise exc.PermissionDenied(_("You don't have permissions for "
- "add attachments to this user story"))
-
-
class UserStoryViewSet(NeighborsApiMixin, NotificationSenderMixin, ModelCrudViewSet):
model = models.UserStory
serializer_class = serializers.UserStoryNeighborsSerializer
@@ -169,11 +137,3 @@ class UserStoryViewSet(NeighborsApiMixin, NotificationSenderMixin, ModelCrudView
if obj.status and obj.status.project != obj.project:
raise exc.PermissionDenied(_("You don't have permissions for add/modify this user story"))
-
- def post_save(self, obj, created=False):
- with reversion.create_revision():
- if "comment" in self.request.DATA:
- # Update the comment in the last version
- reversion.set_comment(self.request.DATA['comment'])
-
- super().post_save(obj, created)
diff --git a/taiga/projects/userstories/models.py b/taiga/projects/userstories/models.py
index 3d3b8287..9e968c75 100644
--- a/taiga/projects/userstories/models.py
+++ b/taiga/projects/userstories/models.py
@@ -24,8 +24,8 @@ from picklefield.fields import PickledObjectField
from taiga.base.models import NeighborsMixin
from taiga.base.utils.slug import ref_uniquely
-from taiga.projects.mixins.blocked.models import BlockedMixin
from taiga.projects.notifications.models import WatchedMixin
+from taiga.projects.mixins.blocked import BlockedMixin
class RolePoints(models.Model):
@@ -92,7 +92,7 @@ class UserStory(NeighborsMixin, WatchedMixin, BlockedMixin, models.Model):
verbose_name=_("is team requirement"))
tags = PickledObjectField(null=False, blank=True,
verbose_name=_("tags"))
- attachments = generic.GenericRelation("projects.Attachment")
+ attachments = generic.GenericRelation("attachments.Attachment")
generated_from_issue = models.ForeignKey("issues.Issue", null=True, blank=True,
related_name="generated_user_stories",
verbose_name=_("generated from issue"))
diff --git a/taiga/projects/wiki/models.py b/taiga/projects/wiki/models.py
index e521f13d..21a09b98 100644
--- a/taiga/projects/wiki/models.py
+++ b/taiga/projects/wiki/models.py
@@ -36,7 +36,7 @@ class WikiPage(models.Model):
verbose_name=_("created date"))
modified_date = models.DateTimeField(auto_now=True, null=False, blank=False,
verbose_name=_("modified date"))
- attachments = generic.GenericRelation("projects.Attachment")
+ attachments = generic.GenericRelation("attachments.Attachment")
class Meta:
verbose_name = "wiki page"
diff --git a/taiga/routers.py b/taiga/routers.py
index d41197ea..f1ace3e2 100644
--- a/taiga/routers.py
+++ b/taiga/routers.py
@@ -74,6 +74,16 @@ router.register(r"priorities", PriorityViewSet, base_name="priorities")
router.register(r"severities",SeverityViewSet , base_name="severities")
+# Attachments
+from taiga.projects.attachments.api import UserStoryAttachmentViewSet
+from taiga.projects.attachments.api import IssueAttachmentViewSet
+from taiga.projects.attachments.api import TaskAttachmentViewSet
+from taiga.projects.attachments.api import WikiAttachmentViewSet
+
+router.register(r"userstories/attachments", UserStoryAttachmentViewSet, base_name="userstory-attachments")
+router.register(r"tasks/attachments", TaskAttachmentViewSet, base_name="task-attachments")
+router.register(r"issues/attachments", IssueAttachmentViewSet, base_name="issue-attachments")
+router.register(r"wiki/attachments", WikiAttachmentViewSet, base_name="wiki-attachments")
# History & Components
@@ -91,20 +101,12 @@ router.register(r"history/wiki", WikiHistory, base_name="wiki-history")
# Project components
from taiga.projects.milestones.api import MilestoneViewSet
from taiga.projects.userstories.api import UserStoryViewSet
-from taiga.projects.userstories.api import UserStoryAttachmentViewSet
from taiga.projects.tasks.api import TaskViewSet
-from taiga.projects.tasks.api import TaskAttachmentViewSet
from taiga.projects.issues.api import IssueViewSet
-from taiga.projects.issues.api import IssueAttachmentViewSet
from taiga.projects.wiki.api import WikiViewSet
-from taiga.projects.wiki.api import WikiAttachmentViewSet
router.register(r"milestones", MilestoneViewSet, base_name="milestones")
router.register(r"userstories", UserStoryViewSet, base_name="userstories")
-router.register(r"userstory-attachments", UserStoryAttachmentViewSet, base_name="userstory-attachments")
router.register(r"tasks", TaskViewSet, base_name="tasks")
-router.register(r"task-attachments", TaskAttachmentViewSet, base_name="task-attachments")
router.register(r"issues", IssueViewSet, base_name="issues")
-router.register(r"issue-attachments", IssueAttachmentViewSet, base_name="issue-attachments")
router.register(r"wiki", WikiViewSet, base_name="wiki")
-router.register(r"wiki-attachments", WikiAttachmentViewSet, base_name="wiki-attachments")