diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 6057c30f..0a4189b7 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -596,7 +596,7 @@ class UserStoryDueDateViewSet(BlockedByProjectMixin, ModelCrudViewSet): def create(self, request, *args, **kwargs): project_id = request.DATA.get("project", 0) - with advisory_lock("user-story-duedate-creation-{}".format(project_id)): + with advisory_lock("user-story-due-date-creation-{}".format(project_id)): return super().create(request, *args, **kwargs) @@ -622,6 +622,21 @@ class TaskStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, return super().create(request, *args, **kwargs) +class TaskDueDateViewSet(BlockedByProjectMixin, ModelCrudViewSet): + + model = models.TaskDueDate + serializer_class = serializers.TaskDueDateSerializer + validator_class = validators.TaskDueDateValidator + permission_classes = (permissions.TaskDueDatePermission,) + filter_backends = (filters.CanViewProjectFilterBackend,) + filter_fields = ('project',) + + def create(self, request, *args, **kwargs): + project_id = request.DATA.get("project", 0) + with advisory_lock("task-due-date-creation-{}".format(project_id)): + return super().create(request, *args, **kwargs) + + class SeverityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, ModelCrudViewSet, BulkUpdateOrderMixin): @@ -707,6 +722,21 @@ class IssueStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, return super().create(request, *args, **kwargs) +class IssueDueDateViewSet(BlockedByProjectMixin, ModelCrudViewSet): + + model = models.IssueDueDate + serializer_class = serializers.IssueDueDateSerializer + validator_class = validators.IssueDueDateValidator + permission_classes = (permissions.IssueDueDatePermission,) + filter_backends = (filters.CanViewProjectFilterBackend,) + filter_fields = ('project',) + + def create(self, request, *args, **kwargs): + project_id = request.DATA.get("project", 0) + with advisory_lock("issue-due-date-creation-{}".format(project_id)): + return super().create(request, *args, **kwargs) + + ###################################################### ## Project Template ###################################################### diff --git a/taiga/projects/migrations/0061_auto_20180606_1034.py b/taiga/projects/migrations/0061_auto_20180606_1034.py new file mode 100644 index 00000000..5ed52004 --- /dev/null +++ b/taiga/projects/migrations/0061_auto_20180606_1034.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.2 on 2018-06-06 10:34 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0060_auto_20180605_1042'), + ] + + operations = [ + migrations.CreateModel( + name='IssueDueDate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='name')), + ('slug', models.SlugField(blank=True, max_length=255, verbose_name='slug')), + ('order', models.IntegerField(default=10, verbose_name='order')), + ('by_default', models.BooleanField(default=False, verbose_name='by default')), + ('color', models.CharField(default='#999999', max_length=20, verbose_name='color')), + ('days_to_due', models.IntegerField(blank=True, default=None, null=True, verbose_name='days to due')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_duedates', to='projects.Project', verbose_name='project')), + ], + options={ + 'verbose_name': 'issue due date', + 'verbose_name_plural': 'issue due dates', + 'ordering': ['project', 'order', 'name'], + }, + ), + migrations.CreateModel( + name='TaskDueDate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='name')), + ('slug', models.SlugField(blank=True, max_length=255, verbose_name='slug')), + ('order', models.IntegerField(default=10, verbose_name='order')), + ('by_default', models.BooleanField(default=False, verbose_name='by default')), + ('color', models.CharField(default='#999999', max_length=20, verbose_name='color')), + ('days_to_due', models.IntegerField(blank=True, default=None, null=True, verbose_name='days to due')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='task_duedates', to='projects.Project', verbose_name='project')), + ], + options={ + 'verbose_name': 'task due date', + 'verbose_name_plural': 'task due dates', + 'ordering': ['project', 'order', 'name'], + }, + ), + migrations.AlterUniqueTogether( + name='taskduedate', + unique_together=set([('project', 'name'), ('project', 'slug')]), + ), + migrations.AlterUniqueTogether( + name='issueduedate', + unique_together=set([('project', 'name'), ('project', 'slug')]), + ), + ] diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 6fd1887d..afd67c7c 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -677,6 +677,40 @@ class TaskStatus(models.Model): return super().save(*args, **kwargs) +class TaskDueDate(models.Model): + name = models.CharField(max_length=255, null=False, blank=False, + verbose_name=_("name")) + slug = models.SlugField(max_length=255, null=False, blank=True, + verbose_name=_("slug")) + order = models.IntegerField(default=10, null=False, blank=False, + verbose_name=_("order")) + by_default = models.BooleanField(default=False, null=False, blank=True, + verbose_name=_("by default")) + color = models.CharField(max_length=20, null=False, blank=False, default="#999999", + verbose_name=_("color")) + days_to_due = models.IntegerField(null=True, blank=True, default=None, + verbose_name=_("days to due")) + project = models.ForeignKey("Project", null=False, blank=False, + related_name="task_duedates", verbose_name=_("project")) + + class Meta: + verbose_name = "task due date" + verbose_name_plural = "task due dates" + ordering = ["project", "order", "name"] + unique_together = (("project", "name"), ("project", "slug")) + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + qs = self.project.task_duedates + if self.id: + qs = qs.exclude(id=self.id) + + self.slug = slugify_uniquely_for_queryset(self.name, qs) + return super().save(*args, **kwargs) + + # Issue common Models class Priority(models.Model): @@ -771,6 +805,40 @@ class IssueType(models.Model): return self.name +class IssueDueDate(models.Model): + name = models.CharField(max_length=255, null=False, blank=False, + verbose_name=_("name")) + slug = models.SlugField(max_length=255, null=False, blank=True, + verbose_name=_("slug")) + order = models.IntegerField(default=10, null=False, blank=False, + verbose_name=_("order")) + by_default = models.BooleanField(default=False, null=False, blank=True, + verbose_name=_("by default")) + color = models.CharField(max_length=20, null=False, blank=False, default="#999999", + verbose_name=_("color")) + days_to_due = models.IntegerField(null=True, blank=True, default=None, + verbose_name=_("days to due")) + project = models.ForeignKey("Project", null=False, blank=False, + related_name="issue_duedates", verbose_name=_("project")) + + class Meta: + verbose_name = "issue due date" + verbose_name_plural = "issue due dates" + ordering = ["project", "order", "name"] + unique_together = (("project", "name"), ("project", "slug")) + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + qs = self.project.issue_duedates + if self.id: + qs = qs.exclude(id=self.id) + + self.slug = slugify_uniquely_for_queryset(self.name, qs) + return super().save(*args, **kwargs) + + class ProjectTemplate(TaggedMixin, TagsColorsMixin, models.Model): name = models.CharField(max_length=250, null=False, blank=False, verbose_name=_("name")) diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index da5866f1..3e1452de 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -167,6 +167,15 @@ class TaskStatusPermission(TaigaResourcePermission): bulk_update_order_perms = IsProjectAdmin() +class TaskDueDatePermission(TaigaResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + create_perms = IsProjectAdmin() + update_perms = IsProjectAdmin() + partial_update_perms = IsProjectAdmin() + destroy_perms = IsProjectAdmin() + list_perms = AllowAny() + bulk_update_order_perms = IsProjectAdmin() + # Issues class SeverityPermission(TaigaResourcePermission): @@ -209,6 +218,16 @@ class IssueTypePermission(TaigaResourcePermission): bulk_update_order_perms = IsProjectAdmin() +class IssueDueDatePermission(TaigaResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + create_perms = IsProjectAdmin() + update_perms = IsProjectAdmin() + partial_update_perms = IsProjectAdmin() + destroy_perms = IsProjectAdmin() + list_perms = AllowAny() + bulk_update_order_perms = IsProjectAdmin() + + # Project Templates class ProjectTemplatePermission(TaigaResourcePermission): diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index aeb9e5e3..64d36fc3 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -67,7 +67,7 @@ class PointsSerializer(serializers.LightSerializer): project = Field(attr="project_id") -class UserStoryDueDateSerializer(serializers.LightSerializer): +class BaseDueDateSerializer(serializers.LightSerializer): id = Field() name = I18NField() slug = Field() @@ -78,6 +78,10 @@ class UserStoryDueDateSerializer(serializers.LightSerializer): project = Field(attr="project_id") +class UserStoryDueDateSerializer(BaseDueDateSerializer): + pass + + class TaskStatusSerializer(serializers.LightSerializer): id = Field() name = I18NField() @@ -88,6 +92,10 @@ class TaskStatusSerializer(serializers.LightSerializer): project = Field(attr="project_id") +class TaskDueDateSerializer(BaseDueDateSerializer): + pass + + class SeveritySerializer(serializers.LightSerializer): id = Field() name = I18NField() @@ -122,6 +130,10 @@ class IssueTypeSerializer(serializers.LightSerializer): project = Field(attr="project_id") +class IssueDueDateSerializer(BaseDueDateSerializer): + pass + + ###################################################### # Members ###################################################### diff --git a/taiga/projects/validators.py b/taiga/projects/validators.py index cc49a881..87035464 100644 --- a/taiga/projects/validators.py +++ b/taiga/projects/validators.py @@ -94,6 +94,11 @@ class TaskStatusValidator(DuplicatedNameInProjectValidator, validators.ModelVali model = models.TaskStatus +class TaskDueDateValidator(DuplicatedNameInProjectValidator, validators.ModelValidator): + class Meta: + model = models.TaskDueDate + + class SeverityValidator(DuplicatedNameInProjectValidator, validators.ModelValidator): class Meta: model = models.Severity @@ -114,6 +119,11 @@ class IssueTypeValidator(DuplicatedNameInProjectValidator, validators.ModelValid model = models.IssueType +class IssueDueDateValidator(DuplicatedNameInProjectValidator, validators.ModelValidator): + class Meta: + model = models.IssueDueDate + + ###################################################### # Members ###################################################### diff --git a/taiga/routers.py b/taiga/routers.py index d85fe6c2..b521885f 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -60,8 +60,10 @@ from taiga.projects.api import UserStoryStatusViewSet from taiga.projects.api import PointsViewSet from taiga.projects.api import UserStoryDueDateViewSet from taiga.projects.api import TaskStatusViewSet +from taiga.projects.api import TaskDueDateViewSet from taiga.projects.api import IssueStatusViewSet from taiga.projects.api import IssueTypeViewSet +from taiga.projects.api import IssueDueDateViewSet from taiga.projects.api import PriorityViewSet from taiga.projects.api import SeverityViewSet from taiga.projects.api import ProjectTemplateViewSet @@ -77,8 +79,10 @@ router.register(r"userstory-statuses", UserStoryStatusViewSet, base_name="userst router.register(r"points", PointsViewSet, base_name="points") router.register(r"userstory-due-dates", UserStoryDueDateViewSet, base_name="userstory-due-dates") router.register(r"task-statuses", TaskStatusViewSet, base_name="task-statuses") +router.register(r"task-due-dates", TaskDueDateViewSet, base_name="task-due-dates") router.register(r"issue-statuses", IssueStatusViewSet, base_name="issue-statuses") router.register(r"issue-types", IssueTypeViewSet, base_name="issue-types") +router.register(r"issue-due-dates", IssueDueDateViewSet, base_name="issue-due-dates") router.register(r"priorities", PriorityViewSet, base_name="priorities") router.register(r"severities",SeverityViewSet , base_name="severities")