diff --git a/taiga/projects/api.py b/taiga/projects/api.py index a8349551..6057c30f 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -585,6 +585,21 @@ class PointsViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, return super().create(request, *args, **kwargs) +class UserStoryDueDateViewSet(BlockedByProjectMixin, ModelCrudViewSet): + + model = models.UserStoryDueDate + serializer_class = serializers.UserStoryDueDateSerializer + validator_class = validators.UserStoryDueDateValidator + permission_classes = (permissions.UserStoryDueDatePermission,) + filter_backends = (filters.CanViewProjectFilterBackend,) + filter_fields = ('project',) + + def create(self, request, *args, **kwargs): + project_id = request.DATA.get("project", 0) + with advisory_lock("user-story-duedate-creation-{}".format(project_id)): + return super().create(request, *args, **kwargs) + + class TaskStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, ModelCrudViewSet, BulkUpdateOrderMixin): diff --git a/taiga/projects/migrations/0060_auto_20180605_1042.py b/taiga/projects/migrations/0060_auto_20180605_1042.py new file mode 100644 index 00000000..134f15bd --- /dev/null +++ b/taiga/projects/migrations/0060_auto_20180605_1042.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.2 on 2018-06-05 10:42 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0059_auto_20170116_1633'), + ] + + operations = [ + migrations.CreateModel( + name='UserStoryDueDate', + 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='us_duedates', to='projects.Project', verbose_name='project')), + ], + options={ + 'verbose_name': 'user story due date', + 'verbose_name_plural': 'user story due dates', + 'ordering': ['project', 'order', 'name'], + }, + ), + migrations.AddField( + model_name='project', + name='default_us_duedate', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='projects.UserStoryDueDate', verbose_name='default US duedate'), + ), + migrations.AlterUniqueTogether( + name='userstoryduedate', + unique_together=set([('project', 'slug'), ('project', 'name')]), + ), + ] diff --git a/taiga/projects/models.py b/taiga/projects/models.py index f28072ed..6fd1887d 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -120,6 +120,11 @@ class ProjectDefaults(models.Model): default_points = models.OneToOneField("projects.Points", on_delete=models.SET_NULL, related_name="+", null=True, blank=True, verbose_name=_("default points")) + default_us_duedate = models.OneToOneField("projects.UserStoryDueDate", + on_delete=models.SET_NULL, + related_name="+", + null=True, blank=True, + verbose_name=_("default US duedate")) default_task_status = models.OneToOneField("projects.TaskStatus", on_delete=models.SET_NULL, related_name="+", null=True, blank=True, @@ -605,8 +610,41 @@ class Points(models.Model): return self.name -# Tasks common models +class UserStoryDueDate(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="us_duedates", verbose_name=_("project")) + class Meta: + verbose_name = "user story due date" + verbose_name_plural = "user story 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.us_duedates + if self.id: + qs = qs.exclude(id=self.id) + + self.slug = slugify_uniquely_for_queryset(self.name, qs) + return super().save(*args, **kwargs) + + +# Tasks common models class TaskStatus(models.Model): name = models.CharField(max_length=255, null=False, blank=False, verbose_name=_("name")) diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index 6df6ae73..da5866f1 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -145,6 +145,16 @@ class UserStoryStatusPermission(TaigaResourcePermission): bulk_update_order_perms = IsProjectAdmin() +class UserStoryDueDatePermission(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() + + # Tasks class TaskStatusPermission(TaigaResourcePermission): diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index 42e72575..aeb9e5e3 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -67,6 +67,17 @@ class PointsSerializer(serializers.LightSerializer): project = Field(attr="project_id") +class UserStoryDueDateSerializer(serializers.LightSerializer): + id = Field() + name = I18NField() + slug = Field() + order = Field() + by_default = Field() + days_to_due = Field() + color = Field() + project = Field(attr="project_id") + + class TaskStatusSerializer(serializers.LightSerializer): id = Field() name = I18NField() diff --git a/taiga/projects/validators.py b/taiga/projects/validators.py index fdbba552..cc49a881 100644 --- a/taiga/projects/validators.py +++ b/taiga/projects/validators.py @@ -84,6 +84,11 @@ class PointsValidator(DuplicatedNameInProjectValidator, validators.ModelValidato model = models.Points +class UserStoryDueDateValidator(DuplicatedNameInProjectValidator, validators.ModelValidator): + class Meta: + model = models.UserStoryDueDate + + class TaskStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator): class Meta: model = models.TaskStatus diff --git a/taiga/routers.py b/taiga/routers.py index 231755f2..d85fe6c2 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -58,6 +58,7 @@ from taiga.projects.api import InvitationViewSet from taiga.projects.api import EpicStatusViewSet 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 IssueStatusViewSet from taiga.projects.api import IssueTypeViewSet @@ -74,6 +75,7 @@ router.register(r"invitations", InvitationViewSet, base_name="invitations") router.register(r"epic-statuses", EpicStatusViewSet, base_name="epic-statuses") router.register(r"userstory-statuses", UserStoryStatusViewSet, base_name="userstory-statuses") 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"issue-statuses", IssueStatusViewSet, base_name="issue-statuses") router.register(r"issue-types", IssueTypeViewSet, base_name="issue-types") diff --git a/taiga/users/migrations/0027_auto_20180605_1031.py b/taiga/users/migrations/0027_auto_20180605_1031.py new file mode 100644 index 00000000..7382fcb5 --- /dev/null +++ b/taiga/users/migrations/0027_auto_20180605_1031.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.2 on 2018-06-05 10:31 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0026_auto_20180514_1513'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='read_new_terms', + field=models.BooleanField(default=False, verbose_name='new terms read'), + ), + ]