Added due_date to taks, us and issues

remotes/origin/3.4.0rc
Miguel Gonzalez 2018-02-13 09:23:45 +01:00 committed by Alex Hermida
parent 8b76b5787b
commit c2149ef6a5
20 changed files with 197 additions and 16 deletions

View File

View File

@ -0,0 +1,28 @@
# Copyright (C) 2018 Miguel González <migonzalvar@gmail.com>
# 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 <http://www.gnu.org/licenses/>.
from django.db import models
from django.utils.translation import ugettext_lazy as _
class DueDateMixin(models.Model):
due_date = models.DateField(
blank=True, null=True, default=None, verbose_name=_('due date'),
)
due_date_reason = models.TextField(
null=False, blank=True, default='', verbose_name=_('reason for the due date'),
)
class Meta:
abstract = True

View File

@ -0,0 +1,39 @@
# Copyright (C) 2018 Miguel González <migonzalvar@gmail.com>
# 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 <http://www.gnu.org/licenses/>.
import datetime as dt
from django.utils import timezone
from taiga.base.api import serializers
from taiga.base.fields import Field, MethodField
class DueDateSerializerMixin(serializers.LightSerializer):
due_date = Field()
due_date_reason = Field()
due_date_status = MethodField()
THRESHOLD = 3
def get_due_date_status(self, obj):
if obj.due_date is None:
return 'not_set'
elif obj.status.is_closed:
return 'no_longer_applicable'
elif timezone.now().date() > obj.due_date:
return 'past_due'
elif (timezone.now().date() + dt.timedelta(days=self.THRESHOLD)) >= obj.due_date:
return 'due_soon'
else:
return 'set'

View File

@ -357,6 +357,8 @@ def userstory_freezer(us) -> dict:
"blocked_note_html": mdrender(us.project, us.blocked_note), "blocked_note_html": mdrender(us.project, us.blocked_note),
"custom_attributes": extract_user_story_custom_attributes(us), "custom_attributes": extract_user_story_custom_attributes(us),
"tribe_gig": us.tribe_gig, "tribe_gig": us.tribe_gig,
"due_date": str(us.due_date),
"due_date_reason": str(us.due_date_reason),
} }
return snapshot return snapshot
@ -381,6 +383,8 @@ def issue_freezer(issue) -> dict:
"blocked_note": issue.blocked_note, "blocked_note": issue.blocked_note,
"blocked_note_html": mdrender(issue.project, issue.blocked_note), "blocked_note_html": mdrender(issue.project, issue.blocked_note),
"custom_attributes": extract_issue_custom_attributes(issue), "custom_attributes": extract_issue_custom_attributes(issue),
"due_date": str(issue.due_date),
"due_date_reason": str(issue.due_date_reason),
} }
return snapshot return snapshot
@ -406,6 +410,8 @@ def task_freezer(task) -> dict:
"blocked_note": task.blocked_note, "blocked_note": task.blocked_note,
"blocked_note_html": mdrender(task.project, task.blocked_note), "blocked_note_html": mdrender(task.project, task.blocked_note),
"custom_attributes": extract_task_custom_attributes(task), "custom_attributes": extract_task_custom_attributes(task),
"due_date": str(task.due_date),
"due_date_reason": str(task.due_date_reason),
} }
return snapshot return snapshot

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2018-04-09 09:06
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('issues', '0007_auto_20160614_1201'),
]
operations = [
migrations.AddField(
model_name='issue',
name='due_date',
field=models.DateField(blank=True, default=None, null=True, verbose_name='due date'),
),
migrations.AddField(
model_name='issue',
name='due_date_reason',
field=models.TextField(blank=True, default='', verbose_name='reason for the due date'),
),
]

View File

@ -24,13 +24,14 @@ from django.utils import timezone
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from taiga.projects.due_dates.models import DueDateMixin
from taiga.projects.occ import OCCModelMixin from taiga.projects.occ import OCCModelMixin
from taiga.projects.notifications.mixins import WatchedModelMixin from taiga.projects.notifications.mixins import WatchedModelMixin
from taiga.projects.mixins.blocked import BlockedMixin from taiga.projects.mixins.blocked import BlockedMixin
from taiga.projects.tagging.models import TaggedMixin from taiga.projects.tagging.models import TaggedMixin
class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model): class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, DueDateMixin, models.Model):
ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None,
verbose_name=_("ref")) verbose_name=_("ref"))
owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, default=None, owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, default=None,

View File

@ -21,6 +21,7 @@ from taiga.base.fields import Field, MethodField
from taiga.base.neighbors import NeighborsSerializerMixin from taiga.base.neighbors import NeighborsSerializerMixin
from taiga.mdrender.service import render as mdrender from taiga.mdrender.service import render as mdrender
from taiga.projects.due_dates.serializers import DueDateSerializerMixin
from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin
from taiga.projects.mixins.serializers import ProjectExtraInfoSerializerMixin from taiga.projects.mixins.serializers import ProjectExtraInfoSerializerMixin
from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin
@ -33,7 +34,8 @@ from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
class IssueListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer, class IssueListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer,
OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin, OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin,
StatusExtraInfoSerializerMixin, ProjectExtraInfoSerializerMixin, StatusExtraInfoSerializerMixin, ProjectExtraInfoSerializerMixin,
TaggedInProjectResourceSerializer, serializers.LightSerializer): DueDateSerializerMixin, TaggedInProjectResourceSerializer,
serializers.LightSerializer):
id = Field() id = Field()
ref = Field() ref = Field()
severity = Field(attr="severity_id") severity = Field(attr="severity_id")

View File

@ -82,7 +82,7 @@ def issues_to_csv(project, queryset):
"sprint_estimated_finish", "owner", "owner_full_name", "assigned_to", "sprint_estimated_finish", "owner", "owner_full_name", "assigned_to",
"assigned_to_full_name", "status", "severity", "priority", "type", "assigned_to_full_name", "status", "severity", "priority", "type",
"is_closed", "attachments", "external_reference", "tags", "watchers", "is_closed", "attachments", "external_reference", "tags", "watchers",
"voters", "created_date", "modified_date", "finished_date"] "voters", "created_date", "modified_date", "finished_date", "due_date"]
custom_attrs = project.issuecustomattributes.all() custom_attrs = project.issuecustomattributes.all()
for custom_attr in custom_attrs: for custom_attr in custom_attrs:
@ -125,6 +125,7 @@ def issues_to_csv(project, queryset):
"created_date": issue.created_date, "created_date": issue.created_date,
"modified_date": issue.modified_date, "modified_date": issue.modified_date,
"finished_date": issue.finished_date, "finished_date": issue.finished_date,
"due_date": issue.due_date,
} }
for custom_attr in custom_attrs: for custom_attr in custom_attrs:

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2018-04-09 09:06
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tasks', '0011_auto_20160928_0755'),
]
operations = [
migrations.AddField(
model_name='task',
name='due_date',
field=models.DateField(blank=True, default=None, null=True, verbose_name='due date'),
),
migrations.AddField(
model_name='task',
name='due_date_reason',
field=models.TextField(blank=True, default='', verbose_name='reason for the due date'),
),
]

View File

@ -24,13 +24,14 @@ from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from taiga.base.utils.time import timestamp_ms from taiga.base.utils.time import timestamp_ms
from taiga.projects.due_dates.models import DueDateMixin
from taiga.projects.occ import OCCModelMixin from taiga.projects.occ import OCCModelMixin
from taiga.projects.notifications.mixins import WatchedModelMixin from taiga.projects.notifications.mixins import WatchedModelMixin
from taiga.projects.mixins.blocked import BlockedMixin from taiga.projects.mixins.blocked import BlockedMixin
from taiga.projects.tagging.models import TaggedMixin from taiga.projects.tagging.models import TaggedMixin
class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model): class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, DueDateMixin, models.Model):
user_story = models.ForeignKey("userstories.UserStory", null=True, blank=True, user_story = models.ForeignKey("userstories.UserStory", null=True, blank=True,
related_name="tasks", verbose_name=_("user story")) related_name="tasks", verbose_name=_("user story"))
ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None,

View File

@ -22,6 +22,7 @@ from taiga.base.neighbors import NeighborsSerializerMixin
from taiga.mdrender.service import render as mdrender from taiga.mdrender.service import render as mdrender
from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin
from taiga.projects.due_dates.serializers import DueDateSerializerMixin
from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin
from taiga.projects.mixins.serializers import ProjectExtraInfoSerializerMixin from taiga.projects.mixins.serializers import ProjectExtraInfoSerializerMixin
from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin
@ -36,7 +37,8 @@ class TaskListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer,
OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin, OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin,
StatusExtraInfoSerializerMixin, ProjectExtraInfoSerializerMixin, StatusExtraInfoSerializerMixin, ProjectExtraInfoSerializerMixin,
BasicAttachmentsInfoSerializerMixin, TaggedInProjectResourceSerializer, BasicAttachmentsInfoSerializerMixin, TaggedInProjectResourceSerializer,
TotalCommentsSerializerMixin, serializers.LightSerializer): TotalCommentsSerializerMixin, DueDateSerializerMixin,
serializers.LightSerializer):
id = Field() id = Field()
user_story = Field(attr="user_story_id") user_story = Field(attr="user_story_id")

View File

@ -122,7 +122,7 @@ def tasks_to_csv(project, queryset):
"sprint_estimated_finish", "owner", "owner_full_name", "assigned_to", "sprint_estimated_finish", "owner", "owner_full_name", "assigned_to",
"assigned_to_full_name", "status", "is_iocaine", "is_closed", "us_order", "assigned_to_full_name", "status", "is_iocaine", "is_closed", "us_order",
"taskboard_order", "attachments", "external_reference", "tags", "watchers", "voters", "taskboard_order", "attachments", "external_reference", "tags", "watchers", "voters",
"created_date", "modified_date", "finished_date"] "created_date", "modified_date", "finished_date", "due_date"]
custom_attrs = project.taskcustomattributes.all() custom_attrs = project.taskcustomattributes.all()
for custom_attr in custom_attrs: for custom_attr in custom_attrs:
@ -167,6 +167,7 @@ def tasks_to_csv(project, queryset):
"created_date": task.created_date, "created_date": task.created_date,
"modified_date": task.modified_date, "modified_date": task.modified_date,
"finished_date": task.finished_date, "finished_date": task.finished_date,
"due_date": task.due_date,
} }
for custom_attr in custom_attrs: for custom_attr in custom_attrs:
value = task.custom_attributes_values.attributes_values.get(str(custom_attr.id), None) value = task.custom_attributes_values.attributes_values.get(str(custom_attr.id), None)

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2018-04-09 09:06
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('userstories', '0014_auto_20160928_0540'),
]
operations = [
migrations.AddField(
model_name='userstory',
name='due_date',
field=models.DateField(blank=True, default=None, null=True, verbose_name='due date'),
),
migrations.AddField(
model_name='userstory',
name='due_date_reason',
field=models.TextField(blank=True, default='', verbose_name='reason for the due date'),
),
]

View File

@ -26,6 +26,7 @@ from django.utils import timezone
from picklefield.fields import PickledObjectField from picklefield.fields import PickledObjectField
from taiga.base.utils.time import timestamp_ms from taiga.base.utils.time import timestamp_ms
from taiga.projects.due_dates.models import DueDateMixin
from taiga.projects.tagging.models import TaggedMixin from taiga.projects.tagging.models import TaggedMixin
from taiga.projects.occ import OCCModelMixin from taiga.projects.occ import OCCModelMixin
from taiga.projects.notifications.mixins import WatchedModelMixin from taiga.projects.notifications.mixins import WatchedModelMixin
@ -57,7 +58,7 @@ class RolePoints(models.Model):
return self.user_story.project return self.user_story.project
class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model): class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, DueDateMixin, models.Model):
ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None,
verbose_name=_("ref")) verbose_name=_("ref"))
milestone = models.ForeignKey("milestones.Milestone", null=True, blank=True, milestone = models.ForeignKey("milestones.Milestone", null=True, blank=True,

View File

@ -22,6 +22,7 @@ from taiga.base.neighbors import NeighborsSerializerMixin
from taiga.mdrender.service import render as mdrender from taiga.mdrender.service import render as mdrender
from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin
from taiga.projects.due_dates.serializers import DueDateSerializerMixin
from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin
from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin
from taiga.projects.mixins.serializers import ProjectExtraInfoSerializerMixin from taiga.projects.mixins.serializers import ProjectExtraInfoSerializerMixin
@ -49,7 +50,7 @@ class UserStoryListSerializer(ProjectExtraInfoSerializerMixin,
OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin, OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin,
StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin, StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin,
TaggedInProjectResourceSerializer, TotalCommentsSerializerMixin, TaggedInProjectResourceSerializer, TotalCommentsSerializerMixin,
serializers.LightSerializer): DueDateSerializerMixin, serializers.LightSerializer):
id = Field() id = Field()
ref = Field() ref = Field()

View File

@ -197,7 +197,7 @@ def userstories_to_csv(project, queryset):
"created_date", "modified_date", "finish_date", "created_date", "modified_date", "finish_date",
"client_requirement", "team_requirement", "attachments", "client_requirement", "team_requirement", "attachments",
"generated_from_issue", "external_reference", "tasks", "generated_from_issue", "external_reference", "tasks",
"tags", "watchers", "voters"] "tags", "watchers", "voters", "due_date"]
custom_attrs = project.userstorycustomattributes.all() custom_attrs = project.userstorycustomattributes.all()
for custom_attr in custom_attrs: for custom_attr in custom_attrs:
@ -249,7 +249,8 @@ def userstories_to_csv(project, queryset):
"tasks": ",".join([str(task.ref) for task in us.tasks.all()]), "tasks": ",".join([str(task.ref) for task in us.tasks.all()]),
"tags": ",".join(us.tags or []), "tags": ",".join(us.tags or []),
"watchers": us.watchers, "watchers": us.watchers,
"voters": us.total_voters "voters": us.total_voters,
"due_date": us.due_date,
} }
us_role_points_by_role_id = {us_rp.role.id: us_rp.points.value for us_rp in us.role_points.all()} us_role_points_by_role_id = {us_rp.role.id: us_rp.points.value for us_rp in us.role_points.all()}

View File

@ -591,9 +591,9 @@ def test_custom_fields_csv_generation():
data.seek(0) data.seek(0)
reader = csv.reader(data) reader = csv.reader(data)
row = next(reader) row = next(reader)
assert row[23] == attr.name assert row[24] == attr.name
row = next(reader) row = next(reader)
assert row[23] == "val1" assert row[24] == "val1"
def test_api_validator_assigned_to_when_update_issues(client): def test_api_validator_assigned_to_when_update_issues(client):

View File

@ -574,9 +574,9 @@ def test_custom_fields_csv_generation():
data.seek(0) data.seek(0)
reader = csv.reader(data) reader = csv.reader(data)
row = next(reader) row = next(reader)
assert row[24] == attr.name assert row[25] == attr.name
row = next(reader) row = next(reader)
assert row[24] == "val1" assert row[25] == "val1"
def test_get_tasks_including_attachments(client): def test_get_tasks_including_attachments(client):

View File

@ -899,9 +899,9 @@ def test_custom_fields_csv_generation():
data.seek(0) data.seek(0)
reader = csv.reader(data) reader = csv.reader(data)
row = next(reader) row = next(reader)
assert row[28] == attr.name assert row[29] == attr.name
row = next(reader) row = next(reader)
assert row[28] == "val1" assert row[29] == "val1"
def test_update_userstory_respecting_watchers(client): def test_update_userstory_respecting_watchers(client):

View File

@ -0,0 +1,22 @@
import datetime as dt
from unittest import mock
import pytest
from django.utils import timezone
from taiga.projects.due_dates.serializers import DueDateSerializerMixin
@pytest.mark.parametrize('due_date, is_closed, expected', [
(None, False, 'not_set'),
(dt.date(2100, 1, 1), True, 'no_longer_applicable'),
(dt.date(2100, 12, 31), False, 'set'),
(dt.date(2000, 1, 1), False, 'past_due'),
(timezone.now().date(), False, 'due_soon'),
])
def test_due_date_status(due_date, is_closed, expected):
serializer = DueDateSerializerMixin()
obj_status = mock.MagicMock(is_closed=is_closed)
obj = mock.MagicMock(due_date=due_date, status=obj_status)
status = serializer.get_due_date_status(obj)
assert status == expected