diff --git a/taiga/base/filters.py b/taiga/base/filters.py
index 1cd19e64..e26e7911 100644
--- a/taiga/base/filters.py
+++ b/taiga/base/filters.py
@@ -152,7 +152,7 @@ class PermissionBasedFilterBackend(FilterBackend):
else:
qs = qs.filter(project__anon_permissions__contains=[self.permission])
- return super().filter_queryset(request, qs.distinct(), view)
+ return super().filter_queryset(request, qs, view)
class CanViewProjectFilterBackend(PermissionBasedFilterBackend):
@@ -268,7 +268,7 @@ class MembersFilterBackend(PermissionBasedFilterBackend):
qs = qs.filter(memberships__project__anon_permissions__contains=[self.permission])
- return qs.distinct()
+ return qs
#####################################################################
@@ -307,7 +307,7 @@ class IsProjectAdminFilterBackend(FilterBackend, BaseIsProjectAdminFilterBackend
else:
queryset = queryset.filter(project_id__in=project_ids)
- return super().filter_queryset(request, queryset.distinct(), view)
+ return super().filter_queryset(request, queryset, view)
class IsProjectAdminFromWebhookLogFilterBackend(FilterBackend, BaseIsProjectAdminFilterBackend):
diff --git a/taiga/projects/attachments/migrations/0006_auto_20160617_1233.py b/taiga/projects/attachments/migrations/0006_auto_20160617_1233.py
new file mode 100644
index 00000000..ee291a9f
--- /dev/null
+++ b/taiga/projects/attachments/migrations/0006_auto_20160617_1233.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-06-17 12:33
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('attachments', '0005_attachment_sha1'),
+ ]
+
+ operations = [
+ migrations.AlterIndexTogether(
+ name='attachment',
+ index_together=set([('content_type', 'object_id')]),
+ ),
+ ]
diff --git a/taiga/projects/attachments/models.py b/taiga/projects/attachments/models.py
index 8bbbee16..a5110a4b 100644
--- a/taiga/projects/attachments/models.py
+++ b/taiga/projects/attachments/models.py
@@ -70,6 +70,7 @@ class Attachment(models.Model):
permissions = (
("view_attachment", "Can view attachment"),
)
+ index_together = [("content_type", "object_id")]
def __init__(self, *args, **kwargs):
super(Attachment, self).__init__(*args, **kwargs)
diff --git a/taiga/projects/attachments/serializers.py b/taiga/projects/attachments/serializers.py
index 904498a9..6c5ee05b 100644
--- a/taiga/projects/attachments/serializers.py
+++ b/taiga/projects/attachments/serializers.py
@@ -16,11 +16,17 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from django.conf import settings
+
from taiga.base.api import serializers
+from taiga.base.utils.thumbnails import get_thumbnail_url
from . import services
from . import models
+import json
+import serpy
+
class AttachmentSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField("get_url")
@@ -37,5 +43,31 @@ class AttachmentSerializer(serializers.ModelSerializer):
def get_url(self, obj):
return obj.attached_file.url
+
def get_thumbnail_card_url(self, obj):
return services.get_card_image_thumbnail_url(obj)
+
+
+class ListBasicAttachmentsInfoSerializerMixin(serpy.Serializer):
+ """
+ Assumptions:
+ - The queryset has an attribute called "include_attachments" indicating if the attachments array should contain information
+ about the related elements, otherwise it will be empty
+ - The method attach_basic_attachments has been used to include the necessary
+ json data about the attachments in the "attachments_attr" column
+ """
+ attachments = serpy.MethodField()
+
+ def get_attachments(self, obj):
+ include_attachments = getattr(obj, "include_attachments", False)
+
+ if include_attachments:
+ assert hasattr(obj, "attachments_attr"), "instance must have a attachments_attr attribute"
+
+ if not include_attachments or obj.attachments_attr is None:
+ return []
+
+ for at in obj.attachments_attr:
+ at["thumbnail_card_url"] = get_thumbnail_url(at["attached_file"], settings.THN_ATTACHMENT_CARD)
+
+ return obj.attachments_attr
diff --git a/taiga/projects/attachments/utils.py b/taiga/projects/attachments/utils.py
new file mode 100644
index 00000000..33e36c44
--- /dev/null
+++ b/taiga/projects/attachments/utils.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# Copyright (C) 2014-2016 Anler Hernández
+# 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.apps import apps
+
+def attach_basic_attachments(queryset, as_field="attachments_attr"):
+ """Attach basic attachments info as json column to each object of the queryset.
+
+ :param queryset: A Django user stories queryset object.
+ :param as_field: Attach the role points as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+
+ model = queryset.model
+ type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
+
+ sql = """SELECT json_agg(row_to_json(t))
+ FROM(
+ SELECT
+ attachments_attachment.id,
+ attachments_attachment.attached_file
+ FROM attachments_attachment
+ WHERE attachments_attachment.object_id = {tbl}.id AND attachments_attachment.content_type_id = {type_id}) t"""
+
+ sql = sql.format(tbl=model._meta.db_table, type_id=type.id)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py
index 4243ea31..099171a1 100644
--- a/taiga/projects/issues/serializers.py
+++ b/taiga/projects/issues/serializers.py
@@ -21,9 +21,9 @@ from taiga.base.fields import PgArrayField
from taiga.base.neighbors import NeighborsSerializerMixin
from taiga.mdrender.service import render as mdrender
-from taiga.projects.mixins.serializers import OwnerExtraInfoMixin
-from taiga.projects.mixins.serializers import AssigedToExtraInfoMixin
-from taiga.projects.mixins.serializers import StatusExtraInfoMixin
+from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
from taiga.projects.notifications.validators import WatchersValidator
@@ -39,6 +39,7 @@ from . import models
import serpy
+
class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
serializers.ModelSerializer):
tags = TagsAndTagsColorsField(default=[], required=False)
@@ -75,8 +76,8 @@ class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWa
class IssueListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer,
- OwnerExtraInfoMixin, AssigedToExtraInfoMixin, StatusExtraInfoMixin,
- serializers.LightSerializer):
+ ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin,
+ ListStatusExtraInfoSerializerMixin, serializers.LightSerializer):
id = serpy.Field()
ref = serpy.Field()
severity = serpy.Field(attr="severity_id")
diff --git a/taiga/projects/milestones/serializers.py b/taiga/projects/milestones/serializers.py
index b3df7c8f..724126fd 100644
--- a/taiga/projects/milestones/serializers.py
+++ b/taiga/projects/milestones/serializers.py
@@ -35,6 +35,7 @@ class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer,
ValidateDuplicatedNameInProjectMixin):
total_points = serializers.SerializerMethodField("get_total_points")
closed_points = serializers.SerializerMethodField("get_closed_points")
+ user_stories = serializers.SerializerMethodField("get_user_stories")
class Meta:
model = models.Milestone
@@ -46,6 +47,9 @@ class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer,
def get_closed_points(self, obj):
return sum(obj.closed_points.values())
+ def get_user_stories(self, obj):
+ return UserStoryListSerializer(obj.user_stories.all(), many=True).data
+
class MilestoneListSerializer(ListWatchedResourceModelSerializer, serializers.LightSerializer):
id = serpy.Field()
@@ -62,8 +66,16 @@ class MilestoneListSerializer(ListWatchedResourceModelSerializer, serializers.Li
order = serpy.Field()
watchers = serpy.Field()
user_stories = serpy.MethodField("get_user_stories")
- total_points = serializers.Field(source="total_points_attr")
- closed_points = serializers.Field(source="closed_points_attr")
+ total_points = serpy.MethodField()
+ closed_points = serpy.MethodField()
def get_user_stories(self, obj):
return UserStoryListSerializer(obj.user_stories.all(), many=True).data
+
+ def get_total_points(self, obj):
+ assert hasattr(obj, "total_points_attr"), "instance must have a total_points_attr attribute"
+ return obj.total_points_attr
+
+ def get_closed_points(self, obj):
+ assert hasattr(obj, "closed_points_attr"), "instance must have a closed_points_attr attribute"
+ return obj.closed_points_attr
diff --git a/taiga/projects/mixins/serializers.py b/taiga/projects/mixins/serializers.py
index 2d788298..a47d9bed 100644
--- a/taiga/projects/mixins/serializers.py
+++ b/taiga/projects/mixins/serializers.py
@@ -44,7 +44,7 @@ class ValidateDuplicatedNameInProjectMixin(serializers.ModelSerializer):
return attrs
-class CachedSerializedUsersMixin(serpy.Serializer):
+class ListCachedUsersSerializerMixin(serpy.Serializer):
def to_value(self, instance):
self._serialized_users = {}
return super().to_value(instance)
@@ -61,7 +61,7 @@ class CachedSerializedUsersMixin(serpy.Serializer):
return serialized_user
-class OwnerExtraInfoMixin(CachedSerializedUsersMixin):
+class ListOwnerExtraInfoSerializerMixin(ListCachedUsersSerializerMixin):
owner = serpy.Field(attr="owner_id")
owner_extra_info = serpy.MethodField()
@@ -69,7 +69,7 @@ class OwnerExtraInfoMixin(CachedSerializedUsersMixin):
return self.get_user_extra_info(obj.owner)
-class AssigedToExtraInfoMixin(CachedSerializedUsersMixin):
+class ListAssignedToExtraInfoSerializerMixin(ListCachedUsersSerializerMixin):
assigned_to = serpy.Field(attr="assigned_to_id")
assigned_to_extra_info = serpy.MethodField()
@@ -77,9 +77,10 @@ class AssigedToExtraInfoMixin(CachedSerializedUsersMixin):
return self.get_user_extra_info(obj.assigned_to)
-class StatusExtraInfoMixin(serpy.Serializer):
+class ListStatusExtraInfoSerializerMixin(serpy.Serializer):
status = serpy.Field(attr="status_id")
status_extra_info = serpy.MethodField()
+
def to_value(self, instance):
self._serialized_status = {}
return super().to_value(instance)
diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py
index b7c13ab1..d7723a26 100644
--- a/taiga/projects/tasks/api.py
+++ b/taiga/projects/tasks/api.py
@@ -25,6 +25,8 @@ from taiga.base import exceptions as exc
from taiga.base.decorators import list_route
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
from taiga.base.api.mixins import BlockedByProjectMixin
+
+from taiga.projects.attachments.utils import attach_basic_attachments
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.models import Project, TaskStatus
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
@@ -94,13 +96,19 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
def get_queryset(self):
qs = super().get_queryset()
qs = self.attach_votes_attrs_to_queryset(qs)
- qs = qs.select_related("milestone",
- "owner",
- "assigned_to",
- "status",
- "project")
+ qs = qs.select_related(
+ "milestone",
+ "owner",
+ "assigned_to",
+ "status",
+ "project")
- return self.attach_watchers_attrs_to_queryset(qs)
+ qs = self.attach_watchers_attrs_to_queryset(qs)
+ if "include_attachments" in self.request.QUERY_PARAMS:
+ qs = attach_basic_attachments(qs)
+ qs = qs.extra(select={"include_attachments": "True"})
+
+ return qs
def pre_save(self, obj):
if obj.user_story:
diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py
index d7423e66..ac82c570 100644
--- a/taiga/projects/tasks/serializers.py
+++ b/taiga/projects/tasks/serializers.py
@@ -23,10 +23,12 @@ from taiga.base.api import serializers
from taiga.base.fields import PgArrayField
from taiga.base.neighbors import NeighborsSerializerMixin
+from taiga.mdrender.service import render as mdrender
+from taiga.projects.attachments.serializers import ListBasicAttachmentsInfoSerializerMixin
from taiga.projects.milestones.validators import SprintExistsValidator
-from taiga.projects.mixins.serializers import OwnerExtraInfoMixin
-from taiga.projects.mixins.serializers import AssigedToExtraInfoMixin
-from taiga.projects.mixins.serializers import StatusExtraInfoMixin
+from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
from taiga.projects.notifications.validators import WatchersValidator
@@ -46,8 +48,10 @@ from . import models
import serpy
+
class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
serializers.ModelSerializer):
+
tags = TagsAndTagsColorsField(default=[], required=False)
external_reference = PgArrayField(required=False)
comment = serializers.SerializerMethodField("get_comment")
@@ -83,8 +87,10 @@ class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWat
class TaskListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer,
- OwnerExtraInfoMixin, AssigedToExtraInfoMixin, StatusExtraInfoMixin,
+ ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin,
+ ListStatusExtraInfoSerializerMixin, ListBasicAttachmentsInfoSerializerMixin,
serializers.LightSerializer):
+
id = serpy.Field()
user_story = serpy.Field(attr="user_story_id")
ref = serpy.Field()
diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py
index dbe5c433..0c54f81d 100644
--- a/taiga/projects/userstories/api.py
+++ b/taiga/projects/userstories/api.py
@@ -36,6 +36,7 @@ from taiga.base.api import ModelCrudViewSet
from taiga.base.api import ModelListViewSet
from taiga.base.api.utils import get_object_or_404
+from taiga.projects.attachments.utils import attach_basic_attachments
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.history.services import take_snapshot
from taiga.projects.milestones.models import Milestone
@@ -49,6 +50,7 @@ from taiga.projects.votes.mixins.viewsets import VotedResourceMixin
from taiga.projects.votes.mixins.viewsets import VotersViewSetMixin
from taiga.projects.userstories.utils import attach_total_points
from taiga.projects.userstories.utils import attach_role_points
+from taiga.projects.userstories.utils import attach_tasks
from . import models
from . import permissions
@@ -105,10 +107,20 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
"owner",
"assigned_to",
"generated_from_issue")
+
qs = self.attach_votes_attrs_to_queryset(qs)
qs = self.attach_watchers_attrs_to_queryset(qs)
qs = attach_total_points(qs)
qs = attach_role_points(qs)
+
+ if "include_attachments" in self.request.QUERY_PARAMS:
+ qs = attach_basic_attachments(qs)
+ qs = qs.extra(select={"include_attachments": "True"})
+
+ if "include_tasks" in self.request.QUERY_PARAMS:
+ qs = attach_tasks(qs)
+ qs = qs.extra(select={"include_tasks": "True"})
+
return qs
def pre_conditions_on_save(self, obj):
diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py
index c863c87b..236a54d8 100644
--- a/taiga/projects/userstories/serializers.py
+++ b/taiga/projects/userstories/serializers.py
@@ -29,20 +29,19 @@ from taiga.base.neighbors import NeighborsSerializerMixin
from taiga.base.utils import json
from taiga.mdrender.service import render as mdrender
-
+from taiga.projects.attachments.serializers import ListBasicAttachmentsInfoSerializerMixin
from taiga.projects.milestones.validators import SprintExistsValidator
+from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin
from taiga.projects.models import Project, UserStoryStatus
-from taiga.projects.mixins.serializers import OwnerExtraInfoMixin
-from taiga.projects.mixins.serializers import AssigedToExtraInfoMixin
-from taiga.projects.mixins.serializers import StatusExtraInfoMixin
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.serializers import BasicUserStoryStatusSerializer
from taiga.projects.tagging.fields import TagsAndTagsColorsField
from taiga.projects.userstories.validators import UserStoryExistsValidator
-from taiga.projects.validators import ProjectExistsValidator
-from taiga.projects.validators import UserStoryStatusExistsValidator
+from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin
@@ -136,7 +135,9 @@ class ListOriginIssueSerializer(serializers.LightSerializer):
class UserStoryListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer,
- OwnerExtraInfoMixin, AssigedToExtraInfoMixin, StatusExtraInfoMixin, serializers.LightSerializer):
+ ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin,
+ ListStatusExtraInfoSerializerMixin, ListBasicAttachmentsInfoSerializerMixin,
+ serializers.LightSerializer):
id = serpy.Field()
ref = serpy.Field()
@@ -163,13 +164,11 @@ class UserStoryListSerializer(ListVoteResourceSerializerMixin, ListWatchedResour
is_blocked = serpy.Field()
blocked_note = serpy.Field()
tags = serpy.Field()
- total_points = serpy.Field("total_points_attr")
+ total_points = serpy.MethodField()
comment = serpy.MethodField("get_comment")
origin_issue = ListOriginIssueSerializer(attr="generated_from_issue")
- def to_value(self, instance):
- self._serialized_status = {}
- return super().to_value(instance)
+ tasks = serpy.MethodField()
def get_milestone_slug(self, obj):
return obj.milestone.slug if obj.milestone else None
@@ -177,15 +176,31 @@ class UserStoryListSerializer(ListVoteResourceSerializerMixin, ListWatchedResour
def get_milestone_name(self, obj):
return obj.milestone.name if obj.milestone else None
+ def get_total_points(self, obj):
+ assert hasattr(obj, "total_points_attr"), "instance must have a total_points_attr attribute"
+ return obj.total_points_attr
+
def get_points(self, obj):
+ assert hasattr(obj, "role_points_attr"), "instance must have a role_points_attr attribute"
if obj.role_points_attr is None:
return {}
- return dict(ChainMap(*json.loads(obj.role_points_attr)))
+ return dict(ChainMap(*obj.role_points_attr))
def get_comment(self, obj):
return ""
+ def get_tasks(self, obj):
+ include_tasks = getattr(obj, "include_tasks", False)
+
+ if include_tasks:
+ assert hasattr(obj, "tasks_attr"), "instance must have a tasks_attr attribute"
+
+ if not include_tasks or obj.tasks_attr is None:
+ return []
+
+ return obj.tasks_attr
+
class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer):
def serialize_neighbor(self, neighbor):
diff --git a/taiga/projects/userstories/utils.py b/taiga/projects/userstories/utils.py
index 36a9970d..809248f7 100644
--- a/taiga/projects/userstories/utils.py
+++ b/taiga/projects/userstories/utils.py
@@ -46,11 +46,39 @@ def attach_role_points(queryset, as_field="role_points_attr"):
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
- sql = """SELECT json_agg(json_build_object(userstories_rolepoints.role_id,
- userstories_rolepoints.points_id))::text
+ sql = """SELECT json_agg((userstories_rolepoints.role_id, userstories_rolepoints.points_id))
FROM userstories_rolepoints
WHERE userstories_rolepoints.user_story_id = {tbl}.id"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
+
+
+def attach_tasks(queryset, as_field="tasks_attr"):
+ """Attach tasks as json column to each object of the queryset.
+
+ :param queryset: A Django user stories queryset object.
+ :param as_field: Attach the role points as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(t))
+ FROM(
+ SELECT
+ tasks_task.id,
+ tasks_task.ref,
+ tasks_task.subject,
+ tasks_task.status_id,
+ tasks_task.is_blocked,
+ tasks_task.is_iocaine,
+ projects_taskstatus.is_closed
+ FROM tasks_task
+ INNER JOIN projects_taskstatus on projects_taskstatus.id = tasks_task.status_id
+ WHERE user_story_id = {tbl}.id) t"""
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py
index fcecee3b..072c0595 100644
--- a/tests/integration/test_tasks.py
+++ b/tests/integration/test_tasks.py
@@ -185,3 +185,24 @@ def test_custom_fields_csv_generation():
assert row[24] == attr.name
row = next(reader)
assert row[24] == "val1"
+
+
+def test_get_tasks_including_attachments(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
+
+ task = f.TaskFactory.create(project=project)
+ f.TaskAttachmentFactory(project=project, content_object=task)
+ url = reverse("tasks-list")
+
+ client.login(project.owner)
+
+ response = client.get(url)
+ assert response.status_code == 200
+ assert response.data[0].get("attachments") == []
+
+ url = reverse("tasks-list") + "?include_attachments=1"
+ response = client.get(url)
+ assert response.status_code == 200
+ assert len(response.data[0].get("attachments")) == 1
diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py
index bc3c5560..e05fff68 100644
--- a/tests/integration/test_userstories.py
+++ b/tests/integration/test_userstories.py
@@ -644,3 +644,45 @@ def test_update_userstory_update_tribe_gig(client):
assert response.status_code == 200
assert response.data["tribe_gig"] == data["tribe_gig"]
+
+
+def test_get_user_stories_including_tasks(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
+
+ user_story = f.UserStoryFactory.create(project=project)
+ f.TaskFactory.create(user_story=user_story)
+ url = reverse("userstories-list")
+
+ client.login(project.owner)
+
+ response = client.get(url)
+ assert response.status_code == 200
+ assert response.data[0].get("tasks") == []
+
+ url = reverse("userstories-list") + "?include_tasks=1"
+ response = client.get(url)
+ assert response.status_code == 200
+ assert len(response.data[0].get("tasks")) == 1
+
+
+def test_get_user_stories_including_attachments(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory.create(project=project, user=user, is_admin=True)
+
+ user_story = f.UserStoryFactory.create(project=project)
+ f.UserStoryAttachmentFactory(project=project, content_object=user_story)
+ url = reverse("userstories-list")
+
+ client.login(project.owner)
+
+ response = client.get(url)
+ assert response.status_code == 200
+ assert response.data[0].get("attachments") == []
+
+ url = reverse("userstories-list") + "?include_attachments=1"
+ response = client.get(url)
+ assert response.status_code == 200
+ assert len(response.data[0].get("attachments")) == 1