Adding tasks and attachments to userstory listing API calls and attachments to task listing API call
parent
4827df0058
commit
34446d8289
|
@ -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):
|
||||
|
|
|
@ -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')]),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -16,11 +16,17 @@
|
|||
# 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.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
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
|
||||
# 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.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
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue