diff --git a/taiga/export_import/serializers/serializers.py b/taiga/export_import/serializers/serializers.py index a9bbe6e3..555ba2ce 100644 --- a/taiga/export_import/serializers/serializers.py +++ b/taiga/export_import/serializers/serializers.py @@ -215,6 +215,8 @@ class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, blocked_note = Field() is_blocked = Field() tags = Field() + due_date = DateTimeField() + due_date_reason = Field() def custom_attributes_queryset(self, project): if project.id not in _custom_tasks_attributes_cache: @@ -256,6 +258,8 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, blocked_note = Field() is_blocked = Field() tags = Field() + due_date = DateTimeField() + due_date_reason = Field() def custom_attributes_queryset(self, project): if project.id not in _custom_userstories_attributes_cache: @@ -339,6 +343,9 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, is_blocked = Field() tags = Field() + due_date = DateTimeField() + due_date_reason = Field() + def get_votes(self, obj): return [x.email for x in votes_service.get_voters(obj)] diff --git a/taiga/export_import/validators/validators.py b/taiga/export_import/validators/validators.py index 484f2c89..c66edaa1 100644 --- a/taiga/export_import/validators/validators.py +++ b/taiga/export_import/validators/validators.py @@ -258,6 +258,7 @@ class TaskExportValidator(WatcheableObjectModelValidatorMixin): milestone = ProjectRelatedField(slug_field="name", required=False) assigned_to = UserRelatedField(required=False) modified_date = serializers.DateTimeField(required=False) + due_date = serializers.DateTimeField(required=False) class Meta: model = tasks_models.Task @@ -305,6 +306,7 @@ class UserStoryExportValidator(WatcheableObjectModelValidatorMixin): milestone = ProjectRelatedField(slug_field="name", required=False) modified_date = serializers.DateTimeField(required=False) generated_from_issue = ProjectRelatedField(slug_field="ref", required=False) + due_date = serializers.DateTimeField(required=False) class Meta: model = userstories_models.UserStory @@ -327,6 +329,7 @@ class IssueExportValidator(WatcheableObjectModelValidatorMixin): type = ProjectRelatedField(slug_field="name") milestone = ProjectRelatedField(slug_field="name", required=False) modified_date = serializers.DateTimeField(required=False) + due_date = serializers.DateTimeField(required=False) class Meta: model = issues_models.Issue diff --git a/taiga/projects/due_dates/serializers.py b/taiga/projects/due_dates/serializers.py index 9f690b38..7f976e80 100644 --- a/taiga/projects/due_dates/serializers.py +++ b/taiga/projects/due_dates/serializers.py @@ -29,7 +29,7 @@ class DueDateSerializerMixin(serializers.LightSerializer): def get_due_date_status(self, obj): if obj.due_date is None: return 'not_set' - elif obj.status.is_closed: + elif obj.status and obj.status.is_closed: return 'no_longer_applicable' elif timezone.now().date() > obj.due_date: return 'past_due' diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py index dfbdd2d8..1a59bcb7 100644 --- a/taiga/projects/issues/services.py +++ b/taiga/projects/issues/services.py @@ -82,7 +82,8 @@ def issues_to_csv(project, queryset): "sprint_estimated_finish", "owner", "owner_full_name", "assigned_to", "assigned_to_full_name", "status", "severity", "priority", "type", "is_closed", "attachments", "external_reference", "tags", "watchers", - "voters", "created_date", "modified_date", "finished_date", "due_date"] + "voters", "created_date", "modified_date", "finished_date", "due_date", + "due_date_reason"] custom_attrs = project.issuecustomattributes.all() for custom_attr in custom_attrs: @@ -126,6 +127,7 @@ def issues_to_csv(project, queryset): "modified_date": issue.modified_date, "finished_date": issue.finished_date, "due_date": issue.due_date, + "due_date_reason": issue.due_date_reason, } for custom_attr in custom_attrs: diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py index 0866ddd7..a4ebb748 100644 --- a/taiga/projects/tasks/services.py +++ b/taiga/projects/tasks/services.py @@ -121,8 +121,9 @@ def tasks_to_csv(project, queryset): fieldnames = ["ref", "subject", "description", "user_story", "sprint", "sprint_estimated_start", "sprint_estimated_finish", "owner", "owner_full_name", "assigned_to", "assigned_to_full_name", "status", "is_iocaine", "is_closed", "us_order", - "taskboard_order", "attachments", "external_reference", "tags", "watchers", "voters", - "created_date", "modified_date", "finished_date", "due_date"] + "taskboard_order", "attachments", "external_reference", "tags", "watchers", + "voters", "created_date", "modified_date", "finished_date", "due_date", + "due_date_reason"] custom_attrs = project.taskcustomattributes.all() for custom_attr in custom_attrs: @@ -168,6 +169,7 @@ def tasks_to_csv(project, queryset): "modified_date": task.modified_date, "finished_date": task.finished_date, "due_date": task.due_date, + "due_date_reason": task.due_date_reason, } for custom_attr in custom_attrs: value = task.custom_attributes_values.attributes_values.get(str(custom_attr.id), None) diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index 83a0d0a8..3af6b59a 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -197,7 +197,7 @@ def userstories_to_csv(project, queryset): "created_date", "modified_date", "finish_date", "client_requirement", "team_requirement", "attachments", "generated_from_issue", "external_reference", "tasks", - "tags", "watchers", "voters", "due_date"] + "tags", "watchers", "voters", "due_date", "due_date_reason"] custom_attrs = project.userstorycustomattributes.all() for custom_attr in custom_attrs: @@ -251,6 +251,7 @@ def userstories_to_csv(project, queryset): "watchers": us.watchers, "voters": us.total_voters, "due_date": us.due_date, + "due_date_reason": us.due_date_reason, } us_role_points_by_role_id = {us_rp.role.id: us_rp.points.value for us_rp in us.role_points.all()} diff --git a/tests/factories.py b/tests/factories.py index 8e98aaa9..90a20b20 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -292,6 +292,8 @@ class UserStoryFactory(Factory): status = factory.SubFactory("tests.factories.UserStoryStatusFactory") milestone = factory.SubFactory("tests.factories.MilestoneFactory") tags = factory.Faker("words") + due_date = factory.LazyAttribute(lambda o: date.today() + timedelta(days=7)) + due_date_reason = factory.Faker("words") class TaskFactory(Factory): @@ -308,6 +310,8 @@ class TaskFactory(Factory): milestone = factory.SubFactory("tests.factories.MilestoneFactory") user_story = factory.SubFactory("tests.factories.UserStoryFactory") tags = factory.Faker("words") + due_date = factory.LazyAttribute(lambda o: date.today() + timedelta(days=7)) + due_date_reason = factory.Faker("words") class IssueFactory(Factory): @@ -326,6 +330,8 @@ class IssueFactory(Factory): type = factory.SubFactory("tests.factories.IssueTypeFactory") milestone = factory.SubFactory("tests.factories.MilestoneFactory") tags = factory.Faker("words") + due_date = factory.LazyAttribute(lambda o: date.today() + timedelta(days=7)) + due_date_reason = factory.Faker("words") class WikiPageFactory(Factory): diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py index 4096c907..5495dd81 100644 --- a/tests/integration/test_issues.py +++ b/tests/integration/test_issues.py @@ -591,9 +591,9 @@ def test_custom_fields_csv_generation(): data.seek(0) reader = csv.reader(data) row = next(reader) - assert row[24] == attr.name + assert row[25] == attr.name row = next(reader) - assert row[24] == "val1" + assert row[25] == "val1" def test_api_validator_assigned_to_when_update_issues(client): diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index 10cab2ee..c687ab7a 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -574,9 +574,9 @@ def test_custom_fields_csv_generation(): data.seek(0) reader = csv.reader(data) row = next(reader) - assert row[25] == attr.name + assert row[26] == attr.name row = next(reader) - assert row[25] == "val1" + assert row[26] == "val1" def test_get_tasks_including_attachments(client): diff --git a/tests/integration/test_timeline.py b/tests/integration/test_timeline.py index 14d84a35..64d656fe 100644 --- a/tests/integration/test_timeline.py +++ b/tests/integration/test_timeline.py @@ -17,6 +17,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import pytz + +from datetime import datetime, timedelta import pytest from .. import factories @@ -469,6 +472,26 @@ def test_assigned_to_user_story_timeline(): assert user_timeline[0].data["userstory"]["subject"] == "test us timeline" +def test_due_date_user_story_timeline(): + initial_due_date = datetime.now(pytz.utc) + timedelta(days=1) + membership = factories.MembershipFactory.create() + user_story = factories.UserStoryFactory.create(subject="test us timeline", + due_date=initial_due_date, + project=membership.project) + history_services.take_snapshot(user_story, user=user_story.owner) + + new_due_date = datetime.now(pytz.utc) + timedelta(days=3) + user_story.due_date = new_due_date + user_story.save() + + history_services.take_snapshot(user_story, user=user_story.owner) + user_timeline = service.get_profile_timeline(user_story.owner) + + assert user_timeline[0].event_type == "userstories.userstory.change" + assert user_timeline[0].data["values_diff"]['due_date'] == [str(initial_due_date.date()), + str(new_due_date.date())] + + def test_user_data_for_non_system_users(): user_story = factories.UserStoryFactory.create(subject="test us timeline") history_services.take_snapshot(user_story, user=user_story.owner) diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index b186413d..a8f4054b 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -899,9 +899,9 @@ def test_custom_fields_csv_generation(): data.seek(0) reader = csv.reader(data) row = next(reader) - assert row[29] == attr.name + assert row.pop() == attr.name row = next(reader) - assert row[29] == "val1" + assert row.pop() == "val1" def test_update_userstory_respecting_watchers(client):