Adding tasks and attachments to userstory listing API calls and attachments to task listing API call

remotes/origin/issue/4795/notification_even_they_are_disabled
Alejandro Alonso 2016-06-02 14:34:44 +02:00 committed by David Barragán Merino
parent 4827df0058
commit 34446d8289
15 changed files with 280 additions and 38 deletions

View File

@ -152,7 +152,7 @@ class PermissionBasedFilterBackend(FilterBackend):
else: else:
qs = qs.filter(project__anon_permissions__contains=[self.permission]) 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): class CanViewProjectFilterBackend(PermissionBasedFilterBackend):
@ -268,7 +268,7 @@ class MembersFilterBackend(PermissionBasedFilterBackend):
qs = qs.filter(memberships__project__anon_permissions__contains=[self.permission]) qs = qs.filter(memberships__project__anon_permissions__contains=[self.permission])
return qs.distinct() return qs
##################################################################### #####################################################################
@ -307,7 +307,7 @@ class IsProjectAdminFilterBackend(FilterBackend, BaseIsProjectAdminFilterBackend
else: else:
queryset = queryset.filter(project_id__in=project_ids) 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): class IsProjectAdminFromWebhookLogFilterBackend(FilterBackend, BaseIsProjectAdminFilterBackend):

View File

@ -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')]),
),
]

View File

@ -70,6 +70,7 @@ class Attachment(models.Model):
permissions = ( permissions = (
("view_attachment", "Can view attachment"), ("view_attachment", "Can view attachment"),
) )
index_together = [("content_type", "object_id")]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Attachment, self).__init__(*args, **kwargs) super(Attachment, self).__init__(*args, **kwargs)

View File

@ -16,11 +16,17 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # 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.api import serializers
from taiga.base.utils.thumbnails import get_thumbnail_url
from . import services from . import services
from . import models from . import models
import json
import serpy
class AttachmentSerializer(serializers.ModelSerializer): class AttachmentSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField("get_url") url = serializers.SerializerMethodField("get_url")
@ -37,5 +43,31 @@ class AttachmentSerializer(serializers.ModelSerializer):
def get_url(self, obj): def get_url(self, obj):
return obj.attached_file.url return obj.attached_file.url
def get_thumbnail_card_url(self, obj): def get_thumbnail_card_url(self, obj):
return services.get_card_image_thumbnail_url(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

View File

@ -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

View File

@ -21,9 +21,9 @@ from taiga.base.fields import PgArrayField
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.mixins.serializers import OwnerExtraInfoMixin from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin
from taiga.projects.mixins.serializers import AssigedToExtraInfoMixin from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin
from taiga.projects.mixins.serializers import StatusExtraInfoMixin from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.notifications.validators import WatchersValidator
@ -39,6 +39,7 @@ from . import models
import serpy import serpy
class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
serializers.ModelSerializer): serializers.ModelSerializer):
tags = TagsAndTagsColorsField(default=[], required=False) tags = TagsAndTagsColorsField(default=[], required=False)
@ -75,8 +76,8 @@ class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWa
class IssueListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer, class IssueListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer,
OwnerExtraInfoMixin, AssigedToExtraInfoMixin, StatusExtraInfoMixin, ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin,
serializers.LightSerializer): ListStatusExtraInfoSerializerMixin, serializers.LightSerializer):
id = serpy.Field() id = serpy.Field()
ref = serpy.Field() ref = serpy.Field()
severity = serpy.Field(attr="severity_id") severity = serpy.Field(attr="severity_id")

View File

@ -35,6 +35,7 @@ class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer,
ValidateDuplicatedNameInProjectMixin): ValidateDuplicatedNameInProjectMixin):
total_points = serializers.SerializerMethodField("get_total_points") total_points = serializers.SerializerMethodField("get_total_points")
closed_points = serializers.SerializerMethodField("get_closed_points") closed_points = serializers.SerializerMethodField("get_closed_points")
user_stories = serializers.SerializerMethodField("get_user_stories")
class Meta: class Meta:
model = models.Milestone model = models.Milestone
@ -46,6 +47,9 @@ class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer,
def get_closed_points(self, obj): def get_closed_points(self, obj):
return sum(obj.closed_points.values()) 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): class MilestoneListSerializer(ListWatchedResourceModelSerializer, serializers.LightSerializer):
id = serpy.Field() id = serpy.Field()
@ -62,8 +66,16 @@ class MilestoneListSerializer(ListWatchedResourceModelSerializer, serializers.Li
order = serpy.Field() order = serpy.Field()
watchers = serpy.Field() watchers = serpy.Field()
user_stories = serpy.MethodField("get_user_stories") user_stories = serpy.MethodField("get_user_stories")
total_points = serializers.Field(source="total_points_attr") total_points = serpy.MethodField()
closed_points = serializers.Field(source="closed_points_attr") closed_points = serpy.MethodField()
def get_user_stories(self, obj): def get_user_stories(self, obj):
return UserStoryListSerializer(obj.user_stories.all(), many=True).data 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

View File

@ -44,7 +44,7 @@ class ValidateDuplicatedNameInProjectMixin(serializers.ModelSerializer):
return attrs return attrs
class CachedSerializedUsersMixin(serpy.Serializer): class ListCachedUsersSerializerMixin(serpy.Serializer):
def to_value(self, instance): def to_value(self, instance):
self._serialized_users = {} self._serialized_users = {}
return super().to_value(instance) return super().to_value(instance)
@ -61,7 +61,7 @@ class CachedSerializedUsersMixin(serpy.Serializer):
return serialized_user return serialized_user
class OwnerExtraInfoMixin(CachedSerializedUsersMixin): class ListOwnerExtraInfoSerializerMixin(ListCachedUsersSerializerMixin):
owner = serpy.Field(attr="owner_id") owner = serpy.Field(attr="owner_id")
owner_extra_info = serpy.MethodField() owner_extra_info = serpy.MethodField()
@ -69,7 +69,7 @@ class OwnerExtraInfoMixin(CachedSerializedUsersMixin):
return self.get_user_extra_info(obj.owner) return self.get_user_extra_info(obj.owner)
class AssigedToExtraInfoMixin(CachedSerializedUsersMixin): class ListAssignedToExtraInfoSerializerMixin(ListCachedUsersSerializerMixin):
assigned_to = serpy.Field(attr="assigned_to_id") assigned_to = serpy.Field(attr="assigned_to_id")
assigned_to_extra_info = serpy.MethodField() assigned_to_extra_info = serpy.MethodField()
@ -77,9 +77,10 @@ class AssigedToExtraInfoMixin(CachedSerializedUsersMixin):
return self.get_user_extra_info(obj.assigned_to) return self.get_user_extra_info(obj.assigned_to)
class StatusExtraInfoMixin(serpy.Serializer): class ListStatusExtraInfoSerializerMixin(serpy.Serializer):
status = serpy.Field(attr="status_id") status = serpy.Field(attr="status_id")
status_extra_info = serpy.MethodField() status_extra_info = serpy.MethodField()
def to_value(self, instance): def to_value(self, instance):
self._serialized_status = {} self._serialized_status = {}
return super().to_value(instance) return super().to_value(instance)

View File

@ -25,6 +25,8 @@ from taiga.base import exceptions as exc
from taiga.base.decorators import list_route from taiga.base.decorators import list_route
from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api import ModelCrudViewSet, ModelListViewSet
from taiga.base.api.mixins import BlockedByProjectMixin 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.history.mixins import HistoryResourceMixin
from taiga.projects.models import Project, TaskStatus from taiga.projects.models import Project, TaskStatus
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
@ -94,13 +96,19 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
qs = self.attach_votes_attrs_to_queryset(qs) qs = self.attach_votes_attrs_to_queryset(qs)
qs = qs.select_related("milestone", qs = qs.select_related(
"owner", "milestone",
"assigned_to", "owner",
"status", "assigned_to",
"project") "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): def pre_save(self, obj):
if obj.user_story: if obj.user_story:

View File

@ -23,10 +23,12 @@ from taiga.base.api import serializers
from taiga.base.fields import PgArrayField from taiga.base.fields import PgArrayField
from taiga.base.neighbors import NeighborsSerializerMixin 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.milestones.validators import SprintExistsValidator
from taiga.projects.mixins.serializers import OwnerExtraInfoMixin from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin
from taiga.projects.mixins.serializers import AssigedToExtraInfoMixin from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin
from taiga.projects.mixins.serializers import StatusExtraInfoMixin from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.notifications.validators import WatchersValidator
@ -46,8 +48,10 @@ from . import models
import serpy import serpy
class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
serializers.ModelSerializer): serializers.ModelSerializer):
tags = TagsAndTagsColorsField(default=[], required=False) tags = TagsAndTagsColorsField(default=[], required=False)
external_reference = PgArrayField(required=False) external_reference = PgArrayField(required=False)
comment = serializers.SerializerMethodField("get_comment") comment = serializers.SerializerMethodField("get_comment")
@ -83,8 +87,10 @@ class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWat
class TaskListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer, class TaskListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer,
OwnerExtraInfoMixin, AssigedToExtraInfoMixin, StatusExtraInfoMixin, ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin,
ListStatusExtraInfoSerializerMixin, ListBasicAttachmentsInfoSerializerMixin,
serializers.LightSerializer): serializers.LightSerializer):
id = serpy.Field() id = serpy.Field()
user_story = serpy.Field(attr="user_story_id") user_story = serpy.Field(attr="user_story_id")
ref = serpy.Field() ref = serpy.Field()

View File

@ -36,6 +36,7 @@ from taiga.base.api import ModelCrudViewSet
from taiga.base.api import ModelListViewSet from taiga.base.api import ModelListViewSet
from taiga.base.api.utils import get_object_or_404 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.mixins import HistoryResourceMixin
from taiga.projects.history.services import take_snapshot from taiga.projects.history.services import take_snapshot
from taiga.projects.milestones.models import Milestone 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.votes.mixins.viewsets import VotersViewSetMixin
from taiga.projects.userstories.utils import attach_total_points 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_role_points
from taiga.projects.userstories.utils import attach_tasks
from . import models from . import models
from . import permissions from . import permissions
@ -105,10 +107,20 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
"owner", "owner",
"assigned_to", "assigned_to",
"generated_from_issue") "generated_from_issue")
qs = self.attach_votes_attrs_to_queryset(qs) qs = self.attach_votes_attrs_to_queryset(qs)
qs = self.attach_watchers_attrs_to_queryset(qs) qs = self.attach_watchers_attrs_to_queryset(qs)
qs = attach_total_points(qs) qs = attach_total_points(qs)
qs = attach_role_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 return qs
def pre_conditions_on_save(self, obj): def pre_conditions_on_save(self, obj):

View File

@ -29,20 +29,19 @@ from taiga.base.neighbors import NeighborsSerializerMixin
from taiga.base.utils import json from taiga.base.utils import json
from taiga.mdrender.service import render as mdrender 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.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.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 EditableWatchedResourceModelSerializer
from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.serializers import BasicUserStoryStatusSerializer from taiga.projects.serializers import BasicUserStoryStatusSerializer
from taiga.projects.tagging.fields import TagsAndTagsColorsField from taiga.projects.tagging.fields import TagsAndTagsColorsField
from taiga.projects.userstories.validators import UserStoryExistsValidator from taiga.projects.userstories.validators import UserStoryExistsValidator
from taiga.projects.validators import ProjectExistsValidator from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator
from taiga.projects.validators import UserStoryStatusExistsValidator
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin
@ -136,7 +135,9 @@ class ListOriginIssueSerializer(serializers.LightSerializer):
class UserStoryListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer, class UserStoryListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer,
OwnerExtraInfoMixin, AssigedToExtraInfoMixin, StatusExtraInfoMixin, serializers.LightSerializer): ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin,
ListStatusExtraInfoSerializerMixin, ListBasicAttachmentsInfoSerializerMixin,
serializers.LightSerializer):
id = serpy.Field() id = serpy.Field()
ref = serpy.Field() ref = serpy.Field()
@ -163,13 +164,11 @@ class UserStoryListSerializer(ListVoteResourceSerializerMixin, ListWatchedResour
is_blocked = serpy.Field() is_blocked = serpy.Field()
blocked_note = serpy.Field() blocked_note = serpy.Field()
tags = serpy.Field() tags = serpy.Field()
total_points = serpy.Field("total_points_attr") total_points = serpy.MethodField()
comment = serpy.MethodField("get_comment") comment = serpy.MethodField("get_comment")
origin_issue = ListOriginIssueSerializer(attr="generated_from_issue") origin_issue = ListOriginIssueSerializer(attr="generated_from_issue")
def to_value(self, instance): tasks = serpy.MethodField()
self._serialized_status = {}
return super().to_value(instance)
def get_milestone_slug(self, obj): def get_milestone_slug(self, obj):
return obj.milestone.slug if obj.milestone else None return obj.milestone.slug if obj.milestone else None
@ -177,15 +176,31 @@ class UserStoryListSerializer(ListVoteResourceSerializerMixin, ListWatchedResour
def get_milestone_name(self, obj): def get_milestone_name(self, obj):
return obj.milestone.name if obj.milestone else None 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): 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: if obj.role_points_attr is None:
return {} return {}
return dict(ChainMap(*json.loads(obj.role_points_attr))) return dict(ChainMap(*obj.role_points_attr))
def get_comment(self, obj): def get_comment(self, obj):
return "" 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): class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer):
def serialize_neighbor(self, neighbor): def serialize_neighbor(self, neighbor):

View File

@ -46,11 +46,39 @@ def attach_role_points(queryset, as_field="role_points_attr"):
:return: Queryset object with the additional `as_field` field. :return: Queryset object with the additional `as_field` field.
""" """
model = queryset.model model = queryset.model
sql = """SELECT json_agg(json_build_object(userstories_rolepoints.role_id, sql = """SELECT json_agg((userstories_rolepoints.role_id, userstories_rolepoints.points_id))
userstories_rolepoints.points_id))::text
FROM userstories_rolepoints FROM userstories_rolepoints
WHERE userstories_rolepoints.user_story_id = {tbl}.id""" WHERE userstories_rolepoints.user_story_id = {tbl}.id"""
sql = sql.format(tbl=model._meta.db_table) sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql}) queryset = queryset.extra(select={as_field: sql})
return queryset 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

View File

@ -185,3 +185,24 @@ def test_custom_fields_csv_generation():
assert row[24] == attr.name assert row[24] == attr.name
row = next(reader) row = next(reader)
assert row[24] == "val1" 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

View File

@ -644,3 +644,45 @@ def test_update_userstory_update_tribe_gig(client):
assert response.status_code == 200 assert response.status_code == 200
assert response.data["tribe_gig"] == data["tribe_gig"] 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