From e8a2b9fbb3fd4a35838b2b9bb2f021281e3fda80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 9 May 2014 12:09:49 +0200 Subject: [PATCH] Refactor attachments --- taiga/projects/admin.py | 16 -- taiga/projects/attachments/__init__.py | 0 taiga/projects/attachments/admin.py | 35 +++ taiga/projects/attachments/api.py | 110 ++++++++ .../migrations/0001_create_attachment.py | 246 ++++++++++++++++++ .../attachments/migrations/__init__.py | 0 taiga/projects/attachments/models.py | 70 +++++ taiga/projects/attachments/permissions.py | 28 ++ taiga/projects/attachments/serializers.py | 50 ++++ taiga/projects/issues/admin.py | 2 +- taiga/projects/issues/api.py | 29 --- taiga/projects/issues/models.py | 2 +- taiga/projects/issues/serializers.py | 2 +- .../management/commands/sample_data.py | 1 + .../migrations/0020_drop_attachment.py | 226 ++++++++++++++++ taiga/projects/models.py | 50 ---- taiga/projects/permissions.py | 10 - taiga/projects/serializers.py | 29 --- taiga/projects/tasks/admin.py | 3 +- taiga/projects/tasks/api.py | 31 +-- taiga/projects/tasks/models.py | 4 +- taiga/projects/tasks/serializers.py | 2 - taiga/projects/userstories/admin.py | 2 +- taiga/projects/userstories/api.py | 44 +--- taiga/projects/userstories/models.py | 4 +- taiga/projects/wiki/models.py | 2 +- taiga/routers.py | 18 +- 27 files changed, 789 insertions(+), 227 deletions(-) create mode 100644 taiga/projects/attachments/__init__.py create mode 100644 taiga/projects/attachments/admin.py create mode 100644 taiga/projects/attachments/api.py create mode 100644 taiga/projects/attachments/migrations/0001_create_attachment.py create mode 100644 taiga/projects/attachments/migrations/__init__.py create mode 100644 taiga/projects/attachments/models.py create mode 100644 taiga/projects/attachments/permissions.py create mode 100644 taiga/projects/attachments/serializers.py create mode 100644 taiga/projects/migrations/0020_drop_attachment.py 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")